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:
parent
13898d1012
commit
695aff4f85
410
api-relay/package-lock.json
generated
410
api-relay/package-lock.json
generated
@ -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": {
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
58
api-relay/src/middleware/auth.ts
Normal file
58
api-relay/src/middleware/auth.ts
Normal 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();
|
||||||
|
};
|
||||||
|
}
|
||||||
82
api-relay/src/scripts/migrate-to-db.ts
Normal file
82
api-relay/src/scripts/migrate-to-db.ts
Normal 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);
|
||||||
|
});
|
||||||
57
api-relay/src/services/apiKeyService.ts
Normal file
57
api-relay/src/services/apiKeyService.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
479
api-relay/src/services/database.ts
Normal file
479
api-relay/src/services/database.ts
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
88
api-relay/src/services/storageAdapter.ts
Normal file
88
api-relay/src/services/storageAdapter.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
|||||||
221
features/OPTIONS_NON_IMPLENTEES.md
Normal file
221
features/OPTIONS_NON_IMPLENTEES.md
Normal 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.
|
||||||
1
service-login-verify/dist/index.d.ts
vendored
1
service-login-verify/dist/index.d.ts
vendored
@ -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
|
||||||
2
service-login-verify/dist/index.d.ts.map
vendored
2
service-login-verify/dist/index.d.ts.map
vendored
@ -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"}
|
||||||
1
service-login-verify/dist/index.js
vendored
1
service-login-verify/dist/index.js
vendored
@ -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';
|
||||||
|
|||||||
36
service-login-verify/dist/persistentNonceCache.d.ts
vendored
Normal file
36
service-login-verify/dist/persistentNonceCache.d.ts
vendored
Normal 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
|
||||||
1
service-login-verify/dist/persistentNonceCache.d.ts.map
vendored
Normal file
1
service-login-verify/dist/persistentNonceCache.d.ts.map
vendored
Normal 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"}
|
||||||
107
service-login-verify/dist/persistentNonceCache.js
vendored
Normal file
107
service-login-verify/dist/persistentNonceCache.js
vendored
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
service-login-verify/dist/types.d.ts
vendored
2
service-login-verify/dist/types.d.ts
vendored
@ -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
|
||||||
2
service-login-verify/dist/types.d.ts.map
vendored
2
service-login-verify/dist/types.d.ts.map
vendored
@ -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"}
|
||||||
@ -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
|
||||||
@ -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"}
|
||||||
39
service-login-verify/dist/verifyLoginProof.js
vendored
39
service-login-verify/dist/verifyLoginProof.js
vendored
@ -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,
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
123
service-login-verify/src/persistentNonceCache.ts
Normal file
123
service-login-verify/src/persistentNonceCache.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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",
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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'identité');
|
||||||
setIsCreating(false);
|
} finally {
|
||||||
}
|
setIsCreating(false);
|
||||||
|
}
|
||||||
|
})();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
216
userwallet/src/components/CryptoSettingsScreen.tsx
Normal file
216
userwallet/src/components/CryptoSettingsScreen.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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'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);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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 (
|
||||||
|
|||||||
@ -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 (
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -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 === '') {
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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'abord.', 'NO_RELAYS');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user