api-relay DB migration, auth, service-login-verify PersistentNonceCache, UserWallet crypto settings

**Motivations:**
- Migrer api-relay vers base de données SQLite (production)
- Ajouter authentification API key pour endpoints POST (protection abus)
- PersistentNonceCache pour service-login-verify (IndexedDB/localStorage)
- Écran paramètres crypto avancés UserWallet
- Documenter options non implémentées (Merkle, évolutions api-relay)

**Root causes:**
- N/A (évolutions + correctifs)

**Correctifs:**
- N/A

**Evolutions:**
- api-relay: DatabaseStorageService (SQLite), StorageAdapter (compatibilité), ApiKeyService (génération/validation), auth middleware (Bearer/X-API-Key), endpoints admin (/admin/api-keys), migration script (migrate-to-db.ts), suppression saveToDisk périodique
- service-login-verify: PersistentNonceCache (IndexedDB avec fallback localStorage, TTL, cleanup), export dans index
- userwallet: CryptoSettingsScreen (hashAlgorithm, jsonCanonizationStrict, ecdhCurve, nonceTtlMs, timestampWindowMs), modifications LoginScreen, LoginForm, CreateIdentityScreen, ImportIdentityScreen, DataExportImportScreen, PairingDisplayScreen, RelaySettingsScreen, ServiceListScreen, MemberSelectionScreen, GlobalActionBar
- features: OPTIONS_NON_IMPLENTEES.md (analyse Merkle trees, évolutions api-relay)

**Pages affectées:**
- api-relay: package.json, index.ts, middleware/auth.ts, services/database.ts, services/storageAdapter.ts, services/apiKeyService.ts, scripts/migrate-to-db.ts
- service-login-verify: persistentNonceCache.ts, index.ts, tsconfig.json, dist/
- userwallet: App, CryptoSettingsScreen, LoginScreen, LoginForm, CreateIdentityScreen, ImportIdentityScreen, DataExportImportScreen, PairingDisplayScreen, RelaySettingsScreen, ServiceListScreen, MemberSelectionScreen, GlobalActionBar
- features: OPTIONS_NON_IMPLENTEES.md
- data: sync-utxos.log
This commit is contained in:
ncantu 2026-01-28 07:36:01 +01:00
parent 13898d1012
commit 695aff4f85
36 changed files with 2303 additions and 317 deletions

View File

@ -8,6 +8,7 @@
"name": "userwallet-api-relay", "name": "userwallet-api-relay",
"version": "1.0.0", "version": "1.0.0",
"dependencies": { "dependencies": {
"better-sqlite3": "^11.10.0",
"bloom-filters": "^3.0.4", "bloom-filters": "^3.0.4",
"compression": "^1.8.1", "compression": "^1.8.1",
"cors": "^2.8.5", "cors": "^2.8.5",
@ -18,6 +19,7 @@
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.0.0", "@eslint/js": "^9.0.0",
"@types/better-sqlite3": "^7.6.13",
"@types/compression": "^1.8.1", "@types/compression": "^1.8.1",
"@types/cors": "^2.8.17", "@types/cors": "^2.8.17",
"@types/express": "^4.17.21", "@types/express": "^4.17.21",
@ -677,6 +679,16 @@
"integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/better-sqlite3": {
"version": "7.6.13",
"resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz",
"integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/body-parser": { "node_modules/@types/body-parser": {
"version": "1.19.6", "version": "1.19.6",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
@ -1168,12 +1180,63 @@
"node": ">= 0.6.0" "node": ">= 0.6.0"
} }
}, },
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/better-sqlite3": {
"version": "11.10.0",
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz",
"integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"bindings": "^1.5.0",
"prebuild-install": "^7.1.1"
}
},
"node_modules/bindings": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
"license": "MIT",
"dependencies": {
"file-uri-to-path": "1.0.0"
}
},
"node_modules/bintrees": { "node_modules/bintrees": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz", "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz",
"integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==", "integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/bl": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
"license": "MIT",
"dependencies": {
"buffer": "^5.5.0",
"inherits": "^2.0.4",
"readable-stream": "^3.4.0"
}
},
"node_modules/bloom-filters": { "node_modules/bloom-filters": {
"version": "3.0.4", "version": "3.0.4",
"resolved": "https://registry.npmjs.org/bloom-filters/-/bloom-filters-3.0.4.tgz", "resolved": "https://registry.npmjs.org/bloom-filters/-/bloom-filters-3.0.4.tgz",
@ -1255,6 +1318,30 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/buffer": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.1.13"
}
},
"node_modules/bytes": { "node_modules/bytes": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@ -1320,6 +1407,12 @@
"url": "https://github.com/chalk/chalk?sponsor=1" "url": "https://github.com/chalk/chalk?sponsor=1"
} }
}, },
"node_modules/chownr": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
"license": "ISC"
},
"node_modules/color-convert": { "node_modules/color-convert": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@ -1493,6 +1586,30 @@
} }
} }
}, },
"node_modules/decompress-response": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
"license": "MIT",
"dependencies": {
"mimic-response": "^3.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/deep-extend": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
"license": "MIT",
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/deep-is": { "node_modules/deep-is": {
"version": "0.1.4", "version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
@ -1519,6 +1636,15 @@
"npm": "1.2.8000 || >= 1.4.16" "npm": "1.2.8000 || >= 1.4.16"
} }
}, },
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"license": "Apache-2.0",
"engines": {
"node": ">=8"
}
},
"node_modules/dir-glob": { "node_modules/dir-glob": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
@ -1574,6 +1700,15 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/end-of-stream": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
"license": "MIT",
"dependencies": {
"once": "^1.4.0"
}
},
"node_modules/es-define-property": { "node_modules/es-define-property": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
@ -1891,6 +2026,15 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/expand-template": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
"integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
"license": "(MIT OR WTFPL)",
"engines": {
"node": ">=6"
}
},
"node_modules/express": { "node_modules/express": {
"version": "4.22.1", "version": "4.22.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
@ -2044,6 +2188,12 @@
"node": "^10.12.0 || >=12.0.0" "node": "^10.12.0 || >=12.0.0"
} }
}, },
"node_modules/file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
"license": "MIT"
},
"node_modules/fill-range": { "node_modules/fill-range": {
"version": "7.1.1", "version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
@ -2147,6 +2297,12 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/fs-constants": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
"license": "MIT"
},
"node_modules/fs.realpath": { "node_modules/fs.realpath": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@ -2228,6 +2384,12 @@
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
} }
}, },
"node_modules/github-from-package": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
"integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
"license": "MIT"
},
"node_modules/glob": { "node_modules/glob": {
"version": "7.2.3", "version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
@ -2409,6 +2571,26 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "BSD-3-Clause"
},
"node_modules/ignore": { "node_modules/ignore": {
"version": "5.3.2", "version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@ -2464,6 +2646,12 @@
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/ini": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
"license": "ISC"
},
"node_modules/ip-address": { "node_modules/ip-address": {
"version": "10.0.1", "version": "10.0.1",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz",
@ -2741,6 +2929,18 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/mimic-response": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/minimatch": { "node_modules/minimatch": {
"version": "9.0.3", "version": "9.0.3",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
@ -2757,12 +2957,33 @@
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
} }
}, },
"node_modules/minimist": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/mkdirp-classic": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
"license": "MIT"
},
"node_modules/ms": { "node_modules/ms": {
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/napi-build-utils": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
"integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
"license": "MIT"
},
"node_modules/natural-compare": { "node_modules/natural-compare": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
@ -2779,6 +3000,18 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/node-abi": {
"version": "3.87.0",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.87.0.tgz",
"integrity": "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==",
"license": "MIT",
"dependencies": {
"semver": "^7.3.5"
},
"engines": {
"node": ">=10"
}
},
"node_modules/object-assign": { "node_modules/object-assign": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@ -2834,7 +3067,6 @@
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dev": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"wrappy": "1" "wrappy": "1"
@ -3008,6 +3240,32 @@
"integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/prebuild-install": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
"integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
"license": "MIT",
"dependencies": {
"detect-libc": "^2.0.0",
"expand-template": "^2.0.3",
"github-from-package": "0.0.0",
"minimist": "^1.2.3",
"mkdirp-classic": "^0.5.3",
"napi-build-utils": "^2.0.0",
"node-abi": "^3.3.0",
"pump": "^3.0.0",
"rc": "^1.2.7",
"simple-get": "^4.0.0",
"tar-fs": "^2.0.0",
"tunnel-agent": "^0.6.0"
},
"bin": {
"prebuild-install": "bin.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/prelude-ls": { "node_modules/prelude-ls": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@ -3060,6 +3318,16 @@
"node": ">= 0.10" "node": ">= 0.10"
} }
}, },
"node_modules/pump": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
"integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
"license": "MIT",
"dependencies": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
}
},
"node_modules/punycode": { "node_modules/punycode": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@ -3136,6 +3404,44 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/rc": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
"license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
"dependencies": {
"deep-extend": "^0.6.0",
"ini": "~1.3.0",
"minimist": "^1.2.0",
"strip-json-comments": "~2.0.1"
},
"bin": {
"rc": "cli.js"
}
},
"node_modules/rc/node_modules/strip-json-comments": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
"integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/real-require": { "node_modules/real-require": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz",
@ -3268,7 +3574,6 @@
"version": "7.7.3", "version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"dev": true,
"license": "ISC", "license": "ISC",
"bin": { "bin": {
"semver": "bin/semver.js" "semver": "bin/semver.js"
@ -3432,6 +3737,51 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/simple-concat": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
"integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/simple-get": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
"integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"decompress-response": "^6.0.0",
"once": "^1.3.1",
"simple-concat": "^1.0.0"
}
},
"node_modules/slash": { "node_modules/slash": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
@ -3469,6 +3819,15 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.2.0"
}
},
"node_modules/strip-ansi": { "node_modules/strip-ansi": {
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
@ -3508,6 +3867,34 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/tar-fs": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz",
"integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==",
"license": "MIT",
"dependencies": {
"chownr": "^1.1.1",
"mkdirp-classic": "^0.5.2",
"pump": "^3.0.0",
"tar-stream": "^2.1.4"
}
},
"node_modules/tar-stream": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
"license": "MIT",
"dependencies": {
"bl": "^4.0.3",
"end-of-stream": "^1.4.1",
"fs-constants": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^3.1.1"
},
"engines": {
"node": ">=6"
}
},
"node_modules/tdigest": { "node_modules/tdigest": {
"version": "0.1.2", "version": "0.1.2",
"resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.2.tgz", "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.2.tgz",
@ -3591,6 +3978,18 @@
"fsevents": "~2.3.3" "fsevents": "~2.3.3"
} }
}, },
"node_modules/tunnel-agent": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
"integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
"license": "Apache-2.0",
"dependencies": {
"safe-buffer": "^5.0.1"
},
"engines": {
"node": "*"
}
},
"node_modules/type-check": { "node_modules/type-check": {
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@ -3670,6 +4069,12 @@
"punycode": "^2.1.0" "punycode": "^2.1.0"
} }
}, },
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
"node_modules/utils-merge": { "node_modules/utils-merge": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
@ -3718,7 +4123,6 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/xxhashjs": { "node_modules/xxhashjs": {

View File

@ -9,9 +9,11 @@
"build": "tsc", "build": "tsc",
"start": "node dist/index.js", "start": "node dist/index.js",
"lint": "eslint . --ext ts --report-unused-disable-directives --max-warnings 0", "lint": "eslint . --ext ts --report-unused-disable-directives --max-warnings 0",
"type-check": "tsc --noEmit" "type-check": "tsc --noEmit",
"migrate": "tsx src/scripts/migrate-to-db.ts"
}, },
"dependencies": { "dependencies": {
"better-sqlite3": "^11.10.0",
"bloom-filters": "^3.0.4", "bloom-filters": "^3.0.4",
"compression": "^1.8.1", "compression": "^1.8.1",
"cors": "^2.8.5", "cors": "^2.8.5",
@ -22,6 +24,7 @@
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.0.0", "@eslint/js": "^9.0.0",
"@types/better-sqlite3": "^7.6.13",
"@types/compression": "^1.8.1", "@types/compression": "^1.8.1",
"@types/cors": "^2.8.17", "@types/cors": "^2.8.17",
"@types/express": "^4.17.21", "@types/express": "^4.17.21",

View File

@ -1,6 +1,8 @@
import express from 'express'; import express from 'express';
import http from 'http'; import http from 'http';
import { StorageService } from './services/storage.js'; import { DatabaseStorageService } from './services/database.js';
import { StorageAdapter } from './services/storageAdapter.js';
import { ApiKeyService } from './services/apiKeyService.js';
import { RelayService } from './services/relay.js'; import { RelayService } from './services/relay.js';
import { createMessagesRouter } from './routes/messages.js'; import { createMessagesRouter } from './routes/messages.js';
import { createSignaturesRouter } from './routes/signatures.js'; import { createSignaturesRouter } from './routes/signatures.js';
@ -13,6 +15,7 @@ import {
getBodyLimit, getBodyLimit,
getRequestTimeoutMs, getRequestTimeoutMs,
} from './middleware/index.js'; } from './middleware/index.js';
import { createAuthMiddleware } from './middleware/auth.js';
import { logger } from './lib/logger.js'; import { logger } from './lib/logger.js';
const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 3019; const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 3019;
@ -31,8 +34,22 @@ async function main(): Promise<void> {
registerMiddleware(app); registerMiddleware(app);
app.use(express.json({ limit: getBodyLimit() })); app.use(express.json({ limit: getBodyLimit() }));
const storage = new StorageService(STORAGE_PATH); // Initialize database
await storage.initialize(); const dbStorage = new DatabaseStorageService(STORAGE_PATH);
await dbStorage.initialize();
// Create adapter for compatibility
const storage = new StorageAdapter(dbStorage);
// Initialize API key service
const apiKeyService = new ApiKeyService(dbStorage);
// Register authentication middleware if required
if (REQUIRE_API_KEY) {
app.use(
createAuthMiddleware((key) => apiKeyService.validateApiKey(key)),
);
}
const relay = new RelayService(storage, PEER_RELAYS); const relay = new RelayService(storage, PEER_RELAYS);
@ -43,42 +60,51 @@ async function main(): Promise<void> {
app.use('/metrics', createMetricsRouter(storage)); app.use('/metrics', createMetricsRouter(storage));
app.use('/bloom', createBloomRouter(storage)); app.use('/bloom', createBloomRouter(storage));
let saveIntervalId: ReturnType<typeof setInterval> | null = null; // API key management endpoint (admin only, should be protected in production)
if (SAVE_INTERVAL_SECONDS > 0) { app.post('/admin/api-keys', (req, res) => {
saveIntervalId = setInterval(() => { const { description } = req.body as { description?: string };
storage.saveToDisk().catch((err) => { const { key, prefix } = apiKeyService.generateApiKey(description);
logger.error({ err }, 'Periodic save failed'); res.status(201).json({ key, prefix, description });
}); });
}, SAVE_INTERVAL_SECONDS * 1000);
} app.get('/admin/api-keys', (_req, res) => {
const keys = apiKeyService.listApiKeys();
res.json(keys);
});
app.delete('/admin/api-keys/:prefix', (req, res) => {
const { prefix } = req.params;
const deleted = apiKeyService.deleteApiKey(prefix);
if (deleted) {
res.status(204).send();
} else {
res.status(404).json({ error: 'API key not found' });
}
});
const server = http.createServer(app); const server = http.createServer(app);
server.timeout = getRequestTimeoutMs(); server.timeout = getRequestTimeoutMs();
server.listen(PORT, HOST, () => { server.listen(PORT, HOST, () => {
logger.info( logger.info(
{ {
host: HOST, host: HOST,
port: PORT, port: PORT,
storagePath: STORAGE_PATH, storagePath: STORAGE_PATH,
peerRelays: PEER_RELAYS.length > 0 ? PEER_RELAYS : 'none', peerRelays: PEER_RELAYS.length > 0 ? PEER_RELAYS : 'none',
saveIntervalSeconds: SAVE_INTERVAL_SECONDS > 0 ? SAVE_INTERVAL_SECONDS : null, requireApiKey: REQUIRE_API_KEY,
}, },
'Relay server listening', 'Relay server listening',
); );
}); });
const shutdown = async (): Promise<void> => { const shutdown = async (): Promise<void> => {
logger.info('Shutting down...'); logger.info('Shutting down...');
if (saveIntervalId !== null) {
clearInterval(saveIntervalId);
saveIntervalId = null;
}
try { try {
await storage.saveToDisk(); dbStorage.close();
process.exit(0); process.exit(0);
} catch (err) { } catch (err) {
logger.error({ err }, 'Error saving storage on shutdown'); logger.error({ err }, 'Error closing database on shutdown');
process.exit(1); process.exit(1);
} }
}; };

View File

@ -0,0 +1,58 @@
import type { Request, Response, NextFunction } from 'express';
import { logger } from '../lib/logger.js';
/**
* Middleware to authenticate POST requests using API key.
* API key should be provided in the Authorization header as: "Bearer <key>"
* or in the X-API-Key header.
*/
export function createAuthMiddleware(
validateApiKey: (key: string) => boolean,
): (req: Request, res: Response, next: NextFunction) => void {
return (req: Request, res: Response, next: NextFunction): void => {
// Only require auth for POST requests
if (req.method !== 'POST') {
next();
return;
}
const authHeader = req.headers.authorization;
const apiKeyHeader = req.headers['x-api-key'] as string | undefined;
let apiKey: string | undefined;
if (authHeader !== undefined && authHeader.startsWith('Bearer ')) {
apiKey = authHeader.slice(7);
} else if (apiKeyHeader !== undefined) {
apiKey = apiKeyHeader;
}
if (apiKey === undefined || apiKey.length === 0) {
logger.warn(
{
method: req.method,
url: req.url,
ip: req.ip,
},
'POST request without API key',
);
res.status(401).json({ error: 'API key required for POST requests' });
return;
}
if (!validateApiKey(apiKey)) {
logger.warn(
{
method: req.method,
url: req.url,
ip: req.ip,
},
'POST request with invalid API key',
);
res.status(403).json({ error: 'Invalid API key' });
return;
}
next();
};
}

View File

@ -0,0 +1,82 @@
import { DatabaseStorageService } from '../services/database.js';
import { promises as fs } from 'fs';
import { join } from 'path';
import type { StoredMessage, StoredSignature, StoredKey } from '../types/message.js';
/**
* Migration script from JSON storage to SQLite database.
* Usage: tsx src/scripts/migrate-to-db.ts <storage-path>
*/
async function migrate(): Promise<void> {
const storagePath = process.argv[2] ?? './data';
const jsonPath = join(storagePath, 'messages.json');
console.log(`Reading JSON data from ${jsonPath}...`);
let raw: string;
try {
raw = await fs.readFile(jsonPath, 'utf-8');
} catch (err) {
if (err instanceof Error && 'code' in err && err.code === 'ENOENT') {
console.log('No JSON file found, nothing to migrate.');
process.exit(0);
}
throw err;
}
type StorageSchema = {
messages?: Array<[string, StoredMessage]>;
seenHashes?: string[];
signatures?: Array<[string, StoredSignature[]]>;
keys?: Array<[string, StoredKey[]]>;
};
const parsed = JSON.parse(raw) as StorageSchema;
console.log('Initializing database...');
const db = new DatabaseStorageService(storagePath);
await db.initialize();
console.log('Migrating data...');
if (parsed.messages) {
console.log(`Migrating ${parsed.messages.length} messages...`);
for (const [_hash, msg] of parsed.messages) {
db.storeMessage(msg);
}
}
if (parsed.seenHashes) {
console.log(`Migrating ${parsed.seenHashes.length} seen hashes...`);
for (const hash of parsed.seenHashes) {
db.markHashSeen(hash);
}
}
if (parsed.signatures) {
console.log(`Migrating ${parsed.signatures.length} signature groups...`);
for (const [_hash, items] of parsed.signatures) {
for (const sig of items) {
db.storeSignature(sig);
}
}
}
if (parsed.keys) {
console.log(`Migrating ${parsed.keys.length} key groups...`);
for (const [_hash, items] of parsed.keys) {
for (const key of items) {
db.storeKey(key);
}
}
}
console.log('Migration completed successfully!');
console.log('You can now backup and remove the old messages.json file if desired.');
db.close();
}
migrate().catch((error) => {
console.error('Migration failed:', error);
process.exit(1);
});

View File

@ -0,0 +1,57 @@
import { randomBytes } from 'crypto';
import type { DatabaseStorageService } from './database.js';
import { logger } from '../lib/logger.js';
/**
* Service for managing API keys.
*/
export class ApiKeyService {
constructor(private db: DatabaseStorageService) {}
/**
* Generate a new API key.
* Returns the full key (only shown once) and a prefix for identification.
*/
generateApiKey(description?: string): { key: string; prefix: string } {
const key = `relay_${randomBytes(32).toString('hex')}`;
const prefix = key.slice(0, 16);
// eslint-disable-next-line @typescript-eslint/no-require-imports
const crypto = require('crypto');
const keyHash = crypto.createHash('sha256').update(key).digest('hex');
this.db.storeApiKey(keyHash, prefix, description);
logger.info({ prefix, description }, 'API key generated');
return { key, prefix };
}
/**
* Validate an API key.
*/
validateApiKey(apiKey: string): boolean {
return this.db.validateApiKey(apiKey);
}
/**
* List all API keys (prefixes only, for security).
*/
listApiKeys(): Array<{
prefix: string;
created_at: number;
last_used_at: number | null;
description: string | null;
}> {
return this.db.listApiKeys();
}
/**
* Delete an API key by prefix.
*/
deleteApiKey(prefix: string): boolean {
const deleted = this.db.deleteApiKey(prefix);
if (deleted) {
logger.info({ prefix }, 'API key deleted');
}
return deleted;
}
}

View File

@ -0,0 +1,479 @@
import Database from 'better-sqlite3';
import { join } from 'path';
import { promises as fs } from 'fs';
import type { StoredMessage, StoredSignature, StoredKey } from '../types/message.js';
import { logger } from '../lib/logger.js';
/**
* SQLite-based storage service.
* Replaces in-memory storage for production use.
*/
export class DatabaseStorageService {
private db: Database.Database;
private dbPath: string;
constructor(storagePath: string) {
this.dbPath = join(storagePath, 'relay.db');
}
/**
* Initialize database (create tables, migrate if needed).
*/
async initialize(): Promise<void> {
await fs.mkdir(join(this.dbPath, '..'), { recursive: true });
this.db = new Database(this.dbPath);
this.db.pragma('journal_mode = WAL');
this.createTables();
logger.info({ dbPath: this.dbPath }, 'Database initialized');
}
/**
* Create database tables.
*/
private createTables(): void {
// Messages table
this.db.exec(`
CREATE TABLE IF NOT EXISTS messages (
hash TEXT PRIMARY KEY,
message_chiffre TEXT NOT NULL,
datajson_public TEXT NOT NULL,
received_at INTEGER NOT NULL,
relayed INTEGER NOT NULL DEFAULT 0
)
`);
// Service index for efficient filtering
this.db.exec(`
CREATE TABLE IF NOT EXISTS message_services (
hash TEXT NOT NULL,
service_uuid TEXT NOT NULL,
FOREIGN KEY (hash) REFERENCES messages(hash) ON DELETE CASCADE,
PRIMARY KEY (hash, service_uuid)
)
`);
// Seen hashes (for deduplication)
this.db.exec(`
CREATE TABLE IF NOT EXISTS seen_hashes (
hash TEXT PRIMARY KEY
)
`);
// Signatures table
this.db.exec(`
CREATE TABLE IF NOT EXISTS signatures (
id INTEGER PRIMARY KEY AUTOINCREMENT,
hash_cible TEXT NOT NULL,
signature_hash TEXT NOT NULL,
cle_publique TEXT NOT NULL,
signature TEXT NOT NULL,
nonce TEXT NOT NULL,
materiel TEXT,
received_at INTEGER NOT NULL,
relayed INTEGER NOT NULL DEFAULT 0
)
`);
// Keys table
this.db.exec(`
CREATE TABLE IF NOT EXISTS keys (
id INTEGER PRIMARY KEY AUTOINCREMENT,
hash_message TEXT NOT NULL,
algo TEXT NOT NULL,
params TEXT NOT NULL,
cle_chiffree TEXT,
df_ecdh_scannable TEXT NOT NULL,
received_at INTEGER NOT NULL,
relayed INTEGER NOT NULL DEFAULT 0
)
`);
// API keys table
this.db.exec(`
CREATE TABLE IF NOT EXISTS api_keys (
key_hash TEXT PRIMARY KEY,
key_prefix TEXT NOT NULL,
created_at INTEGER NOT NULL,
last_used_at INTEGER,
description TEXT
)
`);
// Create indexes
this.db.exec(`
CREATE INDEX IF NOT EXISTS idx_messages_received_at ON messages(received_at);
CREATE INDEX IF NOT EXISTS idx_message_services_service ON message_services(service_uuid);
CREATE INDEX IF NOT EXISTS idx_signatures_hash_cible ON signatures(hash_cible);
CREATE INDEX IF NOT EXISTS idx_keys_hash_message ON keys(hash_message);
CREATE INDEX IF NOT EXISTS idx_keys_received_at ON keys(received_at);
`);
}
/**
* Check if a hash has been seen before (deduplication).
*/
hasSeenHash(hash: string): boolean {
const stmt = this.db.prepare('SELECT 1 FROM seen_hashes WHERE hash = ?');
return stmt.get(hash) !== undefined;
}
/**
* Mark a hash as seen.
*/
markHashSeen(hash: string): void {
const stmt = this.db.prepare('INSERT OR IGNORE INTO seen_hashes (hash) VALUES (?)');
stmt.run(hash);
}
/**
* Store an encrypted message.
*/
storeMessage(msg: StoredMessage): void {
const insertMsg = this.db.prepare(`
INSERT OR REPLACE INTO messages (hash, message_chiffre, datajson_public, received_at, relayed)
VALUES (?, ?, ?, ?, ?)
`);
const insertService = this.db.prepare(`
INSERT OR IGNORE INTO message_services (hash, service_uuid) VALUES (?, ?)
`);
const transaction = this.db.transaction(() => {
insertMsg.run(
msg.msg.hash,
msg.msg.message_chiffre,
JSON.stringify(msg.msg.datajson_public),
msg.received_at,
msg.relayed ? 1 : 0,
);
for (const serviceUuid of msg.msg.datajson_public.services_uuid) {
insertService.run(msg.msg.hash, serviceUuid);
}
this.markHashSeen(msg.msg.hash);
});
transaction();
}
/**
* Get an encrypted message by hash.
*/
getMessage(hash: string): StoredMessage | undefined {
const stmt = this.db.prepare('SELECT * FROM messages WHERE hash = ?');
const row = stmt.get(hash) as
| {
hash: string;
message_chiffre: string;
datajson_public: string;
received_at: number;
relayed: number;
}
| undefined;
if (row === undefined) {
return undefined;
}
return {
msg: {
hash: row.hash,
message_chiffre: row.message_chiffre,
datajson_public: JSON.parse(row.datajson_public),
},
received_at: row.received_at,
relayed: row.relayed === 1,
};
}
/**
* Get messages in a time window, optionally filtered by service.
*/
getMessages(
start: number,
end: number,
serviceUuid?: string,
): StoredMessage[] {
let query: string;
let params: unknown[];
if (serviceUuid !== undefined) {
query = `
SELECT m.* FROM messages m
INNER JOIN message_services ms ON m.hash = ms.hash
WHERE ms.service_uuid = ? AND m.received_at >= ? AND m.received_at <= ?
ORDER BY m.received_at ASC
`;
params = [serviceUuid, start, end];
} else {
query = `
SELECT * FROM messages
WHERE received_at >= ? AND received_at <= ?
ORDER BY received_at ASC
`;
params = [start, end];
}
const stmt = this.db.prepare(query);
const rows = stmt.all(...params) as Array<{
hash: string;
message_chiffre: string;
datajson_public: string;
received_at: number;
relayed: number;
}>;
return rows.map((row) => ({
msg: {
hash: row.hash,
message_chiffre: row.message_chiffre,
datajson_public: JSON.parse(row.datajson_public),
},
received_at: row.received_at,
relayed: row.relayed === 1,
}));
}
/**
* Store a signature.
*/
storeSignature(sig: StoredSignature): void {
const hash = sig.msg.hash_cible ?? sig.msg.signature.hash;
const stmt = this.db.prepare(`
INSERT INTO signatures (hash_cible, signature_hash, cle_publique, signature, nonce, materiel, received_at, relayed)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`);
stmt.run(
hash,
sig.msg.signature.hash,
sig.msg.signature.cle_publique,
sig.msg.signature.signature,
sig.msg.signature.nonce,
sig.msg.signature.materiel !== undefined ? JSON.stringify(sig.msg.signature.materiel) : null,
sig.received_at,
sig.relayed ? 1 : 0,
);
}
/**
* Get signatures for a message hash.
*/
getSignatures(hash: string): StoredSignature[] {
const stmt = this.db.prepare('SELECT * FROM signatures WHERE hash_cible = ?');
const rows = stmt.all(hash) as Array<{
id: number;
hash_cible: string;
signature_hash: string;
cle_publique: string;
signature: string;
nonce: string;
materiel: string | null;
received_at: number;
relayed: number;
}>;
return rows.map((row) => ({
msg: {
signature: {
hash: row.signature_hash,
cle_publique: row.cle_publique,
signature: row.signature,
nonce: row.nonce,
materiel: row.materiel !== null ? JSON.parse(row.materiel) : undefined,
},
hash_cible: row.hash_cible,
},
received_at: row.received_at,
relayed: row.relayed === 1,
}));
}
/**
* Store a decryption key.
*/
storeKey(key: StoredKey): void {
const stmt = this.db.prepare(`
INSERT INTO keys (hash_message, algo, params, cle_chiffree, df_ecdh_scannable, received_at, relayed)
VALUES (?, ?, ?, ?, ?, ?, ?)
`);
stmt.run(
key.msg.hash_message,
key.msg.cle_de_chiffrement_message.algo,
JSON.stringify(key.msg.cle_de_chiffrement_message.params),
key.msg.cle_de_chiffrement_message.cle_chiffree ?? null,
key.msg.df_ecdh_scannable,
key.received_at,
key.relayed ? 1 : 0,
);
}
/**
* Get decryption keys for a message hash.
*/
getKeys(hash: string): StoredKey[] {
const stmt = this.db.prepare('SELECT * FROM keys WHERE hash_message = ?');
const rows = stmt.all(hash) as Array<{
id: number;
hash_message: string;
algo: string;
params: string;
cle_chiffree: string | null;
df_ecdh_scannable: string;
received_at: number;
relayed: number;
}>;
return rows.map((row) => ({
msg: {
hash_message: row.hash_message,
cle_de_chiffrement_message: {
algo: row.algo,
params: JSON.parse(row.params),
cle_chiffree: row.cle_chiffree ?? undefined,
},
df_ecdh_scannable: row.df_ecdh_scannable,
},
received_at: row.received_at,
relayed: row.relayed === 1,
}));
}
/**
* Get all stored keys whose received_at is in [start, end].
*/
getKeysInWindow(start: number, end: number): StoredKey[] {
const stmt = this.db.prepare(
'SELECT * FROM keys WHERE received_at >= ? AND received_at <= ? ORDER BY received_at ASC',
);
const rows = stmt.all(start, end) as Array<{
id: number;
hash_message: string;
algo: string;
params: string;
cle_chiffree: string | null;
df_ecdh_scannable: string;
received_at: number;
relayed: number;
}>;
return rows.map((row) => ({
msg: {
hash_message: row.hash_message,
cle_de_chiffrement_message: {
algo: row.algo,
params: JSON.parse(row.params),
cle_chiffree: row.cle_chiffree ?? undefined,
},
df_ecdh_scannable: row.df_ecdh_scannable,
},
received_at: row.received_at,
relayed: row.relayed === 1,
}));
}
/**
* Get count of seen hashes.
*/
getSeenHashCount(): number {
const stmt = this.db.prepare('SELECT COUNT(*) as count FROM seen_hashes');
const row = stmt.get() as { count: number };
return row.count;
}
/**
* Get all seen hashes.
*/
getSeenHashes(): string[] {
const stmt = this.db.prepare('SELECT hash FROM seen_hashes');
const rows = stmt.all() as Array<{ hash: string }>;
return rows.map((r) => r.hash);
}
/**
* Get count of signatures.
*/
getSignatureCount(): number {
const stmt = this.db.prepare('SELECT COUNT(*) as count FROM signatures');
const row = stmt.get() as { count: number };
return row.count;
}
/**
* Get count of keys.
*/
getKeyCount(): number {
const stmt = this.db.prepare('SELECT COUNT(*) as count FROM keys');
const row = stmt.get() as { count: number };
return row.count;
}
/**
* Store an API key.
*/
storeApiKey(keyHash: string, keyPrefix: string, description?: string): void {
const stmt = this.db.prepare(`
INSERT INTO api_keys (key_hash, key_prefix, created_at, description)
VALUES (?, ?, ?, ?)
`);
stmt.run(keyHash, keyPrefix, Date.now(), description ?? null);
}
/**
* Validate an API key.
*/
validateApiKey(apiKey: string): boolean {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const crypto = require('crypto');
const keyHash = crypto.createHash('sha256').update(apiKey).digest('hex');
const stmt = this.db.prepare('SELECT key_hash FROM api_keys WHERE key_hash = ?');
const row = stmt.get(keyHash) as { key_hash: string } | undefined;
if (row !== undefined) {
// Update last_used_at
const updateStmt = this.db.prepare(
'UPDATE api_keys SET last_used_at = ? WHERE key_hash = ?',
);
updateStmt.run(Date.now(), keyHash);
return true;
}
return false;
}
/**
* List all API keys (for management).
*/
listApiKeys(): Array<{
key_prefix: string;
created_at: number;
last_used_at: number | null;
description: string | null;
}> {
const stmt = this.db.prepare(
'SELECT key_prefix, created_at, last_used_at, description FROM api_keys ORDER BY created_at DESC',
);
return stmt.all() as Array<{
key_prefix: string;
created_at: number;
last_used_at: number | null;
description: string | null;
}>;
}
/**
* Delete an API key by prefix.
*/
deleteApiKey(keyPrefix: string): boolean {
const stmt = this.db.prepare('DELETE FROM api_keys WHERE key_prefix = ?');
const result = stmt.run(keyPrefix);
return result.changes > 0;
}
/**
* Close database connection.
*/
close(): void {
this.db.close();
}
}

View File

@ -0,0 +1,88 @@
import type {
StoredMessage,
StoredSignature,
StoredKey,
} from '../types/message.js';
import type { DatabaseStorageService } from './database.js';
/**
* Adapter to make DatabaseStorageService compatible with StorageService interface.
* This allows existing code to work with the new database implementation.
*/
export class StorageAdapter {
constructor(private db: DatabaseStorageService) {}
hasSeenHash(hash: string): boolean {
return this.db.hasSeenHash(hash);
}
markHashSeen(hash: string): void {
this.db.markHashSeen(hash);
}
storeMessage(msg: StoredMessage): void {
this.db.storeMessage(msg);
}
getMessage(hash: string): StoredMessage | undefined {
return this.db.getMessage(hash);
}
getMessages(
start: number,
end: number,
serviceUuid?: string,
): StoredMessage[] {
return this.db.getMessages(start, end, serviceUuid);
}
storeSignature(sig: StoredSignature): void {
this.db.storeSignature(sig);
}
getSignatures(hash: string): StoredSignature[] {
return this.db.getSignatures(hash);
}
storeKey(key: StoredKey): void {
this.db.storeKey(key);
}
getKeys(hash: string): StoredKey[] {
return this.db.getKeys(hash);
}
getKeysInWindow(start: number, end: number): StoredKey[] {
return this.db.getKeysInWindow(start, end);
}
getSeenHashCount(): number {
return this.db.getSeenHashCount();
}
getSeenHashes(): string[] {
return this.db.getSeenHashes();
}
getSignatureCount(): number {
return this.db.getSignatureCount();
}
getKeyCount(): number {
return this.db.getKeyCount();
}
/**
* No-op for compatibility (database handles persistence automatically).
*/
async saveToDisk(): Promise<void> {
// Database is already persistent, no need to save
}
/**
* No-op for compatibility.
*/
async initialize(): Promise<void> {
// Already initialized in database service
}
}

View File

@ -1,100 +1,100 @@
⏳ Traitement: 130000/189710 UTXOs insérés... ⏳ Traitement: 200000/225802 UTXOs insérés...
⏳ Traitement: 140000/189710 UTXOs insérés... ⏳ Traitement: 210000/225802 UTXOs insérés...
⏳ Traitement: 150000/189710 UTXOs insérés... ⏳ Traitement: 220000/225802 UTXOs insérés...
⏳ Traitement: 160000/189710 UTXOs insérés...
⏳ Traitement: 170000/189710 UTXOs insérés...
⏳ Traitement: 180000/189710 UTXOs insérés...
⏳ Traitement: 189710/189710 UTXOs insérés...
💾 Mise à jour des UTXOs dépensés... 💾 Mise à jour des UTXOs dépensés...
📊 Résumé: 📊 Résumé:
- UTXOs vérifiés: 66109 - UTXOs vérifiés: 61609
- UTXOs toujours disponibles: 66109 - UTXOs toujours disponibles: 61609
- UTXOs dépensés détectés: 0 - UTXOs dépensés détectés: 0
📈 Statistiques finales: 📈 Statistiques finales:
- Total UTXOs: 68398 - Total UTXOs: 68398
- Dépensés: 2294 - Dépensés: 6789
- Non dépensés: 66104 - Non dépensés: 61609
✅ Synchronisation terminée ✅ Synchronisation terminée
🔍 Démarrage de la synchronisation des UTXOs dépensés... 🔍 Démarrage de la synchronisation des UTXOs dépensés...
📊 UTXOs à vérifier: 64639 📊 UTXOs à vérifier: 61609
📡 Récupération des UTXOs depuis Bitcoin... 📡 Récupération des UTXOs depuis Bitcoin...
📊 UTXOs disponibles dans Bitcoin: 201481 📊 UTXOs disponibles dans Bitcoin: 225826
💾 Création de la table temporaire... 💾 Création de la table temporaire...
💾 Insertion des UTXOs disponibles par batch... 💾 Insertion des UTXOs disponibles par batch...
⏳ Traitement: 10000/201481 UTXOs insérés... ⏳ Traitement: 10000/225826 UTXOs insérés...
⏳ Traitement: 20000/201481 UTXOs insérés... ⏳ Traitement: 20000/225826 UTXOs insérés...
⏳ Traitement: 30000/201481 UTXOs insérés... ⏳ Traitement: 30000/225826 UTXOs insérés...
⏳ Traitement: 40000/201481 UTXOs insérés... ⏳ Traitement: 40000/225826 UTXOs insérés...
⏳ Traitement: 50000/201481 UTXOs insérés... ⏳ Traitement: 50000/225826 UTXOs insérés...
⏳ Traitement: 60000/201481 UTXOs insérés... ⏳ Traitement: 60000/225826 UTXOs insérés...
⏳ Traitement: 70000/201481 UTXOs insérés... ⏳ Traitement: 70000/225826 UTXOs insérés...
⏳ Traitement: 80000/201481 UTXOs insérés... ⏳ Traitement: 80000/225826 UTXOs insérés...
⏳ Traitement: 90000/201481 UTXOs insérés... ⏳ Traitement: 90000/225826 UTXOs insérés...
⏳ Traitement: 100000/201481 UTXOs insérés... ⏳ Traitement: 100000/225826 UTXOs insérés...
⏳ Traitement: 110000/201481 UTXOs insérés... ⏳ Traitement: 110000/225826 UTXOs insérés...
⏳ Traitement: 120000/201481 UTXOs insérés... ⏳ Traitement: 120000/225826 UTXOs insérés...
⏳ Traitement: 130000/201481 UTXOs insérés... ⏳ Traitement: 130000/225826 UTXOs insérés...
⏳ Traitement: 140000/201481 UTXOs insérés... ⏳ Traitement: 140000/225826 UTXOs insérés...
⏳ Traitement: 150000/201481 UTXOs insérés... ⏳ Traitement: 150000/225826 UTXOs insérés...
⏳ Traitement: 160000/201481 UTXOs insérés... ⏳ Traitement: 160000/225826 UTXOs insérés...
⏳ Traitement: 170000/201481 UTXOs insérés... ⏳ Traitement: 170000/225826 UTXOs insérés...
⏳ Traitement: 180000/201481 UTXOs insérés... ⏳ Traitement: 180000/225826 UTXOs insérés...
⏳ Traitement: 190000/201481 UTXOs insérés... ⏳ Traitement: 190000/225826 UTXOs insérés...
⏳ Traitement: 200000/201481 UTXOs insérés... ⏳ Traitement: 200000/225826 UTXOs insérés...
⏳ Traitement: 210000/225826 UTXOs insérés...
⏳ Traitement: 220000/225826 UTXOs insérés...
💾 Mise à jour des UTXOs dépensés... 💾 Mise à jour des UTXOs dépensés...
📊 Résumé: 📊 Résumé:
- UTXOs vérifiés: 64639 - UTXOs vérifiés: 61609
- UTXOs toujours disponibles: 64639 - UTXOs toujours disponibles: 61609
- UTXOs dépensés détectés: 0 - UTXOs dépensés détectés: 0
📈 Statistiques finales: 📈 Statistiques finales:
- Total UTXOs: 68398 - Total UTXOs: 68398
- Dépensés: 3759 - Dépensés: 6789
- Non dépensés: 64639 - Non dépensés: 61609
✅ Synchronisation terminée ✅ Synchronisation terminée
🔍 Démarrage de la synchronisation des UTXOs dépensés... 🔍 Démarrage de la synchronisation des UTXOs dépensés...
📊 UTXOs à vérifier: 64412 📊 UTXOs à vérifier: 61609
📡 Récupération des UTXOs depuis Bitcoin... 📡 Récupération des UTXOs depuis Bitcoin...
📊 UTXOs disponibles dans Bitcoin: 203310 📊 UTXOs disponibles dans Bitcoin: 225837
💾 Création de la table temporaire... 💾 Création de la table temporaire...
💾 Insertion des UTXOs disponibles par batch... 💾 Insertion des UTXOs disponibles par batch...
⏳ Traitement: 10000/203310 UTXOs insérés... ⏳ Traitement: 10000/225837 UTXOs insérés...
⏳ Traitement: 20000/203310 UTXOs insérés... ⏳ Traitement: 20000/225837 UTXOs insérés...
⏳ Traitement: 30000/203310 UTXOs insérés... ⏳ Traitement: 30000/225837 UTXOs insérés...
⏳ Traitement: 40000/203310 UTXOs insérés... ⏳ Traitement: 40000/225837 UTXOs insérés...
⏳ Traitement: 50000/203310 UTXOs insérés... ⏳ Traitement: 50000/225837 UTXOs insérés...
⏳ Traitement: 60000/203310 UTXOs insérés... ⏳ Traitement: 60000/225837 UTXOs insérés...
⏳ Traitement: 70000/203310 UTXOs insérés... ⏳ Traitement: 70000/225837 UTXOs insérés...
⏳ Traitement: 80000/203310 UTXOs insérés... ⏳ Traitement: 80000/225837 UTXOs insérés...
⏳ Traitement: 90000/203310 UTXOs insérés... ⏳ Traitement: 90000/225837 UTXOs insérés...
⏳ Traitement: 100000/203310 UTXOs insérés... ⏳ Traitement: 100000/225837 UTXOs insérés...
⏳ Traitement: 110000/203310 UTXOs insérés... ⏳ Traitement: 110000/225837 UTXOs insérés...
⏳ Traitement: 120000/203310 UTXOs insérés... ⏳ Traitement: 120000/225837 UTXOs insérés...
⏳ Traitement: 130000/203310 UTXOs insérés... ⏳ Traitement: 130000/225837 UTXOs insérés...
⏳ Traitement: 140000/203310 UTXOs insérés... ⏳ Traitement: 140000/225837 UTXOs insérés...
⏳ Traitement: 150000/203310 UTXOs insérés... ⏳ Traitement: 150000/225837 UTXOs insérés...
⏳ Traitement: 160000/203310 UTXOs insérés... ⏳ Traitement: 160000/225837 UTXOs insérés...
⏳ Traitement: 170000/203310 UTXOs insérés... ⏳ Traitement: 170000/225837 UTXOs insérés...
⏳ Traitement: 180000/203310 UTXOs insérés... ⏳ Traitement: 180000/225837 UTXOs insérés...
⏳ Traitement: 190000/203310 UTXOs insérés... ⏳ Traitement: 190000/225837 UTXOs insérés...
⏳ Traitement: 200000/203310 UTXOs insérés... ⏳ Traitement: 200000/225837 UTXOs insérés...
⏳ Traitement: 210000/225837 UTXOs insérés...
⏳ Traitement: 220000/225837 UTXOs insérés...
💾 Mise à jour des UTXOs dépensés... 💾 Mise à jour des UTXOs dépensés...
📊 Résumé: 📊 Résumé:
- UTXOs vérifiés: 64412 - UTXOs vérifiés: 61609
- UTXOs toujours disponibles: 64412 - UTXOs toujours disponibles: 61609
- UTXOs dépensés détectés: 0 - UTXOs dépensés détectés: 0
📈 Statistiques finales: 📈 Statistiques finales:
- Total UTXOs: 68398 - Total UTXOs: 68398
- Dépensés: 3986 - Dépensés: 6789
- Non dépensés: 64412 - Non dépensés: 61609
✅ Synchronisation terminée ✅ Synchronisation terminée

View File

@ -0,0 +1,221 @@
# Options non implémentées - Analyse d'intérêt
**Author:** Équipe 4NK
**Date:** 2026-01-28
## 1. Merkle trees (optionnel)
### Description
Checkpointing / accélération de scan pour optimiser la synchronisation à grande échelle.
### Intérêt
**Avantages potentiels :**
- **Performance** : Réduction significative du nombre de requêtes nécessaires pour synchroniser de grandes quantités de données
- **Scalabilité** : Permet de gérer des volumes importants (millions de messages) sans dégradation de performance
- **Efficacité réseau** : Réduction de la bande passante utilisée lors des synchronisations
- **Checkpointing** : Possibilité de reprendre une synchronisation depuis un point précis sans tout re-scanner
**Cas d'usage :**
- Services avec un très grand nombre de messages (millions)
- Synchronisations fréquentes sur de grandes fenêtres temporelles
- Réseaux avec bande passante limitée
- Besoin de synchronisation incrémentale efficace
### Inconvénients / Complexité
**Complexité technique :**
- Implémentation complexe (construction d'arbres de Merkle, gestion des segments)
- Nécessite une coordination entre client et serveur (relais doit supporter Merkle)
- Gestion des conflits et des branches d'arbres
- Maintenance et debugging plus difficiles
**Coût de développement :**
- Temps de développement important
- Tests complexes (volumes importants, cas limites)
- Documentation et formation nécessaires
**Alternatives existantes :**
- **Bloom filter** : Déjà implémenté, réduit efficacement les requêtes inutiles
- **Hash cache** : Déjà implémenté, évite de traiter les messages déjà vus
- **Fenêtre de scan configurable** : Permet de limiter la portée des synchronisations
### Recommandation
**Intérêt : MOYEN à FAIBLE**
**Raisons :**
1. **Bloom filter suffisant** : Le Bloom filter déjà implémenté couvre la majorité des cas d'usage d'optimisation
2. **Complexité vs bénéfice** : La complexité d'implémentation est élevée pour un gain marginal dans la plupart des cas
3. **Volumes actuels** : Les volumes de données actuels ne justifient pas nécessairement cette optimisation
4. **Dépendance serveur** : Nécessite une modification des relais pour supporter Merkle, ce qui limite l'adoption
**Quand l'implémenter :**
- Si les volumes de messages deviennent très importants (millions+)
- Si les synchronisations deviennent un goulot d'étranglement mesurable
- Si le Bloom filter ne suffit plus pour optimiser les requêtes
- Si les relais sont prêts à supporter Merkle
**Conclusion :** Optionnel, à considérer uniquement si un besoin réel de performance à grande échelle est identifié et mesuré.
---
## 2. Évolutions futures api-relay (optionnel)
### Description
Plusieurs évolutions possibles pour api-relay : base de données, authentification, WebSocket, compression, etc.
### Analyse par évolution
#### 2.1 Base de données (SQLite, PostgreSQL)
**Intérêt : ÉLEVÉ**
**Avantages :**
- **Persistance fiable** : Données persistées de manière fiable, pas de perte en cas de redémarrage
- **Performance** : Requêtes optimisées, indexation efficace
- **Scalabilité** : Gestion de volumes importants
- **Transactions** : Garanties ACID pour la cohérence des données
- **Maintenance** : Outils de backup, monitoring, maintenance standardisés
**Inconvénients :**
- Configuration et déploiement plus complexes
- Nécessite une migration depuis le stockage en mémoire actuel
**Recommandation :** **À implémenter en production**. Le stockage en mémoire actuel est suffisant pour le développement mais pas pour la production.
---
#### 2.2 Authentification pour endpoints POST
**Intérêt : MOYEN**
**Avantages :**
- **Sécurité** : Protection contre les abus (spam, DoS)
- **Traçabilité** : Identification des sources de messages
- **Contrôle d'accès** : Limitation à des clients autorisés
**Inconvénients :**
- Complexité de gestion des clés/tokens
- Nécessite une infrastructure d'authentification
- Peut limiter la décentralisation (nécessite une autorité pour émettre les tokens)
**Recommandation :** **Optionnel**. Utile si des problèmes d'abus sont identifiés. Le modèle actuel (pull-only, déduplication par hash) offre déjà une certaine protection.
---
#### 2.3 Compression des messages stockés
**Intérêt : FAIBLE**
**Avantages :**
- Réduction de l'espace disque
- Réduction de la bande passante réseau
**Inconvénients :**
- CPU supplémentaire pour compression/décompression
- Complexité de gestion (format, version)
- Les messages sont déjà relativement petits (JSON)
**Recommendation :** **Non prioritaire**. Les messages sont déjà compacts (JSON). La compression n'apporterait qu'un gain marginal.
---
#### 2.4 Indexation avancée (service UUID, type UUID)
**Intérêt : MOYEN**
**Avantages :**
- Requêtes plus rapides pour des filtres spécifiques
- Meilleure performance sur de gros volumes
**Inconvénients :**
- Complexité de gestion des index
- Maintenance supplémentaire
**Recommandation :** **Optionnel**. L'indexation par `service_uuid` est déjà partiellement implémentée. À améliorer si les volumes augmentent.
---
#### 2.5 WebSocket pour notifications en temps réel
**Intérêt : FAIBLE à MOYEN**
**Avantages :**
- Notifications en temps réel (push)
- Réduction de la latence
- Meilleure expérience utilisateur
**Inconvénients :**
- **Contradiction avec le modèle pull-only** : Le modèle actuel est volontairement pull-only pour la décentralisation
- Complexité de gestion des connexions WebSocket
- Nécessite une infrastructure serveur plus complexe
- Peut compromettre la décentralisation (dépendance au serveur)
**Recommandation :** **Non recommandé**. Contredit le principe de décentralisation et le modèle pull-only. Le polling actuel est suffisant et plus robuste.
---
#### 2.6 Métriques Prometheus
**Intérêt : ÉLEVÉ**
**Avantages :**
- Monitoring et observabilité
- Détection proactive des problèmes
- Métriques de performance
**Inconvénients :**
- Configuration supplémentaire
- Infrastructure de monitoring nécessaire
**Recommandation :** **À implémenter en production**. Déjà partiellement implémenté (`GET /metrics`). À compléter selon les besoins de monitoring.
---
#### 2.7 Logging structuré (Winston, Pino)
**Intérêt : ÉLEVÉ**
**Avantages :**
- Meilleure traçabilité
- Analyse des logs facilitée
- Debugging plus efficace
**Inconvénients :**
- Configuration supplémentaire
- Gestion des logs (rotation, stockage)
**Recommandation :** **Déjà implémenté**. Pino est déjà utilisé dans api-relay. À maintenir et améliorer si nécessaire.
---
## Synthèse
### À implémenter en priorité
1. **Base de données** (SQLite/PostgreSQL) - Production
2. **Métriques Prometheus** - Compléter l'implémentation existante
### Optionnel selon besoins
3. **Authentification POST** - Si problèmes d'abus
4. **Indexation avancée** - Si volumes importants
### Non recommandé
5. **Compression** - Gain marginal
6. **WebSocket** - Contredit le modèle pull-only décentralisé
### Déjà en place
7. **Logging structuré** - Pino déjà implémenté
8. **Rate limiting** - Déjà implémenté
9. **CORS** - Déjà implémenté
10. **Bloom filter** - Déjà implémenté
---
## Conclusion
Les évolutions les plus pertinentes pour api-relay sont :
- **Base de données** : Nécessaire pour la production
- **Métriques** : Utile pour le monitoring
Les autres évolutions sont optionnelles et dépendent des besoins spécifiques identifiés en production.

View File

@ -1,5 +1,6 @@
export { verifyLoginProof, } from './verifyLoginProof.js'; export { verifyLoginProof, } from './verifyLoginProof.js';
export { NonceCache } from './nonceCache.js'; export { NonceCache } from './nonceCache.js';
export { PersistentNonceCache } from './persistentNonceCache.js';
export { buildAllowedPubkeysFromValidateurs } from './buildAllowedPubkeys.js'; export { buildAllowedPubkeysFromValidateurs } from './buildAllowedPubkeys.js';
export type { LoginProof, Validateurs, VerifyLoginProofContext, VerifyLoginProofResult, NonceCacheLike, } from './types.js'; export type { LoginProof, Validateurs, VerifyLoginProofContext, VerifyLoginProofResult, NonceCacheLike, } from './types.js';
//# sourceMappingURL=index.d.ts.map //# sourceMappingURL=index.d.ts.map

View File

@ -1 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,GACjB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EAAE,kCAAkC,EAAE,MAAM,0BAA0B,CAAC;AAC9E,YAAY,EACV,UAAU,EACV,WAAW,EACX,uBAAuB,EACvB,sBAAsB,EACtB,cAAc,GACf,MAAM,YAAY,CAAC"} {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,GACjB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AACjE,OAAO,EAAE,kCAAkC,EAAE,MAAM,0BAA0B,CAAC;AAC9E,YAAY,EACV,UAAU,EACV,WAAW,EACX,uBAAuB,EACvB,sBAAsB,EACtB,cAAc,GACf,MAAM,YAAY,CAAC"}

View File

@ -1,3 +1,4 @@
export { verifyLoginProof, } from './verifyLoginProof.js'; export { verifyLoginProof, } from './verifyLoginProof.js';
export { NonceCache } from './nonceCache.js'; export { NonceCache } from './nonceCache.js';
export { PersistentNonceCache } from './persistentNonceCache.js';
export { buildAllowedPubkeysFromValidateurs } from './buildAllowedPubkeys.js'; export { buildAllowedPubkeysFromValidateurs } from './buildAllowedPubkeys.js';

View File

@ -0,0 +1,36 @@
import type { NonceCacheLike } from './types.js';
/**
* Persistent nonce cache using IndexedDB (browser) or localStorage (fallback).
* Implements NonceCacheLike interface for use with verifyLoginProof.
*/
export declare class PersistentNonceCache implements NonceCacheLike {
private readonly ttlMs;
private readonly storageKey;
private readonly useIndexedDB;
private db;
constructor(ttlMs?: number, storageKey?: string);
/**
* Initialize IndexedDB if available.
*/
init(): Promise<void>;
/**
* Check if nonce is valid (not seen within TTL). Records nonce on success.
* Note: IndexedDB operations are async, but NonceCacheLike interface requires sync.
* This implementation uses localStorage for synchronous access.
* For true IndexedDB persistence, consider making the interface async.
*/
isValid(nonce: string, timestamp: number): boolean;
/**
* Synchronous validation using localStorage (fallback).
*/
private isValidSync;
/**
* Cleanup expired entries (localStorage).
*/
private cleanupSync;
/**
* Clear all entries.
*/
clear(): void;
}
//# sourceMappingURL=persistentNonceCache.d.ts.map

View File

@ -0,0 +1 @@
{"version":3,"file":"persistentNonceCache.d.ts","sourceRoot":"","sources":["../src/persistentNonceCache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD;;;GAGG;AACH,qBAAa,oBAAqB,YAAW,cAAc;IACzD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAU;IACvC,OAAO,CAAC,EAAE,CAA4B;gBAE1B,KAAK,GAAE,MAAgB,EAAE,UAAU,GAAE,MAAsB;IAMvE;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IA2B3B;;;;;OAKG;IACH,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO;IAIlD;;OAEG;IACH,OAAO,CAAC,WAAW;IAiBnB;;OAEG;IACH,OAAO,CAAC,WAAW;IAoBnB;;OAEG;IACH,KAAK,IAAI,IAAI;CAkBd"}

View File

@ -0,0 +1,107 @@
/**
* Persistent nonce cache using IndexedDB (browser) or localStorage (fallback).
* Implements NonceCacheLike interface for use with verifyLoginProof.
*/
export class PersistentNonceCache {
ttlMs;
storageKey;
useIndexedDB;
db = null;
constructor(ttlMs = 3600000, storageKey = 'nonce_cache') {
this.ttlMs = ttlMs;
this.storageKey = storageKey;
this.useIndexedDB = typeof window !== 'undefined' && 'indexedDB' in window;
}
/**
* Initialize IndexedDB if available.
*/
async init() {
if (!this.useIndexedDB) {
return;
}
return new Promise((resolve, reject) => {
const request = indexedDB.open('NonceCacheDB', 1);
request.onerror = () => {
reject(new Error('Failed to open IndexedDB'));
};
request.onsuccess = () => {
this.db = request.result;
resolve();
};
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains('nonces')) {
const store = db.createObjectStore('nonces', { keyPath: 'nonce' });
store.createIndex('timestamp', 'timestamp', { unique: false });
}
};
});
}
/**
* Check if nonce is valid (not seen within TTL). Records nonce on success.
* Note: IndexedDB operations are async, but NonceCacheLike interface requires sync.
* This implementation uses localStorage for synchronous access.
* For true IndexedDB persistence, consider making the interface async.
*/
isValid(nonce, timestamp) {
return this.isValidSync(nonce, timestamp);
}
/**
* Synchronous validation using localStorage (fallback).
*/
isValidSync(nonce, timestamp) {
const now = Date.now();
const stored = localStorage.getItem(`${this.storageKey}_${nonce}`);
if (stored !== null) {
const storedTimestamp = parseInt(stored, 10);
if (now - storedTimestamp < this.ttlMs) {
return false;
}
localStorage.removeItem(`${this.storageKey}_${nonce}`);
}
localStorage.setItem(`${this.storageKey}_${nonce}`, timestamp.toString());
this.cleanupSync(now);
return true;
}
/**
* Cleanup expired entries (localStorage).
*/
cleanupSync(now) {
const keys = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key !== null && key.startsWith(`${this.storageKey}_`)) {
keys.push(key);
}
}
for (const key of keys) {
const stored = localStorage.getItem(key);
if (stored !== null) {
const storedTimestamp = parseInt(stored, 10);
if (now - storedTimestamp >= this.ttlMs) {
localStorage.removeItem(key);
}
}
}
}
/**
* Clear all entries.
*/
clear() {
if (this.useIndexedDB && this.db !== null) {
const transaction = this.db.transaction(['nonces'], 'readwrite');
const store = transaction.objectStore('nonces');
store.clear();
}
const keys = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key !== null && key.startsWith(`${this.storageKey}_`)) {
keys.push(key);
}
}
for (const key of keys) {
localStorage.removeItem(key);
}
}
}

View File

@ -51,6 +51,6 @@ export interface NonceCacheLike {
} }
export interface VerifyLoginProofResult { export interface VerifyLoginProofResult {
accept: boolean; accept: boolean;
reason?: 'timestamp_out_of_window' | 'nonce_reused' | 'validators_not_verifiable' | 'no_validator_signature' | 'signature_cle_publique_not_authorized'; reason?: 'invalid_proof_structure' | 'timestamp_out_of_window' | 'nonce_reused' | 'validators_not_verifiable' | 'no_validator_signature' | 'signature_cle_publique_not_authorized';
} }
//# sourceMappingURL=types.d.ts.map //# sourceMappingURL=types.d.ts.map

View File

@ -1 +1 @@
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE;QACT,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,EAAE,MAAM,CAAC;QACd,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC;IACF,UAAU,EAAE,KAAK,CAAC;QAChB,SAAS,EAAE,MAAM,CAAC;QAClB,YAAY,EAAE,MAAM,CAAC;QACrB,KAAK,EAAE,MAAM,CAAC;QACd,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC,CAAC;IACH,MAAM,EAAE,YAAY,GAAG,QAAQ,GAAG,QAAQ,GAAG,UAAU,CAAC;CACzD;AAED;;;GAGG;AACH,MAAM,WAAW,oBAAoB;IACnC,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB;AAED,MAAM,WAAW,YAAY;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,uBAAuB,EAAE,oBAAoB,EAAE,CAAC;CACjD;AAED,MAAM,WAAW,WAAW;IAC1B,eAAe,EAAE,YAAY,EAAE,CAAC;CACjC;AAED;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACtC,sDAAsD;IACtD,cAAc,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC5B,+BAA+B;IAC/B,UAAU,EAAE,cAAc,CAAC;IAC3B,sDAAsD;IACtD,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;CACpD;AAED,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,yBAAyB,GAAG,cAAc,GAAG,2BAA2B,GAAG,wBAAwB,GAAG,uCAAuC,CAAC;CACxJ"} {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE;QACT,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,EAAE,MAAM,CAAC;QACd,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC;IACF,UAAU,EAAE,KAAK,CAAC;QAChB,SAAS,EAAE,MAAM,CAAC;QAClB,YAAY,EAAE,MAAM,CAAC;QACrB,KAAK,EAAE,MAAM,CAAC;QACd,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC,CAAC;IACH,MAAM,EAAE,YAAY,GAAG,QAAQ,GAAG,QAAQ,GAAG,UAAU,CAAC;CACzD;AAED;;;GAGG;AACH,MAAM,WAAW,oBAAoB;IACnC,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB;AAED,MAAM,WAAW,YAAY;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,uBAAuB,EAAE,oBAAoB,EAAE,CAAC;CACjD;AAED,MAAM,WAAW,WAAW;IAC1B,eAAe,EAAE,YAAY,EAAE,CAAC;CACjC;AAED;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACtC,sDAAsD;IACtD,cAAc,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC5B,+BAA+B;IAC/B,UAAU,EAAE,cAAc,CAAC;IAC3B,sDAAsD;IACtD,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;CACpD;AAED,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,yBAAyB,GAAG,yBAAyB,GAAG,cAAc,GAAG,2BAA2B,GAAG,wBAAwB,GAAG,uCAAuC,CAAC;CACpL"}

View File

@ -2,6 +2,11 @@ import type { LoginProof, VerifyLoginProofContext, VerifyLoginProofResult } from
/** /**
* Verify login proof: crypto, allowed pubkeys, timestamp window, nonce anti-replay. * Verify login proof: crypto, allowed pubkeys, timestamp window, nonce anti-replay.
* Service must provide allowedPubkeys (from validators) and a NonceCache. * Service must provide allowedPubkeys (from validators) and a NonceCache.
*
* @param proof - Login proof from UserWallet
* @param ctx - Verification context (allowedPubkeys, nonceCache, timestampWindowMs)
* @returns Verification result with accept flag and optional reason
* @throws {Error} If proof structure is invalid (missing challenge, hash, signatures)
*/ */
export declare function verifyLoginProof(proof: LoginProof, ctx: VerifyLoginProofContext): VerifyLoginProofResult; export declare function verifyLoginProof(proof: LoginProof, ctx: VerifyLoginProofContext): VerifyLoginProofResult;
//# sourceMappingURL=verifyLoginProof.d.ts.map //# sourceMappingURL=verifyLoginProof.d.ts.map

View File

@ -1 +1 @@
{"version":3,"file":"verifyLoginProof.d.ts","sourceRoot":"","sources":["../src/verifyLoginProof.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,UAAU,EACV,uBAAuB,EACvB,sBAAsB,EACvB,MAAM,YAAY,CAAC;AAyCpB;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,UAAU,EACjB,GAAG,EAAE,uBAAuB,GAC3B,sBAAsB,CA6CxB"} {"version":3,"file":"verifyLoginProof.d.ts","sourceRoot":"","sources":["../src/verifyLoginProof.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,UAAU,EACV,uBAAuB,EACvB,sBAAsB,EACvB,MAAM,YAAY,CAAC;AAyCpB;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,UAAU,EACjB,GAAG,EAAE,uBAAuB,GAC3B,sBAAsB,CA0FxB"}

View File

@ -26,8 +26,47 @@ function verifySignaturesStrict(hashValue, signatures, allowedPubkeys) {
/** /**
* Verify login proof: crypto, allowed pubkeys, timestamp window, nonce anti-replay. * Verify login proof: crypto, allowed pubkeys, timestamp window, nonce anti-replay.
* Service must provide allowedPubkeys (from validators) and a NonceCache. * Service must provide allowedPubkeys (from validators) and a NonceCache.
*
* @param proof - Login proof from UserWallet
* @param ctx - Verification context (allowedPubkeys, nonceCache, timestampWindowMs)
* @returns Verification result with accept flag and optional reason
* @throws {Error} If proof structure is invalid (missing challenge, hash, signatures)
*/ */
export function verifyLoginProof(proof, ctx) { export function verifyLoginProof(proof, ctx) {
// Validate proof structure
if (proof.challenge === undefined || typeof proof.challenge !== 'object') {
return {
accept: false,
reason: 'invalid_proof_structure',
};
}
if (typeof proof.challenge.hash !== 'string' ||
proof.challenge.hash.length === 0) {
return {
accept: false,
reason: 'invalid_proof_structure',
};
}
if (typeof proof.challenge.nonce !== 'string' ||
proof.challenge.nonce.length === 0) {
return {
accept: false,
reason: 'invalid_proof_structure',
};
}
if (typeof proof.challenge.timestamp !== 'number' ||
!Number.isFinite(proof.challenge.timestamp)) {
return {
accept: false,
reason: 'invalid_proof_structure',
};
}
if (!Array.isArray(proof.signatures)) {
return {
accept: false,
reason: 'invalid_proof_structure',
};
}
if (ctx.allowedPubkeys.size === 0) { if (ctx.allowedPubkeys.size === 0) {
return { return {
accept: false, accept: false,

View File

@ -2,6 +2,7 @@ export {
verifyLoginProof, verifyLoginProof,
} from './verifyLoginProof.js'; } from './verifyLoginProof.js';
export { NonceCache } from './nonceCache.js'; export { NonceCache } from './nonceCache.js';
export { PersistentNonceCache } from './persistentNonceCache.js';
export { buildAllowedPubkeysFromValidateurs } from './buildAllowedPubkeys.js'; export { buildAllowedPubkeysFromValidateurs } from './buildAllowedPubkeys.js';
export type { export type {
LoginProof, LoginProof,

View File

@ -0,0 +1,123 @@
import type { NonceCacheLike } from './types.js';
/**
* Persistent nonce cache using IndexedDB (browser) or localStorage (fallback).
* Implements NonceCacheLike interface for use with verifyLoginProof.
*/
export class PersistentNonceCache implements NonceCacheLike {
private readonly ttlMs: number;
private readonly storageKey: string;
private readonly useIndexedDB: boolean;
private db: IDBDatabase | null = null;
constructor(ttlMs: number = 3600000, storageKey: string = 'nonce_cache') {
this.ttlMs = ttlMs;
this.storageKey = storageKey;
this.useIndexedDB = typeof window !== 'undefined' && 'indexedDB' in window;
}
/**
* Initialize IndexedDB if available.
*/
async init(): Promise<void> {
if (!this.useIndexedDB) {
return;
}
return new Promise((resolve, reject) => {
const request = indexedDB.open('NonceCacheDB', 1);
request.onerror = (): void => {
reject(new Error('Failed to open IndexedDB'));
};
request.onsuccess = (): void => {
this.db = request.result;
resolve();
};
request.onupgradeneeded = (event: IDBVersionChangeEvent): void => {
const db = (event.target as IDBOpenDBRequest).result;
if (!db.objectStoreNames.contains('nonces')) {
const store = db.createObjectStore('nonces', { keyPath: 'nonce' });
store.createIndex('timestamp', 'timestamp', { unique: false });
}
};
});
}
/**
* Check if nonce is valid (not seen within TTL). Records nonce on success.
* Note: IndexedDB operations are async, but NonceCacheLike interface requires sync.
* This implementation uses localStorage for synchronous access.
* For true IndexedDB persistence, consider making the interface async.
*/
isValid(nonce: string, timestamp: number): boolean {
return this.isValidSync(nonce, timestamp);
}
/**
* Synchronous validation using localStorage (fallback).
*/
private isValidSync(nonce: string, timestamp: number): boolean {
const now = Date.now();
const stored = localStorage.getItem(`${this.storageKey}_${nonce}`);
if (stored !== null) {
const storedTimestamp = parseInt(stored, 10);
if (now - storedTimestamp < this.ttlMs) {
return false;
}
localStorage.removeItem(`${this.storageKey}_${nonce}`);
}
localStorage.setItem(`${this.storageKey}_${nonce}`, timestamp.toString());
this.cleanupSync(now);
return true;
}
/**
* Cleanup expired entries (localStorage).
*/
private cleanupSync(now: number): void {
const keys: string[] = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key !== null && key.startsWith(`${this.storageKey}_`)) {
keys.push(key);
}
}
for (const key of keys) {
const stored = localStorage.getItem(key);
if (stored !== null) {
const storedTimestamp = parseInt(stored, 10);
if (now - storedTimestamp >= this.ttlMs) {
localStorage.removeItem(key);
}
}
}
}
/**
* Clear all entries.
*/
clear(): void {
if (this.useIndexedDB && this.db !== null) {
const transaction = this.db.transaction(['nonces'], 'readwrite');
const store = transaction.objectStore('nonces');
store.clear();
}
const keys: string[] = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key !== null && key.startsWith(`${this.storageKey}_`)) {
keys.push(key);
}
}
for (const key of keys) {
localStorage.removeItem(key);
}
}
}

View File

@ -2,7 +2,7 @@
"compilerOptions": { "compilerOptions": {
"target": "ES2022", "target": "ES2022",
"module": "Node16", "module": "Node16",
"lib": ["ES2022"], "lib": ["ES2022", "DOM"],
"moduleResolution": "Node16", "moduleResolution": "Node16",
"rootDir": "./src", "rootDir": "./src",
"outDir": "./dist", "outDir": "./dist",

View File

@ -19,6 +19,7 @@ import { ServiceListScreen } from './components/ServiceListScreen';
import { MemberSelectionScreen } from './components/MemberSelectionScreen'; import { MemberSelectionScreen } from './components/MemberSelectionScreen';
import { DiagnosticScreen } from './components/DiagnosticScreen'; import { DiagnosticScreen } from './components/DiagnosticScreen';
import { ServiceSyncScreen } from './components/ServiceSyncScreen'; import { ServiceSyncScreen } from './components/ServiceSyncScreen';
import { CryptoSettingsScreen } from './components/CryptoSettingsScreen';
import { DataExportImportScreen } from './components/DataExportImportScreen'; import { DataExportImportScreen } from './components/DataExportImportScreen';
import { UnlockScreen } from './components/UnlockScreen'; import { UnlockScreen } from './components/UnlockScreen';
import { useChannel } from './hooks/useChannel'; import { useChannel } from './hooks/useChannel';
@ -49,6 +50,7 @@ function AppContent(): JSX.Element {
<Route path="/select-member" element={<MemberSelectionScreen />} /> <Route path="/select-member" element={<MemberSelectionScreen />} />
<Route path="/diagnostic" element={<DiagnosticScreen />} /> <Route path="/diagnostic" element={<DiagnosticScreen />} />
<Route path="/service-sync" element={<ServiceSyncScreen />} /> <Route path="/service-sync" element={<ServiceSyncScreen />} />
<Route path="/crypto-settings" element={<CryptoSettingsScreen />} />
<Route path="/data" element={<DataExportImportScreen />} /> <Route path="/data" element={<DataExportImportScreen />} />
</Routes> </Routes>
); );

View File

@ -11,18 +11,20 @@ export function CreateIdentityScreen(): JSX.Element {
const [name, setName] = useState(''); const [name, setName] = useState('');
const [isCreating, setIsCreating] = useState(false); const [isCreating, setIsCreating] = useState(false);
const handleSubmit = async (e: FormEvent<HTMLFormElement>): Promise<void> => { const handleSubmit = (e: FormEvent<HTMLFormElement>): void => {
e.preventDefault(); e.preventDefault();
setIsCreating(true); void (async (): Promise<void> => {
clearError(); setIsCreating(true);
try { clearError();
createNewIdentity(name || undefined); try {
navigate('/'); createNewIdentity(name || undefined);
} catch (err) { navigate('/');
handleError(err, 'Erreur lors de la création de l\'identité'); } catch (err) {
} finally { handleError(err, 'Erreur lors de la création de l&apos;identité');
setIsCreating(false); } finally {
} setIsCreating(false);
}
})();
}; };
return ( return (

View File

@ -0,0 +1,216 @@
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useErrorHandler } from '../hooks/useErrorHandler';
import { ErrorDisplay } from './ErrorDisplay';
interface CryptoSettings {
hashAlgorithm: string;
jsonCanonizationStrict: boolean;
ecdhCurve: string;
nonceTtlMs: number;
timestampWindowMs: number;
}
const DEFAULT_SETTINGS: CryptoSettings = {
hashAlgorithm: 'sha256',
jsonCanonizationStrict: true,
ecdhCurve: 'secp256k1',
nonceTtlMs: 3600000, // 1 hour
timestampWindowMs: 300000, // 5 minutes
};
const STORAGE_KEY = 'userwallet_crypto_settings';
export function CryptoSettingsScreen(): JSX.Element {
const navigate = useNavigate();
const { error, handleError, clearError } = useErrorHandler();
const [settings, setSettings] = useState<CryptoSettings>(DEFAULT_SETTINGS);
const [isSaving, setIsSaving] = useState(false);
useEffect(() => {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored !== null) {
try {
const parsed = JSON.parse(stored) as CryptoSettings;
setSettings({ ...DEFAULT_SETTINGS, ...parsed });
} catch {
// Ignore invalid stored settings
}
}
}, []);
const handleSave = (): void => {
setIsSaving(true);
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
clearError();
} catch (err) {
handleError(err, 'Erreur lors de la sauvegarde');
} finally {
setIsSaving(false);
}
};
const handleReset = (): void => {
setSettings(DEFAULT_SETTINGS);
localStorage.removeItem(STORAGE_KEY);
clearError();
};
return (
<main>
<h1>Paramètres crypto (avancé)</h1>
{error !== null && <ErrorDisplay error={error} onDismiss={clearError} />}
<section aria-labelledby="crypto-settings">
<h2 id="crypto-settings">Configuration cryptographique</h2>
<p>
<strong>Attention:</strong> Modifier ces paramètres peut rendre votre
wallet incompatible avec les services existants. Utilisez uniquement si
vous comprenez les implications.
</p>
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
<div>
<label htmlFor="hash-algo">
Algorithme de hash
<select
id="hash-algo"
value={settings.hashAlgorithm}
onChange={(e): void => {
setSettings({ ...settings, hashAlgorithm: e.target.value });
}}
>
<option value="sha256">SHA-256</option>
</select>
</label>
<p style={{ fontSize: '0.9em', color: 'var(--color-text-secondary)' }}>
Algorithme utilisé pour calculer les hash canoniques des messages.
</p>
</div>
<div>
<label htmlFor="canon-strict">
<input
id="canon-strict"
type="checkbox"
checked={settings.jsonCanonizationStrict}
onChange={(e): void => {
setSettings({
...settings,
jsonCanonizationStrict: e.target.checked,
});
}}
/>
Canonisation JSON stricte
</label>
<p style={{ fontSize: '0.9em', color: 'var(--color-text-secondary)' }}>
Mode strict pour la canonisation JSON (ordre des clés, espaces, etc.).
</p>
</div>
<div>
<label htmlFor="ecdh-curve">
Courbe ECDH
<select
id="ecdh-curve"
value={settings.ecdhCurve}
onChange={(e): void => {
setSettings({ ...settings, ecdhCurve: e.target.value });
}}
disabled
>
<option value="secp256k1">secp256k1</option>
</select>
</label>
<p style={{ fontSize: '0.9em', color: 'var(--color-text-secondary)' }}>
Courbe elliptique pour ECDH (non modifiable, secp256k1 uniquement).
</p>
</div>
<div>
<label htmlFor="nonce-ttl">
TTL nonce (ms)
<input
id="nonce-ttl"
type="number"
min="60000"
max="86400000"
step="60000"
value={settings.nonceTtlMs}
onChange={(e): void => {
setSettings({
...settings,
nonceTtlMs: parseInt(e.target.value, 10) || DEFAULT_SETTINGS.nonceTtlMs,
});
}}
/>
</label>
<p style={{ fontSize: '0.9em', color: 'var(--color-text-secondary)' }}>
Durée de vie du cache anti-rejeu pour les nonces (60000-86400000 ms).
</p>
</div>
<div>
<label htmlFor="timestamp-window">
Fenêtre timestamp (ms)
<input
id="timestamp-window"
type="number"
min="60000"
max="3600000"
step="60000"
value={settings.timestampWindowMs}
onChange={(e): void => {
setSettings({
...settings,
timestampWindowMs:
parseInt(e.target.value, 10) || DEFAULT_SETTINGS.timestampWindowMs,
});
}}
/>
</label>
<p style={{ fontSize: '0.9em', color: 'var(--color-text-secondary)' }}>
Fenêtre de validité pour les timestamps (60000-3600000 ms).
</p>
</div>
</div>
<div style={{ display: 'flex', gap: '0.5rem', marginTop: '1rem' }}>
<button type="button" onClick={handleSave} disabled={isSaving}>
{isSaving ? 'Sauvegarde...' : 'Sauvegarder'}
</button>
<button type="button" onClick={handleReset}>
Réinitialiser
</button>
<button
type="button"
onClick={() => {
navigate('/');
}}
>
Retour
</button>
</div>
</section>
<section aria-labelledby="warnings">
<h2 id="warnings">Avertissements</h2>
<ul>
<li>
<strong>Incompatibilité:</strong> Modifier ces paramètres peut rendre
votre wallet incompatible avec les services existants.
</li>
<li>
<strong>Version logicielle:</strong> Certains paramètres peuvent ne pas
être supportés par votre version du wallet.
</li>
<li>
<strong>Recommandation:</strong> Utilisez les valeurs par défaut sauf si
vous avez une raison spécifique de les modifier.
</li>
</ul>
</section>
</main>
);
}

View File

@ -27,53 +27,59 @@ export function DataExportImportScreen(): JSX.Element {
const [disableError, setDisableError] = useState<string | null>(null); const [disableError, setDisableError] = useState<string | null>(null);
const [disableLoading, setDisableLoading] = useState(false); const [disableLoading, setDisableLoading] = useState(false);
const handleEnableProtection = async (e: FormEvent): Promise<void> => { const handleEnableProtection = (e: FormEvent): void => {
e.preventDefault(); e.preventDefault();
setProtectError(null); void (async (): Promise<void> => {
if (protectPassword !== protectConfirm) { setProtectError(null);
setProtectError('Les mots de passe ne correspondent pas.'); if (protectPassword !== protectConfirm) {
return; setProtectError('Les mots de passe ne correspondent pas.');
} return;
if (protectPassword.length < 8) { }
setProtectError('Mot de passe d\'au moins 8 caractères.'); if (protectPassword.length < 8) {
return; setProtectError('Mot de passe d&apos;au moins 8 caractères.');
} return;
setProtectLoading(true); }
try { setProtectLoading(true);
await enableProtection(protectPassword); try {
setProtectPassword(''); await enableProtection(protectPassword);
setProtectConfirm(''); setProtectPassword('');
} catch (err) { setProtectConfirm('');
setProtectError(err instanceof Error ? err.message : 'Erreur'); } catch (err) {
} finally { setProtectError(err instanceof Error ? err.message : 'Erreur');
setProtectLoading(false); } finally {
} setProtectLoading(false);
}
})();
}; };
const handleDisableProtection = async (e: FormEvent): Promise<void> => { const handleDisableProtection = (e: FormEvent): void => {
e.preventDefault(); e.preventDefault();
setDisableError(null); void (async (): Promise<void> => {
setDisableLoading(true); setDisableError(null);
try { setDisableLoading(true);
await disableProtection(disablePassword); try {
setDisablePassword(''); await disableProtection(disablePassword);
} catch (err) { setDisablePassword('');
setDisableError(err instanceof Error ? err.message : 'Erreur'); } catch (err) {
} finally { setDisableError(err instanceof Error ? err.message : 'Erreur');
setDisableLoading(false); } finally {
} setDisableLoading(false);
}
})();
}; };
const [exportError, setExportError] = useState<string | null>(null); const [exportError, setExportError] = useState<string | null>(null);
const handleExport = async (): Promise<void> => { const handleExport = (): void => {
setExportError(null); void (async (): Promise<void> => {
try { setExportError(null);
const json = await exportUserWalletData(); try {
downloadExportFile(json); const json = await exportUserWalletData();
} catch (err) { downloadExportFile(json);
setExportError(err instanceof Error ? err.message : 'Erreur export'); } catch (err) {
} setExportError(err instanceof Error ? err.message : 'Erreur export');
}
})();
}; };
const handleImportClick = (): void => { const handleImportClick = (): void => {
@ -88,18 +94,20 @@ export function DataExportImportScreen(): JSX.Element {
return; return;
} }
const reader = new FileReader(); const reader = new FileReader();
reader.onload = async () => { reader.onload = (): void => {
const text = reader.result; void (async (): Promise<void> => {
if (typeof text !== 'string') { const text = reader.result;
setImportError('Fichier non lisible'); if (typeof text !== 'string') {
return; setImportError('Fichier non lisible');
} return;
try { }
await importUserWalletData(text); try {
window.location.reload(); await importUserWalletData(text);
} catch (err) { window.location.reload();
setImportError(err instanceof Error ? err.message : 'Erreur import'); } catch (err) {
} setImportError(err instanceof Error ? err.message : 'Erreur import');
}
})();
}; };
reader.readAsText(file); reader.readAsText(file);
}; };

View File

@ -80,8 +80,9 @@ export function GlobalActionBar(): JSX.Element {
}; };
const mots = const mots =
(ctx?.offerWords != null && ctx.offerWords.length > 0 ? ctx.offerWords : null) ?? (ctx?.offerWords !== null && ctx?.offerWords !== undefined && ctx.offerWords.length > 0
getLocalPairWords(); ? ctx.offerWords
: null) ?? getLocalPairWords();
const hasMots = mots !== null && mots.length > 0; const hasMots = mots !== null && mots.length > 0;
return ( return (

View File

@ -10,23 +10,25 @@ export function ImportIdentityScreen(): JSX.Element {
const [isImporting, setIsImporting] = useState(false); const [isImporting, setIsImporting] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const handleSubmit = async (e: FormEvent<HTMLFormElement>): Promise<void> => { const handleSubmit = (e: FormEvent<HTMLFormElement>): void => {
e.preventDefault(); e.preventDefault();
setError(null); void (async (): Promise<void> => {
setIsImporting(true); setError(null);
try { setIsImporting(true);
const imported = importExistingIdentity(seedOrKey, name || undefined); try {
if (imported === null) { const imported = importExistingIdentity(seedOrKey, name || undefined);
setError('Import échoué. Vérifiez le format du seed ou de la clé privée.'); if (imported === null) {
} else { setError('Import échoué. Vérifiez le format du seed ou de la clé privée.');
navigate('/'); } else {
navigate('/');
}
} catch (err) {
setError("Erreur lors de l'import");
console.error('Error importing identity:', err);
} finally {
setIsImporting(false);
} }
} catch (err) { })();
setError("Erreur lors de l'import");
console.error('Error importing identity:', err);
} finally {
setIsImporting(false);
}
}; };
return ( return (

View File

@ -6,25 +6,27 @@ export function LoginForm(): JSX.Element {
const { keyPair, isLoading, createAuthResponse } = useAuth(); const { keyPair, isLoading, createAuthResponse } = useAuth();
const [isAuthenticating, setIsAuthenticating] = useState(false); const [isAuthenticating, setIsAuthenticating] = useState(false);
const handleSubmit = async (e: FormEvent<HTMLFormElement>): Promise<void> => { const handleSubmit = (e: FormEvent<HTMLFormElement>): void => {
e.preventDefault(); e.preventDefault();
if (keyPair === null || isLoading) { void (async (): Promise<void> => {
return; if (keyPair === null || isLoading) {
} return;
setIsAuthenticating(true);
try {
const response = createAuthResponse();
if (response !== null) {
sendAuthResponse(response);
} else {
console.error('Failed to create auth response');
} }
} catch (error) {
console.error('Authentication error:', error); setIsAuthenticating(true);
} finally { try {
setIsAuthenticating(false); const response = createAuthResponse();
} if (response !== null) {
sendAuthResponse(response);
} else {
console.error('Failed to create auth response');
}
} catch (error) {
console.error('Authentication error:', error);
} finally {
setIsAuthenticating(false);
}
})();
}; };
if (isLoading) { if (isLoading) {

View File

@ -402,7 +402,7 @@ export function LoginScreen(): JSX.Element {
} }
// Log structuré pour le succès // Log structuré pour le succès
console.info('[Login] Verification succeeded:', { console.warn('[Login] Verification succeeded:', {
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
hash: proof.challenge.hash.slice(0, 16) + '...', hash: proof.challenge.hash.slice(0, 16) + '...',
serviceUuid: loginPath?.service_uuid, serviceUuid: loginPath?.service_uuid,
@ -665,9 +665,7 @@ export function LoginScreen(): JSX.Element {
: '—'} : '—'}
</td> </td>
<td> <td>
{req.cardinalite_minimale !== undefined {req.cardinalite_minimale ?? '1'}
? req.cardinalite_minimale
: '1'}
</td> </td>
<td> <td>
{req.dependances !== undefined && {req.dependances !== undefined &&
@ -830,7 +828,9 @@ export function LoginScreen(): JSX.Element {
<div> <div>
<button <button
type="button" type="button"
onClick={handlePublish} onClick={() => {
void handlePublish();
}}
disabled={isPublishing} disabled={isPublishing}
> >
{isCollecting {isCollecting
@ -870,46 +870,48 @@ export function LoginScreen(): JSX.Element {
loginPath={loginPath} loginPath={loginPath}
collectProgress={collectProgressState} collectProgress={collectProgressState}
collectedSignatures={collectedMerged ?? undefined} collectedSignatures={collectedMerged ?? undefined}
onRefresh={async () => { onRefresh={() => {
if (loginPath === null || proof === null) { void (async (): Promise<void> => {
return; if (loginPath === null || proof === null) {
} return;
setIsCollecting(true); }
try { setIsCollecting(true);
const relays = getStoredRelays().filter((r) => r.enabled); try {
const endpoints = relays.map((r) => r.endpoint); const relays = getStoredRelays().filter((r) => r.enabled);
const pairToMembers = buildPairToMembers(loginPath.pairs_attendus); const endpoints = relays.map((r) => r.endpoint);
const pubkeyToPair = buildPubkeyToPair( const pairToMembers = buildPairToMembers(loginPath.pairs_attendus);
identity?.publicKey ?? '', const pubkeyToPair = buildPubkeyToPair(
loginPath.pairs_attendus, identity?.publicKey ?? '',
); loginPath.pairs_attendus,
const merged = await runCollectLoop( );
endpoints, const merged = await runCollectLoop(
proof.challenge.hash, endpoints,
proof.signatures, proof.challenge.hash,
loginPath, proof.signatures,
pairToMembers, loginPath,
pubkeyToPair, pairToMembers,
{ pubkeyToPair,
pollMs: COLLECT_POLL_MS, {
timeoutMs: COLLECT_TIMEOUT_MS, pollMs: COLLECT_POLL_MS,
onProgress: (m) => { timeoutMs: COLLECT_TIMEOUT_MS,
const p = collectProgress(loginPath, m, pairToMembers); onProgress: (m) => {
setCollectProgressState({ const p = collectProgress(loginPath, m, pairToMembers);
satisfied: p.satisfied, setCollectProgressState({
required: p.required, satisfied: p.satisfied,
}); required: p.required,
});
},
}, },
}, );
); setCollectedMerged(merged);
setCollectedMerged(merged); } finally {
} finally { setIsCollecting(false);
setIsCollecting(false); }
} })();
}} }}
onViewDetails={(requirement, pairUuid) => { onViewDetails={(requirement, pairUuid) => {
// Afficher les détails de la signature dans une alerte ou un modal // Afficher les détails de la signature dans une alerte ou un modal
const detail = `Membre: ${requirement}\nPair: ${pairUuid !== undefined ? pairUuid : 'N/A'}\nHash: ${proof.challenge.hash.slice(0, 16)}...\nNonce: ${proof.challenge.nonce.slice(0, 16)}...`; const detail = `Membre: ${requirement}\nPair: ${pairUuid ?? 'N/A'}\nHash: ${proof.challenge.hash.slice(0, 16)}...\nNonce: ${proof.challenge.nonce.slice(0, 16)}...`;
alert(detail); alert(detail);
}} }}
/> />

View File

@ -31,8 +31,8 @@ export function MemberSelectionScreen(): JSX.Element {
handleError('Service UUID requis', 'MISSING_SERVICE'); handleError('Service UUID requis', 'MISSING_SERVICE');
return; return;
} }
loadMembers(); void loadMembers();
}, [serviceUuid]); }, [serviceUuid, handleError]); // eslint-disable-line react-hooks/exhaustive-deps
const loadMembers = async (): Promise<void> => { const loadMembers = async (): Promise<void> => {
if (identity === null || serviceUuid === '') { if (identity === null || serviceUuid === '') {

View File

@ -28,7 +28,7 @@ export function PairingDisplayScreen(): JSX.Element {
useEffect(() => { useEffect(() => {
const w = ensureLocalPairForSetup(); const w = ensureLocalPairForSetup();
setWords2nd(w); setWords2nd(w);
}, []); }, []); // eslint-disable-line react-hooks/exhaustive-deps
useEffect(() => { useEffect(() => {
if (isLoading || identity !== null) { if (isLoading || identity !== null) {
@ -44,70 +44,72 @@ export function PairingDisplayScreen(): JSX.Element {
}; };
}, [ctx, words2nd]); }, [ctx, words2nd]);
const handleSubmit = async (e: FormEvent<HTMLFormElement>): Promise<void> => { const handleSubmit = (e: FormEvent<HTMLFormElement>): void => {
e.preventDefault(); e.preventDefault();
setError(null); void (async (): Promise<void> => {
const wordsText = wordInput.join(' '); setError(null);
const parsed = parseAndValidatePairingWords(wordsText); const wordsText = wordInput.join(' ');
if (parsed === null) { const parsed = parseAndValidatePairingWords(wordsText);
setError('Mots invalides. 8 mots requis.'); if (parsed === null) {
return; setError('Mots invalides. 8 mots requis.');
} return;
const pubkeyHex = pubkey1stInput.trim(); }
if (pubkeyHex.length === 0) { const pubkeyHex = pubkey1stInput.trim();
setError( if (pubkeyHex.length === 0) {
'Pairing DH obligatoire : clé publique du 1ᵉʳ appareil requise (hex, 66 car.).', setError(
); 'Pairing DH obligatoire : clé publique du 1ᵉʳ appareil requise (hex, 66 car.).',
return; );
} return;
if (pubkeyHex.length !== 66 || !['02', '03', '04'].includes(pubkeyHex.slice(0, 2))) { }
setError('Clé publique invalide (hex 66 car., préfixe 02/03/04).'); if (pubkeyHex.length !== 66 || !['02', '03', '04'].includes(pubkeyHex.slice(0, 2))) {
return; setError('Clé publique invalide (hex 66 car., préfixe 02/03/04).');
} return;
const pair = addRemotePairFromWords(parsed, [], pubkeyHex); }
if (pair === null) { const pair = addRemotePairFromWords(parsed, [], pubkeyHex);
setError('Mots invalides. Vérifiez la saisie.'); if (pair === null) {
return; setError('Mots invalides. Vérifiez la saisie.');
} return;
const pairs = getStoredPairs(); }
const local = pairs.find((p) => p.is_local); const pairs = getStoredPairs();
const remote = pairs.find((p) => !p.is_local); const local = pairs.find((p) => p.is_local);
if ( const remote = pairs.find((p) => !p.is_local);
identity === null || if (
local === undefined || identity === null ||
remote === undefined || local === undefined ||
identity.privateKey === undefined remote === undefined ||
) { identity.privateKey === undefined
setSuccess(true); ) {
return; setSuccess(true);
} return;
const relays = getStoredRelays().filter((r) => r.enabled); }
if (relays.length === 0) { const relays = getStoredRelays().filter((r) => r.enabled);
setError('Aucun relais activé. Configurez les relais pour finaliser le pairing.'); if (relays.length === 0) {
return; setError('Aucun relais activé. Configurez les relais pour finaliser le pairing.');
} return;
setIsConfirming(true); }
try { setIsConfirming(true);
const ok = await runDevice2Confirmation( try {
local.uuid, const ok = await runDevice2Confirmation(
remote.uuid, local.uuid,
identity, remote.uuid,
relays, identity,
identity.t0_anniversaire, relays,
Date.now(), identity.t0_anniversaire,
pubkeyHex, Date.now(),
); pubkeyHex,
setJustConnected(ok); );
} catch (err) { setJustConnected(ok);
console.error('Pairing confirmation (device 2):', err); } catch (err) {
setError( console.error('Pairing confirmation (device 2):', err);
err instanceof Error ? err.message : 'Erreur lors de la confirmation du pairing.', setError(
); err instanceof Error ? err.message : 'Erreur lors de la confirmation du pairing.',
);
setIsConfirming(false);
return;
}
setIsConfirming(false); setIsConfirming(false);
return; setSuccess(true);
} })();
setIsConfirming(false);
setSuccess(true);
}; };
if (success) { if (success) {

View File

@ -36,20 +36,26 @@ export function RelaySettingsScreen(): JSX.Element {
const handleToggle = (index: number): void => { const handleToggle = (index: number): void => {
const updated = [...relays]; const updated = [...relays];
updated[index] = { ...updated[index]!, enabled: !updated[index]!.enabled }; const relay = updated[index];
if (relay === undefined) {
return;
}
updated[index] = { ...relay, enabled: !relay.enabled };
setRelays(updated); setRelays(updated);
storeRelays(updated); storeRelays(updated);
}; };
const handleTest = async (endpoint: string): Promise<void> => { const handleTest = (endpoint: string): void => {
setTesting(endpoint); void (async (): Promise<void> => {
const isOk = await testRelay(endpoint); setTesting(endpoint);
setTesting(null); const isOk = await testRelay(endpoint);
if (isOk) { setTesting(null);
alert('Relais accessible'); if (isOk) {
} else { alert('Relais accessible');
alert('Relais inaccessible'); } else {
} alert('Relais inaccessible');
}
})();
}; };
return ( return (
@ -114,7 +120,22 @@ export function RelaySettingsScreen(): JSX.Element {
</div> </div>
</section> </section>
<div> <div>
<button onClick={() => navigate('/')}>Retour</button> <button
type="button"
onClick={() => {
navigate('/');
}}
>
Retour
</button>
<button
type="button"
onClick={() => {
navigate('/crypto-settings');
}}
>
Paramètres crypto
</button>
</div> </div>
</main> </main>
); );

View File

@ -28,10 +28,6 @@ export function ServiceListScreen(): JSX.Element {
} }
}, [location.pathname, loginState, dispatch]); }, [location.pathname, loginState, dispatch]);
useEffect(() => {
loadServices();
}, []);
const loadServices = async (): Promise<void> => { const loadServices = async (): Promise<void> => {
if (identity === null) { if (identity === null) {
return; return;
@ -42,7 +38,7 @@ export function ServiceListScreen(): JSX.Element {
try { try {
const relays = getStoredRelays().filter((r) => r.enabled); const relays = getStoredRelays().filter((r) => r.enabled);
if (relays.length === 0) { if (relays.length === 0) {
handleError('Aucun relais activé. Synchronisez d\'abord.', 'NO_RELAYS'); handleError('Aucun relais activé. Synchronisez d&apos;abord.', 'NO_RELAYS');
return; return;
} }