diff --git a/configure-nginx-proxy.sh b/configure-nginx-proxy.sh index dd26053..02b8502 100755 --- a/configure-nginx-proxy.sh +++ b/configure-nginx-proxy.sh @@ -250,7 +250,34 @@ server { } EOF -# 7. Relay / api-relay (port 3019) +# 7. Website data (port 3025) +echo "📝 Configuration de data.certificator.4nkweb.com..." +${SUDO_CMD} tee "${NGINX_SITES_AVAILABLE}/data.certificator.4nkweb.com" > /dev/null << 'EOF' +# Website data (iframe data, non clĂ©s) +server { + listen 80; + server_name data.certificator.4nkweb.com; + + access_log /var/log/nginx/data.certificator.4nkweb.com.access.log; + error_log /var/log/nginx/data.certificator.4nkweb.com.error.log; + + location / { + proxy_pass http://192.168.1.105:3025; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_cache_bypass $http_upgrade; + proxy_read_timeout 300s; + proxy_connect_timeout 75s; + } +} +EOF + +# 8. Relay / api-relay (port 3019) echo "📝 Configuration de relay.certificator.4nkweb.com..." ${SUDO_CMD} tee "${NGINX_SITES_AVAILABLE}/relay.certificator.4nkweb.com" > /dev/null << 'EOF' # API Relay (UserWallet) @@ -288,6 +315,7 @@ ${SUDO_CMD} ln -sf "${NGINX_SITES_AVAILABLE}/anchorage.certificator.4nkweb.com" ${SUDO_CMD} ln -sf "${NGINX_SITES_AVAILABLE}/watermark.certificator.4nkweb.com" "${NGINX_SITES_ENABLED}/watermark.certificator.4nkweb.com" ${SUDO_CMD} ln -sf "${NGINX_SITES_AVAILABLE}/userwallet.certificator.4nkweb.com" "${NGINX_SITES_ENABLED}/userwallet.certificator.4nkweb.com" ${SUDO_CMD} ln -sf "${NGINX_SITES_AVAILABLE}/skeleton.certificator.4nkweb.com" "${NGINX_SITES_ENABLED}/skeleton.certificator.4nkweb.com" +${SUDO_CMD} ln -sf "${NGINX_SITES_AVAILABLE}/data.certificator.4nkweb.com" "${NGINX_SITES_ENABLED}/data.certificator.4nkweb.com" ${SUDO_CMD} ln -sf "${NGINX_SITES_AVAILABLE}/relay.certificator.4nkweb.com" "${NGINX_SITES_ENABLED}/relay.certificator.4nkweb.com" # Tester la configuration Nginx @@ -319,6 +347,7 @@ DOMAINS=( "watermark.certificator.4nkweb.com" "userwallet.certificator.4nkweb.com" "skeleton.certificator.4nkweb.com" + "data.certificator.4nkweb.com" "relay.certificator.4nkweb.com" ) @@ -349,6 +378,7 @@ echo " - anchorage.certificator.4nkweb.com -> http://192.168.1.105:3010" echo " - watermark.certificator.4nkweb.com -> http://192.168.1.105:3022" echo " - userwallet.certificator.4nkweb.com -> http://192.168.1.105:3018" echo " - skeleton.certificator.4nkweb.com -> http://192.168.1.105:3024" +echo " - data.certificator.4nkweb.com -> http://192.168.1.105:3025" echo " - relay.certificator.4nkweb.com -> http://192.168.1.105:3019" echo "" echo "⚠ Note: Si les services tournent sur une autre machine," diff --git a/data/sync-utxos.log b/data/sync-utxos.log index 5850592..051064e 100644 --- a/data/sync-utxos.log +++ b/data/sync-utxos.log @@ -1,100 +1,100 @@ - ⏳ Traitement: 210000/215703 UTXOs insĂ©rĂ©s... -đŸ’Ÿ Mise Ă  jour des UTXOs dĂ©pensĂ©s... - -📊 RĂ©sumĂ©: - - UTXOs vĂ©rifiĂ©s: 5146 - - UTXOs toujours disponibles: 5146 - - UTXOs dĂ©pensĂ©s dĂ©tectĂ©s: 0 - 📈 Statistiques finales: - - Total UTXOs: 68398 - - DĂ©pensĂ©s: 63307 - - Non dĂ©pensĂ©s: 5091 + - Total UTXOs: 283165 + - DĂ©pensĂ©s: 124307 + - Non dĂ©pensĂ©s: 158858 ✅ Synchronisation terminĂ©e 🔍 DĂ©marrage de la synchronisation des UTXOs dĂ©pensĂ©s... -📊 UTXOs Ă  vĂ©rifier: 213647 +📊 UTXOs Ă  vĂ©rifier: 141938 📡 RĂ©cupĂ©ration des UTXOs depuis Bitcoin... -📊 UTXOs disponibles dans Bitcoin: 223744 +📊 UTXOs disponibles dans Bitcoin: 272415 đŸ’Ÿ CrĂ©ation de la table temporaire... đŸ’Ÿ Insertion des UTXOs disponibles par batch... - ⏳ Traitement: 10000/223744 UTXOs insĂ©rĂ©s... - ⏳ Traitement: 20000/223744 UTXOs insĂ©rĂ©s... - ⏳ Traitement: 30000/223744 UTXOs insĂ©rĂ©s... - ⏳ Traitement: 40000/223744 UTXOs insĂ©rĂ©s... - ⏳ Traitement: 50000/223744 UTXOs insĂ©rĂ©s... - ⏳ Traitement: 60000/223744 UTXOs insĂ©rĂ©s... - ⏳ Traitement: 70000/223744 UTXOs insĂ©rĂ©s... - ⏳ Traitement: 80000/223744 UTXOs insĂ©rĂ©s... - ⏳ Traitement: 90000/223744 UTXOs insĂ©rĂ©s... - ⏳ Traitement: 100000/223744 UTXOs insĂ©rĂ©s... - ⏳ Traitement: 110000/223744 UTXOs insĂ©rĂ©s... - ⏳ Traitement: 120000/223744 UTXOs insĂ©rĂ©s... - ⏳ Traitement: 130000/223744 UTXOs insĂ©rĂ©s... - ⏳ Traitement: 140000/223744 UTXOs insĂ©rĂ©s... - ⏳ Traitement: 150000/223744 UTXOs insĂ©rĂ©s... - ⏳ Traitement: 160000/223744 UTXOs insĂ©rĂ©s... - ⏳ Traitement: 170000/223744 UTXOs insĂ©rĂ©s... - ⏳ Traitement: 180000/223744 UTXOs insĂ©rĂ©s... - ⏳ Traitement: 190000/223744 UTXOs insĂ©rĂ©s... - ⏳ Traitement: 200000/223744 UTXOs insĂ©rĂ©s... - ⏳ Traitement: 210000/223744 UTXOs insĂ©rĂ©s... - ⏳ Traitement: 220000/223744 UTXOs insĂ©rĂ©s... + ⏳ Traitement: 10000/272415 UTXOs insĂ©rĂ©s... + ⏳ Traitement: 20000/272415 UTXOs insĂ©rĂ©s... + ⏳ Traitement: 30000/272415 UTXOs insĂ©rĂ©s... + ⏳ Traitement: 40000/272415 UTXOs insĂ©rĂ©s... + ⏳ Traitement: 50000/272415 UTXOs insĂ©rĂ©s... + ⏳ Traitement: 60000/272415 UTXOs insĂ©rĂ©s... + ⏳ Traitement: 70000/272415 UTXOs insĂ©rĂ©s... + ⏳ Traitement: 80000/272415 UTXOs insĂ©rĂ©s... + ⏳ Traitement: 90000/272415 UTXOs insĂ©rĂ©s... + ⏳ Traitement: 100000/272415 UTXOs insĂ©rĂ©s... + ⏳ Traitement: 110000/272415 UTXOs insĂ©rĂ©s... + ⏳ Traitement: 120000/272415 UTXOs insĂ©rĂ©s... + ⏳ Traitement: 130000/272415 UTXOs insĂ©rĂ©s... + ⏳ Traitement: 140000/272415 UTXOs insĂ©rĂ©s... + ⏳ Traitement: 150000/272415 UTXOs insĂ©rĂ©s... + ⏳ Traitement: 160000/272415 UTXOs insĂ©rĂ©s... + ⏳ Traitement: 170000/272415 UTXOs insĂ©rĂ©s... + ⏳ Traitement: 180000/272415 UTXOs insĂ©rĂ©s... + ⏳ Traitement: 190000/272415 UTXOs insĂ©rĂ©s... + ⏳ Traitement: 200000/272415 UTXOs insĂ©rĂ©s... + ⏳ Traitement: 210000/272415 UTXOs insĂ©rĂ©s... + ⏳ Traitement: 220000/272415 UTXOs insĂ©rĂ©s... + ⏳ Traitement: 230000/272415 UTXOs insĂ©rĂ©s... + ⏳ Traitement: 240000/272415 UTXOs insĂ©rĂ©s... + ⏳ Traitement: 250000/272415 UTXOs insĂ©rĂ©s... + ⏳ Traitement: 260000/272415 UTXOs insĂ©rĂ©s... + ⏳ Traitement: 270000/272415 UTXOs insĂ©rĂ©s... đŸ’Ÿ Mise Ă  jour des UTXOs dĂ©pensĂ©s... 📊 RĂ©sumĂ©: - - UTXOs vĂ©rifiĂ©s: 213647 - - UTXOs toujours disponibles: 213647 + - UTXOs vĂ©rifiĂ©s: 141938 + - UTXOs toujours disponibles: 141938 - UTXOs dĂ©pensĂ©s dĂ©tectĂ©s: 0 📈 Statistiques finales: - Total UTXOs: 283165 - - DĂ©pensĂ©s: 69526 - - Non dĂ©pensĂ©s: 213639 + - DĂ©pensĂ©s: 141287 + - Non dĂ©pensĂ©s: 141878 ✅ Synchronisation terminĂ©e 🔍 DĂ©marrage de la synchronisation des UTXOs dĂ©pensĂ©s... -📊 UTXOs Ă  vĂ©rifier: 211454 +📊 UTXOs Ă  vĂ©rifier: 124298 📡 RĂ©cupĂ©ration des UTXOs depuis Bitcoin... -📊 UTXOs disponibles dans Bitcoin: 241303 +📊 UTXOs disponibles dans Bitcoin: 270669 đŸ’Ÿ CrĂ©ation de la table temporaire... đŸ’Ÿ Insertion des UTXOs disponibles par batch... - ⏳ Traitement: 10000/241303 UTXOs insĂ©rĂ©s... - ⏳ Traitement: 20000/241303 UTXOs insĂ©rĂ©s... - ⏳ Traitement: 30000/241303 UTXOs insĂ©rĂ©s... - ⏳ Traitement: 40000/241303 UTXOs insĂ©rĂ©s... - ⏳ Traitement: 50000/241303 UTXOs insĂ©rĂ©s... - ⏳ Traitement: 60000/241303 UTXOs insĂ©rĂ©s... - ⏳ Traitement: 70000/241303 UTXOs insĂ©rĂ©s... - ⏳ Traitement: 80000/241303 UTXOs insĂ©rĂ©s... - ⏳ Traitement: 90000/241303 UTXOs insĂ©rĂ©s... - ⏳ Traitement: 100000/241303 UTXOs insĂ©rĂ©s... - ⏳ Traitement: 110000/241303 UTXOs insĂ©rĂ©s... - ⏳ Traitement: 120000/241303 UTXOs insĂ©rĂ©s... - ⏳ Traitement: 130000/241303 UTXOs insĂ©rĂ©s... - ⏳ Traitement: 140000/241303 UTXOs insĂ©rĂ©s... - ⏳ Traitement: 150000/241303 UTXOs insĂ©rĂ©s... - ⏳ Traitement: 160000/241303 UTXOs insĂ©rĂ©s... - ⏳ Traitement: 170000/241303 UTXOs insĂ©rĂ©s... - ⏳ Traitement: 180000/241303 UTXOs insĂ©rĂ©s... - ⏳ Traitement: 190000/241303 UTXOs insĂ©rĂ©s... - ⏳ Traitement: 200000/241303 UTXOs insĂ©rĂ©s... - ⏳ Traitement: 210000/241303 UTXOs insĂ©rĂ©s... - ⏳ Traitement: 220000/241303 UTXOs insĂ©rĂ©s... - ⏳ Traitement: 230000/241303 UTXOs insĂ©rĂ©s... - ⏳ Traitement: 240000/241303 UTXOs insĂ©rĂ©s... + ⏳ Traitement: 10000/270669 UTXOs insĂ©rĂ©s... + ⏳ Traitement: 20000/270669 UTXOs insĂ©rĂ©s... + ⏳ Traitement: 30000/270669 UTXOs insĂ©rĂ©s... + ⏳ Traitement: 40000/270669 UTXOs insĂ©rĂ©s... + ⏳ Traitement: 50000/270669 UTXOs insĂ©rĂ©s... + ⏳ Traitement: 60000/270669 UTXOs insĂ©rĂ©s... + ⏳ Traitement: 70000/270669 UTXOs insĂ©rĂ©s... + ⏳ Traitement: 80000/270669 UTXOs insĂ©rĂ©s... + ⏳ Traitement: 90000/270669 UTXOs insĂ©rĂ©s... + ⏳ Traitement: 100000/270669 UTXOs insĂ©rĂ©s... + ⏳ Traitement: 110000/270669 UTXOs insĂ©rĂ©s... + ⏳ Traitement: 120000/270669 UTXOs insĂ©rĂ©s... + ⏳ Traitement: 130000/270669 UTXOs insĂ©rĂ©s... + ⏳ Traitement: 140000/270669 UTXOs insĂ©rĂ©s... + ⏳ Traitement: 150000/270669 UTXOs insĂ©rĂ©s... + ⏳ Traitement: 160000/270669 UTXOs insĂ©rĂ©s... + ⏳ Traitement: 170000/270669 UTXOs insĂ©rĂ©s... + ⏳ Traitement: 180000/270669 UTXOs insĂ©rĂ©s... + ⏳ Traitement: 190000/270669 UTXOs insĂ©rĂ©s... + ⏳ Traitement: 200000/270669 UTXOs insĂ©rĂ©s... + ⏳ Traitement: 210000/270669 UTXOs insĂ©rĂ©s... + ⏳ Traitement: 220000/270669 UTXOs insĂ©rĂ©s... + ⏳ Traitement: 230000/270669 UTXOs insĂ©rĂ©s... + ⏳ Traitement: 240000/270669 UTXOs insĂ©rĂ©s... + ⏳ Traitement: 250000/270669 UTXOs insĂ©rĂ©s... + ⏳ Traitement: 260000/270669 UTXOs insĂ©rĂ©s... + ⏳ Traitement: 270000/270669 UTXOs insĂ©rĂ©s... đŸ’Ÿ Mise Ă  jour des UTXOs dĂ©pensĂ©s... 📊 RĂ©sumĂ©: - - UTXOs vĂ©rifiĂ©s: 211454 - - UTXOs toujours disponibles: 211454 + - UTXOs vĂ©rifiĂ©s: 124298 + - UTXOs toujours disponibles: 124298 - UTXOs dĂ©pensĂ©s dĂ©tectĂ©s: 0 📈 Statistiques finales: - Total UTXOs: 283165 - - DĂ©pensĂ©s: 71719 - - Non dĂ©pensĂ©s: 211446 + - DĂ©pensĂ©s: 158927 + - Non dĂ©pensĂ©s: 124238 ✅ Synchronisation terminĂ©e diff --git a/docs/DOMAINS_AND_PORTS.md b/docs/DOMAINS_AND_PORTS.md index a90bdc7..ef42e88 100644 --- a/docs/DOMAINS_AND_PORTS.md +++ b/docs/DOMAINS_AND_PORTS.md @@ -22,6 +22,7 @@ Ce document liste tous les domaines, ports et services de l'infrastructure Certi | `faucet.certificator.4nkweb.com` | API Faucet | 3021 | API REST pour distribuer des sats | | `mempool.4nkweb.com` | Mempool | 3015 | Explorateur de blockchain Bitcoin | | `skeleton.certificator.4nkweb.com` | Website skeleton | 3024 | Site squelette iframe UserWallet | +| `data.certificator.4nkweb.com` | Website data | 3025 | Iframe data (non clĂ©s), site ↔ data ↔ userwallet | ### Configuration Nginx @@ -44,6 +45,7 @@ Tous les domaines sont routĂ©s via le proxy Nginx sur le serveur `192.168.1.100` | 3022 | API Filigrane | `api-filigrane` | `api-filigrane/filigrane-api.service` | | 3023 | API ClamAV | `api-clamav` | `api-clamav/clamav-api.service` | | 3024 | Website skeleton | `website-skeleton` | `website-skeleton/website-skeleton.service` | +| 3025 | Website data | `website-data` | (Ă  dĂ©finir, ex. `website-data/website-data.service`) | **Important :** Les ports 3010, 3020, 3021, 3022, 3023, 3024 sont fixes et dĂ©finis dans les fichiers de service systemd. Ils ne peuvent pas ĂȘtre modifiĂ©s sans modifier les services. diff --git a/docs/README.md b/docs/README.md index 67ba54d..4183e47 100644 --- a/docs/README.md +++ b/docs/README.md @@ -65,6 +65,8 @@ Ce dossier contient toute la documentation nĂ©cessaire pour la maintenance et l' - Configuration (origine UserWallet, validateurs) - Utilisation, messages postMessage, rĂ©fĂ©rences +- **website-data** (`website-data/`, `features/website-data-architecture.md`) : Iframe data (data.certificator.4nkweb.com), flux site ↔ data (+notifications) ↔ userwallet (relais). + - **[PAIRING_PUBLIC_KEY_ENCODING.md](./PAIRING_PUBLIC_KEY_ENCODING.md)** : Encodage de la clĂ© publique dans les mots BIP32 - ProblĂšme identifiĂ© (rĂ©cupĂ©ration d'un pair par UUID sur un relais) - Solution implĂ©mentĂ©e (encodage direct de la clĂ© publique) diff --git a/features/userwallet-champs-obligatoires-cnil.md b/features/userwallet-champs-obligatoires-cnil.md index a63b240..8e37fb9 100644 --- a/features/userwallet-champs-obligatoires-cnil.md +++ b/features/userwallet-champs-obligatoires-cnil.md @@ -1,6 +1,6 @@ # UserWallet – Champs obligatoires et attributs CNIL (complĂ©ment specs) -**Author:** Équipe 4NK +**Author:** Équipe 4NK **Date:** 2026-01-26 ## Objectif @@ -13,7 +13,7 @@ Documenter le complĂ©ment de spĂ©cifications : champs (Champ) obligatoires des c ## SynthĂšse -- **Champs obligatoires** : tous les contrats ont certains des 9 types (partage institutions, RSSI, Correspondant CNIL, Responsable cybersĂ©curitĂ©, support infogĂ©rant / admin / N1 / N2 / N3). Identification via `types` ou `datajson`. +- **Champs obligatoires (rĂŽles)** : tous les contrats ont certains des 11 types : partage institutions, RSSI, Correspondant CNIL, Responsable cybersĂ©curitĂ©, support infogĂ©rant / admin / N1 / N2 / N3, **validation (contrat)**, **validation du login**. Identification via `types` ou `datajson`. - **Attributs CNIL dans datajson** : `raisons_usage_tiers`, `raisons_partage_tiers` (tableaux [raisons, tiers]), `conditions_conservation` (au moins `delai_expiration`). ## Modifications code diff --git a/features/website-data-architecture.md b/features/website-data-architecture.md new file mode 100644 index 0000000..35a74bb --- /dev/null +++ b/features/website-data-architecture.md @@ -0,0 +1,39 @@ +# website-data — architecture site <-> data <-> userwallet + +**Objectif** + +Projet iframe **data** (data.certificator.4nkweb.com) dĂ©diĂ© aux **donnĂ©es non clĂ©s**, qui communique avec le **site** et avec **UserWallet** (clĂ©s, relais). Flux : + +``` +site <-> data (+notifications) <-> userwallet (sur les relais) +``` + +**Impacts** + +- Nouveau projet `website-data/` (Vite, TypeScript), servi sur data.certificator.4nkweb.com (port 3025). +- Le site parent intĂšgre une seule iframe **Data**. Data embarque **UserWallet** en iframe enfant. +- Contrat, auth, login-proof, etc. transitent par Data entre site et UserWallet. +- Les clĂ©s et la crypto restent dans UserWallet ; Data ne fait aucune opĂ©ration de signature/chiffrement. + +**Modifications** + +- **website-data** : `package.json`, `vite.config.ts`, `index.html`, `src/config.ts`, `src/channel.ts`, `src/main.ts`. +- Messages postMessage : Site ↔ Data (contract, auth-request, data-request, data-response, notification, pong, error, login-proof, auth-response, service-status). Data ↔ UserWallet : mĂȘmes types applicables, relayĂ©s. +- Notifications : Data → Site (`notification` avec `kind` / `detail`). Actuellement pour userwallet:login-proof, userwallet:auth-response, userwallet:error. Placeholder pour notifications relay (data non clĂ©s). + +**ModalitĂ©s de dĂ©ploiement** + +- Build `website-data` (`npm run build`), servir `dist/` sur le port 3025. +- Configurer Nginx (ou Ă©quivalent) pour data.certificator.4nkweb.com → backend 3025, certificat Let’s Encrypt. +- S’aligner sur la procĂ©dure utilisĂ©e pour website-skeleton / userwallet (cf. docs infrastructure 4NK). + +**ModalitĂ©s d’analyse** + +- Ouvrir le site parent avec iframe Data (src = data.certificator.4nkweb.com ou localhost:3025 en dev). +- Envoyer `contract`, `auth-request` au Data iframe ; vĂ©rifier relay vers UserWallet et rĂ©ponses (login-proof, etc.) remontĂ©es au site. +- VĂ©rifier les `notification` reçues par le site (kind `userwallet:*`). + +**Pages affectĂ©es** + +- `website-data/` (nouveau) +- `features/website-data-architecture.md` (ce document) diff --git a/features/website-skeleton-partie-connectee-pairing-relay.md b/features/website-skeleton-partie-connectee-pairing-relay.md new file mode 100644 index 0000000..cb765ea --- /dev/null +++ b/features/website-skeleton-partie-connectee-pairing-relay.md @@ -0,0 +1,21 @@ +# Website-skeleton: partie connectĂ©e conditionnĂ©e au pairing et au relais + +**Objectif:** La partie connectĂ©e du website-skeleton n’est accessible que si le statut pairing est satisfait (Requis: Oui, Satisfait: Oui) et le statut rĂ©seau relais est OK. La page connectĂ©e adopte un style skeleton avec avatar et icĂŽne notifications. + +**Impacts:** +- UserWallet envoie au parent (postMessage) un message `pairing-relay-status` avec `pairingSatisfied` et `relayOk`. +- Website-skeleton affiche la section connectĂ©e uniquement lorsque l’utilisateur est connectĂ© (login-proof reçu) **et** que le dernier `pairing-relay-status` reçu indique les deux conditions remplies. +- La page connectĂ©e affiche un en-tĂȘte type skeleton avec photo d’avatar (placeholder) et bouton icĂŽne notifications. + +**Modifications:** +- **userwallet** + - `src/utils/iframeChannel.ts`: nouveau type de message `pairing-relay-status` et interface `PairingRelayStatusMessage`. + - `src/components/HomeScreen.tsx`: en iframe, envoi de `pairing-relay-status` lorsque pairing et relay changent. + - `src/components/LoginScreen.tsx`: en iframe, envoi de `pairing-relay-status` (pairing satisfait, relay OK) pour que le parent reçoive l’état avant ou avec le login-proof. +- **website-skeleton** + - `src/main.ts`: rĂ©ception de `pairing-relay-status`, stockage du dernier Ă©tat; `canShowConnectedSection()` exige login + pairing satisfait + relay OK; `updateUI()` utilise cette condition pour afficher la section connectĂ©e. VĂ©rification de `msg.origin === USERWALLET_ORIGIN` dans `handleMessage` pour n’accepter que les messages provenant de l’iframe UserWallet (Ă©vite qu’un script ou la console forge un message et force l’accĂšs Ă  la partie connectĂ©e). + - `index.html`: section connectĂ©e refaite avec en-tĂȘte (avatar placeholder, icĂŽne notifications), liens et bouton dĂ©connexion inchangĂ©s. + +**ModalitĂ©s de dĂ©ploiement:** DĂ©ploiement classique du website-skeleton et du userwallet (build puis dĂ©ploiement des artefacts). + +**ModalitĂ©s d’analyse:** VĂ©rifier en conditions rĂ©elles : 1) sans pairing/relay OK, aprĂšs login-proof la section connectĂ©e ne s’affiche pas ; 2) avec pairing satisfait et relay OK, aprĂšs login-proof la section connectĂ©e s’affiche avec avatar et icĂŽne notifications ; 3) si le statut pairing ou relay repasse Ă  non satisfait aprĂšs connexion, la section connectĂ©e disparaĂźt. diff --git a/fixKnowledge/userwallet-skeleton-contract-types-version.md b/fixKnowledge/userwallet-skeleton-contract-types-version.md new file mode 100644 index 0000000..90e0984 --- /dev/null +++ b/fixKnowledge/userwallet-skeleton-contract-types-version.md @@ -0,0 +1,50 @@ +# UserWallet iframe: Invalid contract structure / types_names_chiffres undefined + +**ProblĂšme** + +En iframe (skeleton → UserWallet), console : + +- `Invalid contract structure received via channel: missing required fields` +- `Error updating graph: TypeError: Cannot read properties of undefined (reading 'types_names_chiffres')` dans `syncUpdateGraph.ts` + +**Impacts** + +- Le contrat skeleton n’est pas acceptĂ© par UserWallet. +- Le graphe n’est pas mis Ă  jour ; le login skeleton Ă©choue cĂŽtĂ© graphe. + +**Root causes** + +1. **useChannel** exige sur le contrat principal `uuid`, `version`, `validateurs`. Le skeleton n’envoyait pas `version` → validation « missing required fields ». +2. **syncUpdateGraph** lit `msg.types.types_names_chiffres`. Le skeleton mettait `types_names_chiffres` dans `datajson` uniquement, pas de `types` Ă  la racine → `msg.types` undefined → crash. + +**Correctifs** + +1. **userwallet `syncUpdateGraph.ts`** + - VĂ©rifier `msg?.types?.types_names_chiffres` avant utilisation. + - Si absent : `console.warn` avec `uuid` et `return` (ne pas ajouter au graphe). + - Évite le crash et signale les messages mal formĂ©s. + +2. **website-skeleton `serviceContract.ts`** + - Contrat : ajouter `version: '1.0'` et + `types: { types_uuid: [SKELETON_CONTRACT_TYPE_UUID], types_names_chiffres: 'contrat' }`. + - Action login : idem avec `version: '1.0'` et + `types: { types_uuid: [SKELETON_ACTION_TYPE_UUID], types_names_chiffres: 'action,login' }`. + - Alignement avec `MessageBase` / `MessageAValider` attendus par UserWallet. + - `datajson` inchangĂ© (types_uuid, types_names_chiffres, etc.). + +**Fichiers modifiĂ©s** + +- `userwallet/src/services/syncUpdateGraph.ts` +- `website-skeleton/src/serviceContract.ts` +- `website-skeleton/src/config.ts` (Contrat / Action : `version?`, `types?` documentĂ©s) + +**ModalitĂ©s de dĂ©ploiement** + +- Rebuild UserWallet et website-skeleton. +- RedĂ©ployer les deux selon l’infra (skeleton, UserWallet iframe). + +**ModalitĂ©s d’analyse** + +- Reproduire : ouvrir skeleton en iframe UserWallet, observer la console. +- VĂ©rifier l’absence de « Invalid contract structure » et de « types_names_chiffres » undefined. +- VĂ©rifier que le graphe reçoit bien contrat + action login (login possible). diff --git a/fixKnowledge/website-skeleton-postmessage-origin-check.md b/fixKnowledge/website-skeleton-postmessage-origin-check.md new file mode 100644 index 0000000..090469d --- /dev/null +++ b/fixKnowledge/website-skeleton-postmessage-origin-check.md @@ -0,0 +1,23 @@ +# Website-skeleton: vĂ©rification de l’origine des messages postMessage + +**ProblĂšme:** Un utilisateur pouvait forcer l’accĂšs Ă  la partie connectĂ©e en exĂ©cutant dans la console du navigateur un `window.postMessage({ type: 'pairing-relay-status', payload: { pairingSatisfied: true, relayOk: true } }, '*')`. Si une session (login-proof) existait dĂ©jĂ , la section connectĂ©e s’affichait sans que pairing/relais soient rĂ©ellement OK. + +**Impacts:** Risque de contournement des conditions d’accĂšs (pairing satisfait, relais OK) et affichage de la partie connectĂ©e dans un Ă©tat non conforme. + +**Cause:** Le handler `handleMessage` du website-skeleton ne vĂ©rifiait pas l’origine du message ; tout script s’exĂ©cutant dans la mĂȘme page (ou la console) pouvait envoyer un faux `pairing-relay-status` ou `login-proof` (pour login-proof la vĂ©rification cĂŽtĂ© preuve reste, mais l’origine n’était pas contrĂŽlĂ©e). + +**Root cause:** Absence de contrĂŽle de `event.origin` sur les messages reçus via `window.addEventListener('message', handleMessage)`. + +**Correctifs:** +- Au dĂ©but de `handleMessage`, ignorer tout message dont `msg.origin !== USERWALLET_ORIGIN`. Seuls les messages Ă©mis par l’iframe UserWallet (mĂȘme origine que `USERWALLET_ORIGIN`) sont traitĂ©s. Un `postMessage` depuis la console a pour origine l’URL du site skeleton, pas celle du UserWallet, donc il est rejetĂ©. + +**Evolutions:** Aucune. + +**Pages affectĂ©es:** +- `website-skeleton/src/main.ts` (vĂ©rification `msg.origin !== USERWALLET_ORIGIN` en tĂȘte de `handleMessage`) +- `features/website-skeleton-partie-connectee-pairing-relay.md` (mention de la vĂ©rification d’origine) +- `fixKnowledge/website-skeleton-postmessage-origin-check.md` (ce document) + +**ModalitĂ©s de dĂ©ploiement:** DĂ©ploiement classique du website-skeleton. + +**ModalitĂ©s d’analyse:** En console sur la page skeleton, exĂ©cuter `window.postMessage({ type: 'pairing-relay-status', payload: { pairingSatisfied: true, relayOk: true } }, '*')` alors qu’une session existe : la section connectĂ©e ne doit pas s’afficher si pairing/relay ne sont pas OK (message rejetĂ© car origine ≠ UserWallet). diff --git a/fixKnowledge/website-skeleton-section-connectee-non-affichee.md b/fixKnowledge/website-skeleton-section-connectee-non-affichee.md new file mode 100644 index 0000000..24cc155 --- /dev/null +++ b/fixKnowledge/website-skeleton-section-connectee-non-affichee.md @@ -0,0 +1,26 @@ +# Website-skeleton: section connectĂ©e non affichĂ©e malgrĂ© pairing/relay OK + +**ProblĂšme:** L'utilisateur voit dans UserWallet (HomeScreen) le statut « Pairing Satisfait: Oui » et « Statut rĂ©seau relais: OK », mais la page skeleton ne redirige pas vers la section connectĂ©e. + +**Impacts:** L'utilisateur connectĂ© (session existante) qui rafraĂźchit la page skeleton reste sur la section « Se connecter » au lieu de voir la section connectĂ©e. + +**Cause:** La section connectĂ©e n’est affichĂ©e que si `canShowConnectedSection()` est vrai : session (login-proof) **et** dernier `pairing-relay-status` reçu avec `pairingSatisfied` et `relayOk`. AprĂšs un rafraĂźchissement, la session est en sessionStorage mais `pairingRelayStatus` n’est pas persistĂ©. L’iframe UserWallet est dans un conteneur en `display: none` tant qu’on affiche la section login ; dans ce cas l’iframe peut ne pas charger (ou charger avec retard), donc le parent ne reçoit jamais `pairing-relay-status` et `pairingRelayStatus` reste `null`. + +**Root cause:** Quand l’utilisateur a une session mais pas encore de `pairing-relay-status`, l’iframe est cachĂ©e donc elle ne charge pas (ou pas Ă  temps) et n’envoie pas le message au parent. + +**Correctifs:** +- Quand `isLoggedIn()` est vrai et `pairingRelayStatus === null`, afficher l’iframe (conteneur en `display: block`) pour qu’elle charge et envoie `pairing-relay-status`. +- Nouvelle branche dans `updateUI()` : `else if (isLoggedIn() && pairingRelayStatus === null)` → `showLoginInterfaceWithIframe(true)`. +- `showLoginInterfaceWithIframe(waitingForStatus)` : si `waitingForStatus` est vrai, afficher l’iframe et le message « VĂ©rification du statut pairing et relais
 » ; sinon comportement identique Ă  `showLoginInterface()` (iframe cachĂ©e). +- ÉlĂ©ment `#waiting-status` dans la section login, affichĂ© uniquement en attente du statut. + +**Evolutions:** Aucune. + +**Pages affectĂ©es:** +- `website-skeleton/src/main.ts` (updateUI, showLoginInterfaceWithIframe, ref waitingStatusEl) +- `website-skeleton/index.html` (Ă©lĂ©ment #waiting-status) +- `fixKnowledge/website-skeleton-section-connectee-non-affichee.md` (ce document) + +**ModalitĂ©s de dĂ©ploiement:** Build et dĂ©ploiement classique du website-skeleton. + +**ModalitĂ©s d’analyse:** 1) Se connecter sur le skeleton (login-proof reçu, section connectĂ©e affichĂ©e). 2) RafraĂźchir la page : la section « VĂ©rification du statut pairing et relais
 » et l’iframe s’affichent briĂšvement, puis la section connectĂ©e s’affiche dĂšs rĂ©ception de `pairing-relay-status`. 3) Sans session : pas de changement (clic « Se connecter » puis login dans l’iframe pour obtenir la section connectĂ©e). diff --git a/userwallet/docs/specs-champs-obligatoires-cnil.md b/userwallet/docs/specs-champs-obligatoires-cnil.md index 9d3d364..aef1db3 100644 --- a/userwallet/docs/specs-champs-obligatoires-cnil.md +++ b/userwallet/docs/specs-champs-obligatoires-cnil.md @@ -1,30 +1,32 @@ # ComplĂ©ment de specs – Champs obligatoires et attributs CNIL -**Author:** Équipe 4NK -**Date:** 2026-01-26 +**Author:** Équipe 4NK +**Date:** 2026-01-26 RĂ©fĂ©rence : `specs.md` (Contrat, Champ, MessageBase, DataJson). --- -## 1. Champs obligatoires des contrats +## 1. Champs obligatoires des contrats (rĂŽles) -Tous les contrats ont **certains** des champs (objets `Champ`) suivants. Chaque type correspond Ă  un usage mĂ©tier prĂ©cis ; un contrat donnĂ© en possĂšde un sous-ensemble selon le contexte. +Tous les contrats ont **certains** des champs (objets `Champ`) suivants. Chaque type correspond Ă  un **rĂŽle** / usage mĂ©tier prĂ©cis ; un contrat donnĂ© en possĂšde un sous-ensemble selon le contexte. -| Type de Champ | Description | -|---------------|-------------| -| Partage avec les institutions | Champ dĂ©diĂ© au partage de donnĂ©es avec des institutions | +| Type de Champ (rĂŽle) | Description | +|----------------------|-------------| +| Partage avec les institutions | Partage de donnĂ©es avec des institutions | | Messages au RSSI | Messages au RSSI de la sociĂ©tĂ© responsable du service | | Messages au Correspondant CNIL | Messages au Correspondant CNIL de la sociĂ©tĂ© responsable du service | -| Messages au Responsable cybersĂ©curitĂ© | Messages au Responsable cybersĂ©curitĂ© au bord de la sociĂ©tĂ© responsable du service | -| Messages de support infogĂ©rant | Messages de support de l'infogĂ©rant du service (peut inclure un membre du miner pour la gestion des clĂ©s API) | -| Messages de support administrateur systĂšme | Messages de support de l’administrateur systĂšme du service | -| Messages de support niveau 1 | Messages de support de niveau 1 du service | -| Messages de support niveau 2 | Messages de support de niveau 2 du service | -| Messages de support niveau 3 | Messages de support de niveau 3 du service | +| Messages au Responsable cybersĂ©curitĂ© | Messages au Responsable cybersĂ©curitĂ© de la sociĂ©tĂ© responsable du service | +| Messages de support infogĂ©rant | Support infogĂ©rant (peut inclure membre du miner pour clĂ©s API) | +| Messages de support administrateur systĂšme | Support admin systĂšme du service | +| Messages de support niveau 1 | Support N1 du service | +| Messages de support niveau 2 | Support N2 du service | +| Messages de support niveau 3 | Support N3 du service | +| **Validation (contrat)** | Validation du contrat ; validateurs du contrat (`contrat.validateurs.membres_du_role`) | +| **Validation du login** | Validation du login ; validateurs de l'action login (`action.validateurs_action.membres_du_role`) | - Les `Champ` sont des `MessageAValider` avec `contrats_parents_uuid` (au moins 1). -- L’identification du type (partage, RSSI, CNIL, cybersĂ©curitĂ©, support N1/N2/N3, etc.) se fait via `types.types_names_chiffres` / `types_uuid` ou via des mĂ©tadonnĂ©es dans `datajson`, selon les conventions du catalogue. +- L’identification du type / rĂŽle (partage, RSSI, CNIL, cybersĂ©curitĂ©, support N1/N2/N3, validation, validation login, etc.) se fait via `types.types_names_chiffres` / `types_uuid` ou via des mĂ©tadonnĂ©es dans `datajson`, selon les conventions du catalogue. --- @@ -34,9 +36,9 @@ Pour les objets concernĂ©s (ex. contrats, champs), la partie **`datajson`** peut ### 2.1 Usage et partage avec les tiers -- **Raisons de l’usage avec les tiers et description du tiers** +- **Raisons de l’usage avec les tiers et description du tiers** Tableau de couples `[raisons, tiers]` : pour chaque usage avec un tiers, les raisons et la description du tiers. -- **Raisons du partage avec les tiers et description du tiers** +- **Raisons du partage avec les tiers et description du tiers** Tableau de couples `[raisons, tiers]` : pour chaque partage avec un tiers, les raisons et la description du tiers. Structure suggĂ©rĂ©e (Ă  prĂ©ciser dans le catalogue) : @@ -54,7 +56,7 @@ Structure suggĂ©rĂ©e (Ă  prĂ©ciser dans le catalogue) : ### 2.2 Conditions de conservation -- **Conditions de conservation** +- **Conditions de conservation** Objet JSON contenant **au moins** le **dĂ©lai d’expiration** (durĂ©e de conservation des donnĂ©es). Exemple : @@ -76,7 +78,7 @@ Exemple : | Domaine | Contenu | |--------|---------| -| Champs obligatoires | Sous-ensemble des 9 types (partage, RSSI, CNIL, cybersĂ©curitĂ©, support infogĂ©rant / admin / N1 / N2 / N3) selon le contrat | +| Champs obligatoires (rĂŽles) | Sous-ensemble des 11 types : partage, RSSI, CNIL, cybersĂ©curitĂ©, support infogĂ©rant / admin / N1 / N2 / N3, **validation (contrat)**, **validation du login** | | `datajson` CNIL | `raisons_usage_tiers`, `raisons_partage_tiers` (tableaux [raisons, tiers]) ; `conditions_conservation` (JSON avec au moins `delai_expiration`) | --- diff --git a/userwallet/docs/synthese.md b/userwallet/docs/synthese.md index 54f1397..4d74eb9 100644 --- a/userwallet/docs/synthese.md +++ b/userwallet/docs/synthese.md @@ -1,6 +1,6 @@ # SynthĂšse structurĂ©e – UserWallet & API Relay -**Author:** Équipe 4NK +**Author:** Équipe 4NK **Date:** 2026-01-26 ## 1. userwallet/docs @@ -15,7 +15,7 @@ SpĂ©cification du login dĂ©centralisĂ© (secp256k1, contrats, pairing mFA, relais) : -**ComplĂ©ment** : `specs-champs-obligatoires-cnil.md` — champs obligatoires (partage, RSSI, CNIL, cybersĂ©curitĂ©, support N1/N2/N3, etc.) et attributs CNIL dans `datajson` (`raisons_usage_tiers`, `raisons_partage_tiers`, `conditions_conservation`, `membre_miner_uuid` pour les champs infogĂ©rant). Voir `features/userwallet-membre-miner-infogerant.md`. +**ComplĂ©ment** : `specs-champs-obligatoires-cnil.md` — champs obligatoires / rĂŽles (partage, RSSI, CNIL, cybersĂ©curitĂ©, support N1/N2/N3, validation contrat, validation login, etc.) et attributs CNIL dans `datajson` (`raisons_usage_tiers`, `raisons_partage_tiers`, `conditions_conservation`, `membre_miner_uuid` pour les champs infogĂ©rant). Voir `features/userwallet-membre-miner-infogerant.md`. - **ModĂšle** : messages publiĂ©s sans signatures ni clĂ©s ; signatures et clĂ©s publiĂ©es sĂ©parĂ©ment ; tout adressĂ© par hash canonique ; rĂ©cupĂ©ration par **GET uniquement** (pull). - **Objets** : Service, Contrat, Champ, Action, ActionLogin, Membre, Pair, MessageBase, Hash, Signature, Validateurs, MsgChiffre, MsgSignature, MsgCle. diff --git a/userwallet/features/userwallet-iframe-key-isolation.md b/userwallet/features/userwallet-iframe-key-isolation.md new file mode 100644 index 0000000..acc073d --- /dev/null +++ b/userwallet/features/userwallet-iframe-key-isolation.md @@ -0,0 +1,34 @@ +# UserWallet iframe — cloisonnement des clĂ©s et du chiffrement + +**Objectif** + +Garantir qu’aucune clĂ© secrĂšte ne quitte l’iframe UserWallet et qu’aucune opĂ©ration de signature ou de chiffrement n’est effectuĂ©e en dehors du domaine de l’iframe. + +**RĂšgles** + +1. **ClĂ©s** + - Seules les **clĂ©s publiques** peuvent sortir de l’iframe (via postMessage vers le parent). + - Les clĂ©s privĂ©es (`privateKey`, `cle_privee`) ne doivent jamais ĂȘtre envoyĂ©es au parent ni Ă  un autre domaine. + +2. **Crypto** + - Toutes les opĂ©rations de **signature**, **chiffrement** et **dĂ©chiffrement** sont rĂ©alisĂ©es **uniquement dans l’iframe** (UserWallet). + - Le parent (site intĂ©grateur, ex. skeleton) ne doit **jamais** signer, chiffrer ni dĂ©chiffrer. Il peut uniquement **vĂ©rifier** des signatures Ă  l’aide des clĂ©s publiques (ex. `verifyLoginProof`). + +**ImplĂ©mentation** + +- **iframeChannel** : `sendToChannel` appelle `assertNoSecretsInChannelPayload` avant `postMessage`. Toute prĂ©sence de `privateKey` ou `cle_privee` dans le payload (rĂ©cursif) lĂšve une erreur ; le message n’est pas envoyĂ©. +- **iframe.ts** : `sendMessageToParent` appelle aussi `assertNoSecretsInChannelPayload` sur le payload avant `postMessage`. Tous les envois vers le parent passent par cette vĂ©rification. +- **Messages envoyĂ©s au parent** : `auth-response` (signature, publicKey, message), `login-proof` (challenge, signatures avec `cle_publique`), `error`, `service-status`, `pairing-relay-status` (pairingSatisfied, relayOk). Aucun de ces messages ne contient de clĂ© secrĂšte. +- **Messages reçus du parent** : `auth-request`, `contract`. Le contrat peut contenir des `cle_publique` de service (validateurs) ; ce sont des clĂ©s publiques. Le parent n’envoie jamais de clĂ© privĂ©e. +- **Parent (intĂ©grateur)** : Ne doit jamais signer, chiffrer ni dĂ©chiffrer. Il peut uniquement **vĂ©rifier** des signatures (ex. `verifyLoginProof` avec clĂ©s publiques). Ex. skeleton : vĂ©rification uniquement, pas de crypto secrĂšte. + +**ContrĂŽles** + +- Avant tout envoi vers le parent : vĂ©rification systĂ©matique des payloads pour `privateKey` et `cle_privee`. +- Aucun usage de `privateKey` / `cle_privee` dans les types de messages dĂ©finis pour le canal iframe (`AuthResponseMessage`, `LoginProofMessage`, etc.). + +**Pages affectĂ©es** + +- `userwallet/src/utils/iframeChannel.ts` (assertion, `sendToChannel`) +- `userwallet/src/utils/iframe.ts` (assertion, `sendMessageToParent`) +- `userwallet/features/userwallet-iframe-key-isolation.md` (ce document) diff --git a/userwallet/src/components/HomeScreen.tsx b/userwallet/src/components/HomeScreen.tsx index 892046b..7d95882 100644 --- a/userwallet/src/components/HomeScreen.tsx +++ b/userwallet/src/components/HomeScreen.tsx @@ -4,6 +4,7 @@ import { useIdentity } from '../hooks/useIdentity'; import { isPairingSatisfied, hasRemotePair } from '../utils/pairing'; import { usePairingConnected } from '../hooks/usePairingConnected'; import { getStoredRelays } from '../utils/relay'; +import { isInIframe, sendToChannel } from '../utils/iframeChannel'; import type { LocalIdentity } from '../types/identity'; import { PairingSetupBlock } from './PairingSetupBlock'; @@ -54,6 +55,19 @@ export function HomeScreen(): JSX.Element { logHomeStatus(identity, pairingSatisfied, relayStatus, relays.length); }, [identity, pairingSatisfied, relayStatus, relays.length]); + useEffect(() => { + if (!isInIframe()) { + return; + } + sendToChannel({ + type: 'pairing-relay-status', + payload: { + pairingSatisfied, + relayOk: relayStatus === 'OK', + }, + }); + }, [pairingSatisfied, relayStatus]); + useEffect(() => { if (!showSetupBlock || identity === null || hasScrolledAndLoggedRef.current) { if (!showSetupBlock) { diff --git a/userwallet/src/components/LoginScreen.tsx b/userwallet/src/components/LoginScreen.tsx index 703f3c4..3c0e38d 100644 --- a/userwallet/src/components/LoginScreen.tsx +++ b/userwallet/src/components/LoginScreen.tsx @@ -9,6 +9,7 @@ import { getStoredRelays } from '../utils/relay'; import { GraphResolver } from '../services/graphResolver'; import { LoginBuilder } from '../services/loginBuilder'; import { useChannel } from '../hooks/useChannel'; +import { isInIframe, sendToChannel } from '../utils/iframeChannel'; import { isPairingSatisfied, getPairsForMember } from '../utils/pairing'; import { buildAllowedPubkeys, @@ -65,10 +66,31 @@ export function LoginScreen(): JSX.Element { } | null>(null); const graphResolver = useState(() => new GraphResolver())[0]; + const pairingSatisfied = isPairingSatisfied(); + const relayOk = getStoredRelays().length > 0; - // Rediriger selon l'Ă©tat de la machine Ă  Ă©tats + useEffect(() => { + if (!isInIframe()) { + return; + } + sendToChannel({ + type: 'pairing-relay-status', + payload: { + pairingSatisfied, + relayOk, + }, + }); + }, [pairingSatisfied, relayOk]); + + // Rediriger selon l'Ă©tat de la machine Ă  Ă©tats. Si service+membre en URL (parent iframe), + // ne pas rediriger vers /services; dispatcher E_SELECT_* pour aller en S_LOGIN_BUILD_PATH. useEffect(() => { if (loginState === 'S_LOGIN_SELECT_SERVICE') { + if (serviceUuid !== '' && membreUuid !== '') { + dispatch({ type: 'E_SELECT_SERVICE', serviceUuid }); + dispatch({ type: 'E_SELECT_MEMBER', membreUuid }); + return; + } navigate('/services'); return; } @@ -80,7 +102,7 @@ export function LoginScreen(): JSX.Element { navigate('/services'); return; } - }, [loginState, serviceUuid, navigate]); + }, [loginState, serviceUuid, membreUuid, navigate, dispatch]); const handleBuildPath = useCallback(async (): Promise => { if (serviceUuid === '' || membreUuid === '') { diff --git a/userwallet/src/components/PairManagementScreen.tsx b/userwallet/src/components/PairManagementScreen.tsx index c416689..7376d11 100644 --- a/userwallet/src/components/PairManagementScreen.tsx +++ b/userwallet/src/components/PairManagementScreen.tsx @@ -139,18 +139,63 @@ export function PairManagementScreen(): JSX.Element { }} >

- Mots (8): + Mots (17):

-

- {words.join(' ')} -

+ {words.map((word, wordIndex) => { + const visible = visibleWordIndices.get(pair.uuid)?.has(wordIndex) ?? false; + return ( +
+ + {wordIndex + 1}: + + + {visible ? `${wordIndex + 1}. ${word}` : '‱‱‱‱‱'} + + +
+ ); + })} + )}
diff --git a/userwallet/src/components/PairingDisplayScreen.tsx b/userwallet/src/components/PairingDisplayScreen.tsx index 21822cd..a244ae8 100644 --- a/userwallet/src/components/PairingDisplayScreen.tsx +++ b/userwallet/src/components/PairingDisplayScreen.tsx @@ -23,6 +23,19 @@ export function PairingDisplayScreen(): JSX.Element { const [success, setSuccess] = useState(false); const [isConfirming, setIsConfirming] = useState(false); const [justConnected, setJustConnected] = useState(false); + const [visibleWordIndices, setVisibleWordIndices] = useState>(new Set()); + + const toggleWordVisibility = (index: number): void => { + setVisibleWordIndices((prev) => { + const next = new Set(prev); + if (next.has(index)) { + next.delete(index); + } else { + next.add(index); + } + return next; + }); + }; useEffect(() => { if (identity !== null && identity.publicKey !== undefined) { @@ -106,7 +119,6 @@ export function PairingDisplayScreen(): JSX.Element { if (success) { const showConnected = pairingConnected || justConnected; - const words2ndText = words2nd.length > 0 ? words2nd.join(' ') : '—'; return (
{showConnected && ( @@ -134,44 +146,63 @@ export function PairingDisplayScreen(): JSX.Element {

Mots du 2ᔉ appareil — Ă  copier sur le 1á”‰Êł

-

- {words2ndText} -

- {identity !== null && ( - <> -

- ClĂ© publique de ce dispositif — Ă  copier sur le 1á”‰Êł (hex) -

-

- {identity.publicKey} -

- + {words2nd.length > 0 ? ( +
+ {words2nd.map((word, index) => { + const visible = visibleWordIndices.has(index); + return ( +
+ {index + 1}. + + {visible ? word : '‱‱‱‱‱'} + + +
+ ); + })} +
+ ) : ( +

—

)}

diff --git a/userwallet/src/components/PairingSetupBlock.tsx b/userwallet/src/components/PairingSetupBlock.tsx index 1a01d65..c4dc22d 100644 --- a/userwallet/src/components/PairingSetupBlock.tsx +++ b/userwallet/src/components/PairingSetupBlock.tsx @@ -28,6 +28,19 @@ export function PairingSetupBlock(): JSX.Element { const [remoteError, setRemoteError] = useState(null); const [hasCopiedToSecondDevice, setHasCopiedToSecondDevice] = useState(false); const [isConfirming, setIsConfirming] = useState(false); + const [visibleWordIndices, setVisibleWordIndices] = useState>(new Set()); + + const toggleWordVisibility = (index: number): void => { + setVisibleWordIndices((prev) => { + const next = new Set(prev); + if (next.has(index)) { + next.delete(index); + } else { + next.add(index); + } + return next; + }); + }; useEffect(() => { if (identity !== null && identity.publicKey !== undefined) { @@ -115,12 +128,56 @@ export function PairingSetupBlock(): JSX.Element {

Mots du 1á”‰Êł appareil — Ă  saisir sur le 2ᔉ (QR) :

-

- {words.join(' ')} -

+ {words.map((word, index) => { + const visible = visibleWordIndices.has(index); + return ( +
+ {index + 1}. + + {visible ? word : '‱‱‱‱‱'} + + +
+ ); + })} + {qrDataUrl !== null && (

= MAX_RELAYS) { + return; + } const newRelay: RelayConfig = { endpoint: newEndpoint.trim(), priority: relays.length, @@ -102,6 +105,9 @@ export function RelaySettingsScreen(): JSX.Element {

Ajouter un relais

+

+ Maximum {MAX_RELAYS} relais. Connectez-vous au maximum pour dupliquer les dépÎts et les flux. +

{ setNewEndpoint(e.target.value); }} - placeholder="http://relay.example.com:3019" + placeholder="https://relay.example.com" onKeyDown={(e) => { if (e.key === 'Enter') { handleAdd(); } }} + disabled={relays.length >= MAX_RELAYS} /> - + + {relays.length >= MAX_RELAYS && ( + + Limite de {MAX_RELAYS} relais atteinte. + + )}
diff --git a/userwallet/src/components/WordInputGrid.tsx b/userwallet/src/components/WordInputGrid.tsx index a838290..44e8f58 100644 --- a/userwallet/src/components/WordInputGrid.tsx +++ b/userwallet/src/components/WordInputGrid.tsx @@ -39,6 +39,7 @@ export function WordInputGrid({ const [focusedIndex, setFocusedIndex] = useState(null); const [suggestions, setSuggestions] = useState([]); const [selectedSuggestionIndex, setSelectedSuggestionIndex] = useState(-1); + const [visibleWords, setVisibleWords] = useState>(new Set()); const inputRefs = useRef<(HTMLInputElement | null)[]>([]); const suggestionsRef = useRef(null); @@ -219,7 +220,7 @@ export function WordInputGrid({ inputRefs.current[index] = el; }} id={`${id ?? 'word'}-${index}`} - type="password" + type={visibleWords.has(index) ? 'text' : 'password'} value={word} onChange={(e: ChangeEvent) => { handleInputChange(index, e.target.value); @@ -241,7 +242,7 @@ export function WordInputGrid({ style={{ width: '100%', padding: '0.5rem', - paddingRight: '2.5rem', + paddingRight: word.length > 0 ? '2.5rem' : '0.5rem', fontSize: '1rem', fontFamily: 'monospace', border: '1px solid var(--color-border, #ccc)', @@ -254,10 +255,15 @@ export function WordInputGrid({ )}
diff --git a/userwallet/src/hooks/useChannel.ts b/userwallet/src/hooks/useChannel.ts index bdad09d..ea96e5e 100644 --- a/userwallet/src/hooks/useChannel.ts +++ b/userwallet/src/hooks/useChannel.ts @@ -1,4 +1,5 @@ import { useEffect, useCallback, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; import { listenToChannel, sendToChannel, @@ -15,6 +16,7 @@ import type { LoginProof } from '../types/identity'; import type { Contrat, Action } from '../types/contract'; export function useChannel() { + const navigate = useNavigate(); const { identity } = useIdentity(); const graphResolver = useState(() => { return new GraphResolver(); @@ -121,6 +123,29 @@ export function useChannel() { [graphResolver], ); + const handleNavigateLogin = useCallback( + (message: ChannelMessage): void => { + if (message.type !== 'navigate-login') { + return; + } + const payload = message.payload as { service?: string; member?: string }; + const service = + typeof payload?.service === 'string' ? payload.service : ''; + const member = + typeof payload?.member === 'string' ? payload.member : ''; + const params = new URLSearchParams(); + if (service !== '') { + params.set('service', service); + } + if (member !== '') { + params.set('membre', member); + } + const qs = params.toString(); + navigate(qs !== '' ? `/login?${qs}` : '/login'); + }, + [navigate], + ); + const sendLoginProof = useCallback((proof: LoginProof): void => { sendToChannel({ type: 'login-proof', @@ -138,11 +163,13 @@ export function useChannel() { handleAuthRequest(message as AuthRequestMessage); } else if (message.type === 'contract') { handleContract(message as ContractMessage); + } else if (message.type === 'navigate-login') { + handleNavigateLogin(message); } }); return cleanup; - }, [handleAuthRequest, handleContract]); + }, [handleAuthRequest, handleContract, handleNavigateLogin]); return { sendLoginProof, diff --git a/userwallet/src/services/syncUpdateGraph.ts b/userwallet/src/services/syncUpdateGraph.ts index 6158570..c0fffe0 100644 --- a/userwallet/src/services/syncUpdateGraph.ts +++ b/userwallet/src/services/syncUpdateGraph.ts @@ -12,6 +12,7 @@ import type { /** * Update graph cache with decrypted message. + * Message must have types.types_names_chiffres (MessageBase). Skips otherwise. */ export function updateGraphFromMessage( decrypted: unknown, @@ -19,7 +20,15 @@ export function updateGraphFromMessage( ): void { try { const msg = decrypted as MessageBase; - const typeNames = msg.types.types_names_chiffres.toLowerCase(); + const raw = msg?.types?.types_names_chiffres; + if (typeof raw !== 'string' || raw.length === 0) { + console.warn( + '[SyncService] message missing types.types_names_chiffres, skipped', + { uuid: (msg as { uuid?: string })?.uuid }, + ); + return; + } + const typeNames = raw.toLowerCase(); if (typeNames.includes('service')) { graphResolver.addService(msg as unknown as Service); diff --git a/userwallet/src/utils/crypto.ts b/userwallet/src/utils/crypto.ts index 2534004..1225179 100644 --- a/userwallet/src/utils/crypto.ts +++ b/userwallet/src/utils/crypto.ts @@ -1,13 +1,21 @@ import { + etc as secpEtc, getPublicKey, sign, Signature, utils as secpUtils, verify, } from '@noble/secp256k1'; +import { hmac } from '@noble/hashes/hmac'; import { sha256 } from '@noble/hashes/sha256'; import { bytesToHex, hexToBytes } from '@noble/hashes/utils'; +// Require hmacSha256Sync for secp256k1 sign() (RFC6979). Set once at module load. +if (secpEtc.hmacSha256Sync === undefined) { + secpEtc.hmacSha256Sync = (k: Uint8Array, ...m: Uint8Array[]): Uint8Array => + hmac(sha256, k, secpEtc.concatBytes(...m)); +} + export interface KeyPair { privateKey: string; publicKey: string; diff --git a/userwallet/src/utils/iframe.ts b/userwallet/src/utils/iframe.ts index 05f471d..b81b3a7 100644 --- a/userwallet/src/utils/iframe.ts +++ b/userwallet/src/utils/iframe.ts @@ -1,3 +1,4 @@ +import { assertNoSecretsInChannelPayload } from './iframeChannel'; import type { IframeMessage, AuthResponseMessage, @@ -8,12 +9,14 @@ import type { /** * Sends a message to the parent window if running in an iframe. * Uses postMessage API for cross-origin communication. + * Ensures no private keys or secrets leave the iframe (key isolation). */ export function sendMessageToParent(message: IframeMessage): void { if (window.parent === window) { console.warn('Not in iframe context, message not sent'); return; } + assertNoSecretsInChannelPayload(message.payload); window.parent.postMessage(message, '*'); } diff --git a/userwallet/src/utils/iframeChannel.ts b/userwallet/src/utils/iframeChannel.ts index 7ee136e..f2d9d82 100644 --- a/userwallet/src/utils/iframeChannel.ts +++ b/userwallet/src/utils/iframeChannel.ts @@ -5,10 +5,26 @@ import type { LoginProof } from '../types/identity'; * Messages pour la communication iframe avec Channel Messages. */ export interface ChannelMessage { - type: 'auth-request' | 'auth-response' | 'login-proof' | 'service-status' | 'error' | 'contract'; + type: + | 'auth-request' + | 'auth-response' + | 'login-proof' + | 'service-status' + | 'error' + | 'contract' + | 'pairing-relay-status' + | 'navigate-login'; payload?: unknown; } +export interface PairingRelayStatusMessage extends ChannelMessage { + type: 'pairing-relay-status'; + payload: { + pairingSatisfied: boolean; + relayOk: boolean; + }; +} + export interface AuthRequestMessage extends ChannelMessage { type: 'auth-request'; payload: { @@ -54,14 +70,49 @@ export interface ContractMessage extends ChannelMessage { }; } +const FORBIDDEN_PAYLOAD_KEYS = ['privateKey', 'cle_privee'] as const; + +/** + * Recursively check that no secret keys (privateKey, cle_privee) are present + * in a postMessage-to-parent payload. Throws if any forbidden key is found. + * Use for any postMessage to parent (iframeChannel, iframe.ts). Key isolation. + */ +export function assertNoSecretsInChannelPayload( + value: unknown, + path = 'payload', +): void { + if (value === null || value === undefined) { + return; + } + if (typeof value === 'object' && !Array.isArray(value)) { + const obj = value as Record; + for (const k of Object.keys(obj)) { + if (FORBIDDEN_PAYLOAD_KEYS.includes(k as (typeof FORBIDDEN_PAYLOAD_KEYS)[number])) { + throw new Error( + `[iframeChannel] Forbidden key "${k}" in channel ${path}: no secrets may leave iframe`, + ); + } + assertNoSecretsInChannelPayload(obj[k], `${path}.${k}`); + } + return; + } + if (Array.isArray(value)) { + value.forEach((item, i) => + assertNoSecretsInChannelPayload(item, `${path}[${i}]`), + ); + } +} + /** * Send message to parent window (Channel Messages). + * Ensures no private keys or secrets leave the iframe (key isolation). */ export function sendToChannel(message: ChannelMessage): void { if (window.parent === window) { console.warn('Not in iframe context, message not sent'); return; } + assertNoSecretsInChannelPayload(message.payload); window.parent.postMessage(message, '*'); } diff --git a/userwallet/src/utils/relay.ts b/userwallet/src/utils/relay.ts index f21e8e0..b21b535 100644 --- a/userwallet/src/utils/relay.ts +++ b/userwallet/src/utils/relay.ts @@ -4,19 +4,63 @@ import type { MsgChiffre, MsgSignature, MsgCle } from '../types/message'; /** Timeout for relay fetch (GET/POST). X_RELAY_TIMEOUT when exceeded. */ export const RELAY_FETCH_TIMEOUT_MS = 15000; +/** Maximum number of relays that can be configured. */ +export const MAX_RELAYS = 8; + +/** + * Default relay endpoint. + */ +const DEFAULT_RELAY_ENDPOINT = 'https://relay.certificator.4nkweb.com'; + /** * Get stored relay configurations. + * Initializes with default relay if none are configured. */ export function getStoredRelays(): RelayConfig[] { try { const stored = localStorage.getItem('userwallet_relays'); if (stored === null) { - return []; + // Initialize with default relay + const defaultRelays: RelayConfig[] = [ + { + endpoint: DEFAULT_RELAY_ENDPOINT, + priority: 0, + enabled: true, + }, + ]; + storeRelays(defaultRelays); + return defaultRelays; } - return JSON.parse(stored) as RelayConfig[]; + const raw = JSON.parse(stored) as RelayConfig[]; + // If empty, initialize with default relay + if (raw.length === 0) { + const defaultRelays: RelayConfig[] = [ + { + endpoint: DEFAULT_RELAY_ENDPOINT, + priority: 0, + enabled: true, + }, + ]; + storeRelays(defaultRelays); + return defaultRelays; + } + const relays = raw.slice(0, MAX_RELAYS); + if (raw.length > MAX_RELAYS) { + storeRelays(relays); + } + return relays; } catch (error) { console.error('Error reading stored relays:', error); - return []; + // Initialize with default relay on error + const defaultRelays: RelayConfig[] = [ + { + endpoint: DEFAULT_RELAY_ENDPOINT, + priority: 0, + enabled: true, + }, + ]; + storeRelays(defaultRelays); + return defaultRelays; } } diff --git a/website-data/.gitignore b/website-data/.gitignore new file mode 100644 index 0000000..3db3f53 --- /dev/null +++ b/website-data/.gitignore @@ -0,0 +1,5 @@ +node_modules +dist +*.local +.env.private +.env.backup diff --git a/website-data/README.md b/website-data/README.md new file mode 100644 index 0000000..cad51b7 --- /dev/null +++ b/website-data/README.md @@ -0,0 +1,84 @@ +# website-data + +Iframe dĂ©diĂ©e Ă  la **data** (donnĂ©es non clĂ©s) Ă  `data.certificator.4nkweb.com`. Elle communique avec le **site** (parent) et avec **UserWallet** (clĂ©s, relais). + +## Architecture + +``` +site <-> data (+notifications) <-> userwallet (sur les relais) +``` + +- **Site** : intĂšgre l’iframe Data, envoie `contract`, `auth-request`, `data-request`, etc. +- **Data** : iframe dĂ©diĂ©e aux donnĂ©es (hors clĂ©s). Relaye contrat / auth vers UserWallet ; reçoit `login-proof`, `auth-response`, etc. et les transmet au site. Émet des `notification` (ex. `userwallet:login-proof`). +- **UserWallet** : embarquĂ©e par Data, gĂšre clĂ©s et relais (sync, signatures). Tourne sur les relais (api-relay). + +Data n’hĂ©berge **aucune clĂ©** ; toute la crypto reste dans UserWallet. + +## PrĂ©requis + +- **UserWallet** : servi sur l’URL configurĂ©e (voir ci‑dessous). + +## Installation + +```bash +cd website-data +npm install +npm run build +``` + +## DĂ©veloppement + +```bash +npm run dev +``` + +Ouvre par dĂ©faut sur `http://localhost:3025`. En standalone, la page indique qu’il faut l’embedder dans un parent pour le flux site <-> data <-> userwallet. + +## Configuration + +- **UserWallet** : `src/config.ts`, `USERWALLET_ORIGIN`. En dev : `http://localhost:3018`. En prod : `https://userwallet.certificator.4nkweb.com`. Override : `VITE_USERWALLET_ORIGIN`. +- **Data** : `VITE_DATA_ORIGIN` pour la cible postMessage depuis le site (dev `http://localhost:3025`, prod `https://data.certificator.4nkweb.com`). + +## Messages postMessage + +### Site → Data + +| Type | RĂŽle | +|------|------| +| `contract` | Contrat + contrats_fils + actions → transmis Ă  UserWallet | +| `auth-request` | Demande d’auth → transmise Ă  UserWallet | +| `data-request` | RequĂȘte data (rĂ©ponse `data-response`) | +| `ping` | RĂ©ponse `pong` | + +### Data → Site + +| Type | RĂŽle | +|------|------| +| `data-response` | RĂ©ponse Ă  `data-request` | +| `notification` | ÉvĂ©nements (ex. `userwallet:login-proof`, `userwallet:auth-response`, `userwallet:error`) | +| `pong` | RĂ©ponse Ă  `ping` | +| `error` | Erreur | +| `login-proof`, `auth-response`, `service-status` | RelayĂ©s depuis UserWallet | + +### Data ↔ UserWallet + +Data embarque UserWallet en iframe. Elle envoie `contract`, `auth-request` et reçoit `login-proof`, `auth-response`, `error`, `service-status`, qu’elle relaye au site. + +## Notifications + +Data envoie des `notification` au site avec `payload: { kind, detail }`, notamment : + +- `userwallet:login-proof`, `userwallet:auth-response`, `userwallet:error` lors des Ă©vĂ©nements UserWallet. +- Un flux **relay** (non implĂ©mentĂ©) pourra alimenter des `notification` Ă  partir des relais (data non clĂ©s). + +## Production + +- **Domaine** : `data.certificator.4nkweb.com` +- **Port** : 3025 (configurable dans `vite.config.ts`). +- **DĂ©ploiement** : build `dist/`, servir via Nginx / systemd comme pour `website-skeleton`. Voir `docs/` et infra 4NK pour proxy et certificats. + +## Fichiers principaux + +- `src/config.ts` : origines Data et UserWallet. +- `src/channel.ts` : types et helpers postMessage (site <-> data, data <-> userwallet). +- `src/main.ts` : init, relay site/userwallet, notifications. diff --git a/website-data/index.html b/website-data/index.html new file mode 100644 index 0000000..445fd74 --- /dev/null +++ b/website-data/index.html @@ -0,0 +1,27 @@ + + + + + + Data – data.certificator.4nkweb.com + + + +

Data iframe

+

Chargement


+
+ + + diff --git a/website-data/install-website-data.sh b/website-data/install-website-data.sh new file mode 100755 index 0000000..fd9043a --- /dev/null +++ b/website-data/install-website-data.sh @@ -0,0 +1,29 @@ +#!/bin/bash +# Install and run website-data on this machine (backend port 3025). +# Usage: ./install-website-data.sh +# Then run update-proxy-nginx.sh from repo root for proxy + certificate (data.certificator.4nkweb.com). + +set -e + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +cd "$SCRIPT_DIR" + +echo "=== Installation website-data ===" + +if [ ! -d node_modules ]; then + echo "Installation des dĂ©pendances..." + npm install +fi + +echo "Build..." +npm run build + +echo "Installation du service systemd..." +sudo sed "s|/srv/4NK/data.certificator.4nkweb.com|${SCRIPT_DIR}|g" website-data.service > /tmp/website-data.service +sudo cp /tmp/website-data.service /etc/systemd/system/ +sudo systemctl daemon-reload +sudo systemctl enable website-data +sudo systemctl start website-data + +echo "=== website-data installĂ© et dĂ©marrĂ© (port 3025) ===" +echo "Pour proxy + certificat: ./update-proxy-nginx.sh (depuis la racine du dĂ©pĂŽt)" diff --git a/website-data/package-lock.json b/website-data/package-lock.json new file mode 100644 index 0000000..2517f15 --- /dev/null +++ b/website-data/package-lock.json @@ -0,0 +1,1002 @@ +{ + "name": "website-data", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "website-data", + "version": "1.0.0", + "devDependencies": { + "typescript": "^5.2.2", + "vite": "^5.0.8" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.0.tgz", + "integrity": "sha512-tPgXB6cDTndIe1ah7u6amCI1T0SsnlOuKgg10Xh3uizJk4e5M1JGaUMk7J4ciuAUcFpbOiNhm2XIjP9ON0dUqA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.0.tgz", + "integrity": "sha512-sa4LyseLLXr1onr97StkU1Nb7fWcg6niokTwEVNOO7awaKaoRObQ54+V/hrF/BP1noMEaaAW6Fg2d/CfLiq3Mg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.0.tgz", + "integrity": "sha512-/NNIj9A7yLjKdmkx5dC2XQ9DmjIECpGpwHoGmA5E1AhU0fuICSqSWScPhN1yLCkEdkCwJIDu2xIeLPs60MNIVg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.0.tgz", + "integrity": "sha512-xoh8abqgPrPYPr7pTYipqnUi1V3em56JzE/HgDgitTqZBZ3yKCWI+7KUkceM6tNweyUKYru1UMi7FC060RyKwA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.0.tgz", + "integrity": "sha512-PCkMh7fNahWSbA0OTUQ2OpYHpjZZr0hPr8lId8twD7a7SeWrvT3xJVyza+dQwXSSq4yEQTMoXgNOfMCsn8584g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.0.tgz", + "integrity": "sha512-1j3stGx+qbhXql4OCDZhnK7b01s6rBKNybfsX+TNrEe9JNq4DLi1yGiR1xW+nL+FNVvI4D02PUnl6gJ/2y6WJA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.0.tgz", + "integrity": "sha512-eyrr5W08Ms9uM0mLcKfM/Uzx7hjhz2bcjv8P2uynfj0yU8GGPdz8iYrBPhiLOZqahoAMB8ZiolRZPbbU2MAi6Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.0.tgz", + "integrity": "sha512-Xds90ITXJCNyX9pDhqf85MKWUI4lqjiPAipJ8OLp8xqI2Ehk+TCVhF9rvOoN8xTbcafow3QOThkNnrM33uCFQA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.0.tgz", + "integrity": "sha512-Xws2KA4CLvZmXjy46SQaXSejuKPhwVdaNinldoYfqruZBaJHqVo6hnRa8SDo9z7PBW5x84SH64+izmldCgbezw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.0.tgz", + "integrity": "sha512-hrKXKbX5FdaRJj7lTMusmvKbhMJSGWJ+w++4KmjiDhpTgNlhYobMvKfDoIWecy4O60K6yA4SnztGuNTQF+Lplw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.0.tgz", + "integrity": "sha512-6A+nccfSDGKsPm00d3xKcrsBcbqzCTAukjwWK6rbuAnB2bHaL3r9720HBVZ/no7+FhZLz/U3GwwZZEh6tOSI8Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.0.tgz", + "integrity": "sha512-4P1VyYUe6XAJtQH1Hh99THxr0GKMMwIXsRNOceLrJnaHTDgk1FTcTimDgneRJPvB3LqDQxUmroBclQ1S0cIJwQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.0.tgz", + "integrity": "sha512-8Vv6pLuIZCMcgXre6c3nOPhE0gjz1+nZP6T+hwWjr7sVH8k0jRkH+XnfjjOTglyMBdSKBPPz54/y1gToSKwrSQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.0.tgz", + "integrity": "sha512-r1te1M0Sm2TBVD/RxBPC6RZVwNqUTwJTA7w+C/IW5v9Ssu6xmxWEi+iJQlpBhtUiT1raJ5b48pI8tBvEjEFnFA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.0.tgz", + "integrity": "sha512-say0uMU/RaPm3CDQLxUUTF2oNWL8ysvHkAjcCzV2znxBr23kFfaxocS9qJm+NdkRhF8wtdEEAJuYcLPhSPbjuQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.0.tgz", + "integrity": "sha512-/MU7/HizQGsnBREtRpcSbSV1zfkoxSTR7wLsRmBPQ8FwUj5sykrP1MyJTvsxP5KBq9SyE6kH8UQQQwa0ASeoQQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.0.tgz", + "integrity": "sha512-Q9eh+gUGILIHEaJf66aF6a414jQbDnn29zeu0eX3dHMuysnhTvsUvZTCAyZ6tJhUjnvzBKE4FtuaYxutxRZpOg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.0.tgz", + "integrity": "sha512-OR5p5yG5OKSxHReWmwvM0P+VTPMwoBS45PXTMYaskKQqybkS3Kmugq1W+YbNWArF8/s7jQScgzXUhArzEQ7x0A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.0.tgz", + "integrity": "sha512-XeatKzo4lHDsVEbm1XDHZlhYZZSQYym6dg2X/Ko0kSFgio+KXLsxwJQprnR48GvdIKDOpqWqssC3iBCjoMcMpw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.0.tgz", + "integrity": "sha512-Lu71y78F5qOfYmubYLHPcJm74GZLU6UJ4THkf/a1K7Tz2ycwC2VUbsqbJAXaR6Bx70SRdlVrt2+n5l7F0agTUw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.0.tgz", + "integrity": "sha512-v5xwKDWcu7qhAEcsUubiav7r+48Uk/ENWdr82MBZZRIm7zThSxCIVDfb3ZeRRq9yqk+oIzMdDo6fCcA5DHfMyA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.0.tgz", + "integrity": "sha512-XnaaaSMGSI6Wk8F4KK3QP7GfuuhjGchElsVerCplUuxRIzdvZ7hRBpLR0omCmw+kI2RFJB80nenhOoGXlJ5TfQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.0.tgz", + "integrity": "sha512-3K1lP+3BXY4t4VihLw5MEg6IZD3ojSYzqzBG571W3kNQe4G4CcFpSUQVgurYgib5d+YaCjeFow8QivWp8vuSvA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.0.tgz", + "integrity": "sha512-MDk610P/vJGc5L5ImE4k5s+GZT3en0KoK1MKPXCRgzmksAMk79j4h3k1IerxTNqwDLxsGxStEZVBqG0gIqZqoA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.0.tgz", + "integrity": "sha512-Zv7v6q6aV+VslnpwzqKAmrk5JdVkLUzok2208ZXGipjb+msxBr/fJPZyeEXiFgH7k62Ak0SLIfxQRZQvTuf7rQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.0.tgz", + "integrity": "sha512-e5lPJi/aui4TO1LpAXIRLySmwXSE8k3b9zoGfd42p67wzxog4WHjiZF3M2uheQih4DGyc25QEV4yRBbpueNiUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.57.0", + "@rollup/rollup-android-arm64": "4.57.0", + "@rollup/rollup-darwin-arm64": "4.57.0", + "@rollup/rollup-darwin-x64": "4.57.0", + "@rollup/rollup-freebsd-arm64": "4.57.0", + "@rollup/rollup-freebsd-x64": "4.57.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.0", + "@rollup/rollup-linux-arm-musleabihf": "4.57.0", + "@rollup/rollup-linux-arm64-gnu": "4.57.0", + "@rollup/rollup-linux-arm64-musl": "4.57.0", + "@rollup/rollup-linux-loong64-gnu": "4.57.0", + "@rollup/rollup-linux-loong64-musl": "4.57.0", + "@rollup/rollup-linux-ppc64-gnu": "4.57.0", + "@rollup/rollup-linux-ppc64-musl": "4.57.0", + "@rollup/rollup-linux-riscv64-gnu": "4.57.0", + "@rollup/rollup-linux-riscv64-musl": "4.57.0", + "@rollup/rollup-linux-s390x-gnu": "4.57.0", + "@rollup/rollup-linux-x64-gnu": "4.57.0", + "@rollup/rollup-linux-x64-musl": "4.57.0", + "@rollup/rollup-openbsd-x64": "4.57.0", + "@rollup/rollup-openharmony-arm64": "4.57.0", + "@rollup/rollup-win32-arm64-msvc": "4.57.0", + "@rollup/rollup-win32-ia32-msvc": "4.57.0", + "@rollup/rollup-win32-x64-gnu": "4.57.0", + "@rollup/rollup-win32-x64-msvc": "4.57.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + } + } +} diff --git a/website-data/package.json b/website-data/package.json new file mode 100644 index 0000000..687797b --- /dev/null +++ b/website-data/package.json @@ -0,0 +1,16 @@ +{ + "name": "website-data", + "version": "1.0.0", + "description": "Data iframe (data.certificator.4nkweb.com). Non-key data; communicates with site and UserWallet (relays).", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "type-check": "tsc --noEmit" + }, + "devDependencies": { + "typescript": "^5.2.2", + "vite": "^5.0.8" + } +} diff --git a/website-data/src/channel.ts b/website-data/src/channel.ts new file mode 100644 index 0000000..f676541 --- /dev/null +++ b/website-data/src/channel.ts @@ -0,0 +1,99 @@ +/** + * postMessage channel: Site <-> Data (+notifications), Data <-> UserWallet (relays). + * Data does not hold keys; all crypto stays in UserWallet. + */ + +/** Messages from Site (parent) to Data iframe. */ +export type SiteToDataMessage = + | { type: 'data-request'; payload?: { id?: string; query?: unknown } } + | { type: 'contract'; payload?: { contrat?: unknown; contrats_fils?: unknown[]; actions?: unknown[] } } + | { type: 'auth-request'; payload?: { serviceId: string } } + | { type: 'ping'; payload?: unknown }; + +/** Messages from Data iframe to Site (parent). */ +export type DataToSiteMessage = + | { type: 'data-response'; payload: { id?: string; data?: unknown; error?: string } } + | { type: 'notification'; payload: { kind: string; detail?: unknown } } + | { type: 'pong'; payload?: unknown } + | { type: 'error'; payload: { message: string; code?: string } } + | { type: 'login-proof'; payload: unknown } + | { type: 'auth-response'; payload: unknown } + | { type: 'service-status'; payload: unknown }; + +/** Messages Data -> UserWallet (when Data embeds UserWallet). Forwards contract, auth-request, etc. */ +export type DataToUserWalletMessage = + | { type: 'contract'; payload: { contrat?: unknown; contrats_fils?: unknown[]; actions?: unknown[] } } + | { type: 'auth-request'; payload: { serviceId: string } }; + +/** Messages UserWallet -> Data. login-proof, auth-response, error. */ +export type UserWalletToDataMessage = + | { type: 'login-proof'; payload: unknown } + | { type: 'auth-response'; payload: unknown } + | { type: 'error'; payload: { message: string; code?: string } } + | { type: 'service-status'; payload: unknown }; + +export function isInIframe(): boolean { + return typeof window !== 'undefined' && window.parent !== window; +} + +/** + * Send message to parent (Site). Use when Data runs as iframe. + */ +export function sendToParent(message: T, targetOrigin = '*'): void { + if (typeof window === 'undefined' || window.parent === window) { + return; + } + window.parent.postMessage(message, targetOrigin); +} + +/** + * Listen for messages from parent (Site). Only invokes handler when event.source === window.parent. + */ +export function listenToParent( + handler: (event: MessageEvent) => void, +): () => void { + const wrapped = (event: MessageEvent): void => { + if (event.source !== window.parent || event.data?.type == null) { + return; + } + handler(event); + }; + window.addEventListener('message', wrapped); + return () => window.removeEventListener('message', wrapped); +} + +/** + * Send message to UserWallet iframe (when Data embeds it). Target USERWALLET_ORIGIN. + */ +export function sendToUserWallet( + message: DataToUserWalletMessage, + iframe: HTMLIFrameElement | null, + targetOrigin: string, +): void { + if (iframe?.contentWindow == null) { + return; + } + iframe.contentWindow.postMessage(message, targetOrigin); +} + +/** + * Listen for messages from UserWallet iframe (Data is parent). + * Only invokes handler when event.source === userwalletIframe.contentWindow. + */ +export function listenToUserWallet( + handler: (event: MessageEvent) => void, + userwalletIframe: HTMLIFrameElement | null, +): () => void { + const wrapped = (event: MessageEvent): void => { + if (event.data?.type == null) { + return; + } + const fromUw = userwalletIframe?.contentWindow != null && event.source === userwalletIframe.contentWindow; + if (!fromUw) { + return; + } + handler(event); + }; + window.addEventListener('message', wrapped); + return () => window.removeEventListener('message', wrapped); +} diff --git a/website-data/src/config.ts b/website-data/src/config.ts new file mode 100644 index 0000000..61b54eb --- /dev/null +++ b/website-data/src/config.ts @@ -0,0 +1,20 @@ +/** + * Configuration for Data iframe (data.certificator.4nkweb.com). + * Override via env: VITE_USERWALLET_ORIGIN, VITE_DATA_ORIGIN. + */ + +const env = + typeof import.meta !== 'undefined' + ? (import.meta as { env?: { VITE_USERWALLET_ORIGIN?: string; VITE_DATA_ORIGIN?: string; DEV?: boolean } }) + .env + : undefined; + +/** UserWallet iframe origin (embedded by Data). Dev default localhost:3018. */ +export const USERWALLET_ORIGIN = + env?.VITE_USERWALLET_ORIGIN ?? + (env?.DEV ? 'http://localhost:3018' : 'https://userwallet.certificator.4nkweb.com'); + +/** Data iframe origin (for postMessage target from site). */ +export const DATA_ORIGIN = + env?.VITE_DATA_ORIGIN ?? + (env?.DEV ? 'http://localhost:3025' : 'https://data.certificator.4nkweb.com'); diff --git a/website-data/src/main.ts b/website-data/src/main.ts new file mode 100644 index 0000000..6d89d0b --- /dev/null +++ b/website-data/src/main.ts @@ -0,0 +1,132 @@ +import { USERWALLET_ORIGIN } from './config.js'; +import { + isInIframe, + sendToParent, + listenToParent, + sendToUserWallet, + listenToUserWallet, + type SiteToDataMessage, +} from './channel.js'; + +const statusEl = document.getElementById('status') as HTMLParagraphElement | null; +const userwalletContainer = document.getElementById('userwallet-container') as HTMLDivElement | null; +let userwalletIframe: HTMLIFrameElement | null = null; + +function setStatus(text: string, inIframe = false): void { + if (statusEl != null) { + statusEl.textContent = text; + statusEl.classList.toggle('in-iframe', inIframe); + } +} + +function ensureUserWalletIframe(): HTMLIFrameElement | null { + if (userwalletIframe != null) { + return userwalletIframe; + } + if (userwalletContainer == null) { + return null; + } + const iframe = document.createElement('iframe'); + iframe.id = 'userwallet'; + iframe.title = 'UserWallet'; + iframe.src = USERWALLET_ORIGIN; + iframe.setAttribute('style', 'width:100%;min-height:320px;border:1px solid #dee2e6;border-radius:8px;'); + userwalletContainer.appendChild(iframe); + userwalletIframe = iframe; + return iframe; +} + +function handleSiteMessage(msg: MessageEvent): void { + const d = msg.data as SiteToDataMessage; + if (d?.type == null) { + return; + } + + if (d.type === 'ping') { + sendToParent({ type: 'pong', payload: undefined }); + return; + } + + if (d.type === 'data-request') { + sendToParent({ + type: 'data-response', + payload: { id: (d.payload as { id?: string })?.id, data: null }, + }); + return; + } + + if (d.type === 'contract') { + const m = d as { type: 'contract'; payload?: { contrat?: unknown; contrats_fils?: unknown[]; actions?: unknown[] } }; + const payload = m.payload; + const iframe = ensureUserWalletIframe(); + if (iframe != null && payload != null) { + sendToUserWallet( + { type: 'contract', payload: { contrat: payload.contrat, contrats_fils: payload.contrats_fils ?? [], actions: payload.actions ?? [] } }, + iframe, + USERWALLET_ORIGIN, + ); + } + return; + } + + if (d.type === 'auth-request') { + const m = d as { type: 'auth-request'; payload?: { serviceId: string } }; + const payload = m.payload ?? { serviceId: '' }; + const iframe = ensureUserWalletIframe(); + if (iframe != null) { + sendToUserWallet({ type: 'auth-request', payload: { serviceId: payload.serviceId } }, iframe, USERWALLET_ORIGIN); + } + return; + } +} + +/** Emit notification to site (e.g. relay updates, userwallet events). */ +function notifySite(kind: string, detail?: unknown): void { + sendToParent({ type: 'notification', payload: { kind, detail } }); +} + +/* Placeholder: relay notifications. Data fetches from api-relay (non-key); UserWallet uses relays for keys/signatures. Wire relay subscription -> notifySite('relay:...', detail) when needed. */ + +function handleUserWalletMessage(msg: MessageEvent): void { + const d = msg.data as { type: string; payload?: unknown }; + if (d?.type == null) { + return; + } + if (d.type === 'login-proof') { + sendToParent({ type: 'login-proof', payload: d.payload }); + notifySite('userwallet:login-proof', d.payload); + return; + } + if (d.type === 'auth-response') { + sendToParent({ type: 'auth-response', payload: d.payload }); + notifySite('userwallet:auth-response', d.payload); + return; + } + if (d.type === 'service-status') { + sendToParent({ type: 'service-status', payload: d.payload }); + return; + } + if (d.type === 'error') { + const p = d.payload as { message?: string; code?: string } | undefined; + sendToParent({ type: 'error', payload: { message: p?.message ?? 'unknown', code: p?.code } }); + notifySite('userwallet:error', d.payload); + return; + } +} + +function init(): void { + const inIframe = isInIframe(); + setStatus( + inIframe + ? 'Data iframe actif. En attente de messages du site.' + : 'Data (standalone). Embed in parent for site <-> data <-> userwallet.', + inIframe, + ); + + listenToParent(handleSiteMessage); + ensureUserWalletIframe(); + const uw = userwalletIframe; + listenToUserWallet(handleUserWalletMessage, uw); +} + +init(); diff --git a/website-data/start.sh b/website-data/start.sh new file mode 100755 index 0000000..42c9a56 --- /dev/null +++ b/website-data/start.sh @@ -0,0 +1,20 @@ +#!/bin/bash +# Build and serve website-data on port 3025 (production). +# Usage: ./start.sh +# Proxy: data.certificator.4nkweb.com -> backend:3025. + +set -e + +cd "$(dirname "$0")" +export PORT=${PORT:-3025} + +if [ ! -d node_modules ]; then + echo "Installation des dĂ©pendances..." + npm install +fi + +echo "Build du frontend website-data..." +npm run build + +echo "DĂ©marrage du serveur sur le port $PORT..." +exec npx vite preview --port "$PORT" --host 0.0.0.0 diff --git a/website-data/tsconfig.json b/website-data/tsconfig.json new file mode 100644 index 0000000..b340def --- /dev/null +++ b/website-data/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "noEmit": true, + "skipLibCheck": true, + "isolatedModules": true, + "resolveJsonModule": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true + }, + "include": ["src"] +} diff --git a/website-data/vite.config.ts b/website-data/vite.config.ts new file mode 100644 index 0000000..735266f --- /dev/null +++ b/website-data/vite.config.ts @@ -0,0 +1,20 @@ +import { defineConfig } from 'vite'; + +export default defineConfig({ + root: '.', + build: { + outDir: 'dist', + rollupOptions: { + input: ['index.html'], + }, + }, + server: { + port: 3025, + strictPort: false, + }, + preview: { + port: 3025, + host: '0.0.0.0', + allowedHosts: ['data.certificator.4nkweb.com'], + }, +}); diff --git a/website-data/website-data.service b/website-data/website-data.service new file mode 100644 index 0000000..b17291e --- /dev/null +++ b/website-data/website-data.service @@ -0,0 +1,21 @@ +[Unit] +Description=4NK Website data (iframe data, non clĂ©s) +After=network.target + +[Service] +Type=simple +User=ncantu +WorkingDirectory=/srv/4NK/data.certificator.4nkweb.com +Environment=PORT=3025 +Environment=NODE_ENV=production +ExecStart=/bin/bash /srv/4NK/data.certificator.4nkweb.com/start.sh +Restart=always +RestartSec=10 +StandardOutput=journal +StandardError=journal + +NoNewPrivileges=true +PrivateTmp=true + +[Install] +WantedBy=multi-user.target diff --git a/website-skeleton/README.md b/website-skeleton/README.md index 4498f4c..6a96e63 100644 --- a/website-skeleton/README.md +++ b/website-skeleton/README.md @@ -69,6 +69,10 @@ Le skeleton utilise automatiquement le contrat de service skeleton au dĂ©marrage 7. **Description du contrat** : page `contrat.html` (lien depuis l'accueil) dĂ©crit le contrat de service skeleton. 8. **Description du membre** : page `membre.html` dĂ©crit le membre connectĂ© (l'utilisateur), pas le validateur du service. ClĂ©s gĂ©nĂ©rĂ©es dans l'iframe (UserWallet), stockĂ©es en IndexedDB. Distinction crĂ©ateur du service (wallet .env.private, jamais exposĂ©) vs utilisateur (iframe, IndexedDB). +### Cloisonnement iframe + +Le skeleton (parent) **ne signe, ne chiffre ni ne dĂ©chiffre jamais**. Il utilise uniquement `verifyLoginProof` (vĂ©rification de signatures avec clĂ©s publiques). Aucune clĂ© privĂ©e n’est reçue ni utilisĂ©e par le parent. Toutes les opĂ©rations de signature et de chiffrement restent dans l’iframe UserWallet. Voir `userwallet/features/userwallet-iframe-key-isolation.md`. + ## Exemple d'intĂ©gration ### Envoi de contrat depuis le parent @@ -143,7 +147,7 @@ window.addEventListener('message', (event) => { nonceCache, // Cache anti-rejeu timestampWindowMs: 300000 // 5 minutes }); - + if (result.accept) { // Ouvrir la session utilisateur console.log('Login acceptĂ©'); @@ -171,6 +175,7 @@ Les raisons de refus possibles : - `src/main.ts` : chargement de l'iframe, Ă©coute `message`, envoi du contrat au clic « Se connecter », appel Ă  `verifyLoginProof`, gestion des messages `contract`. - `contrat.html` : page de description du contrat de service skeleton (labels, UUIDs, usage). - `membre.html` : page de description du membre connectĂ© (utilisateur) et de son device (Pair) ; clĂ©s iframe / IndexedDB. Distinction service (.env.private) vs utilisateur. +- `cryptographie.html` : page explicative de la cryptographie utilisĂ©e (ECDH secp256k1, AES-256-GCM, SHA-256, Schnorr, HKDF) avec workflow complet et vulgarisation. - `src/config.ts` : `USERWALLET_ORIGIN`, `DEFAULT_VALIDATEURS` (extraits du contrat skeleton), types `Contrat` et `Action`. - `src/serviceContract.ts` : contrat de service skeleton avec UUID dĂ©diĂ©, action login, et configuration de la clĂ© publique via `VITE_SKELETON_SERVICE_PUBLIC_KEY`. - `src/contract.ts` : extraction des validateurs depuis les contrats (`extractLoginValidators`), validation de structure (`isValidContract`, `isValidAction`). diff --git a/website-skeleton/contrat.html b/website-skeleton/contrat.html index 5d88ece..667d048 100644 --- a/website-skeleton/contrat.html +++ b/website-skeleton/contrat.html @@ -34,6 +34,11 @@ .member-list > li > em { color: #666; font-size: 0.9rem; } .pair-list { list-style: none; margin-top: 0.5rem; padding-left: 1rem; border-left: 3px solid #007bff; } .pair-list > li { background: #fff; padding: 0.5rem 0.75rem; border-radius: 6px; margin: 0.4rem 0; border: 1px solid #e8e8e8; } + .roles-intro { margin-bottom: 1rem; } + .role-block { background: #f8f9fa; border: 1px solid #e0e0e0; border-radius: 8px; padding: 0.75rem 1rem; margin: 0.5rem 0; } + .role-block h4 { margin-top: 0; } + .role-block .usage, .role-block .validation { font-size: 0.9rem; color: #555; margin: 0.25rem 0; } + .role-block .usage strong, .role-block .validation strong { color: #333; } @media (max-width: 768px) { body { padding: 0.75rem; } h1 { font-size: 1.25rem; } @@ -42,7 +47,7 @@

Le contrat

-

← Retour Ă  l'accueil · Qui ĂȘtes-vous ?

+

← Retour Ă  l'accueil · Qui ĂȘtes-vous ? · RĂ©seau P2P · Cryptographie

En résumé : Ce contrat définit les rÚgles de confiance entre vous et le service. @@ -113,6 +118,21 @@
  • VĂ©rifiable par tous — les rĂšgles du contrat sont publiques.
  • +

    Infrastructure, gouvernance et preuves

    +

    + L'infrastructure (relais, stockage des messages et des signatures) est décentralisée et neutre : + elle ne dépend pas d'un acteur unique. +

    +

    + Les membres ont une liberté de gouvernance. Certains peuvent choisir une organisation + centralisée lorsque c'est préférable (équipe, processus, conformité). D'autres restent décentralisés. +

    +

    + L'important est d'avoir les preuves de la bonne exécution du contrat entre les parties : + signatures, nonces, rÚgles publiques. Ces preuves restent vérifiables quelle que soit l'organisation + (décentralisée ou centralisée) des membres. +

    +
    Détails techniques du contrat

    Identifiants

    @@ -122,10 +142,73 @@
  • Action login UUID : 0ac7de59-9e81-4bdc-bd19-c07750fad48e
  • Validateur (membre) : 0e865301-362f-4951-bfbc-531b7bddf820
  • -

    Membres par rĂŽles

    -

    Chaque rÎle définit qui peut valider quoi. Un membre peut avoir plusieurs Pairs (appareils), chaque Pair possÚde une clé publique unique :

    +

    RÎles par défaut (types de Champ)

    +

    + Les 11 rĂŽles ci‑dessous existent par dĂ©faut. Chacun peut ĂȘtre vide (aucun membre) + ou plein (membres et pairs dĂ©finis). Tous sont connus des participants ; + les conditions d'usage et de validation s'appliquent dĂšs qu'un rĂŽle est rempli. +

    + +
    +

    1. Partage avec les institutions

    +

    Usage : Partager des données avec des institutions.

    +

    Validation : Conformité aux validateurs du champ ; signatures requises selon membres_du_role.

    +
    +
    +

    2. Messages au RSSI

    +

    Usage : Messages au RSSI de la société responsable du service.

    +

    Validation : Signatures des membres du rĂŽle conformes aux signatures_obligatoires.

    +
    +
    +

    3. Messages au Correspondant CNIL

    +

    Usage : Messages au Correspondant CNIL de la société responsable du service.

    +

    Validation : Signatures des membres du rĂŽle ; attributs CNIL requis si applicables.

    +
    +
    +

    4. Messages au Responsable cybersécurité

    +

    Usage : Messages au Responsable cybersécurité de la société responsable du service.

    +

    Validation : Signatures conformes aux validateurs du champ.

    +
    +
    +

    5. Messages de support infogérant

    +

    Usage : Support infogérant du service (peut inclure un membre du miner pour la gestion des clés API).

    +

    Validation : Signatures des membres du rĂŽle ; membre_miner_uuid optionnel dans datajson si applicable.

    +
    +
    +

    6. Messages de support administrateur systĂšme

    +

    Usage : Support administrateur systĂšme du service.

    +

    Validation : Signatures conformes aux validateurs du champ.

    +
    +
    +

    7. Messages de support niveau 1

    +

    Usage : Support niveau 1 du service.

    +

    Validation : Signatures conformes aux validateurs du champ.

    +
    +
    +

    8. Messages de support niveau 2

    +

    Usage : Support niveau 2 du service.

    +

    Validation : Signatures conformes aux validateurs du champ.

    +
    +
    +

    9. Messages de support niveau 3

    +

    Usage : Support niveau 3 du service.

    +

    Validation : Signatures conformes aux validateurs du champ.

    +
    +
    +

    10. Validation (contrat)

    +

    Usage : Valider le contrat ; définir qui peut signer pour le contrat (contrat.validateurs.membres_du_role).

    +

    Validation : Au moins une signature valide par membre du rÎle ; clés publiques autorisées dans signatures_obligatoires.

    +
    +
    +

    11. Validation du login

    +

    Usage : Valider le login ; définir qui peut signer pour l'action login (action.validateurs_action.membres_du_role).

    +

    Validation : Preuve de login (hash, nonce) signée par les pairs des membres du rÎle ; nonce unique ; clés autorisées.

    +
    + +

    Membres par rĂŽles (ce contrat)

    +

    Ce contrat skeleton remplit uniquement les rĂŽles Validation (contrat) et Validation du login. Les autres rĂŽles sont vides mais connus.

    -

    Validateur (contrat)

    +

    Validation (contrat)

    • Membre : 0e865301-362f-4951-bfbc-531b7bddf820 @@ -139,7 +222,7 @@
    -

    Validateur login (action)

    +

    Validation du login

    • Membre : 0e865301-362f-4951-bfbc-531b7bddf820 @@ -154,6 +237,6 @@
    -

    ← Retour Ă  l'accueil · Qui ĂȘtes-vous ?

    +

    ← Retour Ă  l'accueil · Qui ĂȘtes-vous ? · RĂ©seau P2P · Cryptographie

    diff --git a/website-skeleton/cryptographie.html b/website-skeleton/cryptographie.html new file mode 100644 index 0000000..e6a90bf --- /dev/null +++ b/website-skeleton/cryptographie.html @@ -0,0 +1,448 @@ + + + + + + Cryptographie expliquĂ©e – 4NK un nouveau web + + + +

    Cryptographie expliquée

    +

    ← Retour Ă  l'accueil · Le contrat · Qui ĂȘtes-vous ? · RĂ©seau P2P

    + +
    + En résumé : Vos messages sont protégés par plusieurs couches de cryptographie. + Seul le destinataire peut les lire (chiffrement), et il peut vérifier que c'est bien vous qui les avez envoyés (signature). + Tout cela sans que personne ne connaisse vos clés secrÚtes. +
    + +

    Comment ça marche en simple ?

    +

    + Imaginez que vous voulez envoyer une lettre secrÚte à un ami. Dans le monde réel, vous pourriez : +

    +
      +
    1. Mettre la lettre dans une enveloppe fermĂ©e (chiffrement) — seul celui qui a la clĂ© peut l'ouvrir
    2. +
    3. Signer l'enveloppe (signature) — pour prouver que c'est bien vous qui l'avez envoyĂ©e
    4. +
    5. Donner la clĂ© au destinataire (Ă©change de clĂ©) — mais sans que personne d'autre ne puisse l'intercepter
    6. +
    +

    + C'est exactement ce que fait la cryptographie numérique, mais de façon mathématiquement impossible à falsifier. +

    + +

    Les algorithmes utilisés

    +

    + Voici les « outils » cryptographiques du systÚme. Chacun a un rÎle précis : +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    AlgorithmeRĂŽleAnalogie
    SHA-256Empreinte unique du messageComme une empreinte digitale : unique et impossible Ă  inverser
    ECDH secp256k1Échange de clĂ© sĂ©curisĂ©Comme mĂ©langer deux couleurs en public pour crĂ©er un secret commun
    HKDF-SHA256Dérivation de cléTransformer le secret partagé en une clé utilisable
    AES-256-GCMChiffrement du messageUn coffre-fort numérique ultra-sécurisé
    Schnorr secp256k1Signature numériqueVotre signature manuscrite, mais impossible à falsifier
    + +

    Détail de chaque algorithme

    + +
    +
    SHA-256 — L'empreinte digitale
    +

    + SHA-256 crée une « empreinte » unique de 64 caractÚres hexadécimaux (256 bits) pour n'importe quel message. + Deux messages différents auront toujours des empreintes différentes. +

    +
      +
    • EntrĂ©e : n'importe quel texte ou donnĂ©es
    • +
    • Sortie : 64 caractĂšres hexadĂ©cimaux (ex: a7f3c9...)
    • +
    • PropriĂ©tĂ© clĂ© : impossible de retrouver le message Ă  partir de l'empreinte
    • +
    +

    Usage : identifier chaque message de façon unique sur le relais.

    +
    + +
    +
    ECDH secp256k1 — Le secret partagĂ©
    +

    + ECDH (Elliptic Curve Diffie-Hellman) permet à deux personnes de créer un secret commun + sans jamais l'échanger directement. C'est comme de la magie mathématique ! +

    +
    + L'analogie des couleurs : Imaginez qu'Alice et Bob veulent créer une couleur secrÚte. +
      +
    • Alice mĂ©lange une couleur publique avec sa couleur secrĂšte → obtient « couleur A »
    • +
    • Bob mĂ©lange la mĂȘme couleur publique avec sa couleur secrĂšte → obtient « couleur B »
    • +
    • Ils Ă©changent « couleur A » et « couleur B » (publiquement, tout le monde peut voir)
    • +
    • Alice mĂ©lange « couleur B » + sa couleur secrĂšte → obtient la couleur finale
    • +
    • Bob mĂ©lange « couleur A » + sa couleur secrĂšte → obtient la mĂȘme couleur finale
    • +
    + RĂ©sultat : Alice et Bob ont la mĂȘme couleur secrĂšte, sans l'avoir jamais Ă©changĂ©e ! + Un espion qui a vu « couleur A » et « couleur B » ne peut pas retrouver la couleur finale. +
    +
      +
    • Courbe : secp256k1 (la mĂȘme que Bitcoin)
    • +
    • EntrĂ©e : votre clĂ© privĂ©e + la clĂ© publique du destinataire
    • +
    • Sortie : un secret partagĂ© (32 octets)
    • +
    +

    Usage : créer un secret pour chiffrer les messages entre deux personnes.

    +
    + +
    +
    HKDF-SHA256 — Le raffineur de clĂ©
    +

    + HKDF (HMAC-based Key Derivation Function) transforme le secret partagé ECDH en une clé + de chiffrement de haute qualité. +

    +
      +
    • EntrĂ©e : le secret partagĂ© ECDH (32 octets)
    • +
    • Sortie : une clĂ© AES-256 (32 octets) de qualitĂ© cryptographique
    • +
    • Pourquoi : s'assurer que la clĂ© est uniformĂ©ment alĂ©atoire
    • +
    +
    + +
    +
    AES-256-GCM — Le coffre-fort
    +

    + AES-256-GCM est l'algorithme de chiffrement qui protÚge le contenu du message. + Il offre à la fois la confidentialité (personne ne peut lire) et + l'intégrité (personne ne peut modifier sans qu'on le sache). +

    +
      +
    • Taille de clĂ© : 256 bits (trĂšs sĂ©curisĂ©)
    • +
    • Mode : GCM (Galois/Counter Mode) avec authentification
    • +
    • IV : vecteur d'initialisation de 12 octets (diffĂ©rent Ă  chaque chiffrement)
    • +
    • Tag : code d'authentification de 16 octets (dĂ©tecte les modifications)
    • +
    +

    Usage : chiffrer le message pour que seul le destinataire puisse le lire.

    +
    + +
    +
    Schnorr secp256k1 — La signature
    +

    + La signature Schnorr prouve que vous ĂȘtes bien l'auteur du message, sans rĂ©vĂ©ler votre clĂ© privĂ©e. + Elle est plus efficace et plus simple que les signatures ECDSA traditionnelles. +

    +
      +
    • EntrĂ©e : le hash du message + votre clĂ© privĂ©e
    • +
    • Sortie : une signature de 64 octets (128 caractĂšres hex)
    • +
    • VĂ©rification : n'importe qui peut vĂ©rifier avec votre clĂ© publique
    • +
    +
    + Important : La signature prouve que vous avez signĂ©, mais elle ne rĂ©vĂšle + jamais votre clĂ© privĂ©e. MĂȘme en voyant 1000 de vos signatures, personne ne peut + retrouver votre clĂ© secrĂšte. +
    +
    + +

    Le workflow complet

    +

    + Voici ce qui se passe quand vous envoyez un message sécurisé, étape par étape : +

    + +

    Phase 1 : Préparation

    +
    + 1 + Création du message
    + Votre message est structuré avec : les données, un horodatage, un identifiant unique (UUID), et les rÚgles de validation. +
    +
    + 2 + Calcul de l'empreinte (hash)
    + Message → SHA-256 → Hash (64 caractùres)
    + Cette empreinte unique identifie le message sur le réseau. +
    +
    + 3 + Génération du nonce
    + Un nombre alĂ©atoire unique (nonce) est créé pour Ă©viter qu'un mĂȘme message soit rejouĂ©. +
    + +

    Phase 2 : Chiffrement

    +
    + 4 + Échange de clĂ© ECDH
    + Votre clĂ© privĂ©e + ClĂ© publique du destinataire → ECDH → Secret partagĂ©
    + Un secret commun est calculĂ© mathĂ©matiquement, sans jamais ĂȘtre transmis. +
    +
    + 5 + Dérivation de la clé AES
    + Secret partagĂ© → HKDF-SHA256 → ClĂ© AES-256
    + Le secret est transformé en une clé de chiffrement de qualité. +
    +
    + 6 + Chiffrement du message
    + ClĂ© AES + IV alĂ©atoire + Message → AES-256-GCM → Message chiffrĂ© + Tag
    + Le message est enfermé dans un « coffre-fort » numérique. +
    + +

    Phase 3 : Signature

    +
    + 7 + Signature Schnorr
    + Hash + Nonce + Votre clĂ© privĂ©e → Schnorr → Signature
    + Vous signez l'empreinte du message avec votre clé privée. +
    + +

    Phase 4 : Publication sur le relais

    +
    + 8 + Envoi au relais
    + Trois éléments sont envoyés séparément : +
      +
    • MsgChiffre : le message chiffrĂ© + son hash
    • +
    • MsgCle : l'IV + votre clĂ© publique (pour que le destinataire puisse dĂ©chiffrer)
    • +
    • MsgSignature : votre signature + clĂ© publique
    • +
    +
    + +

    Phase 5 : Collecte des signatures (multi-signature)

    +
    + 9 + Attente des co-signataires
    + Si le contrat exige plusieurs signatures (ex: 2 appareils sur 3), le systĂšme attend que les autres signent : +
      +
    • Interrogation du relais toutes les 2 secondes
    • +
    • Timeout aprĂšs 5 minutes si signatures manquantes
    • +
    • Progression affichĂ©e (ex: "2/3 signatures")
    • +
    +
    +
    + 10 + Validation
    + Le message est validé quand : +
      +
    • Toutes les signatures requises sont prĂ©sentes (cardinalitĂ©)
    • +
    • Les dĂ©pendances sont respectĂ©es (ex: "A doit signer avant B")
    • +
    • Les clĂ©s publiques correspondent aux signataires autorisĂ©s
    • +
    +
    + +

    Phase 6 : Réception et déchiffrement

    +
    + 11 + Scan des clés
    + Le destinataire interroge le relais pour récupérer les MsgCle récentes. +
    +
    + 12 + Récupération du message
    + Avec le hash trouvé dans la MsgCle, il récupÚre le message chiffré. +
    +
    + 13 + Déchiffrement ECDH inverse
    + Sa clĂ© privĂ©e + ClĂ© publique de l'Ă©metteur (df) → ECDH → MĂȘme secret partagĂ©
    + Il recalcule le mĂȘme secret, puis dĂ©chiffre avec AES-GCM. +
    +
    + 14 + Vérification des signatures
    + Chaque signature est vérifiée avec la clé publique du signataire. +
    + +

    Schéma récapitulatif

    +
    ┌─────────────────────────────────────────────────────────────────┐ +│ ÉMETTEUR │ +├────────────────────────────────────────────────────────────────── +│ Message → SHA-256 → Hash │ +│ │ +│ ClĂ© privĂ©e Ă©metteur ─┐ │ +│ ├──→ ECDH → Secret partagĂ© │ +│ ClĂ© publique dest. ──┘ │ +│ │ +│ Secret partagĂ© → HKDF-SHA256 → ClĂ© AES-256 │ +│ │ +│ ClĂ© AES + IV + Message → AES-256-GCM → Message chiffrĂ© │ +│ │ +│ Hash + Nonce + ClĂ© privĂ©e → Schnorr → Signature │ +└─────────────────────────────────────────────────────────────────┘ + │ + â–Œ +┌─────────────────────────────────────────────────────────────────┐ +│ RELAIS │ +├────────────────────────────────────────────────────────────────── +│ POST /messages ← MsgChiffre (hash, message_chiffre) │ +│ POST /keys ← MsgCle (hash, iv, df_ecdh = clĂ© pub Ă©mett.) │ +│ POST /signatures← MsgSignature (hash, signature, clĂ© publique) │ +│ │ +│ ... attente des co-signatures (GET /signatures/:hash) ... │ +└─────────────────────────────────────────────────────────────────┘ + │ + â–Œ +┌─────────────────────────────────────────────────────────────────┐ +│ DESTINATAIRE │ +├────────────────────────────────────────────────────────────────── +│ GET /keys?start=&end= → Liste des MsgCle rĂ©centes │ +│ GET /messages/:hash → MsgChiffre │ +│ GET /signatures/:hash → MsgSignature[] │ +│ │ +│ ClĂ© privĂ©e dest. ────┐ │ +│ ├──→ ECDH → MĂȘme secret partagĂ© │ +│ ClĂ© publique Ă©mett. ─┘ (df_ecdh_scannable) │ +│ │ +│ Secret partagĂ© → HKDF-SHA256 → ClĂ© AES-256 │ +│ │ +│ ClĂ© AES + IV + Ciphertext → AES-256-GCM → Message original │ +│ │ +│ VĂ©rification : Signature + ClĂ© publique → Schnorr verify → OK │ +└─────────────────────────────────────────────────────────────────┘
    + +

    Pourquoi c'est sécurisé ?

    + +

    Confidentialité

    +
      +
    • Seul le destinataire peut lire : le secret partagĂ© ECDH ne peut ĂȘtre calculĂ© que par l'Ă©metteur et le destinataire
    • +
    • AES-256 : considĂ©rĂ© incassable avec les technologies actuelles (2ÂČ⁔⁶ combinaisons possibles)
    • +
    • IV unique : mĂȘme si vous envoyez le mĂȘme message deux fois, le rĂ©sultat chiffrĂ© sera diffĂ©rent
    • +
    + +

    Intégrité

    +
      +
    • GCM : le mode Galois/Counter dĂ©tecte toute modification du message chiffrĂ©
    • +
    • Hash : l'empreinte SHA-256 garantit que le message n'a pas Ă©tĂ© altĂ©rĂ©
    • +
    + +

    Authenticité

    +
      +
    • Signature Schnorr : prouve mathĂ©matiquement que c'est bien vous qui avez signĂ©
    • +
    • Multi-signature : plusieurs appareils peuvent ĂȘtre requis pour valider
    • +
    • Anti-rejeu : le nonce empĂȘche de rĂ©utiliser une ancienne signature
    • +
    + +

    Séparation des données

    +
      +
    • Message, clĂ© et signature sĂ©parĂ©s : mĂȘme si quelqu'un intercepte un Ă©lĂ©ment, il ne peut rien faire sans les autres
    • +
    • ClĂ© privĂ©e jamais transmise : seules les clĂ©s publiques et les signatures circulent
    • +
    + +
    + Détails techniques pour les développeurs + +

    ParamĂštres cryptographiques

    + + + + + + + + + +
    ParamĂštreValeur
    Courbe elliptiquesecp256k1 (256 bits)
    Taille clé AES256 bits
    Taille IV (AES-GCM)96 bits (12 octets)
    Taille Tag (AES-GCM)128 bits (16 octets)
    HashSHA-256 (256 bits)
    KDFHKDF-SHA256
    SignatureSchnorr secp256k1 (64 octets)
    + +

    BibliothÚques utilisées

    +
      +
    • @noble/secp256k1 : ECDH, Schnorr, manipulation de clĂ©s
    • +
    • @noble/hashes : SHA-256, HKDF
    • +
    • Web Crypto API : AES-256-GCM (navigateur)
    • +
    + +

    Format des clés

    +
      +
    • ClĂ© privĂ©e : 32 octets (64 caractĂšres hex)
    • +
    • ClĂ© publique (compressĂ©e) : 33 octets (66 caractĂšres hex, prĂ©fixe 02 ou 03)
    • +
    • Signature Schnorr : 64 octets (128 caractĂšres hex)
    • +
    + +

    Fichiers source

    +
      +
    • userwallet/src/utils/encryption.ts : encryptWithECDH, decryptWithECDH
    • +
    • userwallet/src/utils/crypto.ts : signMessage, verifySignature, generateChallenge
    • +
    • userwallet/src/utils/relay.ts : postMessageChiffre, postSignature, postKey
    • +
    • userwallet/src/utils/collectSignatures.ts : runCollectLoop, fetchSignaturesForHash
    • +
    • userwallet/src/utils/loginValidation.ts : hasEnoughSignatures, checkDependenciesSatisfied
    • +
    • service-login-verify/ : verifyLoginProof (cĂŽtĂ© parent)
    • +
    + +

    Collecte des signatures

    + + + + + +
    ConstanteValeurDescription
    COLLECT_POLL_MS2 000 msIntervalle entre chaque interrogation
    COLLECT_TIMEOUT_MS300 000 msTimeout global (5 minutes)
    COLLECT_FETCH_TIMEOUT_MS15 000 msTimeout par requĂȘte
    +
    + +

    En résumé

    +
    + Le systĂšme combine plusieurs couches de protection : +
      +
    1. ECDH pour créer un secret sans jamais l'échanger
    2. +
    3. AES-256-GCM pour chiffrer le message
    4. +
    5. Schnorr pour prouver votre identité
    6. +
    7. SHA-256 pour identifier chaque message de façon unique
    8. +
    9. Multi-signature pour exiger plusieurs validations
    10. +
    + Résultat : vos messages sont confidentiels, authentiques et infalsifiables. +
    + +

    ← Retour Ă  l'accueil · Le contrat · Qui ĂȘtes-vous ? · RĂ©seau P2P

    + + diff --git a/website-skeleton/index.html b/website-skeleton/index.html index b067ab7..4c0a536 100644 --- a/website-skeleton/index.html +++ b/website-skeleton/index.html @@ -103,12 +103,51 @@ background: #fafafa; } #connected-section { - padding: 1.5rem; - background: #f9f9f9; - border: 1px solid #ddd; - border-radius: 6px; + padding: 0; margin: 1rem 0; } + .connected-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.75rem 0; + margin-bottom: 1rem; + border-bottom: 1px solid #e0e0e0; + } + .connected-avatar { + width: 48px; + height: 48px; + border-radius: 50%; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + display: flex; + align-items: center; + justify-content: center; + color: white; + font-size: 1.25rem; + flex-shrink: 0; + } + .connected-notifications { + width: 44px; + height: 44px; + min-width: 44px; + min-height: 44px; + border: 1px solid #ccc; + border-radius: 8px; + background: #fff; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.2s; + } + .connected-notifications:hover { + background: #f5f5f5; + border-color: #999; + } + .connected-notifications svg { + width: 24px; + height: 24px; + } #user-info { margin: 1rem 0; padding: 1rem; @@ -155,7 +194,8 @@

    4NK un nouveau web - site d'exemple

    -

    Le contrat · Qui ĂȘtes-vous ?

    +

    Le contrat · Qui ĂȘtes-vous ? · RĂ©seau P2P · Cryptographie

    +
    @@ -170,9 +210,18 @@