diff --git a/.github/workflows/build-react-wallet-webapp.yml b/.github/workflows/build-react-wallet-webapp.yml index fd7305843..b36ad6230 100644 --- a/.github/workflows/build-react-wallet-webapp.yml +++ b/.github/workflows/build-react-wallet-webapp.yml @@ -4,7 +4,7 @@ on: push: jobs: - build-android-webapp: + build-react-wallet-webapp: runs-on: ubuntu-latest steps: @@ -22,13 +22,14 @@ jobs: - name: Compile TypeScript run: npm run build - - name: Install dependencies - run: | - cd ./apps/react-wallet - npm ci + - name: Install react-wallet dependencies + working-directory: ./apps/react-wallet + run: npm ci - - name: Build extension - run: | - cd ./apps/react-wallet - npm run build + - name: Typecheck react-wallet webapp + working-directory: ./apps/react-wallet + run: npm run typecheck + - name: Build react-wallet webapp + working-directory: ./apps/react-wallet + run: npm run build diff --git a/apps/chrome-extension/package-lock.json b/apps/chrome-extension/package-lock.json index e35c39b99..ab242cdba 100644 --- a/apps/chrome-extension/package-lock.json +++ b/apps/chrome-extension/package-lock.json @@ -35,122 +35,10 @@ "webpack-merge": "^6.0.1" } }, - "../../node_modules/@mdip/cipher": { - "version": "0.5.0", - "extraneous": true, - "license": "MIT", - "dependencies": { - "@mdip/browser-hdkey": "^0.1.8", - "@noble/ciphers": "^0.4.1", - "@noble/hashes": "^1.3.3", - "@noble/secp256k1": "^2.0.0", - "bip39": "^3.1.0", - "buffer": "^6.0.3", - "canonicalize": "^2.0.0", - "hdkey": "^2.1.0", - "multiformats": "^13.0.0" - } - }, - "../../node_modules/@mdip/common": { - "version": "0.5.0", - "extraneous": true, - "license": "MIT" - }, - "../../node_modules/@mdip/gatekeeper": { - "version": "0.5.0", - "extraneous": true, - "license": "MIT", - "dependencies": { - "@mdip/cipher": "*", - "@mdip/common": "*", - "@mdip/ipfs": "*", - "axios": "^1.7.7", - "canonicalize": "^2.0.0", - "dotenv": "^16.4.5", - "ioredis": "^5.4.1", - "mongodb": "^6.5.0", - "sqlite": "^5.1.1", - "sqlite3": "^5.1.7" - } - }, - "../../node_modules/@mdip/keymaster": { - "version": "0.5.0", - "extraneous": true, - "license": "MIT", - "dependencies": { - "@mdip/common": "*", - "axios": "^1.7.7" - } - }, - "../../packages/cipher": { - "name": "@mdip/cipher", - "version": "0.5.0", - "extraneous": true, - "license": "MIT", - "dependencies": { - "@mdip/browser-hdkey": "^0.1.8", - "@noble/ciphers": "^0.4.1", - "@noble/hashes": "^1.3.3", - "@noble/secp256k1": "^2.0.0", - "bip39": "^3.1.0", - "buffer": "^6.0.3", - "canonicalize": "^2.0.0", - "hdkey": "^2.1.0", - "multiformats": "^13.0.0" - } - }, - "../../packages/common": { - "name": "@mdip/common", - "version": "0.5.0", - "extraneous": true, - "license": "MIT" - }, - "../../packages/gatekeeper": { - "name": "@mdip/gatekeeper", - "version": "0.5.0", - "extraneous": true, - "license": "MIT", - "dependencies": { - "@mdip/cipher": "*", - "@mdip/common": "*", - "@mdip/ipfs": "*", - "axios": "^1.7.7", - "canonicalize": "^2.0.0", - "dotenv": "^16.4.5", - "ioredis": "^5.4.1", - "mongodb": "^6.5.0", - "sqlite": "^5.1.1", - "sqlite3": "^5.1.7" - } - }, - "../../packages/ipfs": { - "name": "@mdip/ipfs", - "version": "0.5.0", - "extraneous": true, - "license": "MIT", - "dependencies": { - "@helia/json": "^2.0.0", - "axios": "^1.7.7", - "blockstore-fs": "^2.0.2", - "helia": "^3.0.0", - "multiformats": "^13.0.0" - } - }, - "../../packages/keymaster": { - "name": "@mdip/keymaster", - "version": "0.5.0", - "extraneous": true, - "license": "MIT", - "dependencies": { - "@mdip/common": "*", - "axios": "^1.7.7" - } - }, "node_modules/@babel/code-frame": { "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", - "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", "js-tokens": "^4.0.0", @@ -162,9 +50,8 @@ }, "node_modules/@babel/generator": { "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.3.tgz", - "integrity": "sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ==", "dev": true, + "license": "MIT", "dependencies": { "@babel/parser": "^7.26.3", "@babel/types": "^7.26.3", @@ -178,9 +65,8 @@ }, "node_modules/@babel/helper-module-imports": { "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", - "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", "dev": true, + "license": "MIT", "dependencies": { "@babel/traverse": "^7.25.9", "@babel/types": "^7.25.9" @@ -191,27 +77,24 @@ }, "node_modules/@babel/helper-string-parser": { "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", - "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", - "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.3.tgz", - "integrity": "sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==", "dev": true, + "license": "MIT", "dependencies": { "@babel/types": "^7.26.3" }, @@ -223,9 +106,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", - "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", "dev": true, "license": "MIT", "engines": { @@ -234,9 +117,8 @@ }, "node_modules/@babel/template": { "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", - "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", "dev": true, + "license": "MIT", "dependencies": { "@babel/code-frame": "^7.25.9", "@babel/parser": "^7.25.9", @@ -248,9 +130,8 @@ }, "node_modules/@babel/traverse": { "version": "7.26.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.4.tgz", - "integrity": "sha512-fH+b7Y4p3yqvApJALCPJcwb0/XaOSgtK4pzV6WVjPR5GLFQBRI7pfoX2V2iM48NXvX07NUxxm1Vw98YjqTcU5w==", "dev": true, + "license": "MIT", "dependencies": { "@babel/code-frame": "^7.26.2", "@babel/generator": "^7.26.3", @@ -266,9 +147,8 @@ }, "node_modules/@babel/types": { "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz", - "integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" @@ -279,8 +159,6 @@ }, "node_modules/@discoveryjs/json-ext": { "version": "0.6.3", - "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.6.3.tgz", - "integrity": "sha512-4B4OijXeVNOPZlYA2oEwWOTkzyltLao+xbotHQeqN++Rv27Y6s818+n2Qkp8q+Fxhn0t/5lA5X1Mxktud8eayQ==", "dev": true, "license": "MIT", "engines": { @@ -289,9 +167,8 @@ }, "node_modules/@emotion/babel-plugin": { "version": "11.13.5", - "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", - "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.16.7", "@babel/runtime": "^7.18.3", @@ -308,18 +185,16 @@ }, "node_modules/@emotion/babel-plugin/node_modules/source-map": { "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, "node_modules/@emotion/cache": { "version": "11.14.0", - "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", - "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", "dev": true, + "license": "MIT", "dependencies": { "@emotion/memoize": "^0.9.0", "@emotion/sheet": "^1.4.0", @@ -330,30 +205,26 @@ }, "node_modules/@emotion/hash": { "version": "0.9.2", - "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", - "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@emotion/is-prop-valid": { "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.3.1.tgz", - "integrity": "sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw==", "dev": true, + "license": "MIT", "dependencies": { "@emotion/memoize": "^0.9.0" } }, "node_modules/@emotion/memoize": { "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", - "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@emotion/react": { "version": "11.14.0", - "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", - "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", "dev": true, + "license": "MIT", "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -375,9 +246,8 @@ }, "node_modules/@emotion/serialize": { "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", - "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", "dev": true, + "license": "MIT", "dependencies": { "@emotion/hash": "^0.9.2", "@emotion/memoize": "^0.9.0", @@ -388,14 +258,11 @@ }, "node_modules/@emotion/sheet": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", - "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@emotion/styled": { "version": "11.14.1", - "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", - "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", "dev": true, "license": "MIT", "dependencies": { @@ -418,30 +285,26 @@ }, "node_modules/@emotion/unitless": { "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", - "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@emotion/use-insertion-effect-with-fallbacks": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", - "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==", "dev": true, + "license": "MIT", "peerDependencies": { "react": ">=16.8.0" } }, "node_modules/@emotion/utils": { "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", - "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@emotion/weak-memoize": { "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", - "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@fontsource/roboto": { "version": "5.2.10", @@ -455,9 +318,8 @@ }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", - "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -469,27 +331,24 @@ }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/set-array": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/source-map": { "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", - "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" @@ -497,15 +356,13 @@ }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -799,18 +656,17 @@ }, "node_modules/@popperjs/core": { "version": "2.11.8", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", - "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", "dev": true, + "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/popperjs" } }, "node_modules/@types/chrome": { - "version": "0.1.37", - "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.1.37.tgz", - "integrity": "sha512-IJE4ceuDO7lrEuua7Pow47zwNcI8E6qqkowRP7aFPaZ0lrjxh6y836OPqqkIZeTX64FTogbw+4RNH0+QrweCTQ==", + "version": "0.1.38", + "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.1.38.tgz", + "integrity": "sha512-5aK4m9wZqoWAoB98aElESLm/5pXpqJnFWMNoiCs/XdPsXR6wNdVkJFSdQ9Wr4PnTuUrxD0SuNuDHh3EG5QeBzA==", "dev": true, "license": "MIT", "dependencies": { @@ -820,9 +676,8 @@ }, "node_modules/@types/eslint": { "version": "9.6.1", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", - "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "dev": true, + "license": "MIT", "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -830,9 +685,8 @@ }, "node_modules/@types/eslint-scope": { "version": "3.7.7", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", - "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", "dev": true, + "license": "MIT", "dependencies": { "@types/eslint": "*", "@types/estree": "*" @@ -840,31 +694,26 @@ }, "node_modules/@types/estree": { "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true, "license": "MIT" }, "node_modules/@types/filesystem": { "version": "0.0.36", - "resolved": "https://registry.npmjs.org/@types/filesystem/-/filesystem-0.0.36.tgz", - "integrity": "sha512-vPDXOZuannb9FZdxgHnqSwAG/jvdGM8Wq+6N4D/d80z+D4HWH+bItqsZaVRQykAn6WEVeEkLm2oQigyHtgb0RA==", "dev": true, + "license": "MIT", "dependencies": { "@types/filewriter": "*" } }, "node_modules/@types/filewriter": { "version": "0.0.33", - "resolved": "https://registry.npmjs.org/@types/filewriter/-/filewriter-0.0.33.tgz", - "integrity": "sha512-xFU8ZXTw4gd358lb2jw25nxY9QAgqn2+bKKjKOYfNCzN4DKCFetK7sPtrlpg66Ywe3vWY9FNxprZawAh9wfJ3g==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/glob": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", "dev": true, + "license": "MIT", "dependencies": { "@types/minimatch": "*", "@types/node": "*" @@ -872,42 +721,36 @@ }, "node_modules/@types/har-format": { "version": "1.2.16", - "resolved": "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.16.tgz", - "integrity": "sha512-fluxdy7ryD3MV6h8pTfTYpy/xQzCFC7m89nOH9y94cNqJ1mDIDPut7MnRHI3F6qRmh/cT2fUjG1MLdCNb4hE9A==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/html-minifier-terser": { "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", - "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/json-schema": { "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/minimatch": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", - "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/node": { "version": "22.10.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz", - "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==", "dev": true, + "license": "MIT", "dependencies": { "undici-types": "~6.20.0" } }, "node_modules/@types/parse-json": { "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", - "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/prop-types": { "version": "15.7.15", @@ -928,8 +771,6 @@ }, "node_modules/@types/react-dom": { "version": "19.2.3", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", - "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -938,9 +779,8 @@ }, "node_modules/@types/react-transition-group": { "version": "4.4.12", - "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", - "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", "dev": true, + "license": "MIT", "peerDependencies": { "@types/react": "*" } @@ -962,9 +802,8 @@ }, "node_modules/@webassemblyjs/ast": { "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", - "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", "dev": true, + "license": "MIT", "dependencies": { "@webassemblyjs/helper-numbers": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2" @@ -972,27 +811,23 @@ }, "node_modules/@webassemblyjs/floating-point-hex-parser": { "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", - "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@webassemblyjs/helper-api-error": { "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", - "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@webassemblyjs/helper-buffer": { "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", - "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@webassemblyjs/helper-numbers": { "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", - "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", "dev": true, + "license": "MIT", "dependencies": { "@webassemblyjs/floating-point-hex-parser": "1.13.2", "@webassemblyjs/helper-api-error": "1.13.2", @@ -1001,15 +836,13 @@ }, "node_modules/@webassemblyjs/helper-wasm-bytecode": { "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", - "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@webassemblyjs/helper-wasm-section": { "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", - "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", "dev": true, + "license": "MIT", "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -1019,33 +852,29 @@ }, "node_modules/@webassemblyjs/ieee754": { "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", - "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", "dev": true, + "license": "MIT", "dependencies": { "@xtuc/ieee754": "^1.2.0" } }, "node_modules/@webassemblyjs/leb128": { "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", - "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", "dev": true, + "license": "Apache-2.0", "dependencies": { "@xtuc/long": "4.2.2" } }, "node_modules/@webassemblyjs/utf8": { "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", - "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@webassemblyjs/wasm-edit": { "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", - "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", "dev": true, + "license": "MIT", "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -1059,9 +888,8 @@ }, "node_modules/@webassemblyjs/wasm-gen": { "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", - "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", "dev": true, + "license": "MIT", "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", @@ -1072,9 +900,8 @@ }, "node_modules/@webassemblyjs/wasm-opt": { "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", - "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", "dev": true, + "license": "MIT", "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -1084,9 +911,8 @@ }, "node_modules/@webassemblyjs/wasm-parser": { "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", - "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", "dev": true, + "license": "MIT", "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-api-error": "1.13.2", @@ -1098,9 +924,8 @@ }, "node_modules/@webassemblyjs/wast-printer": { "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", - "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", "dev": true, + "license": "MIT", "dependencies": { "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" @@ -1108,8 +933,6 @@ }, "node_modules/@webpack-cli/configtest": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-3.0.1.tgz", - "integrity": "sha512-u8d0pJ5YFgneF/GuvEiDA61Tf1VDomHHYMjv/wc9XzYj7nopltpG96nXN5dJRstxZhcNpV1g+nT6CydO7pHbjA==", "dev": true, "license": "MIT", "engines": { @@ -1122,8 +945,6 @@ }, "node_modules/@webpack-cli/info": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-3.0.1.tgz", - "integrity": "sha512-coEmDzc2u/ffMvuW9aCjoRzNSPDl/XLuhPdlFRpT9tZHmJ/039az33CE7uH+8s0uL1j5ZNtfdv0HkfaKRBGJsQ==", "dev": true, "license": "MIT", "engines": { @@ -1136,8 +957,6 @@ }, "node_modules/@webpack-cli/serve": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-3.0.1.tgz", - "integrity": "sha512-sbgw03xQaCLiT6gcY/6u3qBDn01CWw/nbaXl3gTdTFuJJ75Gffv3E3DBpgvY2fkkrdS1fpjaXNOmJlnbtKauKg==", "dev": true, "license": "MIT", "engines": { @@ -1155,15 +974,13 @@ }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "dev": true + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/@xtuc/long": { "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "dev": true + "dev": true, + "license": "Apache-2.0" }, "node_modules/acorn": { "version": "8.16.0", @@ -1180,8 +997,6 @@ }, "node_modules/acorn-import-phases": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", - "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", "dev": true, "license": "MIT", "engines": { @@ -1193,8 +1008,6 @@ }, "node_modules/ajv": { "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", "dependencies": { @@ -1210,9 +1023,8 @@ }, "node_modules/ajv-formats": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "dev": true, + "license": "MIT", "dependencies": { "ajv": "^8.0.0" }, @@ -1227,8 +1039,6 @@ }, "node_modules/ajv-keywords": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", "dev": true, "license": "MIT", "dependencies": { @@ -1240,18 +1050,16 @@ }, "node_modules/ansi-regex": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/ansi-styles": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, + "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -1264,9 +1072,8 @@ }, "node_modules/array-union": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", - "integrity": "sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng==", "dev": true, + "license": "MIT", "dependencies": { "array-uniq": "^1.0.1" }, @@ -1276,18 +1083,16 @@ }, "node_modules/array-uniq": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", - "integrity": "sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/babel-plugin-macros": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", - "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", "dev": true, + "license": "MIT", "dependencies": { "@babel/runtime": "^7.12.5", "cosmiconfig": "^7.0.0", @@ -1300,14 +1105,11 @@ }, "node_modules/balanced-match": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "dev": true, + "license": "MIT" }, "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==", "dev": true, "funding": [ { @@ -1322,12 +1124,13 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", - "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "version": "2.10.10", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.10.tgz", + "integrity": "sha512-sUoJ3IMxx4AyRqO4MLeHlnGDkyXRoUG0/AI9fjK+vS72ekpV0yWVY7O0BVjmBcRtkNcsAO2QDZ4tdKKGoI6YaQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -1339,15 +1142,13 @@ }, "node_modules/boolbase": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/brace-expansion": { "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -1355,9 +1156,8 @@ }, "node_modules/braces": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, + "license": "MIT", "dependencies": { "fill-range": "^7.1.1" }, @@ -1401,8 +1201,6 @@ }, "node_modules/buffer": { "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", "dev": true, "funding": [ { @@ -1418,6 +1216,7 @@ "url": "https://feross.org/support" } ], + "license": "MIT", "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" @@ -1425,33 +1224,30 @@ }, "node_modules/buffer-from": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/callsites": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/camel-case": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", - "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", "dev": true, + "license": "MIT", "dependencies": { "pascal-case": "^3.1.2", "tslib": "^2.0.3" } }, "node_modules/caniuse-lite": { - "version": "1.0.30001778", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001778.tgz", - "integrity": "sha512-PN7uxFL+ExFJO61aVmP1aIEG4i9whQd4eoSCebav62UwDyp5OHh06zN4jqKSMePVgxHifCw1QJxdRkA1Pisekg==", + "version": "1.0.30001781", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001781.tgz", + "integrity": "sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw==", "dev": true, "funding": [ { @@ -1471,9 +1267,8 @@ }, "node_modules/chalk": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -1487,9 +1282,8 @@ }, "node_modules/chalk/node_modules/supports-color": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -1499,18 +1293,16 @@ }, "node_modules/chrome-trace-event": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", - "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.0" } }, "node_modules/clean-css": { "version": "5.3.3", - "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", - "integrity": "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==", "dev": true, + "license": "MIT", "dependencies": { "source-map": "~0.6.0" }, @@ -1520,9 +1312,8 @@ }, "node_modules/clean-webpack-plugin": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/clean-webpack-plugin/-/clean-webpack-plugin-4.0.0.tgz", - "integrity": "sha512-WuWE1nyTNAyW5T7oNyys2EN0cfP2fdRxhxnIQWiAp0bMabPdHhoGxM8A6YL2GhqwgrPnnaemVE7nv5XJ2Fhh2w==", "dev": true, + "license": "MIT", "dependencies": { "del": "^4.1.1" }, @@ -1535,9 +1326,8 @@ }, "node_modules/clone-deep": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", - "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", "dev": true, + "license": "MIT", "dependencies": { "is-plain-object": "^2.0.4", "kind-of": "^6.0.2", @@ -1549,18 +1339,16 @@ }, "node_modules/clsx": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/color-convert": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -1570,33 +1358,28 @@ }, "node_modules/color-name": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/colorette": { "version": "2.0.20", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/commander": { "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/concat-map": { "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/convert-source-map": { "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/copy-webpack-plugin": { "version": "14.0.0", @@ -1624,9 +1407,8 @@ }, "node_modules/cosmiconfig": { "version": "7.1.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", - "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", "dev": true, + "license": "MIT", "dependencies": { "@types/parse-json": "^4.0.0", "import-fresh": "^3.2.1", @@ -1640,18 +1422,16 @@ }, "node_modules/cosmiconfig/node_modules/path-type": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/cross-spawn": { "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -1699,9 +1479,8 @@ }, "node_modules/css-select": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", - "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.0.1", @@ -1715,9 +1494,8 @@ }, "node_modules/css-what": { "version": "6.1.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", - "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">= 6" }, @@ -1727,9 +1505,8 @@ }, "node_modules/cssesc": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", "dev": true, + "license": "MIT", "bin": { "cssesc": "bin/cssesc" }, @@ -1739,16 +1516,13 @@ }, "node_modules/csstype": { "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "dev": true, "license": "MIT" }, "node_modules/debug": { "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "dev": true, + "license": "MIT", "dependencies": { "ms": "^2.1.3" }, @@ -1763,9 +1537,8 @@ }, "node_modules/del": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/del/-/del-4.1.1.tgz", - "integrity": "sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ==", "dev": true, + "license": "MIT", "dependencies": { "@types/glob": "^7.1.1", "globby": "^6.1.0", @@ -1781,9 +1554,8 @@ }, "node_modules/del/node_modules/globby": { "version": "6.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", - "integrity": "sha512-KVbFv2TQtbzCoxAnfD6JcHZTYCzyliEaaeM/gH8qQdkKr5s0OP9scEgvdcngyk7AVdY6YVW/TJHd+lQ/Df3Daw==", "dev": true, + "license": "MIT", "dependencies": { "array-union": "^1.0.1", "glob": "^7.0.3", @@ -1797,27 +1569,24 @@ }, "node_modules/del/node_modules/globby/node_modules/pify": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/dom-converter": { "version": "0.2.0", - "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", - "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", "dev": true, + "license": "MIT", "dependencies": { "utila": "~0.4" } }, "node_modules/dom-helpers": { "version": "5.2.1", - "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", - "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", "dev": true, + "license": "MIT", "dependencies": { "@babel/runtime": "^7.8.7", "csstype": "^3.0.2" @@ -1825,9 +1594,8 @@ }, "node_modules/dom-serializer": { "version": "1.4.1", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", - "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", "dev": true, + "license": "MIT", "dependencies": { "domelementtype": "^2.0.1", "domhandler": "^4.2.0", @@ -1839,21 +1607,19 @@ }, "node_modules/domelementtype": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", "dev": true, "funding": [ { "type": "github", "url": "https://github.com/sponsors/fb55" } - ] + ], + "license": "BSD-2-Clause" }, "node_modules/domhandler": { "version": "4.3.1", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", - "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "domelementtype": "^2.2.0" }, @@ -1866,9 +1632,8 @@ }, "node_modules/domutils": { "version": "2.8.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", - "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "dom-serializer": "^1.0.1", "domelementtype": "^2.2.0", @@ -1880,25 +1645,24 @@ }, "node_modules/dot-case": { "version": "3.0.4", - "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", - "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", "dev": true, + "license": "MIT", "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" } }, "node_modules/electron-to-chromium": { - "version": "1.5.307", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.307.tgz", - "integrity": "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==", + "version": "1.5.321", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.321.tgz", + "integrity": "sha512-L2C7Q279W2D/J4PLZLk7sebOILDSWos7bMsMNN06rK482umHUrh/3lM8G7IlHFOYip2oAg5nha1rCMxr/rs6ZQ==", "dev": true, "license": "ISC" }, "node_modules/enhanced-resolve": { - "version": "5.20.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz", - "integrity": "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==", + "version": "5.20.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", + "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", "dev": true, "license": "MIT", "dependencies": { @@ -1911,18 +1675,16 @@ }, "node_modules/entities": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", - "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", "dev": true, + "license": "BSD-2-Clause", "funding": { "url": "https://github.com/fb55/entities?sponsor=1" } }, "node_modules/envinfo": { "version": "7.14.0", - "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.14.0.tgz", - "integrity": "sha512-CO40UI41xDQzhLB1hWyqUKgFhs250pNcGbyGKe1l/e4FSaI/+YE4IMG76GDt0In67WLPACIITC+sOi08x4wIvg==", "dev": true, + "license": "MIT", "bin": { "envinfo": "dist/cli.js" }, @@ -1932,9 +1694,8 @@ }, "node_modules/error-ex": { "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", "dev": true, + "license": "MIT", "dependencies": { "is-arrayish": "^0.2.1" } @@ -1958,9 +1719,8 @@ }, "node_modules/escape-string-regexp": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -1970,9 +1730,8 @@ }, "node_modules/eslint-scope": { "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -1983,9 +1742,8 @@ }, "node_modules/esrecurse": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" }, @@ -1995,57 +1753,50 @@ }, "node_modules/esrecurse/node_modules/estraverse": { "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=4.0" } }, "node_modules/estraverse": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=4.0" } }, "node_modules/events": { "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.8.x" } }, "node_modules/fast-deep-equal": { "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/fast-uri": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", - "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==", - "dev": true + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/fastest-levenshtein": { "version": "1.0.16", - "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", - "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 4.9.1" } }, "node_modules/fill-range": { "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, + "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -2055,15 +1806,13 @@ }, "node_modules/find-root": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", - "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/find-up": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "dev": true, + "license": "MIT", "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" @@ -2074,34 +1823,29 @@ }, "node_modules/flat": { "version": "5.0.2", - "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", - "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", "dev": true, + "license": "BSD-3-Clause", "bin": { "flat": "cli.js" } }, "node_modules/fs.realpath": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/function-bind": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/glob": { "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, + "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -2119,9 +1863,8 @@ }, "node_modules/glob-parent": { "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, + "license": "ISC", "dependencies": { "is-glob": "^4.0.3" }, @@ -2138,33 +1881,29 @@ }, "node_modules/globals": { "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/graceful-fs": { "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/has-flag": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/hasown": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dev": true, + "license": "MIT", "dependencies": { "function-bind": "^1.1.2" }, @@ -2174,33 +1913,29 @@ }, "node_modules/he": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "dev": true, + "license": "MIT", "bin": { "he": "bin/he" } }, "node_modules/hoist-non-react-statics": { "version": "3.3.2", - "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", - "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "react-is": "^16.7.0" } }, "node_modules/hoist-non-react-statics/node_modules/react-is": { "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/html-minifier-terser": { "version": "6.1.0", - "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", - "integrity": "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==", "dev": true, + "license": "MIT", "dependencies": { "camel-case": "^4.1.2", "clean-css": "^5.2.2", @@ -2219,9 +1954,8 @@ }, "node_modules/html-minifier-terser/node_modules/commander": { "version": "8.3.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", - "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", "dev": true, + "license": "MIT", "engines": { "node": ">= 12" } @@ -2261,8 +1995,6 @@ }, "node_modules/htmlparser2": { "version": "6.1.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", - "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", "dev": true, "funding": [ "https://github.com/fb55/htmlparser2?sponsor=1", @@ -2271,6 +2003,7 @@ "url": "https://github.com/sponsors/fb55" } ], + "license": "MIT", "dependencies": { "domelementtype": "^2.0.1", "domhandler": "^4.0.0", @@ -2280,9 +2013,8 @@ }, "node_modules/icss-utils": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", - "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", "dev": true, + "license": "ISC", "engines": { "node": "^10 || ^12 || >= 14" }, @@ -2292,8 +2024,6 @@ }, "node_modules/ieee754": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", "dev": true, "funding": [ { @@ -2308,13 +2038,13 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "BSD-3-Clause" }, "node_modules/import-fresh": { "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", "dev": true, + "license": "MIT", "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -2328,18 +2058,16 @@ }, "node_modules/import-fresh/node_modules/resolve-from": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/import-local": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", - "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", "dev": true, + "license": "MIT", "dependencies": { "pkg-dir": "^4.2.0", "resolve-cwd": "^3.0.0" @@ -2356,10 +2084,8 @@ }, "node_modules/inflight": { "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", "dev": true, + "license": "ISC", "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -2367,30 +2093,26 @@ }, "node_modules/inherits": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/interpret": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", - "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=10.13.0" } }, "node_modules/is-arrayish": { "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/is-core-module": { "version": "2.16.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.0.tgz", - "integrity": "sha512-urTSINYfAYgcbLb0yDQ6egFm6h3Mo1DcF9EkyXSRjjzdHbsulg01qhwWuXdOoUBuTkbQ80KDboXa0vFJ+BDH+g==", "dev": true, + "license": "MIT", "dependencies": { "hasown": "^2.0.2" }, @@ -2403,18 +2125,16 @@ }, "node_modules/is-extglob": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/is-glob": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, + "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" }, @@ -2424,27 +2144,24 @@ }, "node_modules/is-number": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.12.0" } }, "node_modules/is-path-cwd": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", - "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/is-path-in-cwd": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz", - "integrity": "sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ==", "dev": true, + "license": "MIT", "dependencies": { "is-path-inside": "^2.1.0" }, @@ -2454,9 +2171,8 @@ }, "node_modules/is-path-inside": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-2.1.0.tgz", - "integrity": "sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg==", "dev": true, + "license": "MIT", "dependencies": { "path-is-inside": "^1.0.2" }, @@ -2466,9 +2182,8 @@ }, "node_modules/is-plain-object": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", "dev": true, + "license": "MIT", "dependencies": { "isobject": "^3.0.1" }, @@ -2478,15 +2193,13 @@ }, "node_modules/isexe": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/isobject": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -2508,15 +2221,13 @@ }, "node_modules/js-tokens": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/jsesc": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "dev": true, + "license": "MIT", "bin": { "jsesc": "bin/jsesc" }, @@ -2526,36 +2237,29 @@ }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/json-schema-traverse": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true, "license": "MIT" }, "node_modules/kind-of": { "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/lines-and-columns": { "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/loader-runner": { "version": "4.3.1", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", - "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", "dev": true, "license": "MIT", "engines": { @@ -2568,9 +2272,8 @@ }, "node_modules/locate-path": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "dev": true, + "license": "MIT", "dependencies": { "p-locate": "^4.1.0" }, @@ -2580,15 +2283,13 @@ }, "node_modules/lodash": { "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/loose-envify": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", "dev": true, + "license": "MIT", "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, @@ -2598,9 +2299,8 @@ }, "node_modules/lower-case": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", - "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", "dev": true, + "license": "MIT", "dependencies": { "tslib": "^2.0.3" } @@ -2614,9 +2314,8 @@ }, "node_modules/micromatch": { "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, + "license": "MIT", "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" @@ -2627,18 +2326,16 @@ }, "node_modules/mime-db": { "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/mime-types": { "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "dev": true, + "license": "MIT", "dependencies": { "mime-db": "1.52.0" }, @@ -2648,9 +2345,8 @@ }, "node_modules/minimatch": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -2660,14 +2356,11 @@ }, "node_modules/ms": { "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/nanoid": { "version": "3.3.8", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", - "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", "dev": true, "funding": [ { @@ -2675,6 +2368,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -2684,15 +2378,13 @@ }, "node_modules/neo-async": { "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/no-case": { "version": "3.0.4", - "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", - "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", "dev": true, + "license": "MIT", "dependencies": { "lower-case": "^2.0.2", "tslib": "^2.0.3" @@ -2707,18 +2399,16 @@ }, "node_modules/normalize-path": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/nth-check": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "boolbase": "^1.0.0" }, @@ -2728,27 +2418,24 @@ }, "node_modules/object-assign": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/once": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dev": true, + "license": "ISC", "dependencies": { "wrappy": "1" } }, "node_modules/p-limit": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "dev": true, + "license": "MIT", "dependencies": { "p-try": "^2.0.0" }, @@ -2761,9 +2448,8 @@ }, "node_modules/p-locate": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", "dev": true, + "license": "MIT", "dependencies": { "p-limit": "^2.2.0" }, @@ -2773,27 +2459,24 @@ }, "node_modules/p-map": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", - "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/p-try": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/param-case": { "version": "3.0.4", - "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", - "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", "dev": true, + "license": "MIT", "dependencies": { "dot-case": "^3.0.4", "tslib": "^2.0.3" @@ -2801,9 +2484,8 @@ }, "node_modules/parent-module": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, + "license": "MIT", "dependencies": { "callsites": "^3.0.0" }, @@ -2813,9 +2495,8 @@ }, "node_modules/parse-json": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", "dev": true, + "license": "MIT", "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", @@ -2831,9 +2512,8 @@ }, "node_modules/pascal-case": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", - "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", "dev": true, + "license": "MIT", "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" @@ -2841,54 +2521,47 @@ }, "node_modules/path-exists": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/path-is-absolute": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/path-is-inside": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", - "integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==", - "dev": true + "dev": true, + "license": "(WTFPL OR MIT)" }, "node_modules/path-key": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/path-parse": { "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/picocolors": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/picomatch": { "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, + "license": "MIT", "engines": { "node": ">=8.6" }, @@ -2898,27 +2571,24 @@ }, "node_modules/pify": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/pinkie": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/pinkie-promise": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", "dev": true, + "license": "MIT", "dependencies": { "pinkie": "^2.0.0" }, @@ -2928,9 +2598,8 @@ }, "node_modules/pkg-dir": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", "dev": true, + "license": "MIT", "dependencies": { "find-up": "^4.0.0" }, @@ -2940,8 +2609,6 @@ }, "node_modules/postcss": { "version": "8.4.49", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", - "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", "dev": true, "funding": [ { @@ -2957,6 +2624,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.1.1", @@ -2968,9 +2636,8 @@ }, "node_modules/postcss-modules-extract-imports": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", - "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", "dev": true, + "license": "ISC", "engines": { "node": "^10 || ^12 || >= 14" }, @@ -2980,9 +2647,8 @@ }, "node_modules/postcss-modules-local-by-default": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", - "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", "dev": true, + "license": "MIT", "dependencies": { "icss-utils": "^5.0.0", "postcss-selector-parser": "^7.0.0", @@ -2997,9 +2663,8 @@ }, "node_modules/postcss-modules-scope": { "version": "3.2.1", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", - "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", "dev": true, + "license": "ISC", "dependencies": { "postcss-selector-parser": "^7.0.0" }, @@ -3012,9 +2677,8 @@ }, "node_modules/postcss-modules-values": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", - "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", "dev": true, + "license": "ISC", "dependencies": { "icss-utils": "^5.0.0" }, @@ -3027,9 +2691,8 @@ }, "node_modules/postcss-selector-parser": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz", - "integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==", "dev": true, + "license": "MIT", "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -3040,15 +2703,13 @@ }, "node_modules/postcss-value-parser": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/pretty-error": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz", - "integrity": "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==", "dev": true, + "license": "MIT", "dependencies": { "lodash": "^4.17.20", "renderkid": "^3.0.0" @@ -3056,18 +2717,16 @@ }, "node_modules/process": { "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6.0" } }, "node_modules/prop-types": { "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", "dev": true, + "license": "MIT", "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -3076,9 +2735,8 @@ }, "node_modules/prop-types/node_modules/react-is": { "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/react": { "version": "19.2.4", @@ -3112,9 +2770,8 @@ }, "node_modules/react-transition-group": { "version": "4.4.5", - "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", - "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "@babel/runtime": "^7.5.5", "dom-helpers": "^5.0.1", @@ -3128,9 +2785,8 @@ }, "node_modules/rechoir": { "version": "0.8.0", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", - "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", "dev": true, + "license": "MIT", "dependencies": { "resolve": "^1.20.0" }, @@ -3140,18 +2796,16 @@ }, "node_modules/relateurl": { "version": "0.2.7", - "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", - "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.10" } }, "node_modules/renderkid": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", - "integrity": "sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==", "dev": true, + "license": "MIT", "dependencies": { "css-select": "^4.1.3", "dom-converter": "^0.2.0", @@ -3162,18 +2816,16 @@ }, "node_modules/require-from-string": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/resolve": { "version": "1.22.9", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.9.tgz", - "integrity": "sha512-QxrmX1DzraFIi9PxdG5VkRfRwIgjwyud+z/iBwfRRrVmHc+P9Q7u2lSSpQ6bjr2gy5lrqIiU9vb6iAeGf2400A==", "dev": true, + "license": "MIT", "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", @@ -3188,9 +2840,8 @@ }, "node_modules/resolve-cwd": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", "dev": true, + "license": "MIT", "dependencies": { "resolve-from": "^5.0.0" }, @@ -3200,19 +2851,16 @@ }, "node_modules/resolve-from": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/rimraf": { "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, + "license": "ISC", "dependencies": { "glob": "^7.1.3" }, @@ -3222,15 +2870,11 @@ }, "node_modules/scheduler": { "version": "0.27.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", - "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", "dev": true, "license": "MIT" }, "node_modules/schema-utils": { "version": "4.3.3", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", - "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "dev": true, "license": "MIT", "dependencies": { @@ -3249,9 +2893,8 @@ }, "node_modules/semver": { "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -3271,9 +2914,8 @@ }, "node_modules/shallow-clone": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", - "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", "dev": true, + "license": "MIT", "dependencies": { "kind-of": "^6.0.2" }, @@ -3283,9 +2925,8 @@ }, "node_modules/shebang-command": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, + "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" }, @@ -3295,36 +2936,32 @@ }, "node_modules/shebang-regex": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/source-map": { "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, "node_modules/source-map-js": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, "node_modules/source-map-support": { "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "dev": true, + "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -3332,9 +2969,8 @@ }, "node_modules/strip-ansi": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" }, @@ -3344,9 +2980,8 @@ }, "node_modules/style-loader": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-4.0.0.tgz", - "integrity": "sha512-1V4WqhhZZgjVAVJyt7TdDPZoPBPNHbekX4fWnCJL1yQukhCeZhJySUL+gL9y6sNdN95uEOS83Y55SqHcP7MzLA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 18.12.0" }, @@ -3360,9 +2995,8 @@ }, "node_modules/stylis": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", - "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/supports-color": { "version": "8.1.1", @@ -3382,9 +3016,8 @@ }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -3394,8 +3027,6 @@ }, "node_modules/tapable": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", - "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", "dev": true, "license": "MIT", "engines": { @@ -3408,9 +3039,8 @@ }, "node_modules/terser": { "version": "5.37.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.37.0.tgz", - "integrity": "sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", @@ -3460,8 +3090,6 @@ }, "node_modules/tinyglobby": { "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3477,8 +3105,6 @@ }, "node_modules/tinyglobby/node_modules/fdir": { "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", "engines": { @@ -3495,8 +3121,6 @@ }, "node_modules/tinyglobby/node_modules/picomatch": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "engines": { @@ -3508,9 +3132,8 @@ }, "node_modules/to-regex-range": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, + "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, @@ -3520,8 +3143,6 @@ }, "node_modules/ts-loader": { "version": "9.5.4", - "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.4.tgz", - "integrity": "sha512-nCz0rEwunlTZiy6rXFByQU1kVVpCIgUpc/psFiKVrUwrizdnIbRFu8w7bxhUF0X613DYwT4XzrZHpVyMe758hQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3541,23 +3162,19 @@ }, "node_modules/ts-loader/node_modules/source-map": { "version": "0.7.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", - "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">= 8" } }, "node_modules/tslib": { "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true + "dev": true, + "license": "0BSD" }, "node_modules/typescript": { "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -3570,9 +3187,8 @@ }, "node_modules/undici-types": { "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/update-browserslist-db": { "version": "1.2.3", @@ -3607,15 +3223,13 @@ }, "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==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/utila": { "version": "0.4.0", - "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", - "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/watchpack": { "version": "2.5.1", @@ -3682,8 +3296,6 @@ }, "node_modules/webpack-cli": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-6.0.1.tgz", - "integrity": "sha512-MfwFQ6SfwinsUVi0rNJm7rHZ31GyTcpVE5pgVA3hwFRb7COD4TzjUUwhGWKfO50+xdc2MQPuEBBJoqIMGt3JDw==", "dev": true, "license": "MIT", "dependencies": { @@ -3725,8 +3337,6 @@ }, "node_modules/webpack-cli/node_modules/commander": { "version": "12.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", - "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", "dev": true, "license": "MIT", "engines": { @@ -3735,9 +3345,8 @@ }, "node_modules/webpack-merge": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-6.0.1.tgz", - "integrity": "sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==", "dev": true, + "license": "MIT", "dependencies": { "clone-deep": "^4.0.1", "flat": "^5.0.2", @@ -3759,9 +3368,8 @@ }, "node_modules/which": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, + "license": "ISC", "dependencies": { "isexe": "^2.0.0" }, @@ -3774,21 +3382,18 @@ }, "node_modules/wildcard": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", - "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/wrappy": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/yaml": { "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", "dev": true, + "license": "ISC", "engines": { "node": ">= 6" } diff --git a/apps/chrome-extension/src/components/WalletTab.tsx b/apps/chrome-extension/src/components/WalletTab.tsx index c64607dab..c5db030fd 100644 --- a/apps/chrome-extension/src/components/WalletTab.tsx +++ b/apps/chrome-extension/src/components/WalletTab.tsx @@ -8,6 +8,7 @@ import { useSnackbar } from "../contexts/SnackbarProvider"; import WarningModal from "../modals/WarningModal"; import MnemonicModal from "../modals/MnemonicModal"; import WalletChrome from "@mdip/keymaster/wallet/chrome"; +import { MdipWalletBundle } from "@mdip/keymaster/types"; const WalletTab = () => { const [open, setOpen] = useState(false); @@ -19,6 +20,7 @@ const WalletTab = () => { const [checkResultMessage, setCheckResultMessage] = useState(""); const { keymaster, + walletProvider, initialiseWallet, handleWalletUploadFile, pendingMnemonic, @@ -46,7 +48,9 @@ const WalletTab = () => { async function createNewWallet() { const chromeWallet = new WalletChrome(); + const providerWallet = new WalletChrome("mdip-wallet-provider"); await chrome.storage.local.remove([chromeWallet.walletName]); + await chrome.storage.local.remove([providerWallet.walletName]); await chrome.runtime.sendMessage({ action: "CLEAR_ALL_STATE"}); await chrome.runtime.sendMessage({ action: "CLEAR_PASSPHRASE"}); await initialiseWallet(); @@ -122,11 +126,11 @@ const WalletTab = () => { }; async function showMnemonic() { - if (!keymaster) { + if (!walletProvider) { return; } try { - const response = await keymaster.decryptMnemonic(); + const response = await walletProvider.decryptMnemonic(); setMnemonicString(response); } catch (error: any) { setError(error); @@ -163,18 +167,25 @@ const WalletTab = () => { } async function downloadWallet() { - if (!keymaster) { + if (!keymaster || !walletProvider) { return; } try { - const wallet = await keymaster.exportEncryptedWallet(); - const walletJSON = JSON.stringify(wallet, null, 4); + const wallet = await keymaster.loadWallet(); + const provider = await walletProvider.backupWallet(); + const bundle: MdipWalletBundle = { + version: 1, + type: "mdip-wallet-bundle", + keymaster: wallet, + provider, + }; + const walletJSON = JSON.stringify(bundle, null, 4); const blob = new Blob([walletJSON], { type: 'application/json' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; - link.download = 'mdip-wallet.json'; + link.download = 'mdip-wallet-bundle.json'; link.click(); URL.revokeObjectURL(url); @@ -209,7 +220,7 @@ const WalletTab = () => { } try { await keymaster.backupWallet(); - setSuccess("Wallet backup successful"); + setSuccess("Wallet metadata backup successful"); } catch (error: any) { setError(error); } diff --git a/apps/chrome-extension/src/contexts/WalletProvider.tsx b/apps/chrome-extension/src/contexts/WalletProvider.tsx index ce6cb81bf..8aadc9461 100644 --- a/apps/chrome-extension/src/contexts/WalletProvider.tsx +++ b/apps/chrome-extension/src/contexts/WalletProvider.tsx @@ -14,17 +14,36 @@ import Keymaster from "@mdip/keymaster"; import SearchClient from "@mdip/keymaster/search"; import CipherWeb from "@mdip/cipher/web"; import WalletChrome from "@mdip/keymaster/wallet/chrome"; -import { isLegacyV0, isV1WithEnc } from '@mdip/keymaster/wallet/typeGuards'; -import { StoredWallet, WalletBase } from "@mdip/keymaster/types"; +import MnemonicHdWalletProvider from "@mdip/keymaster/wallet/mnemonic-hd"; +import { + isLegacyV0, + isV1Decrypted, + isV1WithEnc, + isV2Wallet, +} from "@mdip/keymaster/wallet/typeGuards"; +import { + MdipWalletBundle, + KeymasterStore, + MnemonicHdWalletProviderInterface, + MnemonicHdWalletState, + StoredWallet, + WalletFile, + WalletProviderStore, +} from "@mdip/keymaster/types"; import PassphraseModal from "../modals/PassphraseModal"; import WarningModal from "../modals/WarningModal"; import MnemonicModal from "../modals/MnemonicModal"; -import { encMnemonic } from '@mdip/keymaster/encryption'; +import { encMnemonic } from "@mdip/keymaster/encryption"; import WalletJsonMemory from "@mdip/keymaster/wallet/json-memory"; const gatekeeper = new GatekeeperClient(); const cipher = new CipherWeb(); +const KEYMASTER_STORE_NAME = "mdip-keymaster"; +const WALLET_PROVIDER_STORE_NAME = "mdip-wallet-provider"; + +type UploadAction = "upload-legacy-plain" | "upload-legacy-encrypted" | "upload-bundle"; + interface WalletContextValue { pendingMnemonic: string; setPendingMnemonic: Dispatch>; @@ -37,6 +56,7 @@ interface WalletContextValue { reloadBrowserWallet: () => Promise; refreshFlag: number; keymaster: Keymaster | null; + walletProvider: MnemonicHdWalletProviderInterface | null; } const WalletContext = createContext(null); @@ -45,13 +65,64 @@ let search: SearchClient | undefined; // eslint-disable-next-line sonarjs/no-hardcoded-passwords const INCORRECT_PASSPHRASE = "Incorrect passphrase"; +const INCOMPLETE_WALLET = "Wallet data is incomplete. Restore from an mdip-wallet-bundle or reset the wallet."; + +function createMetadataStore() { + return new WalletChrome(KEYMASTER_STORE_NAME); +} + +function createProviderStore(): WalletProviderStore { + return new WalletChrome(WALLET_PROVIDER_STORE_NAME) as unknown as WalletProviderStore; +} + +function createMemoryProviderStore(): WalletProviderStore { + return new WalletJsonMemory() as unknown as WalletProviderStore; +} + +function createMnemonicWalletProvider( + passphrase: string, + store: WalletProviderStore = createProviderStore(), +) { + return new MnemonicHdWalletProvider({ + store, + cipher, + passphrase, + }); +} + +function isMdipWalletBundle(wallet: unknown): wallet is MdipWalletBundle { + if (!wallet || typeof wallet !== "object") { + return false; + } + + const bundle = wallet as Partial; + return bundle.version === 1 + && bundle.type === "mdip-wallet-bundle" + && isV2Wallet(bundle.keymaster) + && !!bundle.provider + && bundle.provider.version === 1 + && bundle.provider.type === "mnemonic-hd" + && !!bundle.provider.rootPublicJwk; +} + +async function verifyMnemonicAgainstProviderState( + providerState: MnemonicHdWalletState, + mnemonic: string, +) { + const hdKey = cipher.generateHDKey(mnemonic); + const { publicJwk } = cipher.generateJwk(hdKey.privateKey!); + + if (cipher.hashJSON(publicJwk) !== cipher.hashJSON(providerState.rootPublicJwk)) { + throw new Error("Mnemonic does not match wallet."); + } +} export function WalletProvider({ children, isBrowser }: { children: ReactNode, isBrowser: boolean }) { const [passphraseErrorText, setPassphraseErrorText] = useState(""); const [pendingMnemonic, setPendingMnemonic] = useState(""); const [pendingWallet, setPendingWallet] = useState(null); const [modalAction, setModalAction] = useState(null); - const [uploadAction, setUploadAction] = useState(null); + const [uploadAction, setUploadAction] = useState(null); const [isReady, setIsReady] = useState(false); const [showResetConfirm, setShowResetConfirm] = useState(false); const [showResetSetup, setShowResetSetup] = useState(false); @@ -62,29 +133,34 @@ export function WalletProvider({ children, isBrowser }: { children: ReactNode, i const [refreshFlag, setRefreshFlag] = useState(0); const keymasterRef = useRef(null); - - const walletChrome = new WalletChrome(); + const walletProviderRef = useRef(null); useEffect(() => { const initWallet = async () => { await initialiseServices(); await initialiseWallet(); - } + }; initWallet(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); async function initialiseWallet() { - const walletData = await walletChrome.loadWallet(); - - let response = await chrome.runtime.sendMessage({ + const walletStore = createMetadataStore(); + const providerStore = createProviderStore(); + const walletData = await walletStore.loadWallet(); + const providerData = await providerStore.loadWallet(); + const hasIncompleteState = + (!!providerData && !walletData) + || (!!walletData && isV2Wallet(walletData) && !providerData); + + const response = await chrome.runtime.sendMessage({ action: "GET_PASSPHRASE", }); const pass = response?.passphrase || ""; - if (!pendingMnemonic && pass) { - let res = await rebuildKeymaster(pass); + if (!pendingMnemonic && pass && !hasIncompleteState) { + const res = await buildKeymaster(pass); if (res) { return; } @@ -92,11 +168,18 @@ export function WalletProvider({ children, isBrowser }: { children: ReactNode, i await chrome.runtime.sendMessage({ action: "CLEAR_PASSPHRASE" }); } - if (!walletData || pendingMnemonic || isLegacyV0(walletData)) { - // eslint-disable-next-line sonarjs/no-duplicate-string - setModalAction('set-passphrase'); + if (hasIncompleteState) { + setPassphraseErrorText(INCOMPLETE_WALLET); + setModalAction("decrypt"); + return; + } + + if (!walletData || pendingMnemonic || isLegacyV0(walletData) || isV1Decrypted(walletData)) { + setPassphraseErrorText(""); + setModalAction("set-passphrase"); } else { - setModalAction('decrypt'); + setPassphraseErrorText(""); + setModalAction("decrypt"); } } @@ -109,75 +192,130 @@ export function WalletProvider({ children, isBrowser }: { children: ReactNode, i search = await SearchClient.create({ url: searchServerUrl as string }); } - const buildKeymaster = async (wallet: WalletBase, passphrase: string) => { - const instance = new Keymaster({gatekeeper, wallet, cipher, search, passphrase}); + function createKeymaster( + passphrase: string, + store: KeymasterStore = createMetadataStore(), + providerStore: WalletProviderStore = createProviderStore(), + ) { + const walletProvider = createMnemonicWalletProvider(passphrase, providerStore); + const instance = new Keymaster({ + gatekeeper, + store, + walletProvider, + cipher, + search, + }); - if (pendingMnemonic) { - await instance.newWallet(pendingMnemonic, true); - await instance.recoverWallet(); - } else { - try { - // check pass & convert to v1 if needed - await instance.loadWallet(); - } catch { - setPassphraseErrorText(INCORRECT_PASSPHRASE); - return false; - } - } + return { instance, walletProvider }; + } + async function activateWallet( + keymaster: Keymaster, + walletProvider: MnemonicHdWalletProviderInterface, + passphrase: string, + ) { setModalAction(null); setPendingWallet(null); setPendingMnemonic(""); + setRecoveredMnemonic(""); setUploadAction(null); setPassphraseErrorText(""); - keymasterRef.current = instance; - setRefreshFlag(r => r + 1); + keymasterRef.current = keymaster; + walletProviderRef.current = walletProvider; + setRefreshFlag((value) => value + 1); setIsReady(true); await chrome.runtime.sendMessage({ action: "STORE_PASSPHRASE", passphrase, }); + } + + const buildKeymaster = async (passphrase: string) => { + const { instance, walletProvider } = createKeymaster(passphrase); + + try { + if (pendingMnemonic) { + await instance.newWallet(pendingMnemonic, true); + } else { + await instance.loadWallet(); + } + } catch { + setPassphraseErrorText(INCORRECT_PASSPHRASE); + return false; + } + await activateWallet(instance, walletProvider, passphrase); return true; }; - async function rebuildKeymaster(passphrase: string) { - return await buildKeymaster(walletChrome, passphrase); + async function persistWalletData(wallet: WalletFile, providerState: MnemonicHdWalletState) { + const providerStore = createProviderStore(); + const walletStore = createMetadataStore(); + + const providerOk = await providerStore.saveWallet(providerState, true); + if (!providerOk) { + throw new Error("save provider wallet failed"); + } + + const walletOk = await walletStore.saveWallet(wallet, true); + if (!walletOk) { + throw new Error("save wallet failed"); + } + } + + async function importLegacyWallet(wallet: StoredWallet, passphrase: string) { + const memoryStore = new WalletJsonMemory(); + const memoryProviderStore = createMemoryProviderStore(); + const { instance, walletProvider } = createKeymaster(passphrase, memoryStore, memoryProviderStore); + + await memoryStore.saveWallet(wallet, true); + const normalized = await instance.loadWallet(); + const providerState = await walletProvider.backupWallet(); + await persistWalletData(normalized, providerState); + } + + async function importWalletBundle(bundle: MdipWalletBundle, passphrase: string) { + const memoryStore = new WalletJsonMemory(); + const memoryProviderStore = createMemoryProviderStore(); + const { instance, walletProvider } = createKeymaster(passphrase, memoryStore, memoryProviderStore); + + await memoryStore.saveWallet(bundle.keymaster, true); + await walletProvider.saveWallet(bundle.provider, true); + const normalized = await instance.loadWallet(); + const providerState = await walletProvider.backupWallet(); + await persistWalletData(normalized, providerState); } async function handlePassphraseSubmit(passphrase: string) { setPassphraseErrorText(""); - const walletMemory = new WalletJsonMemory(); - if (uploadAction && pendingWallet) { - if (modalAction === 'decrypt') { - await walletMemory.saveWallet(pendingWallet as StoredWallet, true); - - try { - const km = new Keymaster({ gatekeeper, wallet: walletMemory, cipher, search, passphrase }); - // check pass - await km.loadWallet(); - await walletChrome.saveWallet(pendingWallet as StoredWallet, true); - } catch { - setPassphraseErrorText(INCORRECT_PASSPHRASE); - return; + try { + if (uploadAction === "upload-bundle" && isMdipWalletBundle(pendingWallet)) { + await importWalletBundle(pendingWallet, passphrase); + } else { + await importLegacyWallet(pendingWallet as StoredWallet, passphrase); } - } else { // upload-plain-v0 - await walletChrome.saveWallet(pendingWallet as StoredWallet, true); + } catch { + setPassphraseErrorText( + modalAction === "decrypt" ? INCORRECT_PASSPHRASE : "Failed to import wallet." + ); + return; } } - await rebuildKeymaster(passphrase); + await buildKeymaster(passphrase); } async function handlePassphraseClose() { setPendingWallet(null); setPendingMnemonic(""); + setRecoveredMnemonic(""); setPassphraseErrorText(""); - const walletData = await walletChrome.loadWallet(); - if (walletData) { + const walletData = await createMetadataStore().loadWallet(); + const providerData = await createProviderStore().loadWallet(); + if (walletData || providerData) { setModalAction(null); } } @@ -187,7 +325,7 @@ export function WalletProvider({ children, isBrowser }: { children: ReactNode, i return; } - let response = await chrome.runtime.sendMessage({ + const response = await chrome.runtime.sendMessage({ action: "GET_PASSPHRASE", }); const pass = response?.passphrase || ""; @@ -195,21 +333,36 @@ export function WalletProvider({ children, isBrowser }: { children: ReactNode, i return; } - await rebuildKeymaster(pass); + await buildKeymaster(pass); } async function handleWalletUploadFile(uploaded: unknown) { setPendingWallet(uploaded); - if (isLegacyV0(uploaded)) { - setUploadAction('upload-plain-v0'); - setModalAction('set-passphrase'); - } else if (isV1WithEnc(uploaded)) { - setUploadAction('upload-enc-v1'); - setModalAction('decrypt'); - } else { - window.alert('Unsupported wallet type'); + if (isMdipWalletBundle(uploaded)) { + setUploadAction("upload-bundle"); + setModalAction("decrypt"); + return; + } + + if (isLegacyV0(uploaded) || isV1Decrypted(uploaded)) { + setUploadAction("upload-legacy-plain"); + setModalAction("set-passphrase"); + return; + } + + if (isV1WithEnc(uploaded)) { + setUploadAction("upload-legacy-encrypted"); + setModalAction("decrypt"); + return; } + + if (isV2Wallet(uploaded)) { + window.alert("Standalone keymaster metadata is not enough. Upload an mdip-wallet-bundle instead."); + return; + } + + window.alert("Unsupported wallet type"); } function handleStartReset() { @@ -219,11 +372,10 @@ export function WalletProvider({ children, isBrowser }: { children: ReactNode, i function handleStartRecover() { setMnemonicErrorText(""); + setRecoveredMnemonic(""); setShowRecoverMnemonic(true); setPassphraseErrorText(""); - // only nullify modalAction if we are uploading a wallet, otherwise - // leave passphrase modal open in case the user cancels if (uploadAction !== null) { setModalAction(null); } @@ -240,38 +392,47 @@ export function WalletProvider({ children, isBrowser }: { children: ReactNode, i async function handleResetPassphraseSubmit(newPassphrase: string) { try { - const walletWeb = new WalletChrome(); - const km = new Keymaster({ gatekeeper, wallet: walletWeb, cipher, search, passphrase: newPassphrase }); - await km.newWallet(undefined, true); + const { instance } = createKeymaster(newPassphrase); + await instance.newWallet(undefined, true); setShowResetSetup(false); - await rebuildKeymaster(newPassphrase); + await buildKeymaster(newPassphrase); } catch { - setPassphraseErrorText('Failed to reset wallet. Try again.'); + setPassphraseErrorText("Failed to reset wallet. Try again."); } } async function handleRecoverMnemonicSubmit(mnemonic: string) { setMnemonicErrorText(""); + try { - const walletWeb = new WalletChrome(); - let stored = pendingWallet && isV1WithEnc(pendingWallet) + const walletStore = createMetadataStore(); + const providerStore = createProviderStore(); + const storedWallet = pendingWallet && isV1WithEnc(pendingWallet) ? pendingWallet - : await walletWeb.loadWallet(); + : await walletStore.loadWallet(); + + if (isV1WithEnc(storedWallet)) { + const hdkey = cipher.generateHDKey(mnemonic); + const { publicJwk, privateJwk } = cipher.generateJwk(hdkey.privateKey!); + cipher.decryptMessage(publicJwk, privateJwk, storedWallet.enc); + } else { + const providerState = isMdipWalletBundle(pendingWallet) + ? pendingWallet.provider + : await providerStore.loadWallet(); + + if (!providerState) { + setMnemonicErrorText("Recovery not available for this wallet type."); + return; + } - if (!isV1WithEnc(stored)) { - setMnemonicErrorText('Recovery not available for this wallet type.'); - return; + await verifyMnemonicAgainstProviderState(providerState, mnemonic); } - const hdkey = cipher.generateHDKey(mnemonic); - const { publicJwk, privateJwk } = cipher.generateJwk(hdkey.privateKey!); - cipher.decryptMessage(publicJwk, privateJwk, stored.enc); - setRecoveredMnemonic(mnemonic); setShowRecoverMnemonic(false); setShowRecoverSetup(true); } catch { - setMnemonicErrorText('Mnemonic is incorrect. Try again.'); + setMnemonicErrorText("Mnemonic is incorrect. Try again."); } } @@ -279,30 +440,56 @@ export function WalletProvider({ children, isBrowser }: { children: ReactNode, i if (!recoveredMnemonic) { return; } + try { - const walletWeb = new WalletChrome(); - const base = pendingWallet && isV1WithEnc(pendingWallet) + const walletStore = createMetadataStore(); + const providerStore = createProviderStore(); + const storedWallet = pendingWallet && isV1WithEnc(pendingWallet) ? pendingWallet - : await walletWeb.loadWallet(); + : await walletStore.loadWallet(); + + if (isV1WithEnc(storedWallet)) { + const mnemonicEnc = await encMnemonic(recoveredMnemonic, newPassphrase); + const updatedWallet = { + version: storedWallet.version, + seed: { mnemonicEnc }, + enc: storedWallet.enc, + } satisfies StoredWallet; + + await importLegacyWallet(updatedWallet, newPassphrase); + } else { + const providerState = isMdipWalletBundle(pendingWallet) + ? pendingWallet.provider + : await providerStore.loadWallet(); + + if (!providerState) { + setPassphraseErrorText("Recovery not available for this wallet type."); + return; + } - if (!isV1WithEnc(base)) { - setPassphraseErrorText('Recovery not available for this wallet type.'); - return; + const recoveryProvider = createMnemonicWalletProvider(newPassphrase, createMemoryProviderStore()); + await recoveryProvider.saveWallet(providerState, true); + await recoveryProvider.changePassphrase(recoveredMnemonic, newPassphrase); + const updatedProviderState = await recoveryProvider.backupWallet(); + + if (isMdipWalletBundle(pendingWallet)) { + await persistWalletData(pendingWallet.keymaster, updatedProviderState); + } else { + const wallet = await walletStore.loadWallet(); + if (!wallet || !isV2Wallet(wallet)) { + setPassphraseErrorText("Recovery not available for this wallet type."); + return; + } + + await persistWalletData(wallet, updatedProviderState); + } } - const mnemonicEnc = await encMnemonic(recoveredMnemonic, newPassphrase); - const updated = { - version: base.version, - seed: { mnemonicEnc }, - enc: base.enc - }; - - await walletWeb.saveWallet(updated, true); setRecoveredMnemonic(""); setShowRecoverSetup(false); - await rebuildKeymaster(newPassphrase); + await buildKeymaster(newPassphrase); } catch { - setPassphraseErrorText('Failed to update passphrase. Try again.'); + setPassphraseErrorText("Failed to update passphrase. Try again."); } } @@ -318,24 +505,27 @@ export function WalletProvider({ children, isBrowser }: { children: ReactNode, i refreshFlag, isBrowser, keymaster: keymasterRef.current, + walletProvider: walletProviderRef.current, }; return ( <> { } // Create a Blob from the buffer - const blob = new Blob([buffer]); + const blob = new Blob([Uint8Array.from(buffer)]); // Create a temporary link to trigger the download const link = document.createElement('a'); link.href = URL.createObjectURL(blob); diff --git a/apps/react-wallet/src/components/DocumentTab.tsx b/apps/react-wallet/src/components/DocumentTab.tsx index 3445d875b..748501f1b 100644 --- a/apps/react-wallet/src/components/DocumentTab.tsx +++ b/apps/react-wallet/src/components/DocumentTab.tsx @@ -63,7 +63,7 @@ const DocumentTab = () => { const docs = await keymaster.resolveDID(documentName, version ? { versionSequence: version } : {}); setSelectedDocumentDocs(docs); - const currentVersion = docs.didDocumentMetadata?.version ?? 1; + const currentVersion = Number(docs.didDocumentMetadata?.version ?? 1); setDocVersion(currentVersion); if (version === undefined) { setDocVersionMax(currentVersion); diff --git a/apps/react-wallet/src/components/GroupVaultTab.tsx b/apps/react-wallet/src/components/GroupVaultTab.tsx index 081767015..f34900ee8 100644 --- a/apps/react-wallet/src/components/GroupVaultTab.tsx +++ b/apps/react-wallet/src/components/GroupVaultTab.tsx @@ -165,7 +165,7 @@ function GroupVaultTab() { } // Create a Blob from the buffer - const blob = new Blob([buffer]); + const blob = new Blob([Uint8Array.from(buffer)]); // Create a temporary link to trigger the download const link = document.createElement('a'); link.href = URL.createObjectURL(blob); diff --git a/apps/react-wallet/src/components/WalletTab.tsx b/apps/react-wallet/src/components/WalletTab.tsx index 53d6fac81..bf71e9fd1 100644 --- a/apps/react-wallet/src/components/WalletTab.tsx +++ b/apps/react-wallet/src/components/WalletTab.tsx @@ -8,7 +8,8 @@ import { useSnackbar } from "../contexts/SnackbarProvider"; import WarningModal from "../modals/WarningModal"; import MnemonicModal from "../modals/MnemonicModal"; import WalletWeb from "@mdip/keymaster/wallet/web"; -import {clearSessionPassphrase} from "../utils/sessionPassphrase"; +import { MdipWalletBundle } from "@mdip/keymaster/types"; +import { clearSessionPassphrase } from "../utils/sessionPassphrase"; const WalletTab = () => { const [open, setOpen] = useState(false); @@ -20,6 +21,7 @@ const WalletTab = () => { const [checkResultMessage, setCheckResultMessage] = useState(""); const { keymaster, + walletProvider, initialiseWallet, handleWalletUploadFile, pendingMnemonic, @@ -47,7 +49,9 @@ const WalletTab = () => { async function createNewWallet() { const walletWeb = new WalletWeb(); + const providerWallet = new WalletWeb("mdip-wallet-provider"); localStorage.removeItem(walletWeb.walletName); + localStorage.removeItem(providerWallet.walletName); clearSessionPassphrase(); await initialiseWallet(); } @@ -122,11 +126,11 @@ const WalletTab = () => { }; async function showMnemonic() { - if (!keymaster) { + if (!walletProvider) { return; } try { - const response = await keymaster.decryptMnemonic(); + const response = await walletProvider.decryptMnemonic(); setMnemonicString(response); } catch (error: any) { setError(error); @@ -163,18 +167,25 @@ const WalletTab = () => { } async function downloadWallet() { - if (!keymaster) { + if (!keymaster || !walletProvider) { return; } try { - const wallet = await keymaster.exportEncryptedWallet(); - const walletJSON = JSON.stringify(wallet, null, 4); + const wallet = await keymaster.loadWallet(); + const provider = await walletProvider.backupWallet(); + const bundle: MdipWalletBundle = { + version: 1, + type: "mdip-wallet-bundle", + keymaster: wallet, + provider, + }; + const walletJSON = JSON.stringify(bundle, null, 4); const blob = new Blob([walletJSON], { type: 'application/json' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; - link.download = 'mdip-wallet.json'; + link.download = 'mdip-wallet-bundle.json'; link.click(); URL.revokeObjectURL(url); @@ -209,7 +220,7 @@ const WalletTab = () => { } try { await keymaster.backupWallet(); - setSuccess("Wallet backup successful"); + setSuccess("Wallet metadata backup successful"); } catch (error: any) { setError(error); } diff --git a/apps/react-wallet/src/contexts/WalletProvider.tsx b/apps/react-wallet/src/contexts/WalletProvider.tsx index 9250d4e20..6c0d45a4c 100644 --- a/apps/react-wallet/src/contexts/WalletProvider.tsx +++ b/apps/react-wallet/src/contexts/WalletProvider.tsx @@ -10,24 +10,38 @@ import { } from "react"; import GatekeeperClient from "@mdip/gatekeeper/client"; import Keymaster from "@mdip/keymaster"; -import { WalletBase, StoredWallet } from '@mdip/keymaster/types'; -import { isV1WithEnc, isLegacyV0 } from '@mdip/keymaster/wallet/typeGuards'; import SearchClient from "@mdip/keymaster/search"; import CipherWeb from "@mdip/cipher"; import WalletWeb from "@mdip/keymaster/wallet/web"; +import MnemonicHdWalletProvider from "@mdip/keymaster/wallet/mnemonic-hd"; +import { + isLegacyV0, + isV1Decrypted, + isV1WithEnc, + isV2Wallet, +} from "@mdip/keymaster/wallet/typeGuards"; +import { + KeymasterStore, + MdipWalletBundle, + MnemonicHdWalletProviderInterface, + MnemonicHdWalletState, + StoredWallet, + WalletFile, + WalletProviderStore, +} from "@mdip/keymaster/types"; import WalletJsonMemory from "@mdip/keymaster/wallet/json-memory"; import PassphraseModal from "../modals/PassphraseModal"; import WarningModal from "../modals/WarningModal"; import MnemonicModal from "../modals/MnemonicModal"; -import { encMnemonic } from '@mdip/keymaster/encryption'; -import { takeDeepLink } from '../utils/deepLinkQueue'; -import { extractDid } from '../utils/utils'; +import { encMnemonic } from "@mdip/keymaster/encryption"; +import { takeDeepLink } from "../utils/deepLinkQueue"; +import { extractDid } from "../utils/utils"; import { DEFAULT_GATEKEEPER_URL, DEFAULT_SEARCH_SERVER_URL, GATEKEEPER_KEY, SEARCH_SERVER_KEY -} from "../constants" +} from "../constants"; import { getSessionPassphrase, setSessionPassphrase, @@ -37,6 +51,11 @@ import { const gatekeeper = new GatekeeperClient(); const cipher = new CipherWeb(); +const KEYMASTER_STORE_NAME = "mdip-keymaster"; +const WALLET_PROVIDER_STORE_NAME = "mdip-wallet-provider"; + +type UploadAction = "upload-legacy-plain" | "upload-legacy-encrypted" | "upload-bundle"; + interface WalletContextValue { pendingMnemonic: string; setPendingMnemonic: Dispatch>; @@ -47,6 +66,7 @@ interface WalletContextValue { handleWalletUploadFile: (uploaded: unknown) => Promise; refreshFlag: number; keymaster: Keymaster | null; + walletProvider: MnemonicHdWalletProviderInterface | null; } const WalletContext = createContext(null); @@ -55,13 +75,64 @@ let search: SearchClient | undefined; // eslint-disable-next-line sonarjs/no-hardcoded-passwords const INCORRECT_PASSPHRASE = "Incorrect passphrase"; +const INCOMPLETE_WALLET = "Wallet data is incomplete. Restore from an mdip-wallet-bundle or reset the wallet."; + +function createMetadataStore() { + return new WalletWeb(KEYMASTER_STORE_NAME); +} + +function createProviderStore(): WalletProviderStore { + return new WalletWeb(WALLET_PROVIDER_STORE_NAME) as unknown as WalletProviderStore; +} + +function createMemoryProviderStore(): WalletProviderStore { + return new WalletJsonMemory() as unknown as WalletProviderStore; +} + +function createMnemonicWalletProvider( + passphrase: string, + store: WalletProviderStore = createProviderStore(), +) { + return new MnemonicHdWalletProvider({ + store, + cipher, + passphrase, + }); +} + +function isMdipWalletBundle(wallet: unknown): wallet is MdipWalletBundle { + if (!wallet || typeof wallet !== "object") { + return false; + } + + const bundle = wallet as Partial; + return bundle.version === 1 + && bundle.type === "mdip-wallet-bundle" + && isV2Wallet(bundle.keymaster) + && !!bundle.provider + && bundle.provider.version === 1 + && bundle.provider.type === "mnemonic-hd" + && !!bundle.provider.rootPublicJwk; +} + +async function verifyMnemonicAgainstProviderState( + providerState: MnemonicHdWalletState, + mnemonic: string, +) { + const hdKey = cipher.generateHDKey(mnemonic); + const { publicJwk } = cipher.generateJwk(hdKey.privateKey!); + + if (cipher.hashJSON(publicJwk) !== cipher.hashJSON(providerState.rootPublicJwk)) { + throw new Error("Mnemonic does not match wallet."); + } +} export function WalletProvider({ children }: { children: ReactNode }) { const [passphraseErrorText, setPassphraseErrorText] = useState(""); const [pendingMnemonic, setPendingMnemonic] = useState(""); const [pendingWallet, setPendingWallet] = useState(null); const [modalAction, setModalAction] = useState(null); - const [uploadAction, setUploadAction] = useState(null); + const [uploadAction, setUploadAction] = useState(null); const [isReady, setIsReady] = useState(false); const [showResetConfirm, setShowResetConfirm] = useState(false); const [showResetSetup, setShowResetSetup] = useState(false); @@ -72,24 +143,30 @@ export function WalletProvider({ children }: { children: ReactNode }) { const [refreshFlag, setRefreshFlag] = useState(0); const keymasterRef = useRef(null); - - const walletWeb = new WalletWeb(); + const walletProviderRef = useRef(null); useEffect(() => { async function init() { - await initialiseServices() + await initialiseServices(); await initialiseWallet(); } + init(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); async function initialiseWallet() { - const walletData = await walletWeb.loadWallet(); + const walletStore = createMetadataStore(); + const providerStore = createProviderStore(); + const walletData = await walletStore.loadWallet(); + const providerData = await providerStore.loadWallet(); + const hasIncompleteState = + (!!providerData && !walletData) + || (!!walletData && isV2Wallet(walletData) && !providerData); const pass = getSessionPassphrase(); - if (!pendingMnemonic && pass) { - const res = await rebuildKeymaster(pass); + if (!pendingMnemonic && pass && !hasIncompleteState) { + const res = await buildKeymaster(pass); if (res) { return; } @@ -97,11 +174,18 @@ export function WalletProvider({ children }: { children: ReactNode }) { clearSessionPassphrase(); } - if (!walletData || pendingMnemonic || isLegacyV0(walletData)) { - // eslint-disable-next-line sonarjs/no-duplicate-string - setModalAction('set-passphrase'); + if (hasIncompleteState) { + setPassphraseErrorText(INCOMPLETE_WALLET); + setModalAction("decrypt"); + return; + } + + if (!walletData || pendingMnemonic || isLegacyV0(walletData) || isV1Decrypted(walletData)) { + setPassphraseErrorText(""); + setModalAction("set-passphrase"); } else { - setModalAction('decrypt'); + setPassphraseErrorText(""); + setModalAction("decrypt"); } } @@ -114,72 +198,127 @@ export function WalletProvider({ children }: { children: ReactNode }) { search = await SearchClient.create({ url: searchServerUrl }); } - const buildKeymaster = async (wallet: WalletBase, passphrase: string) => { - const instance = new Keymaster({gatekeeper, wallet, cipher, search, passphrase}); - - if (pendingMnemonic) { - await instance.newWallet(pendingMnemonic, true); - await instance.recoverWallet(); - } else { - try { - // check pass & convert to v1 if needed - await instance.loadWallet(); - } catch { - setPassphraseErrorText(INCORRECT_PASSPHRASE); - return false; - } - } + function createKeymaster( + passphrase: string, + store: KeymasterStore = createMetadataStore(), + providerStore: WalletProviderStore = createProviderStore(), + ) { + const walletProvider = createMnemonicWalletProvider(passphrase, providerStore); + const instance = new Keymaster({ + gatekeeper, + store, + walletProvider, + cipher, + search, + }); + + return { instance, walletProvider }; + } + async function activateWallet( + keymaster: Keymaster, + walletProvider: MnemonicHdWalletProviderInterface, + passphrase: string, + ) { setModalAction(null); setPendingWallet(null); setPendingMnemonic(""); + setRecoveredMnemonic(""); setUploadAction(null); setPassphraseErrorText(""); - keymasterRef.current = instance; - setRefreshFlag(r => r + 1); + keymasterRef.current = keymaster; + walletProviderRef.current = walletProvider; + setRefreshFlag((value) => value + 1); setIsReady(true); setSessionPassphrase(passphrase); + } + const buildKeymaster = async (passphrase: string) => { + const { instance, walletProvider } = createKeymaster(passphrase); + + try { + if (pendingMnemonic) { + await instance.newWallet(pendingMnemonic, true); + } else { + await instance.loadWallet(); + } + } catch { + setPassphraseErrorText(INCORRECT_PASSPHRASE); + return false; + } + + await activateWallet(instance, walletProvider, passphrase); return true; }; - async function rebuildKeymaster(passphrase: string) { - return await buildKeymaster(walletWeb, passphrase); + async function persistWalletData(wallet: WalletFile, providerState: MnemonicHdWalletState) { + const providerStore = createProviderStore(); + const walletStore = createMetadataStore(); + + const providerOk = await providerStore.saveWallet(providerState, true); + if (!providerOk) { + throw new Error("save provider wallet failed"); + } + + const walletOk = await walletStore.saveWallet(wallet, true); + if (!walletOk) { + throw new Error("save wallet failed"); + } + } + + async function importLegacyWallet(wallet: StoredWallet, passphrase: string) { + const memoryStore = new WalletJsonMemory(); + const memoryProviderStore = createMemoryProviderStore(); + const { instance, walletProvider } = createKeymaster(passphrase, memoryStore, memoryProviderStore); + + await memoryStore.saveWallet(wallet, true); + const normalized = await instance.loadWallet(); + const providerState = await walletProvider.backupWallet(); + await persistWalletData(normalized, providerState); + } + + async function importWalletBundle(bundle: MdipWalletBundle, passphrase: string) { + const memoryStore = new WalletJsonMemory(); + const memoryProviderStore = createMemoryProviderStore(); + const { instance, walletProvider } = createKeymaster(passphrase, memoryStore, memoryProviderStore); + + await memoryStore.saveWallet(bundle.keymaster, true); + await walletProvider.saveWallet(bundle.provider, true); + const normalized = await instance.loadWallet(); + const providerState = await walletProvider.backupWallet(); + await persistWalletData(normalized, providerState); } async function handlePassphraseSubmit(passphrase: string) { setPassphraseErrorText(""); - const walletMemory = new WalletJsonMemory(); - if (uploadAction && pendingWallet) { - if (modalAction === 'decrypt') { - await walletMemory.saveWallet(pendingWallet as StoredWallet, true); - - try { - const km = new Keymaster({ gatekeeper, wallet: walletMemory, cipher, search, passphrase }); - // check pass - await km.loadWallet(); - await walletWeb.saveWallet(pendingWallet as StoredWallet, true); - } catch { - setPassphraseErrorText(INCORRECT_PASSPHRASE); - return; + try { + if (uploadAction === "upload-bundle" && isMdipWalletBundle(pendingWallet)) { + await importWalletBundle(pendingWallet, passphrase); + } else { + await importLegacyWallet(pendingWallet as StoredWallet, passphrase); } - } else { // upload-plain-v0 - await walletWeb.saveWallet(pendingWallet as StoredWallet, true); + } catch { + setPassphraseErrorText( + modalAction === "decrypt" ? INCORRECT_PASSPHRASE : "Failed to import wallet." + ); + return; } } - await rebuildKeymaster(passphrase); + await buildKeymaster(passphrase); } async function handlePassphraseClose() { setPendingWallet(null); setPendingMnemonic(""); + setRecoveredMnemonic(""); setPassphraseErrorText(""); - const walletData = await walletWeb.loadWallet(); - if (walletData) { + const walletData = await createMetadataStore().loadWallet(); + const providerData = await createProviderStore().loadWallet(); + if (walletData || providerData) { setModalAction(null); } } @@ -189,10 +328,10 @@ export function WalletProvider({ children }: { children: ReactNode }) { window.dispatchEvent(evt); } - function parseMdip(url: string): { action?: string, did?: string | null } { + function parseMdip(url: string): { action?: string; did?: string | null } { try { - const u = new URL(url.replace(/^mdip:\/\//, 'https://')); - const action = (u.hostname || u.pathname.replace(/^\//, '') || '').toLowerCase(); + const parsedUrl = new URL(url.replace(/^mdip:\/\//, "https://")); + const action = (parsedUrl.hostname || parsedUrl.pathname.replace(/^\//, "") || "").toLowerCase(); const did = extractDid(url); return { action, did }; } catch { @@ -213,7 +352,7 @@ export function WalletProvider({ children }: { children: ReactNode }) { const { action, did } = parseMdip(url); - if (action === 'accept' && did) { + if (action === "accept" && did) { openEvent(did, "mdip:openAccept"); return; } @@ -224,22 +363,37 @@ export function WalletProvider({ children }: { children: ReactNode }) { }; handleQueued(); - window.addEventListener('mdip:deepLinkQueued', handleQueued); - return () => window.removeEventListener('mdip:deepLinkQueued', handleQueued); + window.addEventListener("mdip:deepLinkQueued", handleQueued); + return () => window.removeEventListener("mdip:deepLinkQueued", handleQueued); }, [isReady]); async function handleWalletUploadFile(uploaded: unknown) { setPendingWallet(uploaded); - if (isLegacyV0(uploaded)) { - setUploadAction('upload-plain-v0'); - setModalAction('set-passphrase'); - } else if (isV1WithEnc(uploaded)) { - setUploadAction('upload-enc-v1'); - setModalAction('decrypt'); - } else { - window.alert('Unsupported wallet type'); + if (isMdipWalletBundle(uploaded)) { + setUploadAction("upload-bundle"); + setModalAction("decrypt"); + return; + } + + if (isLegacyV0(uploaded) || isV1Decrypted(uploaded)) { + setUploadAction("upload-legacy-plain"); + setModalAction("set-passphrase"); + return; + } + + if (isV1WithEnc(uploaded)) { + setUploadAction("upload-legacy-encrypted"); + setModalAction("decrypt"); + return; } + + if (isV2Wallet(uploaded)) { + window.alert("Standalone keymaster metadata is not enough. Upload an mdip-wallet-bundle instead."); + return; + } + + window.alert("Unsupported wallet type"); } function handleStartReset() { @@ -249,11 +403,10 @@ export function WalletProvider({ children }: { children: ReactNode }) { function handleStartRecover() { setMnemonicErrorText(""); + setRecoveredMnemonic(""); setShowRecoverMnemonic(true); setPassphraseErrorText(""); - // only nullify modalAction if we are uploading a wallet, otherwise - // leave passphrase modal open in case the user cancels if (uploadAction !== null) { setModalAction(null); } @@ -270,38 +423,47 @@ export function WalletProvider({ children }: { children: ReactNode }) { async function handleResetPassphraseSubmit(newPassphrase: string) { try { - const walletWeb = new WalletWeb(); - const km = new Keymaster({ gatekeeper, wallet: walletWeb, cipher, search, passphrase: newPassphrase }); - await km.newWallet(undefined, true); + const { instance } = createKeymaster(newPassphrase); + await instance.newWallet(undefined, true); setShowResetSetup(false); - await rebuildKeymaster(newPassphrase); + await buildKeymaster(newPassphrase); } catch { - setPassphraseErrorText('Failed to reset wallet. Try again.'); + setPassphraseErrorText("Failed to reset wallet. Try again."); } } async function handleRecoverMnemonicSubmit(mnemonic: string) { setMnemonicErrorText(""); + try { - const walletWeb = new WalletWeb(); - let stored = pendingWallet && isV1WithEnc(pendingWallet) + const walletStore = createMetadataStore(); + const providerStore = createProviderStore(); + const storedWallet = pendingWallet && isV1WithEnc(pendingWallet) ? pendingWallet - : await walletWeb.loadWallet(); + : await walletStore.loadWallet(); + + if (isV1WithEnc(storedWallet)) { + const hdKey = cipher.generateHDKey(mnemonic); + const { publicJwk, privateJwk } = cipher.generateJwk(hdKey.privateKey!); + cipher.decryptMessage(publicJwk, privateJwk, storedWallet.enc); + } else { + const providerState = isMdipWalletBundle(pendingWallet) + ? pendingWallet.provider + : await providerStore.loadWallet(); + + if (!providerState) { + setMnemonicErrorText("Recovery not available for this wallet type."); + return; + } - if (!isV1WithEnc(stored)) { - setMnemonicErrorText('Recovery not available for this wallet type.'); - return; + await verifyMnemonicAgainstProviderState(providerState, mnemonic); } - const hdkey = cipher.generateHDKey(mnemonic); - const { publicJwk, privateJwk } = cipher.generateJwk(hdkey.privateKey!); - cipher.decryptMessage(publicJwk, privateJwk, stored.enc); - setRecoveredMnemonic(mnemonic); setShowRecoverMnemonic(false); setShowRecoverSetup(true); } catch { - setMnemonicErrorText('Mnemonic is incorrect. Try again.'); + setMnemonicErrorText("Mnemonic is incorrect. Try again."); } } @@ -309,30 +471,56 @@ export function WalletProvider({ children }: { children: ReactNode }) { if (!recoveredMnemonic) { return; } + try { - const walletWeb = new WalletWeb(); - const base = pendingWallet && isV1WithEnc(pendingWallet) + const walletStore = createMetadataStore(); + const providerStore = createProviderStore(); + const storedWallet = pendingWallet && isV1WithEnc(pendingWallet) ? pendingWallet - : await walletWeb.loadWallet(); + : await walletStore.loadWallet(); + + if (isV1WithEnc(storedWallet)) { + const mnemonicEnc = await encMnemonic(recoveredMnemonic, newPassphrase); + const updatedWallet = { + version: storedWallet.version, + seed: { mnemonicEnc }, + enc: storedWallet.enc, + } satisfies StoredWallet; + + await importLegacyWallet(updatedWallet, newPassphrase); + } else { + const providerState = isMdipWalletBundle(pendingWallet) + ? pendingWallet.provider + : await providerStore.loadWallet(); + + if (!providerState) { + setPassphraseErrorText("Recovery not available for this wallet type."); + return; + } - if (!isV1WithEnc(base)) { - setPassphraseErrorText('Recovery not available for this wallet type.'); - return; + const recoveryProvider = createMnemonicWalletProvider(newPassphrase, createMemoryProviderStore()); + await recoveryProvider.saveWallet(providerState, true); + await recoveryProvider.changePassphrase(recoveredMnemonic, newPassphrase); + const updatedProviderState = await recoveryProvider.backupWallet(); + + if (isMdipWalletBundle(pendingWallet)) { + await persistWalletData(pendingWallet.keymaster, updatedProviderState); + } else { + const wallet = await walletStore.loadWallet(); + if (!wallet || !isV2Wallet(wallet)) { + setPassphraseErrorText("Recovery not available for this wallet type."); + return; + } + + await persistWalletData(wallet, updatedProviderState); + } } - const mnemonicEnc = await encMnemonic(recoveredMnemonic, newPassphrase); - const updated = { - version: base.version, - seed: { mnemonicEnc }, - enc: base.enc - }; - - await walletWeb.saveWallet(updated, true); setRecoveredMnemonic(""); setShowRecoverSetup(false); - await rebuildKeymaster(newPassphrase); + await buildKeymaster(newPassphrase); } catch { - setPassphraseErrorText('Failed to update passphrase. Try again.'); + setPassphraseErrorText("Failed to update passphrase. Try again."); } } @@ -346,24 +534,27 @@ export function WalletProvider({ children }: { children: ReactNode }) { handleWalletUploadFile, refreshFlag, keymaster: keymasterRef.current, + walletProvider: walletProviderRef.current, }; return ( <> /tests/common/pino.mock.ts', '^\\.\\/typeGuards\\.js$': '/packages/keymaster/src/db/typeGuards.ts', '^\\.\\/db\\/typeGuards\\.js$': '/packages/keymaster/src/db/typeGuards.ts', + '^\\.\\/provider\\/mnemonic-hd\\.js$': '/packages/keymaster/src/provider/mnemonic-hd.ts', + '^\\.\\./encryption\\.js$': '/packages/keymaster/src/encryption.ts', + '^\\.\\./db\\/typeGuards\\.js$': '/packages/keymaster/src/db/typeGuards.ts', '^\\.\\/sync-mapping\\.js$': '/services/mediators/hyperswarm/src/sync-mapping.ts', '^\\.\\/sync-persistence\\.js$': '/services/mediators/hyperswarm/src/sync-persistence.ts', '^\\.\\/abstract-json\\.js$': '/packages/gatekeeper/src/db/abstract-json.ts', diff --git a/packages/common/package.json b/packages/common/package.json index dfecac83e..2492a1070 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -14,24 +14,26 @@ }, "exports": { ".": { + "types": "./dist/types/index.d.ts", + "browser": "./dist/esm/index-browser.js", "import": "./dist/esm/index.js", - "require": "./dist/cjs/index.cjs", - "types": "./dist/types/index.d.ts" + "require": "./dist/cjs/index.cjs" }, "./utils": { + "types": "./dist/types/utils.d.ts", "import": "./dist/esm/utils.js", - "require": "./dist/cjs/utils.cjs", - "types": "./dist/types/utils.d.ts" + "require": "./dist/cjs/utils.cjs" }, "./errors": { + "types": "./dist/types/errors.d.ts", "import": "./dist/esm/errors.js", - "require": "./dist/cjs/errors.cjs", - "types": "./dist/types/errors.d.ts" + "require": "./dist/cjs/errors.cjs" }, "./logger": { + "types": "./dist/types/logger.d.ts", + "browser": "./dist/esm/logger-browser.js", "import": "./dist/esm/logger.js", - "require": "./dist/cjs/logger.cjs", - "types": "./dist/types/logger.d.ts" + "require": "./dist/cjs/logger.cjs" } }, "typesVersions": { diff --git a/packages/common/src/index-browser.ts b/packages/common/src/index-browser.ts new file mode 100644 index 000000000..9bb431fd8 --- /dev/null +++ b/packages/common/src/index-browser.ts @@ -0,0 +1,3 @@ +export * from './errors.js'; +export * from './logger-browser.js'; +export * from './utils.js'; diff --git a/packages/common/src/logger-browser.ts b/packages/common/src/logger-browser.ts new file mode 100644 index 000000000..0ab682774 --- /dev/null +++ b/packages/common/src/logger-browser.ts @@ -0,0 +1,124 @@ +import pino, { type Logger, type LoggerOptions, type ChildLoggerOptions } from 'pino'; + +export type LogLevel = 'fatal' | 'error' | 'warn' | 'info' | 'debug' | 'trace' | 'silent'; +export type LoggerLike = Pick; + +const DEFAULT_LEVEL: LogLevel = 'info'; +const DEFAULT_PRETTY = false; +const LEVEL_ALIASES: Record = { + warning: 'warn', +}; +const LEVELS = new Set([ + 'fatal', + 'error', + 'warn', + 'info', + 'debug', + 'trace', + 'silent', +]); + +export function createConsoleLogger(consoleLike: { + debug?: (...args: unknown[]) => void; + info?: (...args: unknown[]) => void; + warn?: (...args: unknown[]) => void; + error?: (...args: unknown[]) => void; + log?: (...args: unknown[]) => void; +}): LoggerLike { + const base = consoleLike.log?.bind(consoleLike); + const wrap = (fn?: (...args: unknown[]) => void, fallback?: (...args: unknown[]) => void) => { + if (fn) { + return (...args: unknown[]) => fn(...args); + } + if (fallback) { + return (...args: unknown[]) => fallback(...args); + } + return () => {}; + }; + + return { + debug: wrap(consoleLike.debug?.bind(consoleLike), base), + info: wrap(consoleLike.info?.bind(consoleLike), base), + warn: wrap(consoleLike.warn?.bind(consoleLike), base), + error: wrap(consoleLike.error?.bind(consoleLike), base), + }; +} + +function normalizeLevel(level?: string): LogLevel { + if (!level) { + return DEFAULT_LEVEL; + } + + const normalized = level.toLowerCase(); + const alias = LEVEL_ALIASES[normalized]; + + if (alias) { + return alias; + } + + if (LEVELS.has(normalized as LogLevel)) { + return normalized as LogLevel; + } + + return DEFAULT_LEVEL; +} + +type Env = Record; + +function resolveEnv(): Env { + const maybeProcess = (globalThis as { process?: { env?: Env } }).process; + return maybeProcess?.env ?? {}; +} + +export function getLogLevel(env: Env = resolveEnv()): LogLevel { + return normalizeLevel(env.KC_LOG_LEVEL); +} + +export function getPrettyEnabled(): boolean { + return DEFAULT_PRETTY; +} + +export function createLogger( + options: LoggerOptions = {}, + env: Env = resolveEnv(), +): Logger { + const resolvedLevel = options.level ?? getLogLevel(env); + + const loggerOptions: LoggerOptions = { + ...options, + level: resolvedLevel, + }; + + return pino(loggerOptions); +} + +export let logger = createLogger(); + +export function setLogger(next: Logger): void { + logger = next; +} + +export function childLogger(bindings: Record, options?: ChildLoggerOptions): Logger { + return logger.child(bindings, options); +} + +export function asError(err: unknown): Error { + if (err instanceof Error) { + return err; + } + + if (typeof err === 'string') { + return new Error(err); + } + + try { + return new Error(JSON.stringify(err)); + } catch { + return new Error(String(err)); + } +} + +export function logError(err: unknown, msg?: string, log: Logger = logger): void { + const error = asError(err); + log.error({ err: error }, msg ?? error.message); +} diff --git a/packages/common/src/logger.ts b/packages/common/src/logger.ts index d899809c6..3b4e432ff 100644 --- a/packages/common/src/logger.ts +++ b/packages/common/src/logger.ts @@ -1,4 +1,5 @@ import pino, { type Logger, type LoggerOptions, type ChildLoggerOptions } from 'pino'; +import pinoPretty from 'pino-pretty'; export type LogLevel = 'fatal' | 'error' | 'warn' | 'info' | 'debug' | 'trace' | 'silent'; export type LoggerLike = Pick; @@ -83,23 +84,24 @@ export function createLogger( env: Env = resolveEnv(), ): Logger { const resolvedLevel = options.level ?? getLogLevel(env); - const transport = options.transport ?? { - target: 'pino-pretty', - options: { - colorize: false, - translateTime: 'SYS:standard', - ignore: 'pid,hostname,service', - singleLine: true, - }, - }; const loggerOptions: LoggerOptions = { ...options, level: resolvedLevel, - transport, }; - return pino(loggerOptions); + if (options.transport) { + return pino(loggerOptions); + } + + const stream = pinoPretty({ + colorize: false, + translateTime: 'SYS:standard', + ignore: 'pid,hostname,service', + singleLine: true, + }); + + return pino(loggerOptions, stream); } export let logger = createLogger(); diff --git a/packages/ipfs/src/helia-client.ts b/packages/ipfs/src/helia-client.ts index 07c690bde..1176009d1 100644 --- a/packages/ipfs/src/helia-client.ts +++ b/packages/ipfs/src/helia-client.ts @@ -2,7 +2,7 @@ import { createHelia, Helia } from 'helia'; import { json, JSON } from '@helia/json'; import { unixfs, UnixFS } from '@helia/unixfs'; import { FsBlockstore } from 'blockstore-fs'; -import { CID } from 'multiformats'; +import { CID } from 'multiformats/cid'; import { base58btc } from 'multiformats/bases/base58'; import * as jsonCodec from 'multiformats/codecs/json'; import * as rawCodec from 'multiformats/codecs/raw'; diff --git a/packages/ipfs/src/utils.ts b/packages/ipfs/src/utils.ts index 5816e8f9d..22d9864f6 100644 --- a/packages/ipfs/src/utils.ts +++ b/packages/ipfs/src/utils.ts @@ -1,4 +1,4 @@ -import { CID } from 'multiformats'; +import { CID } from 'multiformats/cid'; import { base58btc } from 'multiformats/bases/base58'; import * as jsonCodec from 'multiformats/codecs/json'; import * as rawCodec from 'multiformats/codecs/raw'; diff --git a/packages/keymaster/package.json b/packages/keymaster/package.json index 314782b91..094bdb030 100644 --- a/packages/keymaster/package.json +++ b/packages/keymaster/package.json @@ -73,11 +73,6 @@ "require": "./dist/cjs/db/postgres.cjs", "types": "./dist/types/db/postgres.d.ts" }, - "./wallet/cache": { - "import": "./dist/esm/db/cache.js", - "require": "./dist/cjs/db/cache.cjs", - "types": "./dist/types/db/cache.d.ts" - }, "./wallet/web": { "import": "./dist/esm/db/web.js", "require": "./dist/cjs/db/web.cjs", @@ -88,6 +83,10 @@ "require": "./dist/cjs/db/chrome.cjs", "types": "./dist/types/db/chrome.d.ts" }, + "./wallet/mnemonic-hd": { + "import": "./dist/esm/provider/mnemonic-hd.js", + "types": "./dist/types/provider/mnemonic-hd.d.ts" + }, "./wallet/typeGuards": { "import": "./dist/esm/db/typeGuards.js", "require": "./dist/cjs/db/typeGuards.cjs", @@ -126,15 +125,15 @@ "wallet/postgres": [ "./dist/types/db/postgres.d.ts" ], - "wallet/cache": [ - "./dist/types/db/cache.d.ts" - ], "wallet/web": [ "./dist/types/db/web.d.ts" ], "wallet/chrome": [ "./dist/types/db/chrome.d.ts" ], + "wallet/mnemonic-hd": [ + "./dist/types/provider/mnemonic-hd.d.ts" + ], "wallet/typeGuards": [ "./dist/types/db/typeGuards.d.ts" ], diff --git a/packages/keymaster/rollup.cjs.config.js b/packages/keymaster/rollup.cjs.config.js index 1be7d312d..1ca2e1003 100644 --- a/packages/keymaster/rollup.cjs.config.js +++ b/packages/keymaster/rollup.cjs.config.js @@ -23,7 +23,6 @@ const config = { 'db/mongo': 'dist/esm/db/mongo.js', 'db/sqlite': 'dist/esm/db/sqlite.js', 'db/postgres': 'dist/esm/db/postgres.js', - 'db/cache': 'dist/esm/db/cache.js', 'db/web': 'dist/esm/db/web.js', 'db/chrome': 'dist/esm/db/chrome.js', 'db/typeGuards': 'dist/esm/db/typeGuards.js', diff --git a/packages/keymaster/src/db/cache.ts b/packages/keymaster/src/db/cache.ts deleted file mode 100644 index 1761f2b16..000000000 --- a/packages/keymaster/src/db/cache.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { StoredWallet, WalletBase } from '../types.js'; - -export default class WalletCache implements WalletBase { - private baseWallet: WalletBase; - private cachedWallet: StoredWallet; - - constructor(baseWallet: WalletBase) { - this.baseWallet = baseWallet; - this.cachedWallet = null; - } - - async saveWallet(wallet: StoredWallet, overwrite: boolean = false): Promise { - this.cachedWallet = wallet; - return this.baseWallet.saveWallet(wallet, overwrite); - } - - async loadWallet(): Promise { - if (!this.cachedWallet) { - this.cachedWallet = await this.baseWallet.loadWallet(); - } - - return this.cachedWallet; - } -} diff --git a/packages/keymaster/src/db/chrome.ts b/packages/keymaster/src/db/chrome.ts index becd1c7b9..48553f63b 100644 --- a/packages/keymaster/src/db/chrome.ts +++ b/packages/keymaster/src/db/chrome.ts @@ -1,6 +1,6 @@ -import { StoredWallet, WalletBase } from '../types.js'; +import { KeymasterStore, StoredWallet } from '../types.js'; -export default class WalletChrome implements WalletBase { +export default class WalletChrome implements KeymasterStore { walletName: string; constructor(walletName: string = 'mdip-keymaster') { diff --git a/packages/keymaster/src/db/json-memory.ts b/packages/keymaster/src/db/json-memory.ts index ccf5a0c04..f6f1b6ab5 100644 --- a/packages/keymaster/src/db/json-memory.ts +++ b/packages/keymaster/src/db/json-memory.ts @@ -1,6 +1,6 @@ -import { StoredWallet, WalletBase } from '../types.js'; +import { KeymasterStore, StoredWallet } from '../types.js'; -export default class WalletJsonMemory implements WalletBase { +export default class WalletJsonMemory implements KeymasterStore { walletCache: string | null = null; async saveWallet(wallet: StoredWallet, overwrite: boolean = false): Promise { diff --git a/packages/keymaster/src/db/json.ts b/packages/keymaster/src/db/json.ts index 35db7e4a6..781092d69 100644 --- a/packages/keymaster/src/db/json.ts +++ b/packages/keymaster/src/db/json.ts @@ -1,7 +1,7 @@ import fs from 'fs'; -import { StoredWallet, WalletBase } from '../types.js'; +import { KeymasterStore, StoredWallet } from '../types.js'; -export default class WalletJson implements WalletBase { +export default class WalletJson implements KeymasterStore { private readonly dataFolder: string; walletName: string; diff --git a/packages/keymaster/src/db/mongo.ts b/packages/keymaster/src/db/mongo.ts index aa4e070aa..e11609912 100644 --- a/packages/keymaster/src/db/mongo.ts +++ b/packages/keymaster/src/db/mongo.ts @@ -1,15 +1,15 @@ -import { StoredWallet, WalletBase } from '../types.js'; +import { KeymasterStore, StoredWallet } from '../types.js'; import { MongoClient, Db, Collection } from 'mongodb' -export default class WalletMongo implements WalletBase { +export default class WalletMongo implements KeymasterStore { private client: MongoClient; private db?: Db; private collection?: Collection; private dbName = 'keymaster' private readonly collectionName: string; - public static async create(): Promise { - const wallet = new WalletMongo(); + public static async create(walletKey: string = 'wallet'): Promise { + const wallet = new WalletMongo(walletKey); await wallet.connect(); return wallet; } @@ -30,7 +30,7 @@ export default class WalletMongo implements WalletBase { await this.client.close(); } - async saveWallet(wallet: Exclude, overwrite: boolean = false): Promise { + async saveWallet(wallet: StoredWallet, overwrite: boolean = false): Promise { if (!this.collection) { throw new Error('Not connected to MongoDB. Call connect() first or use WalletMongo.create().') } @@ -40,7 +40,7 @@ export default class WalletMongo implements WalletBase { return false; } - await this.collection.replaceOne({}, wallet, { upsert: true }); + await this.collection.replaceOne({}, wallet as Record, { upsert: true }); return true; } diff --git a/packages/keymaster/src/db/postgres.ts b/packages/keymaster/src/db/postgres.ts index db7e981d5..bef7c0a87 100644 --- a/packages/keymaster/src/db/postgres.ts +++ b/packages/keymaster/src/db/postgres.ts @@ -1,11 +1,11 @@ import { Pool } from 'pg'; -import { StoredWallet, WalletBase } from '../types.js'; +import { KeymasterStore, StoredWallet } from '../types.js'; interface WalletRow { data: StoredWallet | string; } -export default class WalletPostgres implements WalletBase { +export default class WalletPostgres implements KeymasterStore { private readonly url: string; private readonly walletKey: string; private pool: Pool | null; diff --git a/packages/keymaster/src/db/redis.ts b/packages/keymaster/src/db/redis.ts index cf3324748..85f34a432 100644 --- a/packages/keymaster/src/db/redis.ts +++ b/packages/keymaster/src/db/redis.ts @@ -1,7 +1,7 @@ -import { StoredWallet, WalletBase } from '../types.js'; +import { KeymasterStore, StoredWallet } from '../types.js'; import { Redis } from 'ioredis' -export default class WalletRedis implements WalletBase { +export default class WalletRedis implements KeymasterStore { private readonly walletKey: string; private readonly url: string; private redis: Redis | null diff --git a/packages/keymaster/src/db/sqlite.ts b/packages/keymaster/src/db/sqlite.ts index fd3c90f94..25b42bd12 100644 --- a/packages/keymaster/src/db/sqlite.ts +++ b/packages/keymaster/src/db/sqlite.ts @@ -1,8 +1,8 @@ -import { StoredWallet, WalletBase } from '../types.js'; +import { KeymasterStore, StoredWallet } from '../types.js'; import sqlite3 from 'sqlite3'; import { open, Database } from 'sqlite'; -export default class WalletSQLite implements WalletBase { +export default class WalletSQLite implements KeymasterStore { private readonly walletName: string; private db: Database | null; diff --git a/packages/keymaster/src/db/typeGuards.ts b/packages/keymaster/src/db/typeGuards.ts index 8f77040d7..f074c68ab 100644 --- a/packages/keymaster/src/db/typeGuards.ts +++ b/packages/keymaster/src/db/typeGuards.ts @@ -1,13 +1,21 @@ -import { WalletFile, WalletEncFile } from "../types.js"; +import { + LegacyWalletEncFile, + LegacyWalletFile, + WalletFile, +} from "../types.js"; -export function isV1WithEnc(obj: any): obj is WalletEncFile { +export function isV2Wallet(obj: any): obj is WalletFile { + return !!obj && obj.version === 2 && typeof obj.provider?.type === 'string' && typeof obj.provider?.walletFingerprint === 'string' && !!obj.ids; +} + +export function isV1WithEnc(obj: any): obj is LegacyWalletEncFile { return !!obj && obj.version === 1 && typeof obj.enc === 'string' && obj.seed?.mnemonicEnc; } -export function isV1Decrypted(obj: any): obj is WalletFile { +export function isV1Decrypted(obj: any): obj is LegacyWalletFile { return !!obj && obj.version === 1 && obj.seed?.mnemonicEnc && !('enc' in obj); } -export function isLegacyV0(obj: any): obj is WalletFile { +export function isLegacyV0(obj: any): obj is LegacyWalletFile { return !!obj && (!obj.version || obj.version === 0) && !!obj.seed?.hdkey && typeof obj.seed.mnemonic === 'string'; } diff --git a/packages/keymaster/src/db/web.ts b/packages/keymaster/src/db/web.ts index 119ff1700..32ca11930 100644 --- a/packages/keymaster/src/db/web.ts +++ b/packages/keymaster/src/db/web.ts @@ -1,6 +1,6 @@ -import { StoredWallet, WalletBase } from '../types.js'; +import { KeymasterStore, StoredWallet } from '../types.js'; -export default class WalletWeb implements WalletBase { +export default class WalletWeb implements KeymasterStore { walletName: string; constructor(walletName: string = 'mdip-keymaster') { diff --git a/packages/keymaster/src/index.ts b/packages/keymaster/src/index.ts index b28cf7553..2585ae627 100644 --- a/packages/keymaster/src/index.ts +++ b/packages/keymaster/src/index.ts @@ -3,7 +3,7 @@ export { default as KeymasterClient } from './keymaster-client.js'; export { default as SearchClient } from './search-client.js'; export { default as WalletJsonMemory } from './db/json-memory.js'; export { default as WalletWeb } from './db/web.js'; -export { default as WalletCache } from './db/cache.js'; export { default as WalletChrome } from './db/chrome.js'; +export { default as MnemonicHdWalletProvider } from './provider/mnemonic-hd.js'; export * from './db/typeGuards.js'; export * from './types.js'; diff --git a/packages/keymaster/src/keymaster-client.ts b/packages/keymaster/src/keymaster-client.ts index a75042a82..235997ad7 100644 --- a/packages/keymaster/src/keymaster-client.ts +++ b/packages/keymaster/src/keymaster-client.ts @@ -21,6 +21,7 @@ import { IssueCredentialsOptions, KeymasterClientOptions, KeymasterInterface, + MdipWalletBundle, NoticeMessage, Poll, StoredWallet, @@ -28,7 +29,6 @@ import { ViewPollResult, WaitUntilReadyOptions, WalletFile, - WalletEncFile, } from './types.js' import { Buffer } from 'buffer'; @@ -158,19 +158,29 @@ export default class KeymasterClient implements KeymasterInterface { } } - async backupWallet(): Promise { + async decryptMnemonic(): Promise { + try { + const response = await axios.get(`${this.API}/wallet/mnemonic`); + return response.data.mnemonic; + } + catch (error) { + throwError(error); + } + } + + async backupWallet(): Promise { try { const response = await axios.post(`${this.API}/wallet/backup`); - return response.data.ok; + return response.data.did; } catch (error) { throwError(error); } } - async recoverWallet(): Promise { + async recoverWallet(did?: string): Promise { try { - const response = await axios.post(`${this.API}/wallet/recover`); + const response = await axios.post(`${this.API}/wallet/recover`, { did }); return response.data.wallet; } catch (error) { @@ -178,30 +188,40 @@ export default class KeymasterClient implements KeymasterInterface { } } - async checkWallet(): Promise { + async exportWalletBundle(): Promise { try { - const response = await axios.post(`${this.API}/wallet/check`); - return response.data.check; + const response = await axios.get(`${this.API}/wallet/bundle`); + return response.data.bundle; } catch (error) { throwError(error); } } - async fixWallet(): Promise { + async importWalletBundle(bundle: MdipWalletBundle): Promise { try { - const response = await axios.post(`${this.API}/wallet/fix`); - return response.data.fix; + const response = await axios.put(`${this.API}/wallet/bundle`, { bundle }); + return response.data.wallet; } catch (error) { throwError(error); } } - async decryptMnemonic(): Promise { + async checkWallet(): Promise { try { - const response = await axios.get(`${this.API}/wallet/mnemonic`); - return response.data.mnemonic; + const response = await axios.post(`${this.API}/wallet/check`); + return response.data.check; + } + catch (error) { + throwError(error); + } + } + + async fixWallet(): Promise { + try { + const response = await axios.post(`${this.API}/wallet/fix`); + return response.data.fix; } catch (error) { throwError(error); @@ -1410,14 +1430,4 @@ export default class KeymasterClient implements KeymasterInterface { throwError(error); } } - - async exportEncryptedWallet(): Promise { - try { - const response = await axios.get(`${this.API}/export/wallet/encrypted`); - return response.data.wallet; - } - catch (error) { - throwError(error); - } - } } diff --git a/packages/keymaster/src/keymaster.ts b/packages/keymaster/src/keymaster.ts index 2e795ddda..523db2478 100644 --- a/packages/keymaster/src/keymaster.ts +++ b/packages/keymaster/src/keymaster.ts @@ -42,25 +42,24 @@ import { StoredWallet, VerifiableCredential, ViewPollResult, - WalletBase, WalletFile, - WalletEncFile, + KeymasterStore, + WalletProvider, SearchEngine, - Seed, } from '@mdip/keymaster/types'; import { + isV2Wallet, isV1WithEnc, isV1Decrypted, isLegacyV0 } from './db/typeGuards.js'; +import MnemonicHdWalletProvider from './provider/mnemonic-hd.js'; import { Cipher, - EcdsaJwkPair, EcdsaJwkPrivate, - EcdsaJwkPublic, + EcdsaJwkPublic } from '@mdip/cipher/types'; import { isValidDID } from '@mdip/ipfs/utils'; -import { decMnemonic, encMnemonic } from "./encryption.js"; const DefaultSchema = { "$schema": "http://json-schema.org/draft-07/schema#", @@ -93,9 +92,9 @@ export enum NoticeTags { } export default class Keymaster implements KeymasterInterface { - private readonly passphrase: string; private gatekeeper: GatekeeperInterface; - private db: WalletBase; + private store: KeymasterStore; + private walletProvider: WalletProvider; private cipher: Cipher; private searchEngine: SearchEngine | undefined; private readonly defaultRegistry: string; @@ -104,14 +103,23 @@ export default class Keymaster implements KeymasterInterface { private readonly maxDataLength: number; private _walletCache?: WalletFile; private _walletMutationLock: Promise = Promise.resolve(); - private _hdkeyCache?: any; constructor(options: KeymasterOptions) { if (!options || !options.gatekeeper || !options.gatekeeper.createDID) { throw new InvalidParameterError('options.gatekeeper'); } - if (!options.wallet || !options.wallet.loadWallet || !options.wallet.saveWallet) { - throw new InvalidParameterError('options.wallet'); + if (!options.store || !options.store.loadWallet || !options.store.saveWallet) { + throw new InvalidParameterError('options.store'); + } + if (!options.walletProvider || + !options.walletProvider.type || + !options.walletProvider.getFingerprint || + !options.walletProvider.resetWallet || + !options.walletProvider.createIdKey || + !options.walletProvider.signDigest || + !options.walletProvider.encrypt || + !options.walletProvider.decrypt) { + throw new InvalidParameterError('options.walletProvider'); } if (!options.cipher || !options.cipher.verifySig) { throw new InvalidParameterError('options.cipher'); @@ -119,13 +127,9 @@ export default class Keymaster implements KeymasterInterface { if (options.search && !options.search.search) { throw new InvalidParameterError('options.search'); } - if (!options.passphrase) { - throw new InvalidParameterError('options.passphrase'); - } - - this.passphrase = options.passphrase; this.gatekeeper = options.gatekeeper; - this.db = options.wallet; + this.store = options.store; + this.walletProvider = options.walletProvider; this.cipher = options.cipher; this.searchEngine = options.search; @@ -139,6 +143,49 @@ export default class Keymaster implements KeymasterInterface { return this.gatekeeper.listRegistries(); } + private async getWalletProviderIdentity() { + return { + type: this.walletProvider.type, + walletFingerprint: await this.walletProvider.getFingerprint(), + }; + } + + private isMnemonicHdWalletProvider(provider: WalletProvider): provider is MnemonicHdWalletProvider { + return provider instanceof MnemonicHdWalletProvider; + } + + private supportsKeyRotation( + provider: WalletProvider + ): provider is WalletProvider & { rotateKey(keyRef: string): Promise<{ publicJwk: EcdsaJwkPublic }> } { + return 'rotateKey' in provider && typeof provider.rotateKey === 'function'; + } + + private async normalizeStoredWallet(stored: StoredWallet): Promise { + if (isV2Wallet(stored)) { + const provider = await this.getWalletProviderIdentity(); + if (stored.provider.type !== provider.type || stored.provider.walletFingerprint !== provider.walletFingerprint) { + throw new KeymasterError('Wallet provider does not match stored metadata.'); + } + + return stored; + } + + if (isLegacyV0(stored) || isV1Decrypted(stored) || isV1WithEnc(stored)) { + if (!this.isMnemonicHdWalletProvider(this.walletProvider)) { + throw new KeymasterError('Legacy wallet migration requires MnemonicHdWalletProvider.'); + } + + const migrated = await this.walletProvider.migrateLegacyWallet(stored); + const ok = await this.store.saveWallet(migrated, true); + if (!ok) { + throw new KeymasterError('save wallet failed'); + } + return migrated; + } + + throw new KeymasterError('Unsupported wallet version.'); + } + private async mutateWallet( mutator: (wallet: WalletFile) => void | Promise ): Promise { @@ -158,8 +205,7 @@ export default class Keymaster implements KeymasterInterface { return; } - const reenc = await this.encryptWalletForStorage(decrypted); - const ok = await this.db.saveWallet(reenc, true); + const ok = await this.store.saveWallet(decrypted, true); if (!ok) { throw new KeymasterError('save wallet failed'); } @@ -177,14 +223,14 @@ export default class Keymaster implements KeymasterInterface { return this._walletCache; } - let stored = await this.db.loadWallet() as WalletFile | null; + const stored = await this.store.loadWallet(); if (!stored) { - stored = await this.newWallet(); + this._walletCache = await this.newWallet(); + return this._walletCache; } - const upgraded: WalletFile = await this.upgradeWallet(stored); - this._walletCache = await this.decryptWallet(upgraded); + this._walletCache = await this.normalizeStoredWallet(stored); return this._walletCache; } @@ -192,12 +238,11 @@ export default class Keymaster implements KeymasterInterface { wallet: StoredWallet, overwrite = true ): Promise { - let upgraded: WalletFile = await this.upgradeWallet(wallet); - let toStore: WalletEncFile = await this.encryptWallet(upgraded); + const normalized = await this.normalizeStoredWallet(wallet); - const ok = await this.db.saveWallet(toStore, overwrite); + const ok = await this.store.saveWallet(normalized, overwrite); if (ok) { - this._walletCache = await this.decryptWalletFromStorage(toStore); + this._walletCache = normalized; } return ok; } @@ -206,25 +251,21 @@ export default class Keymaster implements KeymasterInterface { mnemonic?: string, overwrite = false ): Promise { - try { - if (!mnemonic) { - mnemonic = this.cipher.generateMnemonic(); - } - - this._hdkeyCache = this.cipher.generateHDKey(mnemonic); - } catch { - throw new InvalidParameterError('mnemonic'); + if (this.isMnemonicHdWalletProvider(this.walletProvider)) { + await this.walletProvider.newWallet(mnemonic, overwrite); + } else if (mnemonic) { + throw new KeymasterError('Wallet provider does not support mnemonic initialization.'); + } else { + await this.walletProvider.resetWallet(overwrite); } - const mnemonicEnc = await encMnemonic(mnemonic, this.passphrase); const wallet: WalletFile = { - version: 1, - seed: { mnemonicEnc }, - counter: 0, - ids: {} + version: 2, + provider: await this.getWalletProviderIdentity(), + ids: {}, }; - const ok = await this.saveWallet(wallet, overwrite) + const ok = await this.saveWallet(wallet, overwrite); if (!ok) { throw new KeymasterError('save wallet failed'); } @@ -232,15 +273,6 @@ export default class Keymaster implements KeymasterInterface { return wallet; } - async decryptMnemonic(): Promise { - const wallet = await this.loadWallet(); - return this.getMnemonicForDerivation(wallet); - } - - async getMnemonicForDerivation(wallet: WalletFile): Promise { - return decMnemonic(wallet.seed.mnemonicEnc!, this.passphrase!); - } - async checkWallet(): Promise { const wallet = await this.loadWallet(); @@ -248,9 +280,6 @@ export default class Keymaster implements KeymasterInterface { let invalid = 0; let deleted = 0; - // Validate keys - await this.resolveSeedBank(); - for (const name of Object.keys(wallet.ids)) { try { const doc = await this.resolveDID(wallet.ids[name].did); @@ -416,165 +445,49 @@ export default class Keymaster implements KeymasterInterface { return { idsRemoved, ownedRemoved, heldRemoved, namesRemoved }; } - async resolveSeedBank(): Promise { - const keypair = await this.hdKeyPair(); - - const operation: Operation = { - type: "create", - created: new Date(0).toISOString(), - mdip: { - version: 1, - type: "agent", - registry: this.defaultRegistry, - }, - publicJwk: keypair.publicJwk, - }; - - const msgHash = this.cipher.hashJSON(operation); - const signature = this.cipher.signHash(msgHash, keypair.privateJwk); - const signed: Operation = { - ...operation, - signature: { - signed: new Date(0).toISOString(), - hash: msgHash, - value: signature - } - } - const did = await this.gatekeeper.createDID(signed); - return this.gatekeeper.resolveDID(did); - } - - async updateSeedBank(doc: MdipDocument): Promise { - const keypair = await this.hdKeyPair(); - const did = doc.didDocument?.id; - if (!did) { - throw new InvalidParameterError('seed bank missing DID'); - } - const current = await this.gatekeeper.resolveDID(did); - const previd = current.didDocumentMetadata?.versionId; - - const operation: Operation = { - type: "update", - did, - previd, - doc, - }; - - const msgHash = this.cipher.hashJSON(operation); - const signature = this.cipher.signHash(msgHash, keypair.privateJwk); - const signed = { - ...operation, - signature: { - signer: did, - signed: new Date().toISOString(), - hash: msgHash, - value: signature, - } - }; - - return await this.gatekeeper.updateDID(signed); - } - async backupWallet(registry = this.defaultRegistry, wallet?: WalletFile): Promise { - if (!wallet) { wallet = await this.loadWallet(); } - const keypair = await this.hdKeyPair(); - const seedBank = await this.resolveSeedBank(); - const msg = JSON.stringify(wallet); - const backup = this.cipher.encryptMessage(keypair.publicJwk, keypair.privateJwk, msg); - - const operation: Operation = { - type: "create", - created: new Date().toISOString(), - mdip: { - version: 1, - type: "asset", - registry: registry, - }, - controller: seedBank.didDocument?.id, - data: { backup: backup }, - }; - - const msgHash = this.cipher.hashJSON(operation); - const signature = this.cipher.signHash(msgHash, keypair.privateJwk); - - const signed: Operation = { - ...operation, - signature: { - signer: seedBank.didDocument?.id, - signed: new Date().toISOString(), - hash: msgHash, - value: signature, - } - }; - - const backupDID = await this.gatekeeper.createDID(signed); - - if (seedBank.didDocumentData && typeof seedBank.didDocumentData === 'object' && !Array.isArray(seedBank.didDocumentData)) { - const data = seedBank.didDocumentData as { wallet?: string }; - data.wallet = backupDID; - await this.updateSeedBank(seedBank); - } - + const backupDID = await this.createAsset({ backup: wallet }, { registry }); + await this.mutateWallet((current) => { + current.backupDid = backupDID; + }); return backupDID; } async recoverWallet(did?: string): Promise { try { if (!did) { - const seedBank = await this.resolveSeedBank(); - if (seedBank.didDocumentData && typeof seedBank.didDocumentData === 'object' && !Array.isArray(seedBank.didDocumentData)) { - const data = seedBank.didDocumentData as { wallet?: string }; - did = data.wallet; - } - if (!did) { - throw new InvalidParameterError('No backup DID found'); - } + const wallet = await this.loadWallet(); + did = wallet.backupDid; + } + + if (!did) { + throw new InvalidParameterError('No backup DID found'); } - const keypair = await this.hdKeyPair(); const data = await this.resolveAsset(did); if (!data) { throw new InvalidParameterError('No asset data found'); } - const castData = data as { backup?: string }; + const castData = data as { backup?: WalletFile | string }; - if (typeof castData.backup !== 'string') { - throw new InvalidParameterError('Asset "backup" is missing or not a string'); + if (castData.backup == null) { + throw new InvalidParameterError('Asset "backup" is missing'); } - const backup = this.cipher.decryptMessage(keypair.publicJwk, keypair.privateJwk, castData.backup); - let wallet = JSON.parse(backup); - - if (isV1Decrypted(wallet)) { - const mnemonic = await this.decryptMnemonic(); - // Backup might have a different mnemonic passphase so re-encrypt - wallet.seed.mnemonicEnc = await encMnemonic(mnemonic, this.passphrase); + const backup = typeof castData.backup === 'string' ? JSON.parse(castData.backup) : castData.backup; + const recovered = await this.normalizeStoredWallet(backup); + const ok = await this.store.saveWallet(recovered, true); + if (!ok) { + throw new KeymasterError('save wallet failed'); } - await this.mutateWallet(async (current) => { - // Clear all existing properties from the current wallet - // This ensures a clean slate before restoring the recovered wallet - for (const k in current) { - delete current[k as keyof StoredWallet]; - } - - // Upgrade the recovered wallet to the latest version if needed - wallet = await this.upgradeWallet(wallet); - - // Decrypt the wallet if needed - wallet = isV1WithEnc(wallet) ? await this.decryptWalletFromStorage(wallet) : wallet; - - // Copy all properties from the recovered wallet into the cleared current wallet - // This effectively replaces the current wallet with the recovered one - Object.assign(current, wallet); - }); - - return this.loadWallet(); + this._walletCache = recovered; + return recovered; } catch { // If we can't recover the wallet, just return the current one @@ -650,12 +563,6 @@ export default class Keymaster implements KeymasterInterface { return idInfo; } - async hdKeyPair(): Promise { - const wallet = await this.loadWallet(); - const hdkey = await this.getHDKeyFromCacheOrMnemonic(wallet); - return this.cipher.generateJwk(hdkey.privateKey!); - } - getPublicKeyJwk(doc: MdipDocument): EcdsaJwkPublic { // TBD Return the right public key, not just the first one if (!doc.didDocument) { @@ -672,26 +579,62 @@ export default class Keymaster implements KeymasterInterface { return publicKeyJwk; } - async fetchKeyPair(name?: string): Promise { - const wallet = await this.loadWallet(); - const id = await this.fetchIdInfo(name); - const hdkey = await this.getHDKeyFromCacheOrMnemonic(wallet); + private async getConfirmedPublicKeyJwk(id: IDInfo): Promise { const doc = await this.resolveDID(id.did, { confirm: true }); - const confirmedPublicKeyJwk = this.getPublicKeyJwk(doc); + return this.getPublicKeyJwk(doc); + } - for (let i = id.index; i >= 0; i--) { - const path = `m/44'/0'/${id.account}'/0/${i}`; - const didkey = hdkey.derive(path); - const keypair = this.cipher.generateJwk(didkey.privateKey!); + private parseVersionedKeyRef(keyRef: string): { baseKeyRef: string; version?: number } { + const hashIndex = keyRef.lastIndexOf('#'); + if (hashIndex < 0) { + return { baseKeyRef: keyRef }; + } - if (keypair.publicJwk.x === confirmedPublicKeyJwk.x && - keypair.publicJwk.y === confirmedPublicKeyJwk.y - ) { - return keypair; - } + const baseKeyRef = keyRef.slice(0, hashIndex); + const versionPart = keyRef.slice(hashIndex + 1); + const version = Number(versionPart); + + if (!Number.isInteger(version) || version < 0) { + throw new KeymasterError(`Unsupported keyRef: ${keyRef}`); } - return null; + return { baseKeyRef, version }; + } + + private incrementKeyRefVersion(keyRef: string): string { + const { baseKeyRef, version } = this.parseVersionedKeyRef(keyRef); + const nextVersion = typeof version === 'number' ? version + 1 : 1; + return `${baseKeyRef}#${nextVersion}`; + } + + private decrementKeyRefVersion(keyRef: string): string { + const { baseKeyRef, version } = this.parseVersionedKeyRef(keyRef); + const currentVersion = typeof version === 'number' ? version : 1; + + if (currentVersion <= 0) { + throw new KeymasterError(`Unsupported keyRef: ${keyRef}`); + } + + return `${baseKeyRef}#${currentVersion - 1}`; + } + + private async getActiveKeyRef(id: IDInfo): Promise { + if (!this.supportsKeyRotation(this.walletProvider)) { + return id.keyRef; + } + + const currentDoc = await this.resolveDID(id.did); + if (currentDoc.didDocumentMetadata?.confirmed !== false) { + return id.keyRef; + } + + const currentPublicJwk = this.getPublicKeyJwk(currentDoc); + const confirmedPublicJwk = await this.getConfirmedPublicKeyJwk(id); + if (this.cipher.hashJSON(currentPublicJwk) === this.cipher.hashJSON(confirmedPublicJwk)) { + return id.keyRef; + } + + return this.decrementKeyRefVersion(id.keyRef); } async createAsset( @@ -920,16 +863,16 @@ export default class Keymaster implements KeymasterInterface { } = options; const id = await this.fetchIdInfo(); - const senderKeypair = await this.fetchKeyPair(); - if (!senderKeypair) { - throw new KeymasterError('No valid sender keypair'); - } + const senderKeyRef = await this.getActiveKeyRef(id); + const senderPublicJwk = await this.getConfirmedPublicKeyJwk(id); const doc = await this.resolveDID(receiver, { confirm: true }); const receivePublicJwk = this.getPublicKeyJwk(doc); - const cipher_sender = encryptForSender ? this.cipher.encryptMessage(senderKeypair.publicJwk, senderKeypair.privateJwk, msg) : null; - const cipher_receiver = this.cipher.encryptMessage(receivePublicJwk, senderKeypair.privateJwk, msg); + const cipher_sender = encryptForSender + ? await this.walletProvider.encrypt(senderKeyRef, senderPublicJwk, msg) + : null; + const cipher_receiver = await this.walletProvider.encrypt(senderKeyRef, receivePublicJwk, msg); const cipher_hash = includeHash ? this.cipher.hashMessage(msg) : null; const encrypted: EncryptedMessage = { @@ -943,28 +886,7 @@ export default class Keymaster implements KeymasterInterface { return await this.createAsset({ encrypted }, options); } - private async decryptWithDerivedKeys(wallet: WalletFile, id: IDInfo, senderPublicJwk: EcdsaJwkPublic, ciphertext: string): Promise { - const hdkey = await this.getHDKeyFromCacheOrMnemonic(wallet); - - // Try all private keys for this ID, starting with the most recent and working backward - let index = id.index; - while (index >= 0) { - const path = `m/44'/0'/${id.account}'/0/${index}`; - const didkey = hdkey.derive(path); - const receiverKeypair = this.cipher.generateJwk(didkey.privateKey!); - try { - return this.cipher.decryptMessage(senderPublicJwk, receiverKeypair.privateJwk, ciphertext); - } - catch { - index -= 1; - } - } - - throw new KeymasterError("ID can't decrypt ciphertext"); - } - async decryptMessage(did: string): Promise { - const wallet = await this.loadWallet(); const id = await this.fetchIdInfo(); const asset = await this.resolveAsset(did); @@ -983,7 +905,7 @@ export default class Keymaster implements KeymasterInterface { const senderPublicJwk = this.getPublicKeyJwk(doc); const ciphertext = (crypt.sender === id.did && crypt.cipher_sender) ? crypt.cipher_sender : crypt.cipher_receiver; - return await this.decryptWithDerivedKeys(wallet, id, senderPublicJwk, ciphertext!); + return await this.walletProvider.decrypt(id.keyRef, senderPublicJwk, ciphertext!); } async encryptJSON( @@ -1008,7 +930,7 @@ export default class Keymaster implements KeymasterInterface { async addSignature( obj: T, - controller?: string + controller?: string, ): Promise { if (obj == null) { throw new InvalidParameterError('obj'); @@ -1016,15 +938,10 @@ export default class Keymaster implements KeymasterInterface { // Fetches current ID if name is missing const id = await this.fetchIdInfo(controller); - const keypair = await this.fetchKeyPair(controller); - - if (!keypair) { - throw new KeymasterError('addSignature: no keypair'); - } - try { const msgHash = this.cipher.hashJSON(obj); - const signature = this.cipher.signHash(msgHash, keypair.privateJwk); + const signingKeyRef = await this.getActiveKeyRef(id); + const signature = await this.walletProvider.signDigest(signingKeyRef, msgHash); return { ...obj, @@ -1376,14 +1293,13 @@ export default class Keymaster implements KeymasterInterface { ): Promise { let did = ''; await this.mutateWallet(async (wallet) => { - const account = wallet.counter; - const index = 0; - const signed = await this.createIdOperation(name, account, options); + this.validateName(name, wallet); + const createdKey = await this.walletProvider.createIdKey(); + const signed = await this.createIdOperation(name, options, createdKey); did = await this.gatekeeper.createDID(signed); - wallet.ids[name] = { did, account, index }; - wallet.counter += 1; + wallet.ids[name] = { did, keyRef: createdKey.keyRef }; wallet.current = name; }); @@ -1392,18 +1308,15 @@ export default class Keymaster implements KeymasterInterface { async createIdOperation( name: string, - account: number = 0, - options: { registry?: string } = {} + options: { registry?: string } = {}, + createdKey?: { keyRef: string; publicJwk: EcdsaJwkPublic }, ): Promise { const { registry = this.defaultRegistry } = options; const wallet = await this.loadWallet(); this.validateName(name, wallet); - const hdkey = await this.getHDKeyFromCacheOrMnemonic(wallet); - const path = `m/44'/0'/${account}'/0/0`; - const didkey = hdkey.derive(path); - const keypair = this.cipher.generateJwk(didkey.privateKey!); + const { keyRef, publicJwk } = createdKey ?? await this.walletProvider.createIdKey(); const block = await this.gatekeeper.getBlock(registry); const blockid = block?.hash; @@ -1417,11 +1330,11 @@ export default class Keymaster implements KeymasterInterface { type: 'agent', registry }, - publicJwk: keypair.publicJwk, + publicJwk, }; const msgHash = this.cipher.hashJSON(operation); - const signature = this.cipher.signHash(msgHash, keypair.privateJwk); + const signature = await this.walletProvider.signDigest(keyRef, msgHash); const signed: Operation = { ...operation, signature: { @@ -1475,20 +1388,17 @@ export default class Keymaster implements KeymasterInterface { const wallet = await this.loadWallet(); const name = id || wallet.current; const idInfo = await this.fetchIdInfo(name, wallet); - const keypair = await this.hdKeyPair(); const data = { name: name, id: idInfo, }; - const msg = JSON.stringify(data); - const backup = this.cipher.encryptMessage(keypair.publicJwk, keypair.privateJwk, msg); const doc = await this.resolveDID(idInfo.did); const registry = doc.mdip?.registry; if (!registry) { throw new InvalidParameterError('no registry found for agent DID'); } - const vaultDid = await this.createAsset({ backup: backup }, { registry, controller: name }); + const vaultDid = await this.createAsset({ backup: data }, { registry, controller: name }); if (doc.didDocumentData) { const docData = doc.didDocumentData as { vault: string }; @@ -1500,21 +1410,20 @@ export default class Keymaster implements KeymasterInterface { async recoverId(did: string): Promise { try { - const keypair = await this.hdKeyPair(); - const doc = await this.resolveDID(did); const docData = doc.didDocumentData as { vault?: string }; if (!docData.vault) { throw new InvalidDIDError('didDocumentData missing vault'); } - const vault = await this.resolveAsset(docData.vault) as { backup?: string }; - if (typeof vault.backup !== 'string') { + const vault = await this.resolveAsset(docData.vault) as { backup?: { name: string; id: IDInfo } | string }; + if (vault.backup == null) { throw new InvalidDIDError('backup not found in vault'); } - const backup = this.cipher.decryptMessage(keypair.publicJwk, keypair.privateJwk, vault.backup); - const data = JSON.parse(backup) as { name: string; id: IDInfo }; + const data = typeof vault.backup === 'string' + ? JSON.parse(vault.backup) as { name: string; id: IDInfo } + : vault.backup; await this.mutateWallet((wallet) => { if (wallet.ids[data.name]) { @@ -1522,7 +1431,6 @@ export default class Keymaster implements KeymasterInterface { } wallet.ids[data.name] = data.id; wallet.current = data.name; - wallet.counter += 1; }); return data.name; @@ -1540,14 +1448,13 @@ export default class Keymaster implements KeymasterInterface { await this.mutateWallet(async (wallet) => { const id = wallet.ids[wallet.current!]; - const nextIndex = id.index + 1; - - const hdkey = await this.getHDKeyFromCacheOrMnemonic(wallet); - const path = `m/44'/0'/${id.account}'/0/${nextIndex}`; - const didkey = hdkey.derive(path); - const keypair = this.cipher.generateJwk(didkey.privateKey!); + if (!this.supportsKeyRotation(this.walletProvider)) { + throw new KeymasterError('Wallet provider does not support key rotation.'); + } const doc = await this.resolveDID(id.did); + const currentKeyRef = id.keyRef; + const { publicJwk } = await this.walletProvider.rotateKey(currentKeyRef); if (!doc.didDocumentMetadata?.confirmed) { throw new KeymasterError('Cannot rotate keys'); @@ -1557,8 +1464,12 @@ export default class Keymaster implements KeymasterInterface { } const vmethod = doc.didDocument.verificationMethod[0]; - vmethod.id = `#key-${nextIndex + 1}`; - vmethod.publicKeyJwk = keypair.publicJwk; + if (!vmethod.publicKeyJwk) { + throw new KeymasterError('DID Document missing verificationMethod'); + } + const currentKeyCount = doc.didDocument.verificationMethod.length || 1; + vmethod.id = `#key-${currentKeyCount + 1}`; + vmethod.publicKeyJwk = publicJwk; doc.didDocument.authentication = [vmethod.id]; ok = await this.updateDID(doc); @@ -1566,7 +1477,7 @@ export default class Keymaster implements KeymasterInterface { throw new KeymasterError('Cannot rotate keys'); } - id.index = nextIndex; // persist in same mutation + id.keyRef = this.incrementKeyRefVersion(currentKeyRef); }); return ok; @@ -1741,16 +1652,14 @@ export default class Keymaster implements KeymasterInterface { const msg = JSON.stringify(signed); const id = await this.fetchIdInfo(); - const senderKeypair = await this.fetchKeyPair(); - if (!senderKeypair) { - throw new KeymasterError('No valid sender keypair'); - } + const senderKeyRef = await this.getActiveKeyRef(id); + const senderPublicJwk = await this.getConfirmedPublicKeyJwk(id); const holder = credential.credentialSubject.id; const holderDoc = await this.resolveDID(holder, { confirm: true }); const receivePublicJwk = this.getPublicKeyJwk(holderDoc); - const cipher_sender = this.cipher.encryptMessage(senderKeypair.publicJwk, senderKeypair.privateJwk, msg); - const cipher_receiver = this.cipher.encryptMessage(receivePublicJwk, senderKeypair.privateJwk, msg); + const cipher_sender = await this.walletProvider.encrypt(senderKeyRef, senderPublicJwk, msg); + const cipher_receiver = await this.walletProvider.encrypt(senderKeyRef, receivePublicJwk, msg); const msgHash = this.cipher.hashMessage(msg); const doc = await this.resolveDID(did); @@ -2796,7 +2705,7 @@ export default class Keymaster implements KeymasterInterface { async createGroupVault(options: GroupVaultOptions = {}): Promise { const id = await this.fetchIdInfo(); - const idKeypair = await this.fetchKeyPair(); + const idPublicJwk = await this.getConfirmedPublicKeyJwk(id); // version defaults to 1. To make version undefined (unit testing), set options.version to 0 const version = typeof options.version === 'undefined' ? 1 @@ -2804,8 +2713,8 @@ export default class Keymaster implements KeymasterInterface { const salt = this.cipher.generateRandomSalt(); const vaultKeypair = this.cipher.generateRandomJwk(); const keys = {}; - const config = this.cipher.encryptMessage(idKeypair!.publicJwk, vaultKeypair.privateJwk, JSON.stringify(options)); - const publicJwk = options.secretMembers ? idKeypair!.publicJwk : vaultKeypair.publicJwk; // If secret, encrypt for the owner only + const config = this.cipher.encryptMessage(idPublicJwk, vaultKeypair.privateJwk, JSON.stringify(options)); + const publicJwk = options.secretMembers ? idPublicJwk : vaultKeypair.publicJwk; // If secret, encrypt for the owner only const members = this.cipher.encryptMessage(publicJwk, vaultKeypair.privateJwk, JSON.stringify({})); const items = this.cipher.encryptMessage(vaultKeypair.publicJwk, vaultKeypair.privateJwk, JSON.stringify({})); const sha256 = this.cipher.hashJSON({}); @@ -2854,7 +2763,6 @@ export default class Keymaster implements KeymasterInterface { } private async decryptGroupVault(groupVault: GroupVault) { - const wallet = await this.loadWallet(); const id = await this.fetchIdInfo(); const myMemberId = this.generateSaltedId(groupVault, id.did); const myVaultKey = groupVault.keys[myMemberId]; @@ -2863,13 +2771,13 @@ export default class Keymaster implements KeymasterInterface { throw new KeymasterError('No access to group vault'); } - const privKeyJSON = await this.decryptWithDerivedKeys(wallet, id, groupVault.publicJwk, myVaultKey); + const privKeyJSON = await this.walletProvider.decrypt(id.keyRef, groupVault.publicJwk, myVaultKey); const privateJwk = JSON.parse(privKeyJSON) as EcdsaJwkPrivate; let config: GroupVaultOptions = {}; let isOwner = false; try { - const configJSON = await this.decryptWithDerivedKeys(wallet, id, groupVault.publicJwk, groupVault.config); + const configJSON = await this.walletProvider.decrypt(id.keyRef, groupVault.publicJwk, groupVault.config); config = JSON.parse(configJSON); isOwner = true; } @@ -2881,7 +2789,7 @@ export default class Keymaster implements KeymasterInterface { if (config.secretMembers) { try { - const membersJSON = await this.decryptWithDerivedKeys(wallet, id, groupVault.publicJwk, groupVault.members); + const membersJSON = await this.walletProvider.decrypt(id.keyRef, groupVault.publicJwk, groupVault.members); members = JSON.parse(membersJSON); } catch { @@ -2970,7 +2878,8 @@ export default class Keymaster implements KeymasterInterface { async addGroupVaultMember(vaultId: string, memberId: string): Promise { const owner = await this.checkGroupVaultOwner(vaultId); - const idKeypair = await this.fetchKeyPair(); + const id = await this.fetchIdInfo(); + const idPublicJwk = await this.getConfirmedPublicKeyJwk(id); const groupVault = await this.getGroupVault(vaultId); const { privateJwk, config, members } = await this.decryptGroupVault(groupVault); const memberDoc = await this.resolveDID(memberId, { confirm: true }); @@ -2982,7 +2891,7 @@ export default class Keymaster implements KeymasterInterface { } members[memberDID] = { added: new Date().toISOString() }; - const publicJwk = config.secretMembers ? idKeypair!.publicJwk : groupVault.publicJwk; + const publicJwk = config.secretMembers ? idPublicJwk : groupVault.publicJwk; groupVault.members = this.cipher.encryptMessage(publicJwk, privateJwk, JSON.stringify(members)); await this.addMemberKey(groupVault, memberDID, privateJwk); @@ -2992,7 +2901,8 @@ export default class Keymaster implements KeymasterInterface { async removeGroupVaultMember(vaultId: string, memberId: string): Promise { const owner = await this.checkGroupVaultOwner(vaultId); - const idKeypair = await this.fetchKeyPair(); + const id = await this.fetchIdInfo(); + const idPublicJwk = await this.getConfirmedPublicKeyJwk(id); const groupVault = await this.getGroupVault(vaultId); const { privateJwk, config, members } = await this.decryptGroupVault(groupVault); const memberDoc = await this.resolveDID(memberId, { confirm: true }); @@ -3004,7 +2914,7 @@ export default class Keymaster implements KeymasterInterface { } delete members[memberDID]; - const publicJwk = config.secretMembers ? idKeypair!.publicJwk : groupVault.publicJwk; + const publicJwk = config.secretMembers ? idPublicJwk : groupVault.publicJwk; groupVault.members = this.cipher.encryptMessage(publicJwk, privateJwk, JSON.stringify(members)); const memberKeyId = this.generateSaltedId(groupVault, memberDID); @@ -3569,11 +3479,6 @@ export default class Keymaster implements KeymasterInterface { return this.cleanupNotices(); } - async exportEncryptedWallet(): Promise { - const wallet = await this.loadWallet(); - return this.encryptWalletForStorage(wallet); - } - private async isBallot(ballotDid: string): Promise { let payload: any; try { @@ -3591,84 +3496,4 @@ export default class Keymaster implements KeymasterInterface { await this.addName(fallbackName, did); } catch { } } - - private async getHDKeyFromCacheOrMnemonic(wallet: WalletFile) { - if (this._hdkeyCache) { - return this._hdkeyCache; - } - - const mnemonic = await this.getMnemonicForDerivation(wallet); - return this.cipher.generateHDKey(mnemonic); - } - - private async encryptWalletForStorage(decrypted: WalletFile): Promise { - const { version, seed, ...rest } = decrypted; - - const safeSeed: Seed = { mnemonicEnc: seed.mnemonicEnc }; - - const hdkey = await this.getHDKeyFromCacheOrMnemonic(decrypted); - const { publicJwk, privateJwk } = this.cipher.generateJwk(hdkey.privateKey!); - - const plaintext = JSON.stringify(rest); - const enc = this.cipher.encryptMessage(publicJwk, privateJwk, plaintext); - - return { version: version!, seed: safeSeed, enc }; - } - - private async decryptWalletFromStorage(stored: WalletEncFile): Promise { - let mnemonic: string; - try { - mnemonic = await decMnemonic(stored.seed.mnemonicEnc!, this.passphrase); - } catch { - throw new KeymasterError('Incorrect passphrase.'); - } - - this._hdkeyCache = this.cipher.generateHDKey(mnemonic); - const { publicJwk, privateJwk } = this.cipher.generateJwk(this._hdkeyCache.privateKey!); - - const plaintext = this.cipher.decryptMessage(publicJwk, privateJwk, stored.enc); - const data = JSON.parse(plaintext); - - const wallet: WalletFile = { version: stored.version, seed: stored.seed, ...data }; - return wallet; - } - - private async decryptWallet(wallet: WalletFile): Promise { - if (isV1WithEnc(wallet)) { - wallet = await this.decryptWalletFromStorage(wallet); - } - - if (!isV1Decrypted(wallet)) { - throw new KeymasterError("Unsupported wallet version."); - } - - return wallet; - } - - private async encryptWallet(wallet: WalletFile): Promise { - if (isV1Decrypted(wallet)) { - return this.encryptWalletForStorage(wallet); - } - return wallet; - } - - private async upgradeWallet(wallet: any): Promise { - if (isLegacyV0(wallet)) { - const hdkey = this.cipher.generateHDKeyJSON(wallet.seed.hdkey!); - const keypair = this.cipher.generateJwk(hdkey.privateKey!); - const plaintext = this.cipher.decryptMessage(keypair.publicJwk, keypair.privateJwk, wallet.seed.mnemonic!); - const mnemonicEnc = await encMnemonic(plaintext, this.passphrase); - const { seed, version, ...rest } = wallet; - const newWallet = { version: 1, seed: { mnemonicEnc }, ...rest }; - this._hdkeyCache = this.cipher.generateHDKey(plaintext); - wallet = await this.encryptWallet(newWallet); - await this.db.saveWallet(wallet, true); - } - - if (wallet.version !== 1) { - throw new KeymasterError("Unsupported wallet version."); - } - - return wallet; - } } diff --git a/packages/keymaster/src/node.ts b/packages/keymaster/src/node.ts index d88d14789..63ed54a98 100644 --- a/packages/keymaster/src/node.ts +++ b/packages/keymaster/src/node.ts @@ -4,4 +4,5 @@ export { default as WalletJson } from './db/json.js'; export { default as WalletRedis } from './db/redis.js'; export { default as WalletMongo } from './db/mongo.js'; export { default as WalletSQLite } from './db/sqlite.js'; +export { default as MnemonicHdWalletProvider } from './provider/mnemonic-hd.js'; export { default as WalletPostgres } from './db/postgres.js'; diff --git a/packages/keymaster/src/provider/json-memory.ts b/packages/keymaster/src/provider/json-memory.ts new file mode 100644 index 000000000..59398eefd --- /dev/null +++ b/packages/keymaster/src/provider/json-memory.ts @@ -0,0 +1,22 @@ +import { MnemonicHdWalletState, WalletProviderStore } from '../types.js'; + +export default class WalletProviderJsonMemory implements WalletProviderStore { + walletCache: string | null = null; + + async saveWallet(wallet: MnemonicHdWalletState, overwrite: boolean = false): Promise { + if (this.walletCache && !overwrite) { + return false; + } + + this.walletCache = JSON.stringify(wallet); + return true; + } + + async loadWallet(): Promise { + if (!this.walletCache) { + return null; + } + + return JSON.parse(this.walletCache); + } +} diff --git a/packages/keymaster/src/provider/mnemonic-hd.ts b/packages/keymaster/src/provider/mnemonic-hd.ts new file mode 100644 index 000000000..0f0a9c4a4 --- /dev/null +++ b/packages/keymaster/src/provider/mnemonic-hd.ts @@ -0,0 +1,471 @@ +import { + InvalidParameterError, + KeymasterError, +} from '@mdip/common/errors'; +import type { + Cipher, + EcdsaJwkPair, + EcdsaJwkPublic, +} from '@mdip/cipher/types'; +import { decMnemonic, encMnemonic } from '../encryption.js'; +import { + isLegacyV0, + isV1Decrypted, + isV1WithEnc, +} from '../db/typeGuards.js'; +import type { + IDInfo, + LegacyStoredWallet, + LegacyWalletFile, + MnemonicHdWalletProviderInterface, + MnemonicHdKeyState, + MnemonicHdWalletState, + WalletFile, + WalletProviderKey, + WalletProviderStore, +} from '../types.js'; + +interface MnemonicHdWalletProviderOptions { + store: WalletProviderStore; + cipher: Cipher; + passphrase: string; +} + +export default class MnemonicHdWalletProvider implements MnemonicHdWalletProviderInterface { + readonly type = 'mnemonic-hd'; + + private readonly store: WalletProviderStore; + private readonly cipher: Cipher; + private passphrase: string; + private stateCache?: MnemonicHdWalletState; + private hdKeyCache?: any; + private mutationLock: Promise = Promise.resolve(); + + constructor(options: MnemonicHdWalletProviderOptions) { + if (!options?.store?.loadWallet || !options.store.saveWallet) { + throw new InvalidParameterError('options.store'); + } + if (!options?.cipher?.verifySig) { + throw new InvalidParameterError('options.cipher'); + } + if (!options?.passphrase) { + throw new InvalidParameterError('options.passphrase'); + } + + this.store = options.store; + this.cipher = options.cipher; + this.passphrase = options.passphrase; + } + + async newWallet(mnemonic?: string, overwrite = false): Promise { + + try { + if (!mnemonic) { + mnemonic = this.cipher.generateMnemonic(); + } + + this.hdKeyCache = this.cipher.generateHDKey(mnemonic); + } catch { + throw new InvalidParameterError('mnemonic'); + } + + const rootPublicJwk = this.getRootKeyPair().publicJwk; + + const mnemonicEnc = await encMnemonic(mnemonic, this.passphrase); + const state: MnemonicHdWalletState = { + version: 1, + type: 'mnemonic-hd', + rootPublicJwk, + mnemonicEnc, + nextAccount: 0, + keys: {}, + }; + + const ok = await this.store.saveWallet(state, overwrite); + if (!ok) { + throw new KeymasterError('save wallet failed'); + } + + this.stateCache = state; + } + + async resetWallet(overwrite = false): Promise { + await this.newWallet(undefined, overwrite); + } + + async decryptMnemonic(): Promise { + const state = await this.loadState(); + return this.decryptProviderMnemonic(state.mnemonicEnc); + } + + async changePassphrase(mnemonic: string, newPassphrase: string): Promise { + let hdKey; + try { + hdKey = this.cipher.generateHDKey(mnemonic); + } catch { + throw new InvalidParameterError('mnemonic'); + } + + const { publicJwk } = this.cipher.generateJwk(hdKey.privateKey!); + const state = await this.loadState(); + if (this.cipher.hashJSON(publicJwk) !== this.cipher.hashJSON(state.rootPublicJwk)) { + throw new KeymasterError('Mnemonic does not match wallet.'); + } + + const mnemonicEnc = await encMnemonic(mnemonic, newPassphrase); + await this.mutateState((current) => { + current.rootPublicJwk = publicJwk; + current.mnemonicEnc = mnemonicEnc; + }); + + this.passphrase = newPassphrase; + this.hdKeyCache = hdKey; + } + + async backupWallet(): Promise { + const state = this.stateCache ?? await this.store.loadWallet(); + if (!state) { + throw new KeymasterError('Wallet provider not initialized.'); + } + + this.stateCache = state; + return this.cloneState(state); + } + + async saveWallet(wallet: MnemonicHdWalletState, overwrite = false): Promise { + if (wallet.version !== 1 || wallet.type !== this.type || !wallet.rootPublicJwk) { + throw new InvalidParameterError('wallet'); + } + + const state = this.cloneState(wallet); + const ok = await this.store.saveWallet(state, overwrite); + if (!ok) { + return false; + } + + this.stateCache = state; + this.hdKeyCache = undefined; + return true; + } + + async getFingerprint(): Promise { + await this.getHdKey(); + const publicJwk = this.getRootKeyPair().publicJwk; + return this.cipher.hashJSON({ + type: this.type, + publicJwk, + }); + } + + async createIdKey(): Promise { + let created!: WalletProviderKey; + + await this.mutateState(async (state) => { + await this.getHdKey(); + const account = state.nextAccount; + created = this.deriveNextIdKey(account); + state.keys[this.makeBaseIdKeyRef(account)] = { + currentIndex: 0, + }; + state.nextAccount += 1; + }); + + return created; + } + + async signDigest(keyRef: string, digest: string): Promise { + await this.getHdKey(); + const keyPair = this.findKeyPairForRef(keyRef); + return this.cipher.signHash(digest, keyPair.privateJwk); + } + + async encrypt( + keyRef: string, + receiver: EcdsaJwkPublic, + plaintext: string, + ): Promise { + await this.getHdKey(); + const keyPair = this.findKeyPairForRef(keyRef); + return this.cipher.encryptMessage(receiver, keyPair.privateJwk, plaintext); + } + + async decrypt(keyRef: string, sender: EcdsaJwkPublic, ciphertext: string): Promise { + const state = await this.loadState(); + await this.getHdKey(); + const { baseKeyRef, version } = this.parseKeyRef(keyRef); + const entry = this.getIdKeyState(state, baseKeyRef); + const account = this.parseAccountFromBaseKeyRef(baseKeyRef); + const maxIndex = typeof version === 'number' ? version : entry.currentIndex; + + for (let index = maxIndex; index >= 0; index--) { + const keyPair = this.deriveIdKeyPair(account, index); + try { + return this.cipher.decryptMessage(sender, keyPair.privateJwk, ciphertext); + } catch { + } + } + + throw new KeymasterError("ID can't decrypt ciphertext"); + } + + async rotateKey(keyRef: string): Promise<{ publicJwk: EcdsaJwkPublic }> { + let publicJwk!: EcdsaJwkPublic; + + await this.mutateState(async (state) => { + await this.getHdKey(); + const { baseKeyRef } = this.parseKeyRef(keyRef); + const entry = this.getIdKeyState(state, baseKeyRef); + const account = this.parseAccountFromBaseKeyRef(baseKeyRef); + const nextIndex = entry.currentIndex + 1; + entry.currentIndex = nextIndex; + + publicJwk = this.deriveIdKeyPair(account, nextIndex).publicJwk; + }); + + return { publicJwk }; + } + + async migrateLegacyWallet(wallet: LegacyStoredWallet): Promise { + const decrypted = await this.normalizeLegacyWallet(wallet); + const mnemonic = await this.decryptProviderMnemonic(decrypted.seed.mnemonicEnc!); + const state = await this.buildStateFromLegacyWallet(decrypted, mnemonic); + this.stateCache = state; + const walletFingerprint = await this.getFingerprint(); + + const { seed, counter, version, ids, ...rest } = decrypted; + const migratedIds = Object.entries(ids).reduce>((acc, [name, legacy]) => { + const { account, index, ...info } = legacy; + acc[name] = { + ...info, + keyRef: this.makeIdKeyRef(account, index), + }; + return acc; + }, {}); + + const metadata: WalletFile = { + version: 2, + provider: { + type: this.type, + walletFingerprint, + }, + ids: migratedIds, + ...rest, + }; + return metadata; + } + + private async buildStateFromLegacyWallet(wallet: LegacyWalletFile, mnemonic: string): Promise { + const mnemonicEnc = await encMnemonic(mnemonic, this.passphrase); + this.hdKeyCache = this.cipher.generateHDKey(mnemonic); + const state: MnemonicHdWalletState = { + version: 1, + type: 'mnemonic-hd', + rootPublicJwk: this.getRootKeyPair().publicJwk, + mnemonicEnc, + nextAccount: wallet.counter, + keys: {}, + }; + + for (const legacy of Object.values(wallet.ids)) { + state.keys[this.makeBaseIdKeyRef(legacy.account)] = { + currentIndex: legacy.index, + }; + state.nextAccount = Math.max(state.nextAccount, legacy.account + 1); + } + + this.hdKeyCache = this.cipher.generateHDKey(mnemonic); + const ok = await this.store.saveWallet(state, true); + if (!ok) { + throw new KeymasterError('save wallet failed'); + } + + return state; + } + + private async normalizeLegacyWallet(wallet: LegacyStoredWallet): Promise { + if (isLegacyV0(wallet)) { + const hdkey = this.cipher.generateHDKeyJSON(wallet.seed.hdkey!); + const keypair = this.cipher.generateJwk(hdkey.privateKey!); + const plaintext = this.cipher.decryptMessage(keypair.publicJwk, keypair.privateJwk, wallet.seed.mnemonic!); + const mnemonicEnc = await encMnemonic(plaintext, this.passphrase); + const { seed, version, ...rest } = wallet; + return { + version: 1, + seed: { mnemonicEnc }, + ...rest, + }; + } + + if (isV1WithEnc(wallet)) { + const mnemonic = await this.decryptProviderMnemonic(wallet.seed.mnemonicEnc!); + this.hdKeyCache = this.cipher.generateHDKey(mnemonic); + const root = this.getRootKeyPair(); + const plaintext = this.cipher.decryptMessage(root.publicJwk, root.privateJwk, wallet.enc); + const data = JSON.parse(plaintext); + return { + version: 1, + seed: { mnemonicEnc: wallet.seed.mnemonicEnc }, + ...data, + }; + } + + if (isV1Decrypted(wallet)) { + return wallet; + } + + throw new KeymasterError('Unsupported wallet version.'); + } + + private async decryptProviderMnemonic(mnemonicEnc: MnemonicHdWalletState['mnemonicEnc']): Promise { + try { + return await decMnemonic(mnemonicEnc, this.passphrase); + } catch { + throw new KeymasterError('Incorrect passphrase.'); + } + } + + private async mutateState(mutator: (state: MnemonicHdWalletState) => void | Promise): Promise { + const run = async () => { + const state = await this.loadState(); + const before = JSON.stringify(state); + await mutator(state); + const after = JSON.stringify(state); + + if (before === after) { + return; + } + + const ok = await this.store.saveWallet(state, true); + if (!ok) { + throw new KeymasterError('save wallet failed'); + } + + this.stateCache = state; + }; + + const chained = this.mutationLock.then(run, run); + this.mutationLock = chained.catch(() => { }); + return chained; + } + + private async loadState(): Promise { + if (this.stateCache) { + return this.stateCache; + } + + const state = await this.store.loadWallet(); + if (!state) { + await this.newWallet(); + return this.stateCache!; + } + + this.stateCache = state; + return state; + } + + private async getHdKey() { + if (this.hdKeyCache) { + return this.hdKeyCache; + } + + const state = await this.loadState(); + const mnemonic = await this.decryptProviderMnemonic(state.mnemonicEnc); + this.hdKeyCache = this.cipher.generateHDKey(mnemonic); + return this.hdKeyCache; + } + + private getRootKeyPair(): EcdsaJwkPair { + if (!this.hdKeyCache) { + throw new KeymasterError('HD wallet cache not loaded'); + } + + return this.cipher.generateJwk(this.hdKeyCache.privateKey!); + } + + private deriveNextIdKey(account: number): WalletProviderKey { + const keyRef = this.makeIdKeyRef(account, 0); + const publicJwk = this.deriveIdKeyPair(account, 0).publicJwk; + + return { keyRef, publicJwk }; + } + + private makeBaseIdKeyRef(account: number): string { + return `hd:${account}`; + } + + private makeIdKeyRef(account: number, index: number): string { + return `${this.makeBaseIdKeyRef(account)}#${index}`; + } + + private parseKeyRef(keyRef: string): { baseKeyRef: string; version?: number } { + const hashIndex = keyRef.lastIndexOf('#'); + if (hashIndex < 0) { + return { baseKeyRef: keyRef }; + } + + const baseKeyRef = keyRef.slice(0, hashIndex); + const versionPart = keyRef.slice(hashIndex + 1); + const version = Number(versionPart); + + if (!Number.isInteger(version) || version < 0) { + throw new KeymasterError(`Unknown keyRef: ${keyRef}`); + } + + return { baseKeyRef, version }; + } + + private parseAccountFromBaseKeyRef(baseKeyRef: string): number { + if (!baseKeyRef.startsWith('hd:')) { + throw new KeymasterError(`Unknown keyRef: ${baseKeyRef}`); + } + + const account = Number(baseKeyRef.slice(3)); + if (!Number.isInteger(account) || account < 0) { + throw new KeymasterError(`Unknown keyRef: ${baseKeyRef}`); + } + + return account; + } + + private findKeyPairForRef(keyRef: string): EcdsaJwkPair { + const state = this.stateCache; + if (!state) { + throw new KeymasterError('Wallet provider not initialized.'); + } + + const { baseKeyRef, version } = this.parseKeyRef(keyRef); + const entry = this.getIdKeyState(state, baseKeyRef); + const account = this.parseAccountFromBaseKeyRef(baseKeyRef); + const index = typeof version === 'number' ? version : entry.currentIndex; + + if (index > entry.currentIndex) { + throw new KeymasterError(`Unknown keyRef: ${keyRef}`); + } + + return this.deriveIdKeyPair(account, index); + } + + private getIdKeyState(state: MnemonicHdWalletState, keyRef: string): MnemonicHdKeyState { + const entry = state.keys[keyRef]; + if (!entry) { + throw new KeymasterError(`Unknown keyRef: ${keyRef}`); + } + + return entry; + } + + private deriveIdKeyPair(account: number, index: number): EcdsaJwkPair { + if (!this.hdKeyCache) { + throw new KeymasterError('HD wallet cache not loaded'); + } + + const path = `m/44'/0'/${account}'/0/${index}`; + const didkey = this.hdKeyCache.derive(path); + return this.cipher.generateJwk(didkey.privateKey!); + } + + private cloneState(state: MnemonicHdWalletState): MnemonicHdWalletState { + return JSON.parse(JSON.stringify(state)) as MnemonicHdWalletState; + } + +} diff --git a/packages/keymaster/src/types.ts b/packages/keymaster/src/types.ts index 6f00a2603..701a52520 100644 --- a/packages/keymaster/src/types.ts +++ b/packages/keymaster/src/types.ts @@ -10,7 +10,7 @@ export interface HDKey { xpub: string; } -export interface Seed { +export interface LegacySeed { // v0 legacy mnemonic?: string; hdkey?: HDKey; @@ -24,6 +24,17 @@ export interface Seed { } export interface IDInfo { + did: string; + // Provider-managed key reference. Rotating providers may encode a key version. + keyRef: string; + held?: string[]; + owned?: string[]; + dmail?: Record; + notices?: Record; + [key: string]: any; // Allow custom metadata fields +} + +export interface LegacyIDInfo { did: string; account: number; index: number; @@ -31,23 +42,45 @@ export interface IDInfo { owned?: string[]; dmail?: Record; notices?: Record; - [key: string]: any; // Allow custom metadata fields + [key: string]: any; } -export interface WalletEncFile { +export interface LegacyWalletEncFile { version: number; - seed: Seed; + seed: LegacySeed; enc: string } +export interface WalletProviderIdentity { + type: string; + walletFingerprint: string; +} + export interface WalletFile { + version: 2; + provider: WalletProviderIdentity; + ids: Record; + current?: string; + names?: Record; + [key: string]: any; +} + +export interface LegacyWalletFile { version?: number; - seed: Seed; + seed: LegacySeed; counter: number; - ids: Record; + ids: Record; current?: string; names?: Record; - [key: string]: any; // Allow custom metadata fields + [key: string]: any; +} + +export type LegacyStoredWallet = LegacyWalletFile | LegacyWalletEncFile; + +export interface KeymasterBackupV2 { + version: 1; + provider: WalletProviderIdentity; + store: WalletFile; } export interface CheckWalletResult { @@ -220,21 +253,76 @@ export interface GroupVaultLogin { password: string; } -export type StoredWallet = WalletFile | WalletEncFile | null; +export type StoredWallet = WalletFile | KeymasterBackupV2 | LegacyStoredWallet; + +export interface MnemonicHdKeyState { + currentIndex: number; +} + +export interface MnemonicHdWalletState { + version: 1; + type: 'mnemonic-hd'; + rootPublicJwk: EcdsaJwkPublic; + mnemonicEnc: { + salt: string; + iv: string; + data: string; + }; + nextAccount: number; + keys: Record; +} + +export interface MdipWalletBundle { + version: 1; + type: 'mdip-wallet-bundle'; + keymaster: WalletFile; + provider: MnemonicHdWalletState; +} -export interface WalletBase { +export interface KeymasterStore { saveWallet(wallet: StoredWallet, overwrite?: boolean): Promise; loadWallet(): Promise; } +export interface WalletProviderStore { + saveWallet(wallet: MnemonicHdWalletState, overwrite?: boolean): Promise; + loadWallet(): Promise; +} + +export interface WalletProviderKey { + // Provider-managed key reference for the created ID key version. + keyRef: string; + publicJwk: EcdsaJwkPublic; +} + +export interface WalletProvider { + readonly type: string; + getFingerprint(): Promise; + resetWallet(overwrite?: boolean): Promise; + createIdKey(): Promise; + signDigest(keyRef: string, digest: string): Promise; + encrypt(keyRef: string, receiver: EcdsaJwkPublic, plaintext: string): Promise; + decrypt(keyRef: string, sender: EcdsaJwkPublic, ciphertext: string): Promise; +} + +export interface MnemonicHdWalletProviderInterface extends WalletProvider { + rotateKey(keyRef: string): Promise<{ publicJwk: EcdsaJwkPublic }>; + newWallet(mnemonic?: string, overwrite?: boolean): Promise; + migrateLegacyWallet(wallet: LegacyStoredWallet): Promise; + backupWallet(): Promise; + saveWallet(wallet: MnemonicHdWalletState, overwrite?: boolean): Promise; + decryptMnemonic(): Promise; + changePassphrase(mnemonic: string, newPassphrase: string): Promise; +} + export interface SearchEngine { search(query: object): Promise; } export interface KeymasterOptions { - passphrase: string; gatekeeper: GatekeeperInterface; - wallet: WalletBase; + store: KeymasterStore; + walletProvider: WalletProvider; cipher: Cipher; search?: SearchEngine; defaultRegistry?: string; @@ -305,12 +393,10 @@ export interface KeymasterInterface { loadWallet(): Promise; saveWallet(wallet: StoredWallet, overwrite?: boolean): Promise; newWallet(mnemonic?: string, overwrite?: boolean): Promise; - backupWallet(): Promise; - recoverWallet(): Promise; + backupWallet(): Promise; + recoverWallet(did?: string): Promise; checkWallet(): Promise; fixWallet(): Promise; - decryptMnemonic(): Promise; - exportEncryptedWallet(): Promise; // IDs listIds(): Promise; diff --git a/python/keymaster_sdk/src/keymaster_sdk/keymaster_sdk.py b/python/keymaster_sdk/src/keymaster_sdk/keymaster_sdk.py index b0a82afd2..339092627 100644 --- a/python/keymaster_sdk/src/keymaster_sdk/keymaster_sdk.py +++ b/python/keymaster_sdk/src/keymaster_sdk/keymaster_sdk.py @@ -128,18 +128,16 @@ def backup_wallet(): "POST", f"{_keymaster_api}/wallet/backup", ) - return response["ok"] + return response["did"] -def recover_wallet(): - response = proxy_request( - "POST", - f"{_keymaster_api}/wallet/recover", - ) +def recover_wallet(did=None): + payload = {"did": did} if did else {} + response = proxy_request("POST", f"{_keymaster_api}/wallet/recover", json=payload) return response["wallet"] -def new_wallet(mnemonic, overwrite=False): +def new_wallet(mnemonic=None, overwrite=False): response = proxy_request( "POST", f"{_keymaster_api}/wallet/new", @@ -165,11 +163,10 @@ def fix_wallet(): def decrypt_mnemonic(): - response = proxy_request( - "GET", - f"{_keymaster_api}/wallet/mnemonic", + raise KeymasterError( + "decrypt_mnemonic is no longer available through the generic keymaster API. " + "Mnemonic access now belongs to the mnemonic wallet provider." ) - return response["mnemonic"] def list_registries(): diff --git a/python/keymaster_sdk/tests/test_keymaster_sdk.py b/python/keymaster_sdk/tests/test_keymaster_sdk.py index b41d0c5c6..50895e305 100644 --- a/python/keymaster_sdk/tests/test_keymaster_sdk.py +++ b/python/keymaster_sdk/tests/test_keymaster_sdk.py @@ -3,7 +3,6 @@ import random import string import base64 -from unittest.mock import ANY from copy import deepcopy # Test vars @@ -179,31 +178,29 @@ def test_accept_remove_revoke_credential(): def test_wallet(): wallet = keymaster.load_wallet() - assert "seed" in wallet, "seed not present in wallet" - assert "mnemonicEnc" in wallet["seed"], "mnemonicEnc not present in wallet" - assert "data" in wallet["seed"]["mnemonicEnc"], "data not present in mnemonicEnc" - assert "iv" in wallet["seed"]["mnemonicEnc"], "iv not present in mnemonicEnc" - assert "salt" in wallet["seed"]["mnemonicEnc"], "salt not present in mnemonicEnc" + assert_equal(wallet["version"], 2) + assert_equal(wallet["provider"]["type"], "mnemonic-hd") + assert "walletFingerprint" in wallet["provider"], "walletFingerprint not present in wallet provider" + assert "ids" in wallet, "ids not present in wallet" response = keymaster.save_wallet(wallet) assert_equal(response, True) - did = keymaster.backup_wallet() - doc = keymaster.resolve_did(did) - assert_equal(doc["didDocument"]["id"], did) - - mnemonic1 = keymaster.decrypt_mnemonic() - assert_equal(len(mnemonic1.split()), 12) - - new_wallet = keymaster.new_wallet(mnemonic1, True) + backup_did = keymaster.backup_wallet() + doc = keymaster.resolve_did(backup_did) + assert_equal(doc["didDocument"]["id"], backup_did) - mnemonic2 = keymaster.decrypt_mnemonic() - assert_equal(mnemonic1, mnemonic2) + empty_wallet = { + "version": wallet["version"], + "provider": deepcopy(wallet["provider"]), + "ids": {}, + } + response = keymaster.save_wallet(empty_wallet) + assert_equal(response, True) - recovered = keymaster.recover_wallet() + recovered = keymaster.recover_wallet(backup_did) expected = deepcopy(wallet) - expected["seed"]["mnemonicEnc"] = ANY assert_equal(expected, recovered) @@ -329,9 +326,10 @@ def test_rotate_keys(): alice = generate_id() keymaster.create_id(alice, local_options) + before = keymaster.load_wallet() keymaster.rotate_keys() - wallet = keymaster.load_wallet() - assert_equal(wallet["ids"][alice]["index"], 1) + after = keymaster.load_wallet() + assert before["ids"][alice]["keyRef"] != after["ids"][alice]["keyRef"] def test_signature(): diff --git a/sample.env b/sample.env index 78e8025f3..61da7ba5d 100644 --- a/sample.env +++ b/sample.env @@ -27,8 +27,7 @@ KC_GATEKEEPER_RATE_LIMIT_SKIP_PATHS=/api/v1/ready # API paths to skip rate limit # Keymaster KC_KEYMASTER_PORT=4226 KC_KEYMASTER_DB=json -KC_ENCRYPTED_PASSPHRASE= -KC_WALLET_CACHE=false +KC_WALLET_PROVIDER_PASSPHRASE= KC_DEFAULT_REGISTRY=hyperswarm KC_KEYMASTER_SERVE_CLIENT=true KC_KEYMASTER_TRUST_PROXY=false # Trust proxy headers. Set if behind reverse proxy/load balancer @@ -39,9 +38,6 @@ KC_KEYMASTER_RATE_LIMIT_MAX_REQUESTS=600 # Number of requests per window KC_KEYMASTER_RATE_LIMIT_WHITELIST= # Whitelist as CSV (127.0.0.1,10.0.0.0/8,2001:db8::/32) KC_KEYMASTER_RATE_LIMIT_SKIP_PATHS=/api/v1/ready # API paths to skip rate limiter on -# Search server -KC_SEARCH_SERVER_DB=sqlite # sqlite | postgres - # Postgres KC_POSTGRES_PORT=5432 KC_POSTGRES_DB=mdip @@ -53,17 +49,17 @@ KC_POSTGRES_URL=postgresql://mdip:mdip@localhost:5432/mdip KC_REACT_WALLET_PORT=4228 # Search Server -SEARCH_SERVER_PORT=4002 -SEARCH_SERVER_GATEKEEPER_URL=http://gatekeeper:4224 -SEARCH_SERVER_REFRESH_INTERVAL_MS=5000 -SEARCH_SERVER_DB=sqlite -SEARCH_SERVER_TRUST_PROXY=false -SEARCH_SERVER_RATE_LIMIT_ENABLED=false -SEARCH_SERVER_RATE_LIMIT_WINDOW_VALUE=1 -SEARCH_SERVER_RATE_LIMIT_WINDOW_UNIT=minute -SEARCH_SERVER_RATE_LIMIT_MAX_REQUESTS=600 -SEARCH_SERVER_RATE_LIMIT_WHITELIST= -SEARCH_SERVER_RATE_LIMIT_SKIP_PATHS=/api/v1/ready +KC_SEARCH_SERVER_PORT=4002 +KC_SEARCH_SERVER_GATEKEEPER_URL=http://gatekeeper:4224 +KC_SEARCH_SERVER_REFRESH_INTERVAL_MS=5000 +KC_SEARCH_SERVER_DB=sqlite +KC_SEARCH_SERVER_TRUST_PROXY=false +KC_SEARCH_SERVER_RATE_LIMIT_ENABLED=false +KC_SEARCH_SERVER_RATE_LIMIT_WINDOW_VALUE=1 +KC_SEARCH_SERVER_RATE_LIMIT_WINDOW_UNIT=minute +KC_SEARCH_SERVER_RATE_LIMIT_MAX_REQUESTS=600 +KC_SEARCH_SERVER_RATE_LIMIT_WHITELIST= +KC_SEARCH_SERVER_RATE_LIMIT_SKIP_PATHS=/api/v1/ready # Hyperswarm KC_HYPR_DB=sqlite # Sync store backend for hyperswarm mediator: sqlite | postgres diff --git a/scripts/keychain-cli.js b/scripts/keychain-cli.js index d4ec653f8..d1ddcfdd1 100644 --- a/scripts/keychain-cli.js +++ b/scripts/keychain-cli.js @@ -17,6 +17,10 @@ function hasNameOption(args) { return args.some(arg => NAME_OPTIONS.has(arg) || arg.startsWith('--name=')); } +function isLegacyV0(obj) { + return !!obj && !obj.version && !!obj.seed?.hdkey && typeof obj.seed.mnemonic === 'string'; +} + function splitCommandArgs(args) { const options = []; const operands = []; @@ -184,11 +188,11 @@ program program .command('backup-wallet-file ') - .description('Backup wallet to file') + .description('Backup wallet bundle to file') .action(async (file) => { try { - const wallet = await keymaster.exportEncryptedWallet(); - fs.writeFileSync(file, JSON.stringify(wallet, null, 4)); + const bundle = await keymaster.exportWalletBundle(); + fs.writeFileSync(file, JSON.stringify(bundle, null, 4)); console.log(UPDATE_OK); } catch (error) { @@ -198,11 +202,22 @@ program program .command('restore-wallet-file ') - .description('Restore wallet from backup file') + .description('Restore wallet bundle from backup file') .action(async (file) => { try { const contents = fs.readFileSync(file).toString(); const wallet = JSON.parse(contents); + + if (wallet?.type === 'mdip-wallet-bundle' && wallet?.version === 1) { + await keymaster.importWalletBundle(wallet); + console.log(UPDATE_OK); + return; + } + + if (!isLegacyV0(wallet) && wallet?.version !== 1) { + throw new Error('Unsupported wallet backup format. Expected an mdip-wallet-bundle or legacy v0/v1 wallet.'); + } + const ok = await keymaster.saveWallet(wallet, true); console.log(ok ? UPDATE_OK : UPDATE_FAILED); } @@ -216,8 +231,8 @@ program .description('Show recovery phrase for wallet') .action(async () => { try { - const mnenomic = await keymaster.decryptMnemonic(); - console.log(mnenomic); + const mnemonic = await keymaster.decryptMnemonic(); + console.log(mnemonic); } catch (error) { console.error(error.error || error); @@ -226,7 +241,7 @@ program program .command('backup-wallet-did') - .description('Backup wallet to encrypted DID and seed bank') + .description('Backup wallet metadata to DID') .action(async () => { try { const did = await keymaster.backupWallet(); @@ -239,7 +254,7 @@ program program .command('recover-wallet-did [did]') - .description('Recover wallet from seed bank or encrypted DID') + .description('Recover wallet metadata from backup DID') .action(async (did) => { try { const wallet = await keymaster.recoverWallet(did); diff --git a/services/gatekeeper/client/src/App.jsx b/services/gatekeeper/client/src/App.jsx index 99bfb5d30..6a4756e5d 100644 --- a/services/gatekeeper/client/src/App.jsx +++ b/services/gatekeeper/client/src/App.jsx @@ -1,19 +1,24 @@ import { Buffer } from 'buffer'; -import React, { useState, useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; import GatekeeperClient from '@mdip/gatekeeper/client'; import CipherWeb from '@mdip/cipher/web'; import Keymaster from '@mdip/keymaster'; import SearchClient from '@mdip/keymaster/search'; import WalletWeb from '@mdip/keymaster/wallet/web'; -import WalletCache from '@mdip/keymaster/wallet/cache'; -import WalletJsonMemory from "@mdip/keymaster/wallet/json-memory"; -import { isV1WithEnc, isLegacyV0 } from '@mdip/keymaster/wallet/typeGuards'; +import WalletJsonMemory from '@mdip/keymaster/wallet/json-memory'; +import MnemonicHdWalletProvider from '@mdip/keymaster/wallet/mnemonic-hd'; +import { + isLegacyV0, + isV1Decrypted, + isV1WithEnc, + isV2Wallet, +} from '@mdip/keymaster/wallet/typeGuards'; +import { encMnemonic } from '@mdip/keymaster/encryption'; import KeymasterUI from './KeymasterUI'; import PassphraseModal from './PassphraseModal'; import WarningModal from './WarningModal'; import MnemonicModal from './MnemonicModal'; -import { encMnemonic } from '@mdip/keymaster/encryption'; import './App.css'; globalThis.Buffer = Buffer; @@ -21,24 +26,74 @@ globalThis.Buffer = Buffer; const gatekeeper = new GatekeeperClient(); const cipher = new CipherWeb(); +const KEYMASTER_STORE_NAME = 'mdip-keymaster'; +const WALLET_PROVIDER_STORE_NAME = 'mdip-wallet-provider'; +const INCORRECT_PASSPHRASE = 'Incorrect passphrase'; +const INCOMPLETE_WALLET = 'Wallet data is incomplete. Restore from an mdip-wallet-bundle or reset the wallet.'; + +function createMetadataStore() { + return new WalletWeb(KEYMASTER_STORE_NAME); +} + +function createProviderStore() { + return new WalletWeb(WALLET_PROVIDER_STORE_NAME); +} + +function createMemoryProviderStore() { + return new WalletJsonMemory(); +} + +function createMnemonicWalletProvider(passphrase, store = createProviderStore()) { + return new MnemonicHdWalletProvider({ + store, + cipher, + passphrase, + }); +} + async function createSearchClient() { const { protocol, hostname } = window.location; return SearchClient.create({ url: `${protocol}//${hostname}:4002` }); } +function isMdipWalletBundle(wallet) { + if (!wallet || typeof wallet !== 'object') { + return false; + } + + return wallet.version === 1 + && wallet.type === 'mdip-wallet-bundle' + && isV2Wallet(wallet.keymaster) + && !!wallet.provider + && wallet.provider.version === 1 + && wallet.provider.type === 'mnemonic-hd' + && !!wallet.provider.rootPublicJwk; +} + +async function verifyMnemonicAgainstProviderState(providerState, mnemonic) { + const hdKey = cipher.generateHDKey(mnemonic); + const { publicJwk } = cipher.generateJwk(hdKey.privateKey); + + if (cipher.hashJSON(publicJwk) !== cipher.hashJSON(providerState.rootPublicJwk)) { + throw new Error('Mnemonic does not match wallet.'); + } +} + function App() { const [isReady, setIsReady] = useState(false); const [modalAction, setModalAction] = useState(null); - const [passphraseErrorText, setPassphraseErrorText] = useState(""); + const [passphraseErrorText, setPassphraseErrorText] = useState(''); const [keymaster, setKeymaster] = useState(null); + const [walletProvider, setWalletProvider] = useState(null); const [kmEpoch, setKmEpoch] = useState(0); const [uploadAction, setUploadAction] = useState(null); const [pendingWallet, setPendingWallet] = useState(null); + const [pendingMnemonic, setPendingMnemonic] = useState(''); const [showResetConfirm, setShowResetConfirm] = useState(false); const [showResetSetup, setShowResetSetup] = useState(false); const [showRecoverMnemonic, setShowRecoverMnemonic] = useState(false); - const [mnemonicErrorText, setMnemonicErrorText] = useState(""); - const [recoveredMnemonic, setRecoveredMnemonic] = useState(""); + const [mnemonicErrorText, setMnemonicErrorText] = useState(''); + const [recoveredMnemonic, setRecoveredMnemonic] = useState(''); const [showRecoverSetup, setShowRecoverSetup] = useState(false); const [searchClient, setSearchClient] = useState(null); const [searchParams] = useSearchParams(); @@ -47,24 +102,17 @@ function App() { useEffect(() => { const init = async () => { try { - const [search, walletData] = await Promise.all([ - createSearchClient(), - new WalletWeb().loadWallet(), - ]); - + const search = await createSearchClient(); setSearchClient(search); - - if (!walletData || isLegacyV0(walletData)) { - setModalAction('set-passphrase'); - } else { - setModalAction('decrypt'); - } - } catch { + await initialiseWallet(); + } + catch { setPassphraseErrorText('Failed to initialize wallet services.'); } }; void init(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const getSearchClient = async () => { @@ -77,73 +125,206 @@ function App() { return created; }; - const buildKeymaster = async (wallet, passphrase) => { - const search = await getSearchClient(); - const instance = new Keymaster({gatekeeper, wallet, cipher, search, passphrase}); + async function initialiseWallet() { + const walletStore = createMetadataStore(); + const providerStore = createProviderStore(); + const walletData = await walletStore.loadWallet(); + const providerData = await providerStore.loadWallet(); + const hasIncompleteState = + (!!providerData && !walletData) + || (!!walletData && isV2Wallet(walletData) && !providerData); - try { - // check pass & convert to v1 if needed - await instance.loadWallet(); - } catch { - setPassphraseErrorText('Incorrect passphrase'); + setIsReady(false); + + if (hasIncompleteState) { + setPassphraseErrorText(INCOMPLETE_WALLET); + setModalAction('decrypt'); return; } + if (!walletData || pendingMnemonic || isLegacyV0(walletData) || isV1Decrypted(walletData)) { + setPassphraseErrorText(''); + setModalAction('set-passphrase'); + } + else { + setPassphraseErrorText(''); + setModalAction('decrypt'); + } + } + + async function createKeymaster( + passphrase, + store = createMetadataStore(), + providerStore = createProviderStore(), + ) { + const search = await getSearchClient(); + const nextWalletProvider = createMnemonicWalletProvider(passphrase, providerStore); + const instance = new Keymaster({ + gatekeeper, + store, + walletProvider: nextWalletProvider, + cipher, + search, + }); + + return { instance, walletProvider: nextWalletProvider }; + } + + async function activateWallet(instance, nextWalletProvider) { setModalAction(null); setPendingWallet(null); + setPendingMnemonic(''); + setRecoveredMnemonic(''); setUploadAction(null); - setPassphraseErrorText(""); + setPassphraseErrorText(''); setKeymaster(instance); - setKmEpoch((e) => e + 1); + setWalletProvider(nextWalletProvider); + setKmEpoch((epoch) => epoch + 1); setIsReady(true); - }; + } + + async function buildKeymaster(passphrase) { + const { instance, walletProvider: nextWalletProvider } = await createKeymaster(passphrase); + + try { + if (pendingMnemonic) { + await instance.newWallet(pendingMnemonic, true); + } + else { + await instance.loadWallet(); + } + } + catch { + setPassphraseErrorText(INCORRECT_PASSPHRASE); + return false; + } - async function rebuildKeymaster(passphrase) { - const walletWeb = new WalletWeb(); - const walletCached = new WalletCache(walletWeb); - await buildKeymaster(walletCached, passphrase); + await activateWallet(instance, nextWalletProvider); + return true; } - async function handlePassphraseSubmit(passphrase) { - setPassphraseErrorText(""); + async function persistWalletData(wallet, providerState) { + const providerStore = createProviderStore(); + const walletStore = createMetadataStore(); - const walletWeb = new WalletWeb(); - const walletMemory = new WalletJsonMemory(); - const search = await getSearchClient(); + const providerOk = await providerStore.saveWallet(providerState, true); + if (!providerOk) { + throw new Error('save provider wallet failed'); + } + + const walletOk = await walletStore.saveWallet(wallet, true); + if (!walletOk) { + throw new Error('save wallet failed'); + } + } + + async function importLegacyWallet(wallet, passphrase) { + const memoryStore = new WalletJsonMemory(); + const memoryProviderStore = createMemoryProviderStore(); + const { instance, walletProvider: memoryWalletProvider } = await createKeymaster( + passphrase, + memoryStore, + memoryProviderStore, + ); + + await memoryStore.saveWallet(wallet, true); + const normalized = await instance.loadWallet(); + const providerState = await memoryWalletProvider.backupWallet(); + await persistWalletData(normalized, providerState); + } + + async function importWalletBundle(bundle, passphrase) { + const memoryStore = new WalletJsonMemory(); + const memoryProviderStore = createMemoryProviderStore(); + const { instance, walletProvider: memoryWalletProvider } = await createKeymaster( + passphrase, + memoryStore, + memoryProviderStore, + ); + + await memoryStore.saveWallet(bundle.keymaster, true); + await memoryWalletProvider.saveWallet(bundle.provider, true); + const normalized = await instance.loadWallet(); + const providerState = await memoryWalletProvider.backupWallet(); + await persistWalletData(normalized, providerState); + } + + async function handlePassphraseSubmit(passphrase) { + setPassphraseErrorText(''); if (uploadAction && pendingWallet) { - if (modalAction === 'decrypt') { - await walletMemory.saveWallet(pendingWallet, true); - - try { - const km = new Keymaster({ gatekeeper, wallet: walletMemory, cipher, search, passphrase }); - // check pass - await km.loadWallet(); - await walletWeb.saveWallet(pendingWallet, true); - } catch { - setPassphraseErrorText('Incorrect passphrase'); - return; + try { + if (uploadAction === 'upload-bundle' && isMdipWalletBundle(pendingWallet)) { + await importWalletBundle(pendingWallet, passphrase); } - } else { // upload-plain-v0 - await walletWeb.saveWallet(pendingWallet, true); + else { + await importLegacyWallet(pendingWallet, passphrase); + } + } + catch { + setPassphraseErrorText( + modalAction === 'decrypt' ? INCORRECT_PASSPHRASE : 'Failed to import wallet.', + ); + return; } } - await rebuildKeymaster(passphrase); + await buildKeymaster(passphrase); } - + + async function handleModalClose() { + setPendingWallet(null); + setPendingMnemonic(''); + setRecoveredMnemonic(''); + setPassphraseErrorText(''); + + const walletData = await createMetadataStore().loadWallet(); + const providerData = await createProviderStore().loadWallet(); + if (walletData || providerData) { + setModalAction(null); + } + } + + async function handleWalletUploadFile(uploaded) { + setPendingWallet(uploaded); + + if (isMdipWalletBundle(uploaded)) { + setUploadAction('upload-bundle'); + setModalAction('decrypt'); + return; + } + + if (isLegacyV0(uploaded) || isV1Decrypted(uploaded)) { + setUploadAction('upload-legacy-plain'); + setModalAction('set-passphrase'); + return; + } + + if (isV1WithEnc(uploaded)) { + setUploadAction('upload-legacy-encrypted'); + setModalAction('decrypt'); + return; + } + + if (isV2Wallet(uploaded)) { + window.alert('Standalone keymaster metadata is not enough. Upload an mdip-wallet-bundle instead.'); + return; + } + + window.alert('Unsupported wallet type'); + } + function handleStartReset() { - setPassphraseErrorText(""); + setPassphraseErrorText(''); setShowResetConfirm(true); } function handleStartRecover() { - setMnemonicErrorText(""); + setMnemonicErrorText(''); + setRecoveredMnemonic(''); setShowRecoverMnemonic(true); - setPassphraseErrorText(""); + setPassphraseErrorText(''); - // only nullify modalAction if we are uploading a wallet, otherwise - // leave passphrase modal open in case the user cancels if (uploadAction !== null) { setModalAction(null); } @@ -160,58 +341,49 @@ function App() { async function handleResetPassphraseSubmit(newPassphrase) { try { - const walletWeb = new WalletWeb(); - const search = await getSearchClient(); - const km = new Keymaster({ gatekeeper, wallet: walletWeb, cipher, search, passphrase: newPassphrase }); - await km.newWallet(undefined, true); + const { instance } = await createKeymaster(newPassphrase); + await instance.newWallet(undefined, true); setShowResetSetup(false); - await rebuildKeymaster(newPassphrase); - } catch { - setPassphraseErrorText('Failed to reset wallet. Try again.'); + await buildKeymaster(newPassphrase); } - } - - async function handleWalletUploadFile(uploaded) { - setPendingWallet(uploaded); - - if (isLegacyV0(uploaded)) { - setUploadAction('upload-plain-v0'); - setModalAction('set-passphrase'); - } else if (isV1WithEnc(uploaded)) { - setUploadAction('upload-enc-v1'); - setModalAction('decrypt'); - } else { - window.alert('Unsupported wallet type'); + catch { + setPassphraseErrorText('Failed to reset wallet. Try again.'); } } - function handleModalClose() { - setModalAction(null); - setPendingWallet(null); - setPassphraseErrorText(""); - } - async function handleRecoverMnemonicSubmit(mnemonic) { - setMnemonicErrorText(""); + setMnemonicErrorText(''); + try { - const walletWeb = new WalletWeb(); - let stored = pendingWallet && isV1WithEnc(pendingWallet) + const walletStore = createMetadataStore(); + const providerStore = createProviderStore(); + const storedWallet = pendingWallet && isV1WithEnc(pendingWallet) ? pendingWallet - : await walletWeb.loadWallet(); + : await walletStore.loadWallet(); - if (!isV1WithEnc(stored)) { - setMnemonicErrorText('Recovery not available for this wallet type.'); - return; + if (isV1WithEnc(storedWallet)) { + const hdKey = cipher.generateHDKey(mnemonic); + const { publicJwk, privateJwk } = cipher.generateJwk(hdKey.privateKey); + cipher.decryptMessage(publicJwk, privateJwk, storedWallet.enc); } + else { + const providerState = isMdipWalletBundle(pendingWallet) + ? pendingWallet.provider + : await providerStore.loadWallet(); + + if (!providerState) { + setMnemonicErrorText('Recovery not available for this wallet type.'); + return; + } - const hdkey = cipher.generateHDKey(mnemonic); - const { publicJwk, privateJwk } = cipher.generateJwk(hdkey.privateKey); - cipher.decryptMessage(publicJwk, privateJwk, stored.enc); + await verifyMnemonicAgainstProviderState(providerState, mnemonic); + } setRecoveredMnemonic(mnemonic); setShowRecoverMnemonic(false); setShowRecoverSetup(true); - } catch { + } + catch { setMnemonicErrorText('Mnemonic is incorrect. Try again.'); } } @@ -220,33 +392,113 @@ function App() { if (!recoveredMnemonic) { return; } + try { - const walletWeb = new WalletWeb(); - const base = pendingWallet && isV1WithEnc(pendingWallet) + const walletStore = createMetadataStore(); + const providerStore = createProviderStore(); + const storedWallet = pendingWallet && isV1WithEnc(pendingWallet) ? pendingWallet - : await walletWeb.loadWallet(); + : await walletStore.loadWallet(); - if (!isV1WithEnc(base)) { - setPassphraseErrorText('Recovery not available for this wallet type.'); - return; + if (isV1WithEnc(storedWallet)) { + const mnemonicEnc = await encMnemonic(recoveredMnemonic, newPassphrase); + const updatedWallet = { + version: storedWallet.version, + seed: { mnemonicEnc }, + enc: storedWallet.enc, + }; + + await importLegacyWallet(updatedWallet, newPassphrase); } + else { + const providerState = isMdipWalletBundle(pendingWallet) + ? pendingWallet.provider + : await providerStore.loadWallet(); + + if (!providerState) { + setPassphraseErrorText('Recovery not available for this wallet type.'); + return; + } - const mnemonicEnc = await encMnemonic(recoveredMnemonic, newPassphrase); - const updated = { - version: base.version, - seed: { mnemonicEnc }, - enc: base.enc - }; + const recoveryProvider = createMnemonicWalletProvider(newPassphrase, createMemoryProviderStore()); + await recoveryProvider.saveWallet(providerState, true); + await recoveryProvider.changePassphrase(recoveredMnemonic, newPassphrase); + const updatedProviderState = await recoveryProvider.backupWallet(); - await walletWeb.saveWallet(updated, true); - setRecoveredMnemonic(""); + if (isMdipWalletBundle(pendingWallet)) { + await persistWalletData(pendingWallet.keymaster, updatedProviderState); + } + else { + const wallet = await walletStore.loadWallet(); + if (!wallet || !isV2Wallet(wallet)) { + setPassphraseErrorText('Recovery not available for this wallet type.'); + return; + } + + await persistWalletData(wallet, updatedProviderState); + } + } + + setRecoveredMnemonic(''); setShowRecoverSetup(false); - await rebuildKeymaster(newPassphrase); - } catch { + await buildKeymaster(newPassphrase); + } + catch { setPassphraseErrorText('Failed to update passphrase. Try again.'); } } + async function handleShowMnemonic() { + if (!walletProvider) { + throw new Error('Wallet provider not available.'); + } + + return walletProvider.decryptMnemonic(); + } + + async function handleCreateWallet() { + setPendingWallet(null); + setPendingMnemonic(''); + setRecoveredMnemonic(''); + setUploadAction(null); + setPassphraseErrorText(''); + setShowResetSetup(true); + } + + async function handleImportMnemonic(mnemonic) { + setPendingMnemonic(mnemonic); + setPendingWallet(null); + setRecoveredMnemonic(''); + setUploadAction(null); + setPassphraseErrorText(''); + setModalAction('set-passphrase'); + } + + async function handleDownloadWallet() { + if (!keymaster || !walletProvider) { + return; + } + + const wallet = await keymaster.loadWallet(); + const provider = await walletProvider.backupWallet(); + const bundle = { + version: 1, + type: 'mdip-wallet-bundle', + keymaster: wallet, + provider, + }; + const walletJSON = JSON.stringify(bundle, null, 4); + const blob = new Blob([walletJSON], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + + const link = document.createElement('a'); + link.href = url; + link.download = 'mdip-wallet-bundle.json'; + link.click(); + + URL.revokeObjectURL(url); + } + return ( <> )} diff --git a/services/gatekeeper/client/src/KeymasterUI.jsx b/services/gatekeeper/client/src/KeymasterUI.jsx index d86bb3374..d9f8c26c0 100644 --- a/services/gatekeeper/client/src/KeymasterUI.jsx +++ b/services/gatekeeper/client/src/KeymasterUI.jsx @@ -96,7 +96,17 @@ const DmailTags = { UNREAD: 'unread', }; -function KeymasterUI({ keymaster, title, challengeDID, onWalletUpload }) { +function KeymasterUI({ + keymaster, + title, + challengeDID, + onWalletUpload, + onShowMnemonic, + onCreateWallet, + onImportMnemonic, + onWalletDownload, + onRecoverWallet, +}) { const [tab, setTab] = useState(null); const [currentId, setCurrentId] = useState(''); const [saveId, setSaveId] = useState(''); @@ -1616,7 +1626,9 @@ function KeymasterUI({ keymaster, title, challengeDID, onWalletUpload }) { async function showMnemonic() { try { - const response = await keymaster.decryptMnemonic(); + const response = onShowMnemonic + ? await onShowMnemonic() + : await keymaster.decryptMnemonic(); setMnemonicString(response); } catch (error) { showError(error); @@ -1630,7 +1642,13 @@ function KeymasterUI({ keymaster, title, challengeDID, onWalletUpload }) { async function newWallet() { try { if (window.confirm(`Overwrite wallet with new one?`)) { - await keymaster.newWallet(null, true); + if (onCreateWallet) { + await onCreateWallet(); + return; + } + else { + await keymaster.newWallet(undefined, true); + } refreshAll(); } } catch (error) { @@ -1640,11 +1658,17 @@ function KeymasterUI({ keymaster, title, challengeDID, onWalletUpload }) { async function importWallet() { try { - const mnenomic = window.prompt("Overwrite wallet with mnemonic:"); + const mnemonic = window.prompt("Overwrite wallet with mnemonic:"); - if (mnenomic) { - await keymaster.newWallet(mnenomic, true); - await keymaster.recoverWallet(); + if (mnemonic) { + if (onImportMnemonic) { + await onImportMnemonic(mnemonic); + return; + } + else { + await keymaster.newWallet(mnemonic, true); + await keymaster.recoverWallet(); + } refreshAll(); } } catch (error) { @@ -1655,7 +1679,7 @@ function KeymasterUI({ keymaster, title, challengeDID, onWalletUpload }) { async function backupWallet() { try { await keymaster.backupWallet(); - showSuccess('Wallet backup successful') + showSuccess('Wallet metadata backup successful'); } catch (error) { showError(error); @@ -1665,7 +1689,12 @@ function KeymasterUI({ keymaster, title, challengeDID, onWalletUpload }) { async function recoverWallet() { try { if (window.confirm(`Overwrite wallet from backup?`)) { - await keymaster.recoverWallet(); + if (onRecoverWallet) { + await onRecoverWallet(); + } + else { + await keymaster.recoverWallet(); + } refreshAll(); } } catch (error) { @@ -1732,21 +1761,19 @@ function KeymasterUI({ keymaster, title, challengeDID, onWalletUpload }) { if (onWalletUpload) { await onWalletUpload(wallet); - await refreshAll(); return; } - const backupWallet = await keymaster.exportEncryptedWallet(); - try { - await keymaster.saveWallet(wallet, true); + if (wallet?.type === 'mdip-wallet-bundle' && wallet?.version === 1) { + await keymaster.importWalletBundle(wallet); + } else { + await keymaster.saveWallet(wallet, true); + } await keymaster.loadWallet(); - refreshAll(); + await refreshAll(); } catch (e) { - try { - await keymaster.saveWallet(backupWallet, true); - } catch { } - window.alert('Upload rejected: the server could not decrypt the wallet with its configured passphrase.'); + window.alert('Upload rejected: unsupported wallet format for this client.'); } }; @@ -1759,14 +1786,19 @@ function KeymasterUI({ keymaster, title, challengeDID, onWalletUpload }) { async function downloadWallet() { try { - const wallet = await keymaster.exportEncryptedWallet(); - const walletJSON = JSON.stringify(wallet, null, 4); + if (onWalletDownload) { + await onWalletDownload(); + return; + } + + const bundle = await keymaster.exportWalletBundle(); + const walletJSON = JSON.stringify(bundle, null, 4); const blob = new Blob([walletJSON], { type: 'application/json' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; - link.download = 'mdip-wallet.json'; + link.download = 'mdip-wallet-bundle.json'; link.click(); // The URL.revokeObjectURL() method releases an existing object URL which was previously created by calling URL.createObjectURL(). diff --git a/services/gatekeeper/client/vite.config.mjs b/services/gatekeeper/client/vite.config.mjs index 6cf362a32..bba1d14eb 100644 --- a/services/gatekeeper/client/vite.config.mjs +++ b/services/gatekeeper/client/vite.config.mjs @@ -28,7 +28,7 @@ export default defineConfig({ '@mdip/gatekeeper/client': resolvePackageDist('gatekeeper/dist/esm/gatekeeper-client.js'), '@mdip/keymaster/wallet/web': resolvePackageDist('keymaster/dist/esm/db/web.js'), '@mdip/keymaster/wallet/json-memory': resolvePackageDist('keymaster/dist/esm/db/json-memory.js'), - '@mdip/keymaster/wallet/cache': resolvePackageDist('keymaster/dist/esm/db/cache.js'), + '@mdip/keymaster/wallet/mnemonic-hd': resolvePackageDist('keymaster/dist/esm/provider/mnemonic-hd.js'), '@mdip/keymaster/wallet/typeGuards': resolvePackageDist('keymaster/dist/esm/db/typeGuards.js'), '@mdip/keymaster/search': resolvePackageDist('keymaster/dist/esm/search-client.js'), '@mdip/keymaster/encryption': resolvePackageDist('keymaster/dist/esm/encryption.js'), diff --git a/services/keymaster/client/src/App.jsx b/services/keymaster/client/src/App.jsx index be8e1bdd9..2d375b50c 100644 --- a/services/keymaster/client/src/App.jsx +++ b/services/keymaster/client/src/App.jsx @@ -2,10 +2,66 @@ import KeymasterClient from '@mdip/keymaster/client'; import KeymasterUI from './KeymasterUI'; import './App.css'; +function isMdipWalletBundle(wallet) { + return !!wallet + && typeof wallet === 'object' + && wallet.version === 1 + && wallet.type === 'mdip-wallet-bundle' + && !!wallet.keymaster + && !!wallet.provider; +} + +function isV2Wallet(wallet) { + return !!wallet + && typeof wallet === 'object' + && wallet.version === 2 + && !!wallet.provider + && typeof wallet.ids === 'object'; +} + +function downloadJson(filename, data) { + const walletJSON = JSON.stringify(data, null, 4); + const blob = new Blob([walletJSON], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + + const link = document.createElement('a'); + link.href = url; + link.download = filename; + link.click(); + + URL.revokeObjectURL(url); +} + function App() { const keymaster = new KeymasterClient(); + + async function handleWalletUpload(wallet) { + if (isMdipWalletBundle(wallet)) { + await keymaster.importWalletBundle(wallet); + return; + } + + if (isV2Wallet(wallet)) { + window.alert('Standalone keymaster metadata is not enough. Upload an mdip-wallet-bundle instead.'); + return; + } + + await keymaster.saveWallet(wallet, true); + await keymaster.loadWallet(); + } + + async function handleWalletDownload() { + const bundle = await keymaster.exportWalletBundle(); + downloadJson('mdip-wallet-bundle.json', bundle); + } + return ( - + ); } diff --git a/services/keymaster/client/src/KeymasterUI.jsx b/services/keymaster/client/src/KeymasterUI.jsx index d86bb3374..d9f8c26c0 100644 --- a/services/keymaster/client/src/KeymasterUI.jsx +++ b/services/keymaster/client/src/KeymasterUI.jsx @@ -96,7 +96,17 @@ const DmailTags = { UNREAD: 'unread', }; -function KeymasterUI({ keymaster, title, challengeDID, onWalletUpload }) { +function KeymasterUI({ + keymaster, + title, + challengeDID, + onWalletUpload, + onShowMnemonic, + onCreateWallet, + onImportMnemonic, + onWalletDownload, + onRecoverWallet, +}) { const [tab, setTab] = useState(null); const [currentId, setCurrentId] = useState(''); const [saveId, setSaveId] = useState(''); @@ -1616,7 +1626,9 @@ function KeymasterUI({ keymaster, title, challengeDID, onWalletUpload }) { async function showMnemonic() { try { - const response = await keymaster.decryptMnemonic(); + const response = onShowMnemonic + ? await onShowMnemonic() + : await keymaster.decryptMnemonic(); setMnemonicString(response); } catch (error) { showError(error); @@ -1630,7 +1642,13 @@ function KeymasterUI({ keymaster, title, challengeDID, onWalletUpload }) { async function newWallet() { try { if (window.confirm(`Overwrite wallet with new one?`)) { - await keymaster.newWallet(null, true); + if (onCreateWallet) { + await onCreateWallet(); + return; + } + else { + await keymaster.newWallet(undefined, true); + } refreshAll(); } } catch (error) { @@ -1640,11 +1658,17 @@ function KeymasterUI({ keymaster, title, challengeDID, onWalletUpload }) { async function importWallet() { try { - const mnenomic = window.prompt("Overwrite wallet with mnemonic:"); + const mnemonic = window.prompt("Overwrite wallet with mnemonic:"); - if (mnenomic) { - await keymaster.newWallet(mnenomic, true); - await keymaster.recoverWallet(); + if (mnemonic) { + if (onImportMnemonic) { + await onImportMnemonic(mnemonic); + return; + } + else { + await keymaster.newWallet(mnemonic, true); + await keymaster.recoverWallet(); + } refreshAll(); } } catch (error) { @@ -1655,7 +1679,7 @@ function KeymasterUI({ keymaster, title, challengeDID, onWalletUpload }) { async function backupWallet() { try { await keymaster.backupWallet(); - showSuccess('Wallet backup successful') + showSuccess('Wallet metadata backup successful'); } catch (error) { showError(error); @@ -1665,7 +1689,12 @@ function KeymasterUI({ keymaster, title, challengeDID, onWalletUpload }) { async function recoverWallet() { try { if (window.confirm(`Overwrite wallet from backup?`)) { - await keymaster.recoverWallet(); + if (onRecoverWallet) { + await onRecoverWallet(); + } + else { + await keymaster.recoverWallet(); + } refreshAll(); } } catch (error) { @@ -1732,21 +1761,19 @@ function KeymasterUI({ keymaster, title, challengeDID, onWalletUpload }) { if (onWalletUpload) { await onWalletUpload(wallet); - await refreshAll(); return; } - const backupWallet = await keymaster.exportEncryptedWallet(); - try { - await keymaster.saveWallet(wallet, true); + if (wallet?.type === 'mdip-wallet-bundle' && wallet?.version === 1) { + await keymaster.importWalletBundle(wallet); + } else { + await keymaster.saveWallet(wallet, true); + } await keymaster.loadWallet(); - refreshAll(); + await refreshAll(); } catch (e) { - try { - await keymaster.saveWallet(backupWallet, true); - } catch { } - window.alert('Upload rejected: the server could not decrypt the wallet with its configured passphrase.'); + window.alert('Upload rejected: unsupported wallet format for this client.'); } }; @@ -1759,14 +1786,19 @@ function KeymasterUI({ keymaster, title, challengeDID, onWalletUpload }) { async function downloadWallet() { try { - const wallet = await keymaster.exportEncryptedWallet(); - const walletJSON = JSON.stringify(wallet, null, 4); + if (onWalletDownload) { + await onWalletDownload(); + return; + } + + const bundle = await keymaster.exportWalletBundle(); + const walletJSON = JSON.stringify(bundle, null, 4); const blob = new Blob([walletJSON], { type: 'application/json' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; - link.download = 'mdip-wallet.json'; + link.download = 'mdip-wallet-bundle.json'; link.click(); // The URL.revokeObjectURL() method releases an existing object URL which was previously created by calling URL.createObjectURL(). diff --git a/services/keymaster/server/README.md b/services/keymaster/server/README.md index a7412ec0e..a3369955d 100644 --- a/services/keymaster/server/README.md +++ b/services/keymaster/server/README.md @@ -11,8 +11,7 @@ This service is also useful when clients share a wallet, such as the `kc` CLI an | `KC_GATEKEEPER_URL` | http://localhost:4224 | MDIP gatekeeper service URL | | `KC_KEYMASTER_PORT` | 4226 | Service port | | `KC_KEYMASTER_DB` | json | Wallet database adapter, must be `redis`, `json`, `mongodb`, `sqlite`, or `postgres` | -| `KC_ENCRYPTED_PASSPHRASE` | (no default) | If specified, the wallet will be encrypted and decrypted with this passphrase | -| `KC_WALLET_CACHE` | false | Use wallet cache to increase performance (but understand security implications) | +| `KC_WALLET_PROVIDER_PASSPHRASE` | (no default) | Passphrase for the built-in mnemonic wallet provider state | | `KC_DEFAULT_REGISTRY` | hyperswarm | Default registry to use when creating DIDs | | `KC_KEYMASTER_TRUST_PROXY` | false | If true, trust upstream proxy headers when determining client IP (`req.ip`) | | `KC_KEYMASTER_RATE_LIMIT_ENABLED` | false | Enable API rate limiting | diff --git a/services/keymaster/server/src/config.js b/services/keymaster/server/src/config.js index b7e69aeee..fb4dbe30c 100644 --- a/services/keymaster/server/src/config.js +++ b/services/keymaster/server/src/config.js @@ -66,8 +66,7 @@ const config = { keymasterPort: process.env.KC_KEYMASTER_PORT ? parseInt(process.env.KC_KEYMASTER_PORT) : 4226, nodeID: process.env.KC_NODE_ID || '', db: process.env.KC_KEYMASTER_DB || 'json', - keymasterPassphrase: process.env.KC_ENCRYPTED_PASSPHRASE || '', - walletCache: process.env.KC_WALLET_CACHE ? process.env.KC_WALLET_CACHE === 'true' : false, + walletProviderPassphrase: process.env.KC_WALLET_PROVIDER_PASSPHRASE || process.env.KC_ENCRYPTED_PASSPHRASE || '', defaultRegistry: process.env.KC_DEFAULT_REGISTRY, keymasterTrustProxy: parseBoolean(process.env.KC_KEYMASTER_TRUST_PROXY, false), rateLimitEnabled: parseBoolean(process.env.KC_KEYMASTER_RATE_LIMIT_ENABLED, false), diff --git a/services/keymaster/server/src/keymaster-api.ts b/services/keymaster/server/src/keymaster-api.ts index 047a24a25..b6c70ed98 100644 --- a/services/keymaster/server/src/keymaster-api.ts +++ b/services/keymaster/server/src/keymaster-api.ts @@ -5,15 +5,14 @@ import { fileURLToPath } from 'url'; import rateLimit from 'express-rate-limit'; import GatekeeperClient from '@mdip/gatekeeper/client'; -import Keymaster from '@mdip/keymaster'; +import Keymaster, { MnemonicHdWalletProvider } from '@mdip/keymaster'; import SearchClient from '@mdip/keymaster/search'; -import { WalletBase } from '@mdip/keymaster/types'; +import { KeymasterStore, MdipWalletBundle, WalletProviderStore } from '@mdip/keymaster/types'; import WalletJson from '@mdip/keymaster/wallet/json'; import WalletRedis from '@mdip/keymaster/wallet/redis'; import WalletMongo from '@mdip/keymaster/wallet/mongo'; import WalletSQLite from '@mdip/keymaster/wallet/sqlite'; import WalletPostgres from '@mdip/keymaster/wallet/postgres'; -import WalletCache from '@mdip/keymaster/wallet/cache'; import CipherNode from '@mdip/cipher/node'; import { InvalidParameterError } from '@mdip/common/errors'; import { childLogger } from '@mdip/common/logger'; @@ -210,6 +209,7 @@ if (serveClient) { let gatekeeper: GatekeeperClient; let keymaster: Keymaster; +let walletProvider: MnemonicHdWalletProvider; let serverReady = false; /** @@ -298,20 +298,15 @@ v1router.get('/registries', async (req, res) => { * wallet: * type: object * properties: - * seed: + * version: + * type: integer + * provider: * type: object * properties: - * mnemonic: + * type: + * type: string + * walletFingerprint: * type: string - * hdkey: - * type: object - * properties: - * xpriv: - * type: string - * xpub: - * type: string - * counter: - * type: integer * ids: * type: object * additionalProperties: @@ -319,10 +314,8 @@ v1router.get('/registries', async (req, res) => { * properties: * did: * type: string - * account: - * type: integer - * index: - * type: integer + * keyRef: + * type: string * owned: * type: array * items: @@ -367,20 +360,15 @@ v1router.get('/wallet', async (req, res) => { * wallet: * type: object * properties: - * seed: + * version: + * type: integer + * provider: * type: object * properties: - * mnemonic: + * type: + * type: string + * walletFingerprint: * type: string - * hdkey: - * type: object - * properties: - * xpriv: - * type: string - * xpub: - * type: string - * counter: - * type: integer * ids: * type: object * additionalProperties: @@ -388,10 +376,8 @@ v1router.get('/wallet', async (req, res) => { * properties: * did: * type: string - * account: - * type: integer - * index: - * type: integer + * keyRef: + * type: string * owned: * type: array * items: @@ -464,20 +450,15 @@ v1router.put('/wallet', async (req, res) => { * wallet: * type: object * properties: - * seed: + * version: + * type: integer + * provider: * type: object * properties: - * mnemonic: + * type: + * type: string + * walletFingerprint: * type: string - * hdkey: - * type: object - * properties: - * xpriv: - * type: string - * xpub: - * type: string - * counter: - * type: integer * ids: * type: object * additionalProperties: @@ -485,10 +466,8 @@ v1router.put('/wallet', async (req, res) => { * properties: * did: * type: string - * account: - * type: integer - * index: - * type: integer + * keyRef: + * type: string * owned: * type: array * items: @@ -519,6 +498,99 @@ v1router.post('/wallet/new', async (req, res) => { } }); +/** + * @swagger + * /wallet/bundle: + * get: + * summary: Export the current keymaster metadata and mnemonic wallet provider state. + * responses: + * 200: + * description: The wallet bundle object. + * content: + * application/json: + * schema: + * type: object + * properties: + * bundle: + * type: object + * 500: + * description: Internal server error. + * content: + * application/json: + * schema: + * type: object + * properties: + * error: + * type: string + */ +v1router.get('/wallet/bundle', async (req, res) => { + try { + const wallet = await keymaster.loadWallet(); + const provider = await walletProvider.backupWallet(); + const bundle: MdipWalletBundle = { + version: 1, + type: 'mdip-wallet-bundle', + keymaster: wallet, + provider, + }; + res.json({ bundle }); + } catch (error: any) { + res.status(500).send({ error: error.toString() }); + } +}); + +/** + * @swagger + * /wallet/bundle: + * put: + * summary: Import a wallet bundle containing keymaster metadata and mnemonic wallet provider state. + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * bundle: + * type: object + * responses: + * 200: + * description: The imported wallet object. + * content: + * application/json: + * schema: + * type: object + * properties: + * ok: + * type: boolean + * wallet: + * type: object + * 500: + * description: Internal server error. + * content: + * application/json: + * schema: + * type: object + * properties: + * error: + * type: string + */ +v1router.put('/wallet/bundle', async (req, res) => { + try { + const bundle = req.body?.bundle as MdipWalletBundle; + const providerOk = await walletProvider.saveWallet(bundle.provider, true); + if (!providerOk) { + throw new Error('save provider wallet failed'); + } + + const ok = await keymaster.saveWallet(bundle.keymaster, true); + const wallet = await keymaster.loadWallet(); + res.json({ ok, wallet }); + } catch (error: any) { + res.status(500).send({ error: error.toString() }); + } +}); + /** * @swagger * /wallet/backup: @@ -532,7 +604,7 @@ v1router.post('/wallet/new', async (req, res) => { * schema: * type: object * properties: - * ok: + * did: * type: string * description: The DID associated with the wallet backup. * 500: @@ -547,8 +619,8 @@ v1router.post('/wallet/new', async (req, res) => { */ v1router.post('/wallet/backup', async (req, res) => { try { - const ok = await keymaster.backupWallet(); - res.json({ ok }); + const did = await keymaster.backupWallet(); + res.json({ did }); } catch (error: any) { res.status(500).send({ error: error.toString() }); } @@ -570,20 +642,15 @@ v1router.post('/wallet/backup', async (req, res) => { * wallet: * type: object * properties: - * seed: + * version: + * type: integer + * provider: * type: object * properties: - * mnemonic: + * type: + * type: string + * walletFingerprint: * type: string - * hdkey: - * type: object - * properties: - * xpriv: - * type: string - * xpub: - * type: string - * counter: - * type: integer * ids: * type: object * additionalProperties: @@ -591,10 +658,8 @@ v1router.post('/wallet/backup', async (req, res) => { * properties: * did: * type: string - * account: - * type: integer - * index: - * type: integer + * keyRef: + * type: string * owned: * type: array * items: @@ -617,7 +682,7 @@ v1router.post('/wallet/backup', async (req, res) => { */ v1router.post('/wallet/recover', async (req, res) => { try { - const wallet = await keymaster.recoverWallet(); + const wallet = await keymaster.recoverWallet(req.body?.did); res.json({ wallet }); } catch (error: any) { res.status(500).send({ error: error.toString() }); @@ -711,15 +776,14 @@ v1router.post('/wallet/fix', async (req, res) => { } }); - /** * @swagger * /wallet/mnemonic: * get: - * summary: Decrypt and retrieve the wallet's mnemonic phrase. + * summary: Retrieve the recovery phrase for the mnemonic wallet provider. * responses: * 200: - * description: The mnemonic phrase. + * description: The wallet recovery phrase. * content: * application/json: * schema: @@ -739,72 +803,13 @@ v1router.post('/wallet/fix', async (req, res) => { */ v1router.get('/wallet/mnemonic', async (req, res) => { try { - const mnemonic = await keymaster.decryptMnemonic(); + const mnemonic = await walletProvider.decryptMnemonic(); res.json({ mnemonic }); } catch (error: any) { res.status(500).send({ error: error.toString() }); } }); -/** - * @swagger - * /export/wallet/encrypted: - * get: - * summary: Export the wallet in encrypted form. - * description: > - * Returns the wallet in its encrypted format, which includes the encrypted mnemonic - * and encrypted wallet data. This format is secure for storage or backup purposes. - * responses: - * 200: - * description: The encrypted wallet object. - * content: - * application/json: - * schema: - * type: object - * properties: - * wallet: - * type: object - * properties: - * version: - * type: integer - * description: The wallet format version. - * seed: - * type: object - * properties: - * mnemonicEnc: - * type: object - * properties: - * salt: - * type: string - * description: Base64-encoded salt used for key derivation. - * iv: - * type: string - * description: Base64-encoded initialization vector for AES-GCM encryption. - * data: - * type: string - * description: Base64-encoded encrypted mnemonic. - * enc: - * type: string - * description: Encrypted wallet data (IDs, names, etc.). - * 500: - * description: Internal server error. - * content: - * application/json: - * schema: - * type: object - * properties: - * error: - * type: string - */ -v1router.get('/export/wallet/encrypted', async (req, res) => { - try { - const wallet = await keymaster.exportEncryptedWallet(); - res.json({ wallet }); - } catch (error: any) { - res.status(500).send({ error: error.toString() }); - } -}); - /** * @swagger * /did/{id}: @@ -6277,7 +6282,7 @@ async function waitForNodeId() { } async function initWallet() { - let wallet: WalletBase; + let wallet: KeymasterStore; if (config.db === 'redis') { wallet = await WalletRedis.create(); @@ -6293,8 +6298,23 @@ async function initWallet() { throw new InvalidParameterError(`db=${config.db}`); } - if (config.walletCache) { - wallet = new WalletCache(wallet); + return wallet; +} + +async function initWalletProviderStore() { + let wallet: WalletProviderStore; + const storeName = 'wallet-provider'; + + if (config.db === 'redis') { + wallet = await WalletRedis.create(storeName) as unknown as WalletProviderStore; + } else if (config.db === 'mongodb') { + wallet = await WalletMongo.create(storeName) as unknown as WalletProviderStore; + } else if (config.db === 'sqlite') { + wallet = await WalletSQLite.create('wallet-provider.db') as unknown as WalletProviderStore; + } else if (config.db === 'json') { + wallet = new WalletJson('wallet-provider.json') as unknown as WalletProviderStore; + } else { + throw new InvalidParameterError(`db=${config.db}`); } return wallet; @@ -6328,9 +6348,15 @@ const server = app.listen(port, async () => { } const wallet = await initWallet(); + const walletProviderStore = await initWalletProviderStore(); const cipher = new CipherNode(); const defaultRegistry = config.defaultRegistry; - keymaster = new Keymaster({ gatekeeper, wallet, cipher, search, defaultRegistry, passphrase: config.keymasterPassphrase }); + walletProvider = new MnemonicHdWalletProvider({ + store: walletProviderStore, + cipher, + passphrase: config.walletProviderPassphrase, + }); + keymaster = new Keymaster({ gatekeeper, store: wallet, walletProvider, cipher, search, defaultRegistry }); log.info(`Keymaster server running on port ${port}`); log.info(`Keymaster server persisting to ${config.db}`); diff --git a/services/search-server/README.md b/services/search-server/README.md index 46bcee07e..dbb3a5342 100644 --- a/services/search-server/README.md +++ b/services/search-server/README.md @@ -23,31 +23,31 @@ Then edit the `.env` file to set your desired configuration: ```env # The port the server will run on -SEARCH_SERVER_PORT=4002 +KC_SEARCH_SERVER_PORT=4002 # URL where your Gatekeeper service is running -SEARCH_SERVER_GATEKEEPER_URL=http://localhost:4224 +KC_SEARCH_SERVER_GATEKEEPER_URL=http://localhost:4224 # How often (in ms) to poll Gatekeeper for new or updated DIDs. -SEARCH_SERVER_REFRESH_INTERVAL_MS=5000 +KC_SEARCH_SERVER_REFRESH_INTERVAL_MS=5000 # Database adapter: sqlite | postgres | memory -SEARCH_SERVER_DB=sqlite +KC_SEARCH_SERVER_DB=sqlite -# Used when SEARCH_SERVER_DB=postgres +# Used when KC_SEARCH_SERVER_DB=postgres # Falls back to KC_POSTGRES_URL when unset -SEARCH_SERVER_POSTGRES_URL=postgresql://mdip:mdip@localhost:5432/mdip +KC_SEARCH_SERVER_POSTGRES_URL=postgresql://mdip:mdip@localhost:5432/mdip # Trust proxy headers when determining req.ip -SEARCH_SERVER_TRUST_PROXY=false +KC_SEARCH_SERVER_TRUST_PROXY=false # API rate limiting -SEARCH_SERVER_RATE_LIMIT_ENABLED=false -SEARCH_SERVER_RATE_LIMIT_WINDOW_VALUE=1 -SEARCH_SERVER_RATE_LIMIT_WINDOW_UNIT=minute -SEARCH_SERVER_RATE_LIMIT_MAX_REQUESTS=600 -SEARCH_SERVER_RATE_LIMIT_WHITELIST= -SEARCH_SERVER_RATE_LIMIT_SKIP_PATHS=/api/v1/ready +KC_SEARCH_SERVER_RATE_LIMIT_ENABLED=false +KC_SEARCH_SERVER_RATE_LIMIT_WINDOW_VALUE=1 +KC_SEARCH_SERVER_RATE_LIMIT_WINDOW_UNIT=minute +KC_SEARCH_SERVER_RATE_LIMIT_MAX_REQUESTS=600 +KC_SEARCH_SERVER_RATE_LIMIT_WHITELIST= +KC_SEARCH_SERVER_RATE_LIMIT_SKIP_PATHS=/api/v1/ready # Logging KC_LOG_LEVEL=info diff --git a/services/search-server/sample.env b/services/search-server/sample.env index f3e18baf5..c46d55388 100644 --- a/services/search-server/sample.env +++ b/services/search-server/sample.env @@ -1,13 +1,13 @@ -SEARCH_SERVER_PORT=4002 -SEARCH_SERVER_GATEKEEPER_URL=http://localhost:4224 -SEARCH_SERVER_REFRESH_INTERVAL_MS=5000 -SEARCH_SERVER_DB=sqlite -SEARCH_SERVER_POSTGRES_URL=postgresql://mdip:mdip@localhost:5432/mdip -SEARCH_SERVER_TRUST_PROXY=false -SEARCH_SERVER_RATE_LIMIT_ENABLED=false -SEARCH_SERVER_RATE_LIMIT_WINDOW_VALUE=1 -SEARCH_SERVER_RATE_LIMIT_WINDOW_UNIT=minute -SEARCH_SERVER_RATE_LIMIT_MAX_REQUESTS=600 -SEARCH_SERVER_RATE_LIMIT_WHITELIST= -SEARCH_SERVER_RATE_LIMIT_SKIP_PATHS=/api/v1/ready +KC_SEARCH_SERVER_PORT=4002 +KC_SEARCH_SERVER_GATEKEEPER_URL=http://localhost:4224 +KC_SEARCH_SERVER_REFRESH_INTERVAL_MS=5000 +KC_SEARCH_SERVER_DB=sqlite +KC_SEARCH_SERVER_POSTGRES_URL=postgresql://mdip:mdip@localhost:5432/mdip +KC_SEARCH_SERVER_TRUST_PROXY=false +KC_SEARCH_SERVER_RATE_LIMIT_ENABLED=false +KC_SEARCH_SERVER_RATE_LIMIT_WINDOW_VALUE=1 +KC_SEARCH_SERVER_RATE_LIMIT_WINDOW_UNIT=minute +KC_SEARCH_SERVER_RATE_LIMIT_MAX_REQUESTS=600 +KC_SEARCH_SERVER_RATE_LIMIT_WHITELIST= +KC_SEARCH_SERVER_RATE_LIMIT_SKIP_PATHS=/api/v1/ready KC_LOG_LEVEL=info diff --git a/services/search-server/src/config.ts b/services/search-server/src/config.ts index 9bb569e7e..e4854f33b 100644 --- a/services/search-server/src/config.ts +++ b/services/search-server/src/config.ts @@ -57,23 +57,23 @@ function parseCsv(value: string | undefined): string[] { .filter(Boolean); } -const configuredSkipPaths = parseCsv(process.env.SEARCH_SERVER_RATE_LIMIT_SKIP_PATHS); +const configuredSkipPaths = parseCsv(process.env.KC_SEARCH_SERVER_RATE_LIMIT_SKIP_PATHS); const config = { - port: parsePositiveInteger(process.env.SEARCH_SERVER_PORT, 4002), - gatekeeperURL: process.env.SEARCH_SERVER_GATEKEEPER_URL || 'http://localhost:4224', - refreshIntervalMs: parsePositiveInteger(process.env.SEARCH_SERVER_REFRESH_INTERVAL_MS, 5000), - db: process.env.SEARCH_SERVER_DB || 'sqlite', - postgresURL: process.env.SEARCH_SERVER_POSTGRES_URL + port: parsePositiveInteger(process.env.KC_SEARCH_SERVER_PORT, 4002), + gatekeeperURL: process.env.KC_SEARCH_SERVER_GATEKEEPER_URL || 'http://localhost:4224', + refreshIntervalMs: parsePositiveInteger(process.env.KC_SEARCH_SERVER_REFRESH_INTERVAL_MS, 5000), + db: process.env.KC_SEARCH_SERVER_DB || 'sqlite', + postgresURL: process.env.KC_SEARCH_SERVER_POSTGRES_URL || process.env.KC_POSTGRES_URL || 'postgresql://mdip:mdip@localhost:5432/mdip', - trustProxy: parseBoolean(process.env.SEARCH_SERVER_TRUST_PROXY, false), + trustProxy: parseBoolean(process.env.KC_SEARCH_SERVER_TRUST_PROXY, false), jsonLimit: '2mb', - rateLimitEnabled: parseBoolean(process.env.SEARCH_SERVER_RATE_LIMIT_ENABLED, false), - rateLimitWindowValue: parsePositiveInteger(process.env.SEARCH_SERVER_RATE_LIMIT_WINDOW_VALUE, 1), - rateLimitWindowUnit: parseWindowUnit(process.env.SEARCH_SERVER_RATE_LIMIT_WINDOW_UNIT), - rateLimitMaxRequests: parsePositiveInteger(process.env.SEARCH_SERVER_RATE_LIMIT_MAX_REQUESTS, 600), - rateLimitWhitelist: parseCsv(process.env.SEARCH_SERVER_RATE_LIMIT_WHITELIST), + rateLimitEnabled: parseBoolean(process.env.KC_SEARCH_SERVER_RATE_LIMIT_ENABLED, false), + rateLimitWindowValue: parsePositiveInteger(process.env.KC_SEARCH_SERVER_RATE_LIMIT_WINDOW_VALUE, 1), + rateLimitWindowUnit: parseWindowUnit(process.env.KC_SEARCH_SERVER_RATE_LIMIT_WINDOW_UNIT), + rateLimitMaxRequests: parsePositiveInteger(process.env.KC_SEARCH_SERVER_RATE_LIMIT_MAX_REQUESTS, 600), + rateLimitWhitelist: parseCsv(process.env.KC_SEARCH_SERVER_RATE_LIMIT_WHITELIST), rateLimitSkipPaths: configuredSkipPaths.length > 0 ? configuredSkipPaths : DEFAULT_RATE_LIMIT_SKIP_PATHS, }; diff --git a/start-node-ci b/start-node-ci index ef43ad06c..282659825 100755 --- a/start-node-ci +++ b/start-node-ci @@ -39,6 +39,15 @@ while true; do if [[ "$RETRY_COUNT" -ge "$MAX_RETRIES" ]]; then echo "❌ Timed out waiting for containers to be 'Up'" docker compose ps + + RUNNING_SERVICES=$(docker compose "${PROFILE_ARGS[@]}" ps --services --status running "${SERVICES[@]}") + for service in "${SERVICES[@]}"; do + if ! grep -qx "$service" <<< "$RUNNING_SERVICES"; then + echo "----- logs: $service -----" + docker compose logs --no-color "$service" || true + fi + done + exit 1 fi diff --git a/tests/cli-tests/generate_test_env.sh b/tests/cli-tests/generate_test_env.sh index 76014874d..f9947853f 100755 --- a/tests/cli-tests/generate_test_env.sh +++ b/tests/cli-tests/generate_test_env.sh @@ -15,8 +15,8 @@ cat > "$ENV_FILE" < Add a member to a group vault add-name Add a name for a DID backup-id Backup the current ID to its registry - backup-wallet-did Backup wallet to encrypted DID and seed bank - backup-wallet-file Backup wallet to file + backup-wallet-did Backup wallet metadata to DID + backup-wallet-file Backup wallet bundle to file bind-credential Create bound credential for a user check-wallet Validate DIDs in wallet clone-asset [options] Clone an asset @@ -89,7 +89,7 @@ Commands: publish-credential Publish the existence of a credential to the current user manifest publish-poll Publish results to poll, hiding ballots recover-id Recovers the ID from the DID - recover-wallet-did [did] Recover wallet from seed bank or encrypted DID + recover-wallet-did [did] Recover wallet metadata from backup DID remove-group-member Remove a member from a group remove-group-vault-item Remove an item from a group vault remove-group-vault-member Remove a member from a group vault @@ -99,7 +99,7 @@ Commands: resolve-did [confirm] Return document associated with DID resolve-did-version Return specified version of document associated with DID resolve-id Resolves the current ID - restore-wallet-file Restore wallet from backup file + restore-wallet-file Restore wallet bundle from backup file reveal-credential Reveal a credential to the current user manifest reveal-poll Publish results to poll, revealing ballots revoke-credential Revokes a verifiable credential diff --git a/tests/common/logger.test.ts b/tests/common/logger.test.ts index 1cdcf8ec7..268733904 100644 --- a/tests/common/logger.test.ts +++ b/tests/common/logger.test.ts @@ -49,20 +49,17 @@ describe('logger', () => { expect(loggerMod.getLogLevel()).toBe('info'); }); - it('getPrettyEnabled always returns true', () => { + it('getPrettyEnabled returns the default pretty flag', () => { expect(loggerMod.getPrettyEnabled()).toBe(true); }); - it('createLogger uses env level and pretty transport defaults', () => { + it('createLogger uses env level and pretty stream defaults', () => { loggerMod.createLogger({}, { KC_LOG_LEVEL: 'error' }); const call = pinoMock.mock.calls.at(-1); expect(call).toBeTruthy(); const options = call?.[0] as any; expect(options.level).toBe('error'); - expect(options.transport?.target).toBe('pino-pretty'); - expect(options.transport?.options?.singleLine).toBe(true); - expect(options.transport?.options?.ignore).toContain('service'); - expect(options.transport?.options?.colorize).toBe(false); + expect(call?.[1]).toBeTruthy(); }); it('createLogger uses explicit level over env', () => { diff --git a/tests/keymaster/asset.test.ts b/tests/keymaster/asset.test.ts index 565729684..5f119f4bc 100644 --- a/tests/keymaster/asset.test.ts +++ b/tests/keymaster/asset.test.ts @@ -2,13 +2,12 @@ import Gatekeeper from '@mdip/gatekeeper'; import Keymaster from '@mdip/keymaster'; import CipherNode from '@mdip/cipher/node'; import DbJsonMemory from '@mdip/gatekeeper/db/json-memory'; -import WalletJsonMemory from '@mdip/keymaster/wallet/json-memory'; import { ExpectedExceptionError } from '@mdip/common/errors'; import HeliaClient from '@mdip/ipfs/helia'; +import { createTestKeymaster } from './testUtils.ts'; let ipfs: HeliaClient; let gatekeeper: Gatekeeper; -let wallet: WalletJsonMemory; let cipher: CipherNode; let keymaster: Keymaster; @@ -26,9 +25,8 @@ afterAll(async () => { beforeEach(() => { const db = new DbJsonMemory('test'); gatekeeper = new Gatekeeper({ db, ipfs, registries: ['local', 'hyperswarm', 'TFTC'] }); - wallet = new WalletJsonMemory(); cipher = new CipherNode(); - keymaster = new Keymaster({ gatekeeper, wallet, cipher, passphrase: 'passphrase' }); + ({ keymaster } = createTestKeymaster(gatekeeper, cipher)); }); describe('createAsset', () => { diff --git a/tests/keymaster/challenge.test.ts b/tests/keymaster/challenge.test.ts index e856c4509..f0846a50d 100644 --- a/tests/keymaster/challenge.test.ts +++ b/tests/keymaster/challenge.test.ts @@ -2,14 +2,13 @@ import Gatekeeper from '@mdip/gatekeeper'; import Keymaster from '@mdip/keymaster'; import CipherNode from '@mdip/cipher/node'; import DbJsonMemory from '@mdip/gatekeeper/db/json-memory'; -import WalletJsonMemory from '@mdip/keymaster/wallet/json-memory'; import { ExpectedExceptionError } from '@mdip/common/errors'; import HeliaClient from '@mdip/ipfs/helia'; import { mockSchema } from './helper.ts'; +import { createTestKeymaster } from './testUtils.ts'; let ipfs: HeliaClient; let gatekeeper: Gatekeeper; -let wallet: WalletJsonMemory; let cipher: CipherNode; let keymaster: Keymaster; @@ -27,9 +26,8 @@ afterAll(async () => { beforeEach(() => { const db = new DbJsonMemory('test'); gatekeeper = new Gatekeeper({ db, ipfs, registries: ['local', 'hyperswarm', 'TFTC'] }); - wallet = new WalletJsonMemory(); cipher = new CipherNode(); - keymaster = new Keymaster({ gatekeeper, wallet, cipher, passphrase: 'passphrase' }); + ({ keymaster } = createTestKeymaster(gatekeeper, cipher)); }); describe('createChallenge', () => { diff --git a/tests/keymaster/client.test.ts b/tests/keymaster/client.test.ts index 54f4b3c04..b1e4f625a 100644 --- a/tests/keymaster/client.test.ts +++ b/tests/keymaster/client.test.ts @@ -1,7 +1,7 @@ import nock from 'nock'; import KeymasterClient from '@mdip/keymaster/client'; import { ExpectedExceptionError } from '@mdip/common/errors'; -import {Seed, WalletEncFile, WalletFile} from "@mdip/keymaster/types"; +import { MdipWalletBundle, WalletFile } from "@mdip/keymaster/types"; const KeymasterURL = 'http://keymaster.org'; const ServerError = { message: 'Server error' }; @@ -9,11 +9,12 @@ const Endpoints = { ready: '/api/v1/ready', wallet: '/api/v1/wallet', wallet_new: '/api/v1/wallet/new', + wallet_mnemonic: '/api/v1/wallet/mnemonic', wallet_backup: '/api/v1/wallet/backup', wallet_recover: '/api/v1/wallet/recover', + wallet_bundle: '/api/v1/wallet/bundle', wallet_check: '/api/v1/wallet/check', wallet_fix: '/api/v1/wallet/fix', - wallet_mnemonic: '/api/v1/wallet/mnemonic', registries: '/api/v1/registries', ids: '/api/v1/ids', ids_current: '/api/v1/ids/current', @@ -43,7 +44,6 @@ const Endpoints = { groupVaults: `/api/v1/groupVaults`, dmail: '/api/v1/dmail', notices: '/api/v1/notices', - export_wallet_encrypted: '/api/v1/export/wallet/encrypted', }; const mockConsole = { @@ -87,6 +87,36 @@ const mockCredential = { } }; +const mockWalletBundle: MdipWalletBundle = { + version: 1, + type: 'mdip-wallet-bundle', + keymaster: { + version: 2, + provider: { + type: 'mnemonic-hd', + walletFingerprint: 'fingerprint', + }, + ids: {}, + }, + provider: { + version: 1, + type: 'mnemonic-hd', + rootPublicJwk: { + kty: 'EC', + crv: 'secp256k1', + x: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + y: 'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB', + }, + mnemonicEnc: { + salt: 'salt', + iv: 'iv', + data: 'data', + }, + nextAccount: 0, + keys: {}, + }, +}; + describe('isReady', () => { it('should return ready flag', async () => { nock(KeymasterURL) @@ -193,7 +223,14 @@ describe('loadWallet', () => { }); describe('saveWallet', () => { - const mockWallet: WalletFile = { seed: {} as Seed, counter: 0, ids: {} }; + const mockWallet: WalletFile = { + version: 2, + provider: { + type: 'mnemonic-hd', + walletFingerprint: 'fingerprint', + }, + ids: {}, + }; it('should save wallet', async () => { nock(KeymasterURL) @@ -254,16 +291,45 @@ describe('newWallet', () => { }); }); +describe('decryptMnemonic', () => { + it('should return the decrypted mnemonic', async () => { + nock(KeymasterURL) + .get(Endpoints.wallet_mnemonic) + .reply(200, { mnemonic: 'alpha beta gamma' }); + + const keymaster = await KeymasterClient.create({ url: KeymasterURL }); + const mnemonic = await keymaster.decryptMnemonic(); + + expect(mnemonic).toBe('alpha beta gamma'); + }); + + it('should throw exception on decryptMnemonic server error', async () => { + nock(KeymasterURL) + .get(Endpoints.wallet_mnemonic) + .reply(500, ServerError); + + const keymaster = await KeymasterClient.create({ url: KeymasterURL }); + + try { + await keymaster.decryptMnemonic(); + throw new ExpectedExceptionError(); + } + catch (error: any) { + expect(error.message).toBe(ServerError.message); + } + }); +}); + describe('backupWallet', () => { it('should backup wallet', async () => { nock(KeymasterURL) .post(Endpoints.wallet_backup) - .reply(200, { ok: true }); + .reply(200, { did: 'did:test:backup' }); const keymaster = await KeymasterClient.create({ url: KeymasterURL }); - const ok = await keymaster.backupWallet(); + const did = await keymaster.backupWallet(); - expect(ok).toStrictEqual(true); + expect(did).toStrictEqual('did:test:backup'); }); it('should throw exception on backupWallet server error', async () => { @@ -314,56 +380,63 @@ describe('recoverWallet', () => { }); }); -describe('checkWallet', () => { - it('should check wallet', async () => { +describe('wallet bundle', () => { + it('should export wallet bundle', async () => { nock(KeymasterURL) - .post(Endpoints.wallet_check) - .reply(200, { check: true }); + .get(Endpoints.wallet_bundle) + .reply(200, { bundle: mockWalletBundle }); const keymaster = await KeymasterClient.create({ url: KeymasterURL }); - const check = await keymaster.checkWallet(); + const bundle = await keymaster.exportWalletBundle(); - expect(check).toStrictEqual(true); + expect(bundle).toStrictEqual(mockWalletBundle); }); - it('should throw exception on checkWallet server error', async () => { + it('should throw exception on exportWalletBundle server error', async () => { nock(KeymasterURL) - .post(Endpoints.wallet_check) + .get(Endpoints.wallet_bundle) .reply(500, ServerError); const keymaster = await KeymasterClient.create({ url: KeymasterURL }); try { - await keymaster.checkWallet(); + await keymaster.exportWalletBundle(); throw new ExpectedExceptionError(); } catch (error: any) { expect(error.message).toBe(ServerError.message); } }); -}); -describe('fixWallet', () => { - it('should fix wallet', async () => { + it('should import wallet bundle', async () => { + const mockWallet: WalletFile = { + version: 2, + provider: { + type: 'mnemonic-hd', + walletFingerprint: 'fingerprint', + }, + ids: {}, + }; + nock(KeymasterURL) - .post(Endpoints.wallet_fix) - .reply(200, { fix: true }); + .put(Endpoints.wallet_bundle) + .reply(200, { wallet: mockWallet }); const keymaster = await KeymasterClient.create({ url: KeymasterURL }); - const fix = await keymaster.fixWallet(); + const wallet = await keymaster.importWalletBundle(mockWalletBundle); - expect(fix).toStrictEqual(true); + expect(wallet).toStrictEqual(mockWallet); }); - it('should throw exception on fixWallet server error', async () => { + it('should throw exception on importWalletBundle server error', async () => { nock(KeymasterURL) - .post(Endpoints.wallet_fix) + .put(Endpoints.wallet_bundle) .reply(500, ServerError); const keymaster = await KeymasterClient.create({ url: KeymasterURL }); try { - await keymaster.fixWallet(); + await keymaster.importWalletBundle(mockWalletBundle); throw new ExpectedExceptionError(); } catch (error: any) { @@ -372,29 +445,56 @@ describe('fixWallet', () => { }); }); -describe('decryptMnemonic', () => { - const mockMnemonic = 'mock mnemonic phrase'; +describe('checkWallet', () => { + it('should check wallet', async () => { + nock(KeymasterURL) + .post(Endpoints.wallet_check) + .reply(200, { check: true }); + + const keymaster = await KeymasterClient.create({ url: KeymasterURL }); + const check = await keymaster.checkWallet(); - it('should decrypt mnemonic', async () => { + expect(check).toStrictEqual(true); + }); + + it('should throw exception on checkWallet server error', async () => { nock(KeymasterURL) - .get(Endpoints.wallet_mnemonic) - .reply(200, { mnemonic: mockMnemonic }); + .post(Endpoints.wallet_check) + .reply(500, ServerError); const keymaster = await KeymasterClient.create({ url: KeymasterURL }); - const mnemonic = await keymaster.decryptMnemonic(); - expect(mnemonic).toStrictEqual(mockMnemonic); + try { + await keymaster.checkWallet(); + throw new ExpectedExceptionError(); + } + catch (error: any) { + expect(error.message).toBe(ServerError.message); + } + }); +}); + +describe('fixWallet', () => { + it('should fix wallet', async () => { + nock(KeymasterURL) + .post(Endpoints.wallet_fix) + .reply(200, { fix: true }); + + const keymaster = await KeymasterClient.create({ url: KeymasterURL }); + const fix = await keymaster.fixWallet(); + + expect(fix).toStrictEqual(true); }); - it('should throw exception on decryptMnemonic server error', async () => { + it('should throw exception on fixWallet server error', async () => { nock(KeymasterURL) - .get(Endpoints.wallet_mnemonic) + .post(Endpoints.wallet_fix) .reply(500, ServerError); const keymaster = await KeymasterClient.create({ url: KeymasterURL }); try { - await keymaster.decryptMnemonic(); + await keymaster.fixWallet(); throw new ExpectedExceptionError(); } catch (error: any) { @@ -2745,6 +2845,7 @@ describe('createGroupVault', () => { describe('getGroupVault', () => { const mockVaultId = 'vault1'; const mockVault = { salt: 'mockSalt', keys: {}, items: 'mockItems' }; + const resolveOptions = { confirm: 'true', versionTime: '2025-01-01T00:00:00Z' } as any; it('should get document', async () => { nock(KeymasterURL) @@ -2772,6 +2873,18 @@ describe('getGroupVault', () => { expect(error.message).toBe(ServerError.message); } }); + + it('should get document with resolve options', async () => { + nock(KeymasterURL) + .get(`${Endpoints.groupVaults}/${mockVaultId}`) + .query(resolveOptions) + .reply(200, { groupVault: mockVault }); + + const keymaster = await KeymasterClient.create({ url: KeymasterURL }); + const vault = await keymaster.getGroupVault(mockVaultId, resolveOptions); + + expect(vault).toStrictEqual(mockVault); + }); }); describe('testGroupVault', () => { @@ -2970,6 +3083,7 @@ describe('removeGroupVaultItem', () => { describe('listGroupVaultItems', () => { const mockVaultId = 'vault6'; const mockItems = { item1: 'item1', item2: 'item2' }; + const resolveOptions = { confirm: 'true', versionTime: '2025-01-01T00:00:00Z' } as any; it('should list vault items', async () => { nock(KeymasterURL) @@ -2997,12 +3111,25 @@ describe('listGroupVaultItems', () => { expect(error.message).toBe(ServerError.message); } }); + + it('should list vault items with resolve options', async () => { + nock(KeymasterURL) + .get(`${Endpoints.groupVaults}/${mockVaultId}/items`) + .query(resolveOptions) + .reply(200, { items: mockItems }); + + const keymaster = await KeymasterClient.create({ url: KeymasterURL }); + const items = await keymaster.listGroupVaultItems(mockVaultId, resolveOptions); + + expect(items).toStrictEqual(mockItems); + }); }); describe('getGroupVaultItem', () => { const mockVaultId = 'vault7'; const mockName = 'mockName'; const mockData = Buffer.from('mockData'); + const resolveOptions = { confirm: 'true', versionTime: '2025-01-01T00:00:00Z' } as any; it('should return group vault item data', async () => { nock(KeymasterURL) @@ -3052,6 +3179,18 @@ describe('getGroupVaultItem', () => { expect(error.message).toBe(ServerError.message); } }); + + it('should return group vault item data with resolve options', async () => { + nock(KeymasterURL) + .get(`${Endpoints.groupVaults}/${mockVaultId}/items/${mockName}`) + .query(resolveOptions) + .reply(200, mockData); + + const keymaster = await KeymasterClient.create({ url: KeymasterURL }); + const data = await keymaster.getGroupVaultItem(mockVaultId, mockName, resolveOptions); + + expect(data).toStrictEqual(mockData); + }); }); const mockDmailId = 'did:mdip:dmail'; @@ -3272,6 +3411,7 @@ describe('removeDmailAttachment', () => { describe('listDmailAttachments', () => { const mockDmailId = 'dmail3'; const mockAttachments = { item1: 'item1', item2: 'item2' }; + const resolveOptions = { confirm: 'true', versionTime: '2025-01-01T00:00:00Z' } as any; it('should list vault items', async () => { nock(KeymasterURL) @@ -3299,6 +3439,18 @@ describe('listDmailAttachments', () => { expect(error.message).toBe(ServerError.message); } }); + + it('should list attachments with resolve options', async () => { + nock(KeymasterURL) + .get(`${Endpoints.dmail}/${mockDmailId}/attachments`) + .query(resolveOptions) + .reply(200, { attachments: mockAttachments }); + + const keymaster = await KeymasterClient.create({ url: KeymasterURL }); + const attachments = await keymaster.listDmailAttachments(mockDmailId, resolveOptions); + + expect(attachments).toStrictEqual(mockAttachments); + }); }); describe('getDmailAttachment', () => { @@ -3386,6 +3538,8 @@ describe('importDmail', () => { }); describe('getDmailMessage', () => { + const resolveOptions = { confirm: 'true', versionTime: '2025-01-01T00:00:00Z' } as any; + it('should get message', async () => { nock(KeymasterURL) .get(`${Endpoints.dmail}/${mockDmailId}`) @@ -3412,6 +3566,18 @@ describe('getDmailMessage', () => { expect(error.message).toBe(ServerError.message); } }); + + it('should get message with resolve options', async () => { + nock(KeymasterURL) + .get(`${Endpoints.dmail}/${mockDmailId}`) + .query(resolveOptions) + .reply(200, { message: mockDmail }); + + const keymaster = await KeymasterClient.create({ url: KeymasterURL }); + const message = await keymaster.getDmailMessage(mockDmailId, resolveOptions); + + expect(message).toStrictEqual(mockDmail); + }); }); describe('listDmail', () => { @@ -3534,44 +3700,3 @@ describe('refreshNotices', () => { } }); }); - -describe('exportEncryptedWallet', () => { - const mockEncWallet: WalletEncFile = { - version: 1, - seed: { - mnemonicEnc: { - salt: 'salt==', - iv: 'iviviviv', - data: 'ciphertext' - } - }, - enc: 'top-level-seal' - }; - - it('should export encrypted wallet', async () => { - nock(KeymasterURL) - .get(Endpoints.export_wallet_encrypted) - .reply(200, { wallet: mockEncWallet }); - - const keymaster = await KeymasterClient.create({ url: KeymasterURL }); - const wallet = await keymaster.exportEncryptedWallet(); - - expect(wallet).toStrictEqual(mockEncWallet); - }); - - it('should throw exception on exportEncryptedWallet server error', async () => { - nock(KeymasterURL) - .get(Endpoints.export_wallet_encrypted) - .reply(500, ServerError); - - const keymaster = await KeymasterClient.create({ url: KeymasterURL }); - - try { - await keymaster.exportEncryptedWallet(); - throw new ExpectedExceptionError(); - } - catch (error: any) { - expect(error.message).toBe(ServerError.message); - } - }); -}); diff --git a/tests/keymaster/credential.test.ts b/tests/keymaster/credential.test.ts index 906742fb4..d7c06fc15 100644 --- a/tests/keymaster/credential.test.ts +++ b/tests/keymaster/credential.test.ts @@ -3,15 +3,14 @@ import Keymaster from '@mdip/keymaster'; import { VerifiableCredential } from '@mdip/keymaster/types'; import CipherNode from '@mdip/cipher/node'; import DbJsonMemory from '@mdip/gatekeeper/db/json-memory'; -import WalletJsonMemory from '@mdip/keymaster/wallet/json-memory'; import { copyJSON } from '@mdip/common/utils'; import { InvalidDIDError, ExpectedExceptionError, UnknownIDError } from '@mdip/common/errors'; import HeliaClient from '@mdip/ipfs/helia'; import { TestHelper, mockJson, mockSchema } from './helper.ts'; +import { createTestKeymaster } from './testUtils.ts'; let ipfs: HeliaClient; let gatekeeper: Gatekeeper; -let wallet: WalletJsonMemory; let cipher: CipherNode; let keymaster: Keymaster; let helper: TestHelper; @@ -30,9 +29,8 @@ afterAll(async () => { beforeEach(() => { const db = new DbJsonMemory('test'); gatekeeper = new Gatekeeper({ db, ipfs, registries: ['local', 'hyperswarm', 'TFTC'] }); - wallet = new WalletJsonMemory(); cipher = new CipherNode(); - keymaster = new Keymaster({ gatekeeper, wallet, cipher, passphrase: 'passphrase' }); + ({ keymaster } = createTestKeymaster(gatekeeper, cipher)); helper = new TestHelper(keymaster); }); diff --git a/tests/keymaster/crypto.test.ts b/tests/keymaster/crypto.test.ts index 9ee097db2..f89c4412f 100644 --- a/tests/keymaster/crypto.test.ts +++ b/tests/keymaster/crypto.test.ts @@ -3,13 +3,12 @@ import Keymaster from '@mdip/keymaster'; import { EncryptedMessage } from '@mdip/keymaster/types'; import CipherNode from '@mdip/cipher/node'; import DbJsonMemory from '@mdip/gatekeeper/db/json-memory'; -import WalletJsonMemory from '@mdip/keymaster/wallet/json-memory'; import { ExpectedExceptionError } from '@mdip/common/errors'; import HeliaClient from '@mdip/ipfs/helia'; +import { createTestKeymaster } from './testUtils.ts'; let ipfs: HeliaClient; let gatekeeper: Gatekeeper; -let wallet: WalletJsonMemory; let cipher: CipherNode; let keymaster: Keymaster; @@ -27,9 +26,8 @@ afterAll(async () => { beforeEach(() => { const db = new DbJsonMemory('test'); gatekeeper = new Gatekeeper({ db, ipfs, registries: ['local', 'hyperswarm', 'TFTC'] }); - wallet = new WalletJsonMemory(); cipher = new CipherNode(); - keymaster = new Keymaster({ gatekeeper, wallet, cipher, passphrase: 'passphrase' }); + ({ keymaster } = createTestKeymaster(gatekeeper, cipher)); }); function generateRandomString(length: number) { diff --git a/tests/keymaster/dmail.test.ts b/tests/keymaster/dmail.test.ts index 61df0434c..19301cd71 100644 --- a/tests/keymaster/dmail.test.ts +++ b/tests/keymaster/dmail.test.ts @@ -2,14 +2,13 @@ import Gatekeeper from '@mdip/gatekeeper'; import Keymaster, { DmailTags } from '@mdip/keymaster'; import CipherNode from '@mdip/cipher/node'; import DbJsonMemory from '@mdip/gatekeeper/db/json-memory'; -import WalletJsonMemory from '@mdip/keymaster/wallet/json-memory'; import { ExpectedExceptionError } from '@mdip/common/errors'; import HeliaClient from '@mdip/ipfs/helia'; import { DmailMessage, NoticeMessage } from '@mdip/keymaster/types'; +import { createTestKeymaster } from './testUtils.ts'; let ipfs: HeliaClient; let gatekeeper: Gatekeeper; -let wallet: WalletJsonMemory; let cipher: CipherNode; let keymaster: Keymaster; @@ -27,9 +26,8 @@ afterAll(async () => { beforeEach(() => { const db = new DbJsonMemory('test'); gatekeeper = new Gatekeeper({ db, ipfs, registries: ['local', 'hyperswarm', 'TFTC'] }); - wallet = new WalletJsonMemory(); cipher = new CipherNode(); - keymaster = new Keymaster({ gatekeeper, wallet, cipher, passphrase: 'passphrase' }); + ({ keymaster } = createTestKeymaster(gatekeeper, cipher)); }); describe('verifyTagList', () => { diff --git a/tests/keymaster/document.test.ts b/tests/keymaster/document.test.ts index 1f2689a83..9adb2e1cd 100644 --- a/tests/keymaster/document.test.ts +++ b/tests/keymaster/document.test.ts @@ -2,13 +2,12 @@ import Gatekeeper from '@mdip/gatekeeper'; import Keymaster from '@mdip/keymaster'; import CipherNode from '@mdip/cipher/node'; import DbJsonMemory from '@mdip/gatekeeper/db/json-memory'; -import WalletJsonMemory from '@mdip/keymaster/wallet/json-memory'; import HeliaClient from '@mdip/ipfs/helia'; import { generateCID } from '@mdip/ipfs/utils'; +import { createTestKeymaster } from './testUtils.ts'; let ipfs: HeliaClient; let gatekeeper: Gatekeeper; -let wallet: WalletJsonMemory; let cipher: CipherNode; let keymaster: Keymaster; @@ -26,9 +25,8 @@ afterAll(async () => { beforeEach(() => { const db = new DbJsonMemory('test'); gatekeeper = new Gatekeeper({ db, ipfs, registries: ['local', 'hyperswarm', 'TFTC'] }); - wallet = new WalletJsonMemory(); cipher = new CipherNode(); - keymaster = new Keymaster({ gatekeeper, wallet, cipher, passphrase: 'passphrase' }); + ({ keymaster } = createTestKeymaster(gatekeeper, cipher)); }); describe('createDocument', () => { diff --git a/tests/keymaster/group-vault.test.ts b/tests/keymaster/group-vault.test.ts index f022c6c80..d02d2003d 100644 --- a/tests/keymaster/group-vault.test.ts +++ b/tests/keymaster/group-vault.test.ts @@ -7,13 +7,12 @@ import { } from '@mdip/keymaster/types'; import CipherNode from '@mdip/cipher/node'; import DbJsonMemory from '@mdip/gatekeeper/db/json-memory'; -import WalletJsonMemory from '@mdip/keymaster/wallet/json-memory'; import { ExpectedExceptionError, UnknownIDError, InvalidParameterError } from '@mdip/common/errors'; import HeliaClient from '@mdip/ipfs/helia'; +import { createTestKeymaster } from './testUtils.ts'; let ipfs: HeliaClient; let gatekeeper: Gatekeeper; -let wallet: WalletJsonMemory; let cipher: CipherNode; let keymaster: Keymaster; @@ -31,9 +30,8 @@ afterAll(async () => { beforeEach(() => { const db = new DbJsonMemory('test'); gatekeeper = new Gatekeeper({ db, ipfs, registries: ['local', 'hyperswarm', 'TFTC'] }); - wallet = new WalletJsonMemory(); cipher = new CipherNode(); - keymaster = new Keymaster({ gatekeeper, wallet, cipher, passphrase: 'passphrase' }); + ({ keymaster } = createTestKeymaster(gatekeeper, cipher)); }); describe('createGroupVault', () => { diff --git a/tests/keymaster/group.test.ts b/tests/keymaster/group.test.ts index 4eacd648b..1f25fe3e0 100644 --- a/tests/keymaster/group.test.ts +++ b/tests/keymaster/group.test.ts @@ -2,13 +2,12 @@ import Gatekeeper from '@mdip/gatekeeper'; import Keymaster from '@mdip/keymaster'; import CipherNode from '@mdip/cipher/node'; import DbJsonMemory from '@mdip/gatekeeper/db/json-memory'; -import WalletJsonMemory from '@mdip/keymaster/wallet/json-memory'; import { InvalidDIDError, ExpectedExceptionError, UnknownIDError } from '@mdip/common/errors'; import HeliaClient from '@mdip/ipfs/helia'; +import { createTestKeymaster } from './testUtils.ts'; let ipfs: HeliaClient; let gatekeeper: Gatekeeper; -let wallet: WalletJsonMemory; let cipher: CipherNode; let keymaster: Keymaster; @@ -26,9 +25,8 @@ afterAll(async () => { beforeEach(() => { const db = new DbJsonMemory('test'); gatekeeper = new Gatekeeper({ db, ipfs, registries: ['local', 'hyperswarm', 'TFTC'] }); - wallet = new WalletJsonMemory(); cipher = new CipherNode(); - keymaster = new Keymaster({ gatekeeper, wallet, cipher, passphrase: 'passphrase' }); + ({ keymaster } = createTestKeymaster(gatekeeper, cipher)); }); describe('createGroup', () => { diff --git a/tests/keymaster/id.test.ts b/tests/keymaster/id.test.ts index dc52aa357..2e7dc9e44 100644 --- a/tests/keymaster/id.test.ts +++ b/tests/keymaster/id.test.ts @@ -5,12 +5,15 @@ import DbJsonMemory from '@mdip/gatekeeper/db/json-memory'; import WalletJsonMemory from '@mdip/keymaster/wallet/json-memory'; import { InvalidDIDError, ExpectedExceptionError, UnknownIDError, InvalidParameterError } from '@mdip/common/errors'; import HeliaClient from '@mdip/ipfs/helia'; +import MnemonicHdWalletProvider from '../../packages/keymaster/src/provider/mnemonic-hd.ts'; +import { createTestKeymaster } from './testUtils.ts'; let ipfs: HeliaClient; let gatekeeper: Gatekeeper; -let wallet: WalletJsonMemory; +let store: WalletJsonMemory; let cipher: CipherNode; let keymaster: Keymaster; +let walletProvider: MnemonicHdWalletProvider; beforeAll(async () => { ipfs = new HeliaClient(); @@ -26,9 +29,9 @@ afterAll(async () => { beforeEach(() => { const db = new DbJsonMemory('test'); gatekeeper = new Gatekeeper({ db, ipfs, registries: ['local', 'hyperswarm', 'TFTC'] }); - wallet = new WalletJsonMemory(); + store = new WalletJsonMemory(); cipher = new CipherNode(); - keymaster = new Keymaster({ gatekeeper, wallet, cipher, passphrase: 'passphrase' }); + ({ keymaster, walletProvider } = createTestKeymaster(gatekeeper, cipher, { store })); }); describe('createId', () => { @@ -60,11 +63,11 @@ describe('createId', () => { it('should create a new ID on customized default registry', async () => { const defaultRegistry = 'TFTC'; - const keymaster = new Keymaster({ gatekeeper, wallet, cipher, defaultRegistry, passphrase: 'passphrase' }); + const { keymaster: customKeymaster } = createTestKeymaster(gatekeeper, cipher, { store, defaultRegistry }); const name = 'Bob'; - const did = await keymaster.createId(name); - const doc = await keymaster.resolveDID(did); + const did = await customKeymaster.createId(name); + const doc = await customKeymaster.resolveDID(did); expect(doc.mdip!.registry).toBe(defaultRegistry); }); @@ -185,7 +188,7 @@ describe('createIdOperation', () => { it('should create operation with custom registry', async () => { const name = 'Alice'; const registry = 'TFTC'; - const operation = await keymaster.createIdOperation(name, 0, { registry }); + const operation = await keymaster.createIdOperation(name, { registry }); expect(operation.mdip!.registry).toBe(registry); expect(operation.type).toBe('create'); @@ -196,7 +199,7 @@ describe('createIdOperation', () => { it('should create operation with local registry', async () => { const name = 'Charlie'; const registry = 'local'; - const operation = await keymaster.createIdOperation(name, 0, { registry }); + const operation = await keymaster.createIdOperation(name, { registry }); expect(operation.mdip!.registry).toBe(registry); expect(operation.type).toBe('create'); @@ -207,12 +210,10 @@ describe('createIdOperation', () => { it('should create operation without modifying wallet', async () => { const name = 'Dave'; const walletBefore = await keymaster.loadWallet(); - const counterBefore = walletBefore.counter; await keymaster.createIdOperation(name); const walletAfter = await keymaster.loadWallet(); - expect(walletAfter.counter).toBe(counterBefore); expect(walletAfter.ids[name]).toBeUndefined(); expect(walletAfter.current).toBe(walletBefore.current); }); @@ -286,13 +287,7 @@ describe('createIdOperation', () => { it('should use customized default registry when none specified', async () => { const defaultRegistry = 'local'; - const customKeymaster = new Keymaster({ - gatekeeper, - wallet, - cipher, - defaultRegistry, - passphrase: 'passphrase' - }); + const { keymaster: customKeymaster } = createTestKeymaster(gatekeeper, cipher, { store, defaultRegistry }); const name = 'Henry'; const operation = await customKeymaster.createIdOperation(name); @@ -406,7 +401,17 @@ describe('backupId', () => { const vault = await keymaster.resolveDID((doc.didDocumentData! as { vault: string }).vault); expect(ok).toBe(true); - expect((vault.didDocumentData as { backup: string }).backup.length > 0).toBe(true); + expect(vault.didDocumentData).toEqual( + expect.objectContaining({ + backup: expect.objectContaining({ + name: 'Bob', + id: expect.objectContaining({ + did: expect.any(String), + keyRef: expect.any(String), + }), + }), + }) + ); }); it('should backup a non-current ID', async () => { @@ -418,7 +423,17 @@ describe('backupId', () => { const vault = await keymaster.resolveDID((doc.didDocumentData! as { vault: string }).vault); expect(ok).toBe(true); - expect((vault.didDocumentData as { backup: string }).backup.length > 0).toBe(true); + expect(vault.didDocumentData).toEqual( + expect.objectContaining({ + backup: expect.objectContaining({ + name: 'Alice', + id: expect.objectContaining({ + did: aliceDid, + keyRef: expect.any(String), + }), + }), + }) + ); }); }); @@ -428,20 +443,20 @@ describe('recoverId', () => { const did = await keymaster.createId(name); let wallet = await keymaster.loadWallet(); const bob = JSON.parse(JSON.stringify(wallet.ids['Bob'])); - const mnemonic = await keymaster.decryptMnemonic(); + const providerBackup = await walletProvider.backupWallet(); await keymaster.backupId(); // reset wallet - await keymaster.newWallet(mnemonic, true); + await keymaster.newWallet(undefined, true); wallet = await keymaster.loadWallet(); expect(wallet.ids).toStrictEqual({}); + await walletProvider.saveWallet(providerBackup, true); await keymaster.recoverId(did); wallet = await keymaster.loadWallet(); expect(wallet.ids[name]).toStrictEqual(bob); expect(wallet.current).toBe(name); - expect(wallet.counter).toBe(1); }); it('should not overwrite an id with the same name', async () => { @@ -456,22 +471,6 @@ describe('recoverId', () => { expect(error.message).toBe('Keymaster: Bob already exists in wallet'); } }); - - it('should not recover an id to a different wallet', async () => { - const did = await keymaster.createId('Bob'); - await keymaster.backupId(); - - // reset to a different wallet - await keymaster.newWallet(undefined, true); - - try { - await keymaster.recoverId(did); - throw new ExpectedExceptionError(); - } - catch (error: any) { - expect(error.message).toBe(InvalidDIDError.type); - } - }); }); describe('testAgent', () => { diff --git a/tests/keymaster/image.test.ts b/tests/keymaster/image.test.ts index 15a80b070..e7b3ee11b 100644 --- a/tests/keymaster/image.test.ts +++ b/tests/keymaster/image.test.ts @@ -3,14 +3,13 @@ import Gatekeeper from '@mdip/gatekeeper'; import Keymaster from '@mdip/keymaster'; import CipherNode from '@mdip/cipher/node'; import DbJsonMemory from '@mdip/gatekeeper/db/json-memory'; -import WalletJsonMemory from '@mdip/keymaster/wallet/json-memory'; import { ExpectedExceptionError, UnknownIDError } from '@mdip/common/errors'; import HeliaClient from '@mdip/ipfs/helia'; import { generateCID } from '@mdip/ipfs/utils'; +import { createTestKeymaster } from './testUtils.ts'; let ipfs: HeliaClient; let gatekeeper: Gatekeeper; -let wallet: WalletJsonMemory; let cipher: CipherNode; let keymaster: Keymaster; @@ -28,9 +27,8 @@ afterAll(async () => { beforeEach(() => { const db = new DbJsonMemory('test'); gatekeeper = new Gatekeeper({ db, ipfs, registries: ['local', 'hyperswarm', 'TFTC'] }); - wallet = new WalletJsonMemory(); cipher = new CipherNode(); - keymaster = new Keymaster({ gatekeeper, wallet, cipher, passphrase: 'passphrase' }); + ({ keymaster } = createTestKeymaster(gatekeeper, cipher)); }); describe('createImage', () => { diff --git a/tests/keymaster/legacy-migration-provider-gate.test.ts b/tests/keymaster/legacy-migration-provider-gate.test.ts new file mode 100644 index 000000000..89ad3fd9a --- /dev/null +++ b/tests/keymaster/legacy-migration-provider-gate.test.ts @@ -0,0 +1,102 @@ +import Keymaster from '@mdip/keymaster'; +import type { + LegacyWalletFile, + WalletProvider, + WalletProviderKey, +} from '@mdip/keymaster/types'; +import type { EcdsaJwkPublic } from '@mdip/cipher/types'; +import WalletJsonMemory from '@mdip/keymaster/wallet/json-memory'; + +const gatekeeper = { + createDID: async () => 'did:test:stub', + listRegistries: async () => [], +} as any; + +const cipher = { + verifySig: () => true, +} as any; + +class DummyWalletProvider implements WalletProvider { + readonly type = 'dummy'; + called = false; + + private fail(): never { + this.called = true; + throw new Error('provider should not be called'); + } + + async getFingerprint(): Promise { + return this.fail(); + } + + async resetWallet(_overwrite: boolean = false): Promise { + return this.fail(); + } + + async createIdKey(): Promise { + return this.fail(); + } + + async getPublicKey(_keyRef: string): Promise { + return this.fail(); + } + + async signDigest(_keyRef: string, _digest: string): Promise { + return this.fail(); + } + + async encrypt(_keyRef: string, _receiver: EcdsaJwkPublic, _plaintext: string): Promise { + return this.fail(); + } + + async decrypt(_keyRef: string, _sender: EcdsaJwkPublic, _ciphertext: string): Promise { + return this.fail(); + } + + async rotateKey(_keyRef: string): Promise<{ publicJwk: EcdsaJwkPublic }> { + return this.fail(); + } + +} + +const legacyWallet: LegacyWalletFile = { + version: 1, + seed: { + mnemonicEnc: { + salt: 'salt', + iv: 'iv', + data: 'data', + }, + }, + counter: 1, + ids: { + alice: { + did: 'did:test:alice', + account: 0, + index: 0, + }, + }, + current: 'alice', +}; + +describe('legacy wallet migration provider gate', () => { + it('rejects legacy wallets when the active provider is not MnemonicHdWalletProvider', async () => { + const store = new WalletJsonMemory(); + const provider = new DummyWalletProvider(); + + await store.saveWallet(legacyWallet, true); + + const keymaster = new Keymaster({ + gatekeeper, + store, + walletProvider: provider, + cipher, + }); + + await expect(keymaster.loadWallet()).rejects.toThrow( + 'Keymaster: Legacy wallet migration requires MnemonicHdWalletProvider.' + ); + expect(provider.called).toBe(false); + expect(await store.loadWallet()).toEqual(legacyWallet); + }); +}); diff --git a/tests/keymaster/modular-wallet-coverage.test.ts b/tests/keymaster/modular-wallet-coverage.test.ts new file mode 100644 index 000000000..eb5e8759d --- /dev/null +++ b/tests/keymaster/modular-wallet-coverage.test.ts @@ -0,0 +1,544 @@ +import { jest } from '@jest/globals'; +import Keymaster from '@mdip/keymaster'; +import type { + KeymasterStore, + LegacyWalletFile, + MnemonicHdWalletState, + StoredWallet, + WalletFile, + WalletProvider, + WalletProviderKey, +} from '@mdip/keymaster/types'; +import type { EcdsaJwkPublic } from '@mdip/cipher/types'; +import CipherNode from '@mdip/cipher/node'; +import MnemonicHdWalletProvider from '../../packages/keymaster/src/provider/mnemonic-hd.ts'; +import WalletProviderJsonMemory from '../../packages/keymaster/src/provider/json-memory.ts'; +import { encMnemonic } from '@mdip/keymaster/encryption'; + +const gatekeeper = { + createDID: async () => 'did:test:stub', + listRegistries: async () => [], +} as any; + +const cipherStub = { + verifySig: () => true, +} as any; + +const dummyPublicJwk = { + kty: 'EC', + crv: 'secp256k1', + x: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + y: 'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB', +} as EcdsaJwkPublic; + +const PASSPHRASE = 'passphrase'; + +class MemoryStore { + protected wallet: T | null; + protected readonly saveResults: boolean[]; + + constructor(initial: T | null = null, saveResults: boolean[] = []) { + this.wallet = initial ? structuredClone(initial) : null; + this.saveResults = [...saveResults]; + } + + protected async saveValue(wallet: T, overwrite: boolean = false): Promise { + if (this.wallet && !overwrite) { + return false; + } + + if (this.saveResults.length > 0) { + const result = this.saveResults.shift()!; + if (!result) { + return false; + } + } + + this.wallet = structuredClone(wallet); + return true; + } + + protected async loadValue(): Promise { + return this.wallet ? structuredClone(this.wallet) : null; + } +} + +class MemoryKeymasterStore extends MemoryStore implements KeymasterStore { + constructor(initial: StoredWallet | null = null, saveResults: boolean[] = []) { + super(initial, saveResults); + } + + async saveWallet(wallet: StoredWallet, overwrite: boolean = false): Promise { + return this.saveValue(wallet, overwrite); + } + + async loadWallet(): Promise { + return this.loadValue(); + } +} + +class ControlledProviderStore extends WalletProviderJsonMemory { + private readonly saveResults: boolean[]; + + constructor(initial: MnemonicHdWalletState | null = null, saveResults: boolean[] = []) { + super(); + this.saveResults = [...saveResults]; + if (initial) { + this.walletCache = JSON.stringify(initial); + } + } + + queueSaveResult(result: boolean): void { + this.saveResults.push(result); + } + + override async saveWallet(wallet: MnemonicHdWalletState, overwrite: boolean = false): Promise { + if (this.saveResults.length > 0) { + const result = this.saveResults.shift()!; + if (!result) { + return false; + } + } + + return super.saveWallet(wallet, overwrite); + } +} + +class DummyWalletProvider implements WalletProvider { + readonly type = 'dummy'; + fingerprint = 'dummy-fingerprint'; + resetCalls: boolean[] = []; + + async getFingerprint(): Promise { + return this.fingerprint; + } + + async resetWallet(overwrite: boolean = false): Promise { + this.resetCalls.push(overwrite); + } + + async createIdKey(): Promise { + return { + keyRef: 'dummy:0#0', + publicJwk: dummyPublicJwk, + }; + } + + async signDigest(_keyRef: string, _digest: string): Promise { + return 'signature'; + } + + async encrypt(_keyRef: string, _receiver: EcdsaJwkPublic, plaintext: string): Promise { + return plaintext; + } + + async decrypt(_keyRef: string, _sender: EcdsaJwkPublic, ciphertext: string): Promise { + return ciphertext; + } +} + +async function makeWalletFile(provider: WalletProvider, ids: WalletFile['ids'] = {}): Promise { + return { + version: 2, + provider: { + type: provider.type, + walletFingerprint: await provider.getFingerprint(), + }, + ids, + }; +} + +async function makeLegacyV1Decrypted( + cipher: CipherNode, + passphrase: string = PASSPHRASE, +): Promise { + const mnemonic = cipher.generateMnemonic(); + const mnemonicEnc = await encMnemonic(mnemonic, passphrase); + return { + version: 1, + seed: { mnemonicEnc }, + counter: 2, + ids: { + Alice: { + did: 'did:test:alice', + account: 0, + index: 1, + }, + }, + current: 'Alice', + }; +} + +describe('Keymaster modular wallet coverage', () => { + it('rejects v2 metadata when the active provider identity does not match', async () => { + const provider = new DummyWalletProvider(); + const storedWallet = await makeWalletFile(provider); + storedWallet.provider.walletFingerprint = 'other-fingerprint'; + const store = new MemoryKeymasterStore(storedWallet); + const keymaster = new Keymaster({ gatekeeper, store, walletProvider: provider, cipher: cipherStub }); + + await expect(keymaster.loadWallet()).rejects.toThrow( + 'Keymaster: Wallet provider does not match stored metadata.' + ); + }); + + it('throws if migrating legacy metadata cannot be saved back to the keymaster store', async () => { + const cipher = new CipherNode(); + const legacyWallet = await makeLegacyV1Decrypted(cipher); + const store = new MemoryKeymasterStore(legacyWallet, [false]); + const providerStore = new ControlledProviderStore(); + const walletProvider = new MnemonicHdWalletProvider({ store: providerStore, cipher, passphrase: PASSPHRASE }); + const keymaster = new Keymaster({ gatekeeper, store, walletProvider, cipher }); + + await expect(keymaster.loadWallet()).rejects.toThrow('Keymaster: save wallet failed'); + }); + + it('uses resetWallet for non-mnemonic providers', async () => { + const store = new MemoryKeymasterStore(); + const provider = new DummyWalletProvider(); + const keymaster = new Keymaster({ gatekeeper, store, walletProvider: provider, cipher: cipherStub }); + + const wallet = await keymaster.newWallet(undefined, true); + + expect(provider.resetCalls).toEqual([true]); + expect(wallet).toEqual({ + version: 2, + provider: { + type: 'dummy', + walletFingerprint: 'dummy-fingerprint', + }, + ids: {}, + }); + }); + + it('rejects mnemonic initialization for non-mnemonic providers', async () => { + const store = new MemoryKeymasterStore(); + const provider = new DummyWalletProvider(); + const keymaster = new Keymaster({ gatekeeper, store, walletProvider: provider, cipher: cipherStub }); + + await expect(keymaster.newWallet('mnemonic words')).rejects.toThrow( + 'Keymaster: Wallet provider does not support mnemonic initialization.' + ); + }); + + it('throws if saving new v2 metadata fails', async () => { + const store = new MemoryKeymasterStore(null, [false]); + const provider = new DummyWalletProvider(); + const keymaster = new Keymaster({ gatekeeper, store, walletProvider: provider, cipher: cipherStub }); + + await expect(keymaster.newWallet(undefined, true)).rejects.toThrow('Keymaster: save wallet failed'); + }); + + it('throws if a metadata mutation cannot be saved', async () => { + const provider = new DummyWalletProvider(); + const wallet = await makeWalletFile(provider, { + Alice: { + did: 'did:test:alice', + keyRef: 'dummy:0#0', + }, + }); + wallet.current = 'Alice'; + + const store = new MemoryKeymasterStore(wallet, [false]); + const keymaster = new Keymaster({ gatekeeper, store, walletProvider: provider, cipher: cipherStub }); + + await expect(keymaster.removeId('Alice')).rejects.toThrow('Keymaster: save wallet failed'); + }); + + it('rejects key rotation when the provider does not expose rotateKey', async () => { + const provider = new DummyWalletProvider(); + const wallet = await makeWalletFile(provider, { + Alice: { + did: 'did:test:alice', + keyRef: 'dummy:0#0', + }, + }); + wallet.current = 'Alice'; + + const store = new MemoryKeymasterStore(wallet); + const keymaster = new Keymaster({ gatekeeper, store, walletProvider: provider, cipher: cipherStub }); + + await expect(keymaster.rotateKeys()).rejects.toThrow( + 'Keymaster: Wallet provider does not support key rotation.' + ); + }); +}); + +describe('MnemonicHdWalletProvider coverage', () => { + it('validates constructor arguments', () => { + const cipher = new CipherNode(); + + expect(() => new MnemonicHdWalletProvider({ store: undefined as any, cipher, passphrase: PASSPHRASE })) + .toThrow('Invalid parameter: options.store'); + expect(() => new MnemonicHdWalletProvider({ store: new ControlledProviderStore(), cipher: undefined as any, passphrase: PASSPHRASE })) + .toThrow('Invalid parameter: options.cipher'); + expect(() => new MnemonicHdWalletProvider({ store: new ControlledProviderStore(), cipher, passphrase: '' as any })) + .toThrow('Invalid parameter: options.passphrase'); + }); + + it('auto-initializes provider state when getFingerprint is called on an empty store', async () => { + const cipher = new CipherNode(); + const store = new ControlledProviderStore(); + const provider = new MnemonicHdWalletProvider({ store, cipher, passphrase: PASSPHRASE }); + + const fingerprint = await provider.getFingerprint(); + const savedState = await store.loadWallet(); + + expect(fingerprint).toEqual(expect.any(String)); + expect(savedState?.type).toBe('mnemonic-hd'); + expect(savedState?.rootPublicJwk).toBeDefined(); + }); + + it('resetWallet creates provider state', async () => { + const cipher = new CipherNode(); + const store = new ControlledProviderStore(); + const provider = new MnemonicHdWalletProvider({ store, cipher, passphrase: PASSPHRASE }); + + await provider.resetWallet(true); + const backup = await provider.backupWallet(); + + expect(backup.type).toBe('mnemonic-hd'); + expect(backup.keys).toEqual({}); + }); + + it('backupWallet rejects when the provider has not been initialized', async () => { + const cipher = new CipherNode(); + const store = new ControlledProviderStore(); + const provider = new MnemonicHdWalletProvider({ store, cipher, passphrase: PASSPHRASE }); + + await expect(provider.backupWallet()).rejects.toThrow('Keymaster: Wallet provider not initialized.'); + }); + + it('saveWallet rejects invalid imported provider state', async () => { + const cipher = new CipherNode(); + const store = new ControlledProviderStore(); + const provider = new MnemonicHdWalletProvider({ store, cipher, passphrase: PASSPHRASE }); + + await expect(provider.saveWallet({ + version: 1, + type: 'mnemonic-hd', + mnemonicEnc: { salt: 'salt', iv: 'iv', data: 'data' }, + nextAccount: 0, + keys: {}, + } as any, true)).rejects.toThrow('Invalid parameter: wallet'); + }); + + it('saveWallet returns false when the provider store refuses the restore write', async () => { + const cipher = new CipherNode(); + const sourceProvider = new MnemonicHdWalletProvider({ + store: new ControlledProviderStore(), + cipher, + passphrase: PASSPHRASE, + }); + await sourceProvider.newWallet(undefined, true); + const backup = await sourceProvider.backupWallet(); + + const failingProvider = new MnemonicHdWalletProvider({ + store: new ControlledProviderStore(null, [false]), + cipher, + passphrase: PASSPHRASE, + }); + + await expect(failingProvider.saveWallet(backup, true)).resolves.toBe(false); + }); + + it('changePassphrase rejects an invalid mnemonic', async () => { + const cipher = new CipherNode(); + const provider = new MnemonicHdWalletProvider({ + store: new ControlledProviderStore(), + cipher, + passphrase: PASSPHRASE, + }); + + await provider.newWallet(undefined, true); + + await expect(provider.changePassphrase([] as any, 'updated')).rejects.toThrow( + 'Invalid parameter: mnemonic' + ); + }); + + it('migrates a decrypted v1 wallet into v2 metadata with versioned key refs', async () => { + const cipher = new CipherNode(); + const legacyWallet = await makeLegacyV1Decrypted(cipher); + const provider = new MnemonicHdWalletProvider({ + store: new ControlledProviderStore(), + cipher, + passphrase: PASSPHRASE, + }); + + const migrated = await provider.migrateLegacyWallet(legacyWallet); + + expect(migrated).toEqual({ + version: 2, + provider: { + type: 'mnemonic-hd', + walletFingerprint: expect.any(String), + }, + ids: { + Alice: { + did: 'did:test:alice', + keyRef: 'hd:0#1', + }, + }, + current: 'Alice', + }); + }); + + it('throws if migrating a legacy wallet cannot save provider state', async () => { + const cipher = new CipherNode(); + const legacyWallet = await makeLegacyV1Decrypted(cipher); + const provider = new MnemonicHdWalletProvider({ + store: new ControlledProviderStore(null, [false]), + cipher, + passphrase: PASSPHRASE, + }); + + await expect(provider.migrateLegacyWallet(legacyWallet)).rejects.toThrow( + 'Keymaster: save wallet failed' + ); + }); + + it('rejects unsupported legacy wallet shapes', async () => { + const cipher = new CipherNode(); + const provider = new MnemonicHdWalletProvider({ + store: new ControlledProviderStore(), + cipher, + passphrase: PASSPHRASE, + }); + + await expect(provider.migrateLegacyWallet({ version: 99 } as any)).rejects.toThrow( + 'Keymaster: Unsupported wallet version.' + ); + }); + + it('loads provider state from the store when the cache is empty', async () => { + const cipher = new CipherNode(); + const mnemonic = cipher.generateMnemonic(); + const sourceProvider = new MnemonicHdWalletProvider({ + store: new ControlledProviderStore(), + cipher, + passphrase: PASSPHRASE, + }); + + await sourceProvider.newWallet(mnemonic, true); + const backup = await sourceProvider.backupWallet(); + + const restoredProvider = new MnemonicHdWalletProvider({ + store: new ControlledProviderStore(backup), + cipher, + passphrase: PASSPHRASE, + }); + + await expect(restoredProvider.decryptMnemonic()).resolves.toBe(mnemonic); + }); + + it('supports base key refs for rotation', async () => { + const cipher = new CipherNode(); + const provider = new MnemonicHdWalletProvider({ + store: new ControlledProviderStore(), + cipher, + passphrase: PASSPHRASE, + }); + + await provider.newWallet(undefined, true); + await provider.createIdKey(); + + await expect(provider.rotateKey('hd:0')).resolves.toEqual({ + publicJwk: expect.any(Object), + }); + }); + + it('rejects malformed versioned key refs', async () => { + const cipher = new CipherNode(); + const provider = new MnemonicHdWalletProvider({ + store: new ControlledProviderStore(), + cipher, + passphrase: PASSPHRASE, + }); + + await provider.newWallet(undefined, true); + + await expect(provider.signDigest('hd:0#bad', 'digest')).rejects.toThrow( + 'Keymaster: Unknown keyRef: hd:0#bad' + ); + }); + + it('rejects unknown imported key refs', async () => { + const cipher = new CipherNode(); + const provider = new MnemonicHdWalletProvider({ + store: new ControlledProviderStore(), + cipher, + passphrase: PASSPHRASE, + }); + + await provider.newWallet(undefined, true); + + await expect(provider.signDigest('hd:999#0', 'digest')).rejects.toThrow( + 'Keymaster: Unknown keyRef: hd:999' + ); + }); + + it('rejects unknown rotated key versions for known key rings', async () => { + const cipher = new CipherNode(); + const provider = new MnemonicHdWalletProvider({ + store: new ControlledProviderStore(), + cipher, + passphrase: PASSPHRASE, + }); + + await provider.newWallet(undefined, true); + await provider.createIdKey(); + + await expect(provider.signDigest('hd:0#1', 'digest')).rejects.toThrow( + 'Keymaster: Unknown keyRef: hd:0#1' + ); + }); + + it('guards private helper access when state or hd cache is missing', () => { + const cipher = new CipherNode(); + const provider = new MnemonicHdWalletProvider({ + store: new ControlledProviderStore(), + cipher, + passphrase: PASSPHRASE, + }); + + expect(() => (provider as any).getRootKeyPair()).toThrow('Keymaster: HD wallet cache not loaded'); + expect(() => (provider as any).deriveIdKeyPair(0, 0)).toThrow('Keymaster: HD wallet cache not loaded'); + expect(() => (provider as any).findKeyPairForRef('hd:0#0')).toThrow('Keymaster: Wallet provider not initialized.'); + }); + + it('does not save provider state when a private mutation is a no-op', async () => { + const cipher = new CipherNode(); + const store = new ControlledProviderStore(); + const provider = new MnemonicHdWalletProvider({ + store, + cipher, + passphrase: PASSPHRASE, + }); + + await provider.newWallet(undefined, true); + const saveSpy = jest.spyOn(store, 'saveWallet'); + saveSpy.mockClear(); + + await (provider as any).mutateState(() => { }); + + expect(saveSpy).not.toHaveBeenCalled(); + }); + + it('throws when a private provider state mutation cannot be saved', async () => { + const cipher = new CipherNode(); + const store = new ControlledProviderStore(); + const provider = new MnemonicHdWalletProvider({ + store, + cipher, + passphrase: PASSPHRASE, + }); + + await provider.newWallet(undefined, true); + store.queueSaveResult(false); + + await expect((provider as any).mutateState((state: MnemonicHdWalletState) => { + state.nextAccount += 1; + })).rejects.toThrow('Keymaster: save wallet failed'); + }); +}); diff --git a/tests/keymaster/name.test.ts b/tests/keymaster/name.test.ts index ac1e4c60e..17b0b950f 100644 --- a/tests/keymaster/name.test.ts +++ b/tests/keymaster/name.test.ts @@ -2,13 +2,12 @@ import Gatekeeper from '@mdip/gatekeeper'; import Keymaster from '@mdip/keymaster'; import CipherNode from '@mdip/cipher/node'; import DbJsonMemory from '@mdip/gatekeeper/db/json-memory'; -import WalletJsonMemory from '@mdip/keymaster/wallet/json-memory'; import { ExpectedExceptionError } from '@mdip/common/errors'; import HeliaClient from '@mdip/ipfs/helia'; +import { createTestKeymaster } from './testUtils.ts'; let ipfs: HeliaClient; let gatekeeper: Gatekeeper; -let wallet: WalletJsonMemory; let cipher: CipherNode; let keymaster: Keymaster; @@ -26,9 +25,8 @@ afterAll(async () => { beforeEach(() => { const db = new DbJsonMemory('test'); gatekeeper = new Gatekeeper({ db, ipfs, registries: ['local', 'hyperswarm', 'TFTC'] }); - wallet = new WalletJsonMemory(); cipher = new CipherNode(); - keymaster = new Keymaster({ gatekeeper, wallet, cipher, passphrase: 'passphrase' }); + ({ keymaster } = createTestKeymaster(gatekeeper, cipher)); }); describe('addName', () => { diff --git a/tests/keymaster/notice.test.ts b/tests/keymaster/notice.test.ts index bda40d103..915d0c51c 100644 --- a/tests/keymaster/notice.test.ts +++ b/tests/keymaster/notice.test.ts @@ -5,7 +5,8 @@ import DbJsonMemory from '@mdip/gatekeeper/db/json-memory'; import WalletJsonMemory from '@mdip/keymaster/wallet/json-memory'; import { ExpectedExceptionError } from '@mdip/common/errors'; import HeliaClient from '@mdip/ipfs/helia'; -import { NoticeMessage, SearchEngine } from '@mdip/keymaster/types'; +import { NoticeMessage, SearchEngine, WalletProviderStore } from '@mdip/keymaster/types'; +import { createTestKeymaster } from './testUtils.ts'; class MockSearch implements SearchEngine { private results: string[] = []; @@ -30,7 +31,8 @@ class MockSearch implements SearchEngine { let ipfs: HeliaClient; let gatekeeper: Gatekeeper; -let wallet: WalletJsonMemory; +let store: WalletJsonMemory; +let providerStore: WalletProviderStore; let cipher: CipherNode; let keymaster: Keymaster; let search: MockSearch; @@ -49,10 +51,9 @@ afterAll(async () => { beforeEach(() => { const db = new DbJsonMemory('test'); gatekeeper = new Gatekeeper({ db, ipfs, registries: ['local', 'hyperswarm', 'TFTC'] }); - wallet = new WalletJsonMemory(); cipher = new CipherNode(); search = new MockSearch(); - keymaster = new Keymaster({ gatekeeper, wallet, cipher, search, passphrase: 'passphrase' }); + ({ keymaster, store, providerStore } = createTestKeymaster(gatekeeper, cipher, { search })); }); describe('verifyNotice', () => { @@ -418,7 +419,7 @@ describe('searchNotices', () => { }); it('should return false if search engine not configured', async () => { - keymaster = new Keymaster({ gatekeeper, wallet, cipher, passphrase: 'passphrase' }); + ({ keymaster } = createTestKeymaster(gatekeeper, cipher, { store, providerStore })); await keymaster.createId('Alice'); const ok = await keymaster.searchNotices(); diff --git a/tests/keymaster/poll.test.ts b/tests/keymaster/poll.test.ts index 1b4bdfe30..1ebe5644a 100644 --- a/tests/keymaster/poll.test.ts +++ b/tests/keymaster/poll.test.ts @@ -3,13 +3,12 @@ import Keymaster from '@mdip/keymaster'; import { Poll } from '@mdip/keymaster/types'; import CipherNode from '@mdip/cipher/node'; import DbJsonMemory from '@mdip/gatekeeper/db/json-memory'; -import WalletJsonMemory from '@mdip/keymaster/wallet/json-memory'; import { InvalidDIDError, ExpectedExceptionError } from '@mdip/common/errors'; import HeliaClient from '@mdip/ipfs/helia'; +import { createTestKeymaster } from './testUtils.ts'; let ipfs: HeliaClient; let gatekeeper: Gatekeeper; -let wallet: WalletJsonMemory; let cipher: CipherNode; let keymaster: Keymaster; @@ -27,9 +26,8 @@ afterAll(async () => { beforeEach(() => { const db = new DbJsonMemory('test'); gatekeeper = new Gatekeeper({ db, ipfs, registries: ['local', 'hyperswarm', 'TFTC'] }); - wallet = new WalletJsonMemory(); cipher = new CipherNode(); - keymaster = new Keymaster({ gatekeeper, wallet, cipher, passphrase: 'passphrase' }); + ({ keymaster } = createTestKeymaster(gatekeeper, cipher)); }); describe('pollTemplate', () => { diff --git a/tests/keymaster/response.test.ts b/tests/keymaster/response.test.ts index cc96fb3ce..3e590a811 100644 --- a/tests/keymaster/response.test.ts +++ b/tests/keymaster/response.test.ts @@ -3,14 +3,13 @@ import Keymaster from '@mdip/keymaster'; import { ChallengeResponse } from '@mdip/keymaster/types'; import CipherNode from '@mdip/cipher/node'; import DbJsonMemory from '@mdip/gatekeeper/db/json-memory'; -import WalletJsonMemory from '@mdip/keymaster/wallet/json-memory'; import { InvalidDIDError, ExpectedExceptionError, UnknownIDError } from '@mdip/common/errors'; import HeliaClient from '@mdip/ipfs/helia'; import { mockSchema } from './helper.ts'; +import { createTestKeymaster } from './testUtils.ts'; let ipfs: HeliaClient; let gatekeeper: Gatekeeper; -let wallet: WalletJsonMemory; let cipher: CipherNode; let keymaster: Keymaster; @@ -28,9 +27,8 @@ afterAll(async () => { beforeEach(() => { const db = new DbJsonMemory('test'); gatekeeper = new Gatekeeper({ db, ipfs, registries: ['local', 'hyperswarm', 'TFTC'] }); - wallet = new WalletJsonMemory(); cipher = new CipherNode(); - keymaster = new Keymaster({ gatekeeper, wallet, cipher, passphrase: 'passphrase' }); + ({ keymaster } = createTestKeymaster(gatekeeper, cipher)); }); describe('createResponse', () => { diff --git a/tests/keymaster/schema.test.ts b/tests/keymaster/schema.test.ts index 2231ab599..516ca8ad0 100644 --- a/tests/keymaster/schema.test.ts +++ b/tests/keymaster/schema.test.ts @@ -2,14 +2,13 @@ import Gatekeeper from '@mdip/gatekeeper'; import Keymaster from '@mdip/keymaster'; import CipherNode from '@mdip/cipher/node'; import DbJsonMemory from '@mdip/gatekeeper/db/json-memory'; -import WalletJsonMemory from '@mdip/keymaster/wallet/json-memory'; import { ExpectedExceptionError, UnknownIDError } from '@mdip/common/errors'; import HeliaClient from '@mdip/ipfs/helia'; import { mockSchema } from './helper.ts'; +import { createTestKeymaster } from './testUtils.ts'; let ipfs: HeliaClient; let gatekeeper: Gatekeeper; -let wallet: WalletJsonMemory; let cipher: CipherNode; let keymaster: Keymaster; @@ -27,9 +26,8 @@ afterAll(async () => { beforeEach(() => { const db = new DbJsonMemory('test'); gatekeeper = new Gatekeeper({ db, ipfs, registries: ['local', 'hyperswarm', 'TFTC'] }); - wallet = new WalletJsonMemory(); cipher = new CipherNode(); - keymaster = new Keymaster({ gatekeeper, wallet, cipher, passphrase: 'passphrase' }); + ({ keymaster } = createTestKeymaster(gatekeeper, cipher)); }); describe('createSchema', () => { diff --git a/tests/keymaster/testUtils.ts b/tests/keymaster/testUtils.ts index 94897a7bd..46b0ea5a9 100644 --- a/tests/keymaster/testUtils.ts +++ b/tests/keymaster/testUtils.ts @@ -1,5 +1,53 @@ +import Gatekeeper from '@mdip/gatekeeper'; +import Keymaster from '@mdip/keymaster'; +import type { SearchEngine, WalletProviderStore } from '@mdip/keymaster/types'; +import CipherNode from '@mdip/cipher/node'; +import MnemonicHdWalletProvider from '../../packages/keymaster/src/provider/mnemonic-hd.ts'; +import WalletProviderJsonMemory from '../../packages/keymaster/src/provider/json-memory.ts'; +import WalletJsonMemory from '@mdip/keymaster/wallet/json-memory'; + type CryptoLike = typeof globalThis.crypto | undefined; +export const TEST_PASSPHRASE = 'passphrase'; + +export function createProviderStore(): WalletProviderStore { + return new WalletProviderJsonMemory(); +} + +export function createMnemonicWalletProvider( + cipher: CipherNode, + passphrase: string = TEST_PASSPHRASE, + store: WalletProviderStore = createProviderStore(), +): MnemonicHdWalletProvider { + return new MnemonicHdWalletProvider({ store, cipher, passphrase }); +} + +export function createTestKeymaster( + gatekeeper: Gatekeeper, + cipher: CipherNode, + options: { + store?: WalletJsonMemory; + providerStore?: WalletProviderStore; + passphrase?: string; + defaultRegistry?: string; + search?: SearchEngine; + } = {}, +) { + const store = options.store ?? new WalletJsonMemory(); + const providerStore = options.providerStore ?? createProviderStore(); + const walletProvider = createMnemonicWalletProvider(cipher, options.passphrase, providerStore); + const keymaster = new Keymaster({ + gatekeeper, + store, + walletProvider, + cipher, + defaultRegistry: options.defaultRegistry, + search: options.search, + }); + + return { keymaster, store, providerStore, walletProvider }; +} + export function disableSubtle(): () => void { const originalDesc = Object.getOwnPropertyDescriptor(globalThis, 'crypto'); const originalCrypto: CryptoLike = originalDesc?.value; diff --git a/tests/keymaster/utils.test.ts b/tests/keymaster/utils.test.ts index b3e385e1a..cc999ed2d 100644 --- a/tests/keymaster/utils.test.ts +++ b/tests/keymaster/utils.test.ts @@ -6,15 +6,15 @@ import WalletJsonMemory from '@mdip/keymaster/wallet/json-memory'; import { ExpectedExceptionError, UnknownIDError } from '@mdip/common/errors'; import HeliaClient from '@mdip/ipfs/helia'; import { MdipDocument } from "@mdip/gatekeeper/types"; +import { createTestKeymaster } from './testUtils.ts'; let ipfs: HeliaClient; let gatekeeper: Gatekeeper; -let wallet: WalletJsonMemory; +let store: WalletJsonMemory; +let walletProvider: any; let cipher: CipherNode; let keymaster: Keymaster; -const PASSPHRASE = 'passphrase'; - beforeAll(async () => { ipfs = new HeliaClient(); await ipfs.start(); @@ -29,9 +29,8 @@ afterAll(async () => { beforeEach(() => { const db = new DbJsonMemory('test'); gatekeeper = new Gatekeeper({ db, ipfs, registries: ['local', 'hyperswarm', 'TFTC'] }); - wallet = new WalletJsonMemory(); cipher = new CipherNode(); - keymaster = new Keymaster({ gatekeeper, wallet, cipher, passphrase: PASSPHRASE }); + ({ keymaster, store, walletProvider } = createTestKeymaster(gatekeeper, cipher)); }); describe('constructor', () => { @@ -48,7 +47,7 @@ describe('constructor', () => { try { // @ts-expect-error Testing invalid usage, missing gatekeeper arg - new Keymaster({ wallet, cipher, passphrase: PASSPHRASE }); + new Keymaster({ store, walletProvider, cipher }); throw new ExpectedExceptionError(); } catch (error: any) { @@ -56,17 +55,17 @@ describe('constructor', () => { } try { - // @ts-expect-error Testing invalid usage, missing wallet arg - new Keymaster({ gatekeeper, cipher, passphrase: PASSPHRASE }); + // @ts-expect-error Testing invalid usage, missing store arg + new Keymaster({ gatekeeper, walletProvider, cipher }); throw new ExpectedExceptionError(); } catch (error: any) { - expect(error.message).toBe('Invalid parameter: options.wallet'); + expect(error.message).toBe('Invalid parameter: options.store'); } try { // @ts-expect-error Testing invalid usage, missing cipher arg - new Keymaster({ gatekeeper, wallet, passphrase: PASSPHRASE }); + new Keymaster({ gatekeeper, store, walletProvider }); throw new ExpectedExceptionError(); } catch (error: any) { @@ -75,7 +74,7 @@ describe('constructor', () => { try { // @ts-expect-error Testing invalid usage, invalid gatekeeper arg - new Keymaster({ gatekeeper: {}, wallet, cipher, passphrase: PASSPHRASE }); + new Keymaster({ gatekeeper: {}, store, walletProvider, cipher }); throw new ExpectedExceptionError(); } catch (error: any) { @@ -83,44 +82,44 @@ describe('constructor', () => { } try { - // @ts-expect-error Testing invalid usage, invalid wallet arg - new Keymaster({ gatekeeper, wallet: {}, cipher, passphrase: PASSPHRASE }); + // @ts-expect-error Testing invalid usage, invalid store arg + new Keymaster({ gatekeeper, store: {}, walletProvider, cipher }); throw new ExpectedExceptionError(); } catch (error: any) { - expect(error.message).toBe('Invalid parameter: options.wallet'); + expect(error.message).toBe('Invalid parameter: options.store'); } try { - // @ts-expect-error Testing invalid usage, invalid cipher arg - new Keymaster({ gatekeeper, wallet, cipher: {}, passphrase: PASSPHRASE }); + // @ts-expect-error Testing invalid usage, invalid walletProvider arg + new Keymaster({ gatekeeper, store, walletProvider: {}, cipher }); throw new ExpectedExceptionError(); } catch (error: any) { - expect(error.message).toBe('Invalid parameter: options.cipher'); + expect(error.message).toBe('Invalid parameter: options.walletProvider'); } try { - // @ts-expect-error Testing invalid usage, invalid search arg - new Keymaster({ gatekeeper, wallet, cipher, search: {}, passphrase: PASSPHRASE }); + // @ts-expect-error Testing invalid usage, invalid cipher arg + new Keymaster({ gatekeeper, store, walletProvider, cipher: {} }); throw new ExpectedExceptionError(); } catch (error: any) { - expect(error.message).toBe('Invalid parameter: options.search'); + expect(error.message).toBe('Invalid parameter: options.cipher'); } try { - // @ts-expect-error Testing invalid usage, missing passphrase arg - new Keymaster({ gatekeeper, wallet, cipher }); + // @ts-expect-error Testing invalid usage, invalid search arg + new Keymaster({ gatekeeper, store, walletProvider, cipher, search: {} }); throw new ExpectedExceptionError(); } catch (error: any) { - expect(error.message).toBe('Invalid parameter: options.passphrase'); + expect(error.message).toBe('Invalid parameter: options.search'); } // Cover the ExpectedExceptionError class for completeness try { - new Keymaster({ gatekeeper, wallet, cipher, passphrase: PASSPHRASE }); + new Keymaster({ gatekeeper, store, walletProvider, cipher }); throw new ExpectedExceptionError(); } catch (error: any) { diff --git a/tests/keymaster/wallet.test.ts b/tests/keymaster/wallet.test.ts index f7c2d6337..f7cbaa6bd 100644 --- a/tests/keymaster/wallet.test.ts +++ b/tests/keymaster/wallet.test.ts @@ -1,92 +1,107 @@ import Gatekeeper from '@mdip/gatekeeper'; import Keymaster from '@mdip/keymaster'; -import { - WalletEncFile, +import type { + LegacyWalletEncFile, + LegacyWalletFile, WalletFile, + WalletProviderStore, } from '@mdip/keymaster/types'; import CipherNode from '@mdip/cipher/node'; import DbJsonMemory from '@mdip/gatekeeper/db/json-memory'; import WalletJsonMemory from '@mdip/keymaster/wallet/json-memory'; -import { ExpectedExceptionError } from '@mdip/common/errors'; import HeliaClient from '@mdip/ipfs/helia'; -import { MdipDocument } from "@mdip/gatekeeper/types"; +import MnemonicHdWalletProvider from '../../packages/keymaster/src/provider/mnemonic-hd.ts'; +import WalletProviderJsonMemory from '../../packages/keymaster/src/provider/json-memory.ts'; import { TestHelper } from './helper.ts'; import { disableSubtle } from './testUtils.ts'; import { encMnemonic, decMnemonic } from '@mdip/keymaster/encryption'; let ipfs: HeliaClient; let gatekeeper: Gatekeeper; -let wallet: WalletJsonMemory; +let walletStore: WalletJsonMemory; +let providerStore: WalletJsonMemory; let cipher: CipherNode; +let walletProvider: MnemonicHdWalletProvider; let keymaster: Keymaster; let helper: TestHelper; const PASSPHRASE = 'passphrase'; -const MOCK_WALLET_V0_UNENCRYPTED: WalletFile = { - "seed": { - "mnemonic": "wp3keoeTNleruzCiTOrCgDmm6viThBq_GWdNIGzXKcS62XqtrBkm0-jDhEUoU1FvB5oWnmCqkSIhnKKeaUwPbK5ysjCHbIVrf9JAr-91FabxtX0B2dctgccg_MEVk88u6anmcFP4DAEhK5zUDXCYGgFR", - "hdkey": { - "xpriv": "xprv9s21ZrQH143K2JL3GWr8NVjn1XR9kpKpKX4G4g5cvYKyrGShVz7ro2zf75AYyArqm8b7VQGpbvcLXGw6Sp5sa5pAPfHMfbjsPkgiezjHSGN", - "xpub": "xpub661MyMwAqRbcEnQWNYP8jdgWZZFeAH3fgjyrs4VEUsrxj4mr3XS7LqK8xNiAKdSdnCb5zbdxPvgu49fdGgzMgDW8AfbyP6CQjWFkYgFbNdB" - } +const MOCK_WALLET_V0_UNENCRYPTED: LegacyWalletFile = { + seed: { + mnemonic: 'wp3keoeTNleruzCiTOrCgDmm6viThBq_GWdNIGzXKcS62XqtrBkm0-jDhEUoU1FvB5oWnmCqkSIhnKKeaUwPbK5ysjCHbIVrf9JAr-91FabxtX0B2dctgccg_MEVk88u6anmcFP4DAEhK5zUDXCYGgFR', + hdkey: { + xpriv: 'xprv9s21ZrQH143K2JL3GWr8NVjn1XR9kpKpKX4G4g5cvYKyrGShVz7ro2zf75AYyArqm8b7VQGpbvcLXGw6Sp5sa5pAPfHMfbjsPkgiezjHSGN', + xpub: 'xpub661MyMwAqRbcEnQWNYP8jdgWZZFeAH3fgjyrs4VEUsrxj4mr3XS7LqK8xNiAKdSdnCb5zbdxPvgu49fdGgzMgDW8AfbyP6CQjWFkYgFbNdB', + }, }, - "counter": 0, - "ids": {} -} + counter: 0, + ids: {}, +}; -const MOCK_WALLET_V0_WITH_IDS: WalletFile = { - "seed": { - "mnemonic": "WLWbs2iHBobOaKVJXViqefiTYayURf-_6gh_ndflhTACKYG8WKn8WWsQHXNiyNYjU9sfM9kOce8fyAyKjUERgdjnZv2_y6MKO9QsnQMd4XUZceKSa22QGdzBSBFOZ13Odzj9fVd4W-bfvgSZuJJqMWwNhw", - "hdkey": { - "xpriv": "xprv9s21ZrQH143K2v1nGQ7a6WnEH9VQv6AT7FrxSPGPfSuvgz1mxGsazcTKNk58oRWVpB2MqgaRBPXevSuRbtUziXeQT2ZYmCXnUe6JRHomHrn", - "xpub": "xpub661MyMwAqRbcFQ6FNReaTeixqBKuKYtJUUnZEmg1DnSuZnLvVpBqYQmoE31V13nDfVQ8kMkfPKkMk1oWw77jUjXZJT22jH5dpRTvE8M84m9" - } +const MOCK_WALLET_V0_WITH_IDS: LegacyWalletFile = { + seed: { + mnemonic: 'WLWbs2iHBobOaKVJXViqefiTYayURf-_6gh_ndflhTACKYG8WKn8WWsQHXNiyNYjU9sfM9kOce8fyAyKjUERgdjnZv2_y6MKO9QsnQMd4XUZceKSa22QGdzBSBFOZ13Odzj9fVd4W-bfvgSZuJJqMWwNhw', + hdkey: { + xpriv: 'xprv9s21ZrQH143K2v1nGQ7a6WnEH9VQv6AT7FrxSPGPfSuvgz1mxGsazcTKNk58oRWVpB2MqgaRBPXevSuRbtUziXeQT2ZYmCXnUe6JRHomHrn', + xpub: 'xpub661MyMwAqRbcFQ6FNReaTeixqBKuKYtJUUnZEmg1DnSuZnLvVpBqYQmoE31V13nDfVQ8kMkfPKkMk1oWw77jUjXZJT22jH5dpRTvE8M84m9', + }, }, - "counter": 2, - "ids": { - "id_1": { - "did": "did:test:z3v8AuakAd5R7WeGZUin2TtsqyxJPxouLfMEbpn5CmaNXChWq7r", - "account": 0, - "index": 0 + counter: 2, + ids: { + id_1: { + did: 'did:test:z3v8AuakAd5R7WeGZUin2TtsqyxJPxouLfMEbpn5CmaNXChWq7r', + account: 0, + index: 0, + }, + id_2: { + did: 'did:test:z3v8AuaiAYJ263LLYdApaUmGjy8Dnhx46LU1YDUvGHAcj9Ykgxg', + account: 1, + index: 0, }, - "id_2": { - "did": "did:test:z3v8AuaiAYJ263LLYdApaUmGjy8Dnhx46LU1YDUvGHAcj9Ykgxg", - "account": 1, - "index": 0 - } }, - "current": "id_2" -} + current: 'id_2', +}; const MOCK_WALLET_V0_ENCRYPTED = { - "salt": "SHUIyrheMkaGv7uyV+6ZHw==", - "iv": "nW4a05eR2rxHY0T7", - "data": "O+UlnXsCA522UwUwpFqtybIKwrJsHrVatrUJgNVBjFUk6TAdMsdGzW49WiJt+lF4iJe6ftETd1wjSretZc97gi+VzZzX0Ggba6rmXnuD189jRFg7eudCqG4y6Rgt72SYxZu3pgaEJ146Ntj+H6cAcSIfYyhNgtPmlpWBZcm68wP8YRaP5i0/mZF89md4DjjyFOv8qTLG4m42fmoCmliIeJdmBChjPdpAm8V/ZOwkULjKQPpLAjDe4uCwvgenZduSJEDyP8m1jAcwGFxcI1mcXVYunR/YruczYXGY4dPnmW03lXinOX+5SR/bs9Z23uhqoVgUgW25Rfz/5zr4YFVXBQcVQXEvLtR38KPWeuOKltvU3FbysSgIrM6WBSkJt5chfYCGg7a554lqHyeGTxrlUa8th+hXSv/LVkvl+juhq+yd85QqyX8gLhxZxw4lx5eeaU3uJ+BJ33onI2y4sr02ZU5fYOIPFKS7IGCE0KK2hv0NwNvSv8oy402m9xU+iCIr19Xs28jm61/difLh/x1g/RXQUV/07b8tZLbB6n6hBC/h+3jLexJeFIpn1C1yBY+JQopTS+NgXEZZK+HuFp3k/JjI0ImxIy/2gPSm3jRAs1f8GfLLEMdJWoseZ/laPhD0QdWPQt7oGqKTfn7G72os8gGsme4AiFtKzg0zEv3whzLvOW6W2uUXAR83cXdlKcLpju7vrjjdfrcqYxkR3VDp" -} + salt: 'SHUIyrheMkaGv7uyV+6ZHw==', + iv: 'nW4a05eR2rxHY0T7', + data: 'O+UlnXsCA522UwUwpFqtybIKwrJsHrVatrUJgNVBjFUk6TAdMsdGzW49WiJt+lF4iJe6ftETd1wjSretZc97gi+VzZzX0Ggba6rmXnuD189jRFg7eudCqG4y6Rgt72SYxZu3pgaEJ146Ntj+H6cAcSIfYyhNgtPmlpWBZcm68wP8YRaP5i0/mZF89md4DjjyFOv8qTLG4m42fmoCmliIeJdmBChjPdpAm8V/ZOwkULjKQPpLAjDe4uCwvgenZduSJEDyP8m1jAcwGFxcI1mcXVYunR/YruczYXGY4dPnmW03lXinOX+5SR/bs9Z23uhqoVgUgW25Rfz/5zr4YFVXBQcVQXEvLtR38KPWeuOKltvU3FbysSgIrM6WBSkJt5chfYCGg7a554lqHyeGTxrlUa8th+hXSv/LVkvl+juhq+yd85QqyX8gLhxZxw4lx5eeaU3uJ+BJ33onI2y4sr02ZU5fYOIPFKS7IGCE0KK2hv0NwNvSv8oy402m9xU+iCIr19Xs28jm61/difLh/x1g/RXQUV/07b8tZLbB6n6hBC/h+3jLexJeFIpn1C1yBY+JQopTS+NgXEZZK+HuFp3k/JjI0ImxIy/2gPSm3jRAs1f8GfLLEMdJWoseZ/laPhD0QdWPQt7oGqKTfn7G72os8gGsme4AiFtKzg0zEv3whzLvOW6W2uUXAR83cXdlKcLpju7vrjjdfrcqYxkR3VDp', +}; -const MOCK_WALLET_V1: WalletFile = { - "version": 1, - "seed": { - "mnemonicEnc": { - "data": "p3gKBzVtJTflKBHSDgrMiuncBH4foJM++DyoQAZD/cVeQDCY4aFTxSC0nkylGcpi88Odq0SXkc2nAHyjA7+D6FZzbiTDdgqu3SJXznZEMCJDzHTkpLOa", - "iv": "2mHu57FRcEERBLMv", - "salt": "m74zOr/8etDRMoU8dnriXA==", +const MOCK_WALLET_V1: LegacyWalletFile = { + version: 1, + seed: { + mnemonicEnc: { + data: 'p3gKBzVtJTflKBHSDgrMiuncBH4foJM++DyoQAZD/cVeQDCY4aFTxSC0nkylGcpi88Odq0SXkc2nAHyjA7+D6FZzbiTDdgqu3SJXznZEMCJDzHTkpLOa', + iv: '2mHu57FRcEERBLMv', + salt: 'm74zOr/8etDRMoU8dnriXA==', }, }, - "counter": 0, - "ids": {} + counter: 0, + ids: {}, }; -const MOCK_WALLET_V1_ENCRYPTED: WalletEncFile = { - "version": 1, - "seed": { - "mnemonicEnc": { - "salt": "8c+TrInC7EJZAnwjD6k8+A==", - "iv": "EkeweG9JHYjXr7cN", - "data": "4MLe/4SX9unO+7DTK1KUKLBLeHuJNS4bT9yjp8L/xnLzexpobGEmRJebUuv3e0aIs4krINlkTlP4krmqkI3p/EVlu9Ap6GRNoogZR4ZC1EtKUTwgNaQ7058o0/d1LQ8wSA==" - } +const MOCK_WALLET_V1_ENCRYPTED: LegacyWalletEncFile = { + version: 1, + seed: { + mnemonicEnc: { + salt: '8c+TrInC7EJZAnwjD6k8+A==', + iv: 'EkeweG9JHYjXr7cN', + data: '4MLe/4SX9unO+7DTK1KUKLBLeHuJNS4bT9yjp8L/xnLzexpobGEmRJebUuv3e0aIs4krINlkTlP4krmqkI3p/EVlu9Ap6GRNoogZR4ZC1EtKUTwgNaQ7058o0/d1LQ8wSA==', + }, }, - "enc": "CAKfW05djVJ2VnkLLbiBgtJpfC3x8xvc4_-M0OJBA6N7YcuXyd1F3GhifoUZ2Zdy2XGP_nGzhjS2u3NXgIM" + enc: 'CAKfW05djVJ2VnkLLbiBgtJpfC3x8xvc4_-M0OJBA6N7YcuXyd1F3GhifoUZ2Zdy2XGP_nGzhjS2u3NXgIM', +}; + +function createProviderStore(): WalletProviderStore { + return new WalletProviderJsonMemory(); +} + +function createWalletProvider( + store: WalletProviderStore = createProviderStore(), + passphrase: string = PASSPHRASE, +): MnemonicHdWalletProvider { + return new MnemonicHdWalletProvider({ store, cipher, passphrase }); } beforeAll(async () => { @@ -103,30 +118,26 @@ afterAll(async () => { beforeEach(() => { const db = new DbJsonMemory('test'); gatekeeper = new Gatekeeper({ db, ipfs, registries: ['local', 'hyperswarm', 'TFTC'] }); - wallet = new WalletJsonMemory(); + walletStore = new WalletJsonMemory(); + providerStore = new WalletProviderJsonMemory(); cipher = new CipherNode(); - keymaster = new Keymaster({ gatekeeper, wallet, cipher, passphrase: PASSPHRASE }); + walletProvider = createWalletProvider(providerStore); + keymaster = new Keymaster({ gatekeeper, store: walletStore, walletProvider, cipher }); helper = new TestHelper(keymaster); }); describe('loadWallet', () => { - it('should create a wallet on first load', async () => { + it('should create v2 metadata on first load', async () => { const wallet = await keymaster.loadWallet(); - expect(wallet).toEqual( - expect.objectContaining({ - version: 1, - counter: 0, - seed: expect.objectContaining({ - mnemonicEnc: { - salt: expect.any(String), - iv: expect.any(String), - data: expect.any(String), - }, - }), - ids: {} - }) - ); + expect(wallet).toEqual({ + version: 2, + provider: { + type: 'mnemonic-hd', + walletFingerprint: expect.any(String), + }, + ids: {}, + }); }); it('should return the same wallet on second load', async () => { @@ -137,482 +148,280 @@ describe('loadWallet', () => { }); it('should throw exception on load with incorrect passphrase', async () => { - await wallet.saveWallet(MOCK_WALLET_V1_ENCRYPTED); - const keymasterIncorrect = new Keymaster({ gatekeeper, wallet, cipher, passphrase: 'incorrect' }); - await expect(keymasterIncorrect.loadWallet()).rejects.toThrow('Keymaster: Incorrect passphrase.'); - }); + await walletStore.saveWallet(structuredClone(MOCK_WALLET_V1_ENCRYPTED), true); - it('should throw exception saving a deprecated encrypted wallet', async () => { - const mockWallet = { salt: "", iv: "", data: "" }; + const incorrectKeymaster = new Keymaster({ + gatekeeper, + store: walletStore, + walletProvider: createWalletProvider(createProviderStore(), 'incorrect'), + cipher, + }); - try { - // @ts-expect-error Testing unsupported historical wallet shape - await keymaster.saveWallet(mockWallet); - throw new ExpectedExceptionError(); - } catch (error: any) { - // eslint-disable-next-line sonarjs/no-duplicate-string - expect(error.message).toBe('Keymaster: Unsupported wallet version.'); - } + await expect(incorrectKeymaster.loadWallet()).rejects.toThrow('Keymaster: Incorrect passphrase.'); }); - it('should upgrade a v0 unencrypted wallet to v1', async () => { - await wallet.saveWallet(MOCK_WALLET_V0_UNENCRYPTED as any); + it('should upgrade a v0 wallet to v2', async () => { + await walletStore.saveWallet(structuredClone(MOCK_WALLET_V0_UNENCRYPTED), true); - const res = await keymaster.loadWallet(); - expect(res).toEqual( - expect.objectContaining({ - version: 1, - counter: 0, - seed: expect.objectContaining({ - mnemonicEnc: expect.any(Object), - }), - }) - ); + const wallet = await keymaster.loadWallet(); + + expect(wallet).toEqual({ + version: 2, + provider: { + type: 'mnemonic-hd', + walletFingerprint: expect.any(String), + }, + ids: {}, + }); }); - it('should throw on deprecated encrypted v0 wallet', async () => { - // @ts-expect-error Testing unsupported historical wallet shape - await wallet.saveWallet(MOCK_WALLET_V0_ENCRYPTED, true); + it('should migrate a v1 encrypted wallet to v2', async () => { + await walletStore.saveWallet(structuredClone(MOCK_WALLET_V1_ENCRYPTED), true); - await expect(keymaster.loadWallet()).rejects.toThrow('Keymaster: Unsupported wallet version.'); - }); + const wallet = await keymaster.loadWallet(); - it('should load a v1 encrypted wallet without hdkey', async () => { - await wallet.saveWallet(MOCK_WALLET_V1_ENCRYPTED); - const res = await keymaster.loadWallet(); - expect(res).toEqual( - expect.objectContaining({ - version: 1, - counter: 0, - seed: expect.objectContaining({ - mnemonicEnc: expect.any(Object) - }) - }) - ); - expect(res.seed?.hdkey).toBeUndefined(); + expect(wallet.version).toBe(2); + expect(wallet.provider).toEqual({ + type: 'mnemonic-hd', + walletFingerprint: expect.any(String), + }); + expect(wallet.ids).toEqual({}); + expect((wallet as any).seed).toBeUndefined(); + expect((wallet as any).counter).toBeUndefined(); }); - it('should load a v1 encrypted wallet from cache without hdkey', async () => { - await wallet.saveWallet(MOCK_WALLET_V1_ENCRYPTED); - // prime cache - await keymaster.loadWallet(); - // load from cache - const res = await keymaster.loadWallet(); - expect(res).toEqual( - expect.objectContaining({ - version: 1, - counter: 0, - seed: expect.objectContaining({ - mnemonicEnc: expect.any(Object) - }) - }) - ); - expect(res.seed?.hdkey).toBeUndefined(); + it('should throw on deprecated encrypted v0 wallet', async () => { + await walletStore.saveWallet(structuredClone(MOCK_WALLET_V0_ENCRYPTED) as any, true); + + await expect(keymaster.loadWallet()).rejects.toThrow('Keymaster: Unsupported wallet version.'); }); it('should throw on unsupported wallet version', async () => { - let clone = structuredClone(MOCK_WALLET_V1_ENCRYPTED); - delete clone.seed.mnemonicEnc; - await wallet.saveWallet(clone); + const invalidWallet: any = structuredClone(MOCK_WALLET_V1_ENCRYPTED); + delete invalidWallet.seed.mnemonicEnc; + await walletStore.saveWallet(invalidWallet, true); - try { - await keymaster.loadWallet(); - throw new ExpectedExceptionError(); - } catch (error: any) { - expect(error.message).toBe('Keymaster: Unsupported wallet version.'); - } + await expect(keymaster.loadWallet()).rejects.toThrow('Keymaster: Unsupported wallet version.'); }); }); describe('saveWallet', () => { - it('test saving directly on the unencrypted wallet', async () => { - const ok = await wallet.saveWallet(MOCK_WALLET_V1); - expect(ok).toBe(true); - }); - - it('should save a wallet', async () => { - const ok = await keymaster.saveWallet(MOCK_WALLET_V1); - const wallet = await keymaster.loadWallet(); - - expect(ok).toBe(true); - expect(wallet).toStrictEqual(MOCK_WALLET_V1); - }); - - it('should ignore overwrite flag if unnecessary', async () => { - const ok = await keymaster.saveWallet(MOCK_WALLET_V1, false); + it('should save a v2 wallet', async () => { const wallet = await keymaster.loadWallet(); + wallet.metadata = { foo: 'bar' }; - expect(ok).toBe(true); - expect(wallet).toStrictEqual(MOCK_WALLET_V1); - }); - - it('should overwrite an existing wallet', async () => { - const mockWallet = MOCK_WALLET_V1; - mockWallet.counter = 1; - - await keymaster.saveWallet(MOCK_WALLET_V1); - const ok = await keymaster.saveWallet(mockWallet); - const wallet = await keymaster.loadWallet(); + const ok = await keymaster.saveWallet(wallet, true); expect(ok).toBe(true); - expect(wallet).toStrictEqual(mockWallet); + expect(await keymaster.loadWallet()).toStrictEqual(wallet); }); it('should not overwrite an existing wallet if specified', async () => { - const mockWallet = MOCK_WALLET_V1; - mockWallet.counter = 1; - - await keymaster.saveWallet(MOCK_WALLET_V1); - const ok = await keymaster.saveWallet(mockWallet, false); const wallet = await keymaster.loadWallet(); + const updated = structuredClone(wallet); + updated.metadata = { foo: 'bar' }; - expect(ok).toBe(false); - expect(wallet).toStrictEqual(MOCK_WALLET_V1); - }); - - it('should overwrite an existing wallet in a loop', async () => { - for (let i = 0; i < 10; i++) { - const mockWallet = MOCK_WALLET_V1; - mockWallet.counter = i + 1; - - const ok = await keymaster.saveWallet(mockWallet); - const wallet = await keymaster.loadWallet(); - - expect(ok).toBe(true); - expect(wallet).toStrictEqual(mockWallet); - } - }); - - it('should not overwrite an existing wallet if specified', async () => { - const mockWallet = MOCK_WALLET_V1; - mockWallet.counter = 2; - - await keymaster.saveWallet(MOCK_WALLET_V1); - const ok = await keymaster.saveWallet(mockWallet, false); - const walletData = await keymaster.loadWallet(); + const ok = await keymaster.saveWallet(updated, false); expect(ok).toBe(false); - expect(walletData).toStrictEqual(MOCK_WALLET_V1); + expect(await keymaster.loadWallet()).toStrictEqual(wallet); }); - it('should save augmented wallet', async () => { + it('should save augmented wallet metadata', async () => { await keymaster.createId('Bob'); const wallet = await keymaster.loadWallet(); - wallet.ids['Bob'].icon = 'smiley'; + wallet.ids.Bob.icon = 'smiley'; wallet.metadata = { foo: 'bar' }; await keymaster.saveWallet(wallet, true); - const wallet2 = await keymaster.loadWallet(); - - expect(wallet).toStrictEqual(wallet2); - }); - - it('should upgrade a v0 wallet to v1', async () => { - const ok = await keymaster.saveWallet(MOCK_WALLET_V0_UNENCRYPTED); - expect(ok).toBe(true); - - const res = await wallet.loadWallet(); - expect(res).toEqual( - expect.objectContaining({ - version: 1, - enc: expect.any(String), - seed: expect.objectContaining({ - mnemonicEnc: expect.any(Object), - }), - }) - ); + expect(await keymaster.loadWallet()).toStrictEqual(wallet); }); - it('v0 upgrade must not use stale _hdkeyCache', async () => { - await keymaster.newWallet(undefined, true); - expect( - await keymaster.saveWallet(MOCK_WALLET_V0_UNENCRYPTED, true) - ).toBe(true); - }); + it('should upgrade a v0 wallet with ids to v2', async () => { + const ok = await keymaster.saveWallet(structuredClone(MOCK_WALLET_V0_WITH_IDS), true); + const wallet = await keymaster.loadWallet(); - it('should encrypt an unencrypted v1 wallet contents and remove hdkey', async () => { - const ok = await keymaster.saveWallet(MOCK_WALLET_V1); expect(ok).toBe(true); + expect(wallet.version).toBe(2); + expect(wallet.current).toBe('id_2'); + expect(wallet.ids.id_1).toEqual({ + did: MOCK_WALLET_V0_WITH_IDS.ids.id_1.did, + keyRef: 'hd:0#0', + }); + expect(wallet.ids.id_2).toEqual({ + did: MOCK_WALLET_V0_WITH_IDS.ids.id_2.did, + keyRef: 'hd:1#0', + }); + }); + + it('should save a v1 encrypted wallet as v2 metadata', async () => { + const ok = await keymaster.saveWallet(structuredClone(MOCK_WALLET_V1_ENCRYPTED), true); + const stored = await walletStore.loadWallet() as WalletFile; - const res = await wallet.loadWallet(); - expect(res).toEqual( - expect.objectContaining({ - version: 1, - enc: expect.any(String), - seed: expect.objectContaining({ - mnemonicEnc: expect.any(Object), - }), - }) - ); - }); - - it('should save a v1 encrypted wallet', async () => { - const ok = await keymaster.saveWallet(MOCK_WALLET_V1_ENCRYPTED, true); expect(ok).toBe(true); + expect(stored.version).toBe(2); + expect(stored.provider).toEqual({ + type: 'mnemonic-hd', + walletFingerprint: expect.any(String), + }); }); it('should throw on incorrect passphrase', async () => { - const wallet = new WalletJsonMemory(); - const keymaster = new Keymaster({ gatekeeper, wallet, cipher, passphrase: 'incorrect' }); - - try { - await keymaster.saveWallet(MOCK_WALLET_V1_ENCRYPTED, true); - throw new ExpectedExceptionError(); - } catch (error: any) { - expect(error.message).toBe('Keymaster: Incorrect passphrase.'); - } - }); -}); - -describe('decryptMnemonic', () => { - it('should return 12 words', async () => { - const wallet = await keymaster.loadWallet(); - const mnemonic = await keymaster.decryptMnemonic(); - - expect(mnemonic !== wallet.seed!.mnemonic).toBe(true); - - // Split the mnemonic into words - const words = mnemonic.split(' '); - expect(words.length).toBe(12); - }); -}); - -describe('exportEncryptedWallet', () => { - it('should export the wallet in encrypted form', async () => { - const res = await keymaster.exportEncryptedWallet(); - expect(res).toEqual( - expect.objectContaining({ - version: 1, - seed: expect.objectContaining({ - mnemonicEnc: expect.any(Object) - }), - enc: expect.any(String) - }) - ); - }); -}); + const incorrectKeymaster = new Keymaster({ + gatekeeper, + store: walletStore, + walletProvider: createWalletProvider(createProviderStore(), 'incorrect'), + cipher, + }); -describe('updateSeedBank', () => { - it('should throw error on missing DID', async () => { - const doc: MdipDocument = {}; - - try { - await keymaster.updateSeedBank(doc); - throw new ExpectedExceptionError(); - } - catch (error: any) { - expect(error.message).toBe('Invalid parameter: seed bank missing DID'); - } + await expect( + incorrectKeymaster.saveWallet(structuredClone(MOCK_WALLET_V1_ENCRYPTED), true) + ).rejects.toThrow('Keymaster: Incorrect passphrase.'); }); }); describe('newWallet', () => { it('should overwrite an existing wallet when allowed', async () => { const wallet1 = await keymaster.loadWallet(); + await keymaster.newWallet(undefined, true); const wallet2 = await keymaster.loadWallet(); - expect(wallet1.seed!.mnemonicEnc !== wallet2.seed!.mnemonicEnc).toBe(true); + expect(wallet1.provider.walletFingerprint).not.toBe(wallet2.provider.walletFingerprint); }); it('should not overwrite an existing wallet by default', async () => { await keymaster.loadWallet(); - try { - await keymaster.newWallet(); - throw new ExpectedExceptionError(); - } - catch (error: any) { - expect(error.message).toBe('Keymaster: save wallet failed'); - } + await expect(keymaster.newWallet()).rejects.toThrow('Keymaster: save wallet failed'); }); it('should create a wallet from a mnemonic', async () => { - const mnemonic1 = cipher.generateMnemonic(); - await keymaster.newWallet(mnemonic1); - const mnemonic2 = await keymaster.decryptMnemonic(); + const mnemonic = cipher.generateMnemonic(); + await keymaster.newWallet(mnemonic); + const wallet = await keymaster.loadWallet(); + + const comparisonProvider = createWalletProvider(createProviderStore()); + await comparisonProvider.newWallet(mnemonic, true); - expect(mnemonic1 === mnemonic2).toBe(true); + expect(wallet.provider.walletFingerprint).toBe(await comparisonProvider.getFingerprint()); }); it('should throw exception on invalid mnemonic', async () => { - try { - // @ts-expect-error Testing invalid usage, incorrect argument - await keymaster.newWallet([]); - throw new ExpectedExceptionError(); - } - catch (error: any) { - expect(error.message).toBe('Invalid parameter: mnemonic'); - } + await expect( + keymaster.newWallet([] as any) + ).rejects.toThrow('Invalid parameter: mnemonic'); }); }); -describe('resolveSeedBank', () => { - it('should create a deterministic seed bank ID', async () => { - const bank1 = await keymaster.resolveSeedBank(); - const bank2 = await keymaster.resolveSeedBank(); +describe('MnemonicHdWalletProvider backup', () => { + it('should backup and restore provider state directly', async () => { + await keymaster.createId('Bob'); - // Update the retrieved timestamp to match any value - bank1.didResolutionMetadata!.retrieved = expect.any(String); + const backup = await walletProvider.backupWallet(); + const restoredProvider = createWalletProvider(createProviderStore()); + const ok = await restoredProvider.saveWallet(backup, true); - expect(bank1).toStrictEqual(bank2); + expect(ok).toBe(true); + expect(await restoredProvider.getFingerprint()).toBe(await walletProvider.getFingerprint()); }); -}); -describe('backupWallet', () => { - it('should return a valid DID', async () => { - await keymaster.createId('Bob'); - const did = await keymaster.backupWallet(); - const doc = await keymaster.resolveDID(did); + it('should decrypt the mnemonic and change passphrase with a matching mnemonic', async () => { + const mnemonic = cipher.generateMnemonic(); + await walletProvider.newWallet(mnemonic, true); + + const backup = await walletProvider.backupWallet(); + const restoredProvider = createWalletProvider(createProviderStore(), 'temporary'); + await restoredProvider.saveWallet(backup, true); + await restoredProvider.changePassphrase(mnemonic, 'updated-passphrase'); - expect(did === doc.didDocument!.id).toBe(true); + expect(await restoredProvider.decryptMnemonic()).toBe(mnemonic); + expect(await restoredProvider.getFingerprint()).toBe(await walletProvider.getFingerprint()); }); - it('should store backup in seed bank', async () => { - await keymaster.createId('Bob'); - const did = await keymaster.backupWallet(); - const bank = await keymaster.resolveSeedBank(); + it('should reject passphrase change when the mnemonic does not match', async () => { + const mnemonic = cipher.generateMnemonic(); + await walletProvider.newWallet(mnemonic, true); + + const backup = await walletProvider.backupWallet(); + const restoredProvider = createWalletProvider(createProviderStore(), 'temporary'); + await restoredProvider.saveWallet(backup, true); - expect(did === (bank.didDocumentData! as { wallet: string }).wallet).toBe(true); + await expect( + restoredProvider.changePassphrase(cipher.generateMnemonic(), 'updated-passphrase') + ).rejects.toThrow('Keymaster: Mnemonic does not match wallet.'); }); }); -describe('recoverWallet', () => { - it('should recover wallet from seed bank', async () => { +describe('backupWallet', () => { + it('should return a valid DID and store backupDid in metadata', async () => { await keymaster.createId('Bob'); - const wallet = await keymaster.loadWallet(); - const mnemonic = await keymaster.decryptMnemonic(); - await keymaster.backupWallet(); - // Recover wallet from mnemonic - await keymaster.newWallet(mnemonic, true); - const recovered = await keymaster.recoverWallet(); + const did = await keymaster.backupWallet(); + const doc = await keymaster.resolveDID(did); + const wallet = await keymaster.loadWallet(); - expect(recovered).toEqual( - expect.objectContaining({ - counter: wallet.counter, - version: wallet.version, - seed: { - mnemonicEnc: expect.any(Object), - }, - current: wallet.current, - ids: wallet.ids - }) - ); + expect(did).toBe(doc.didDocument!.id); + expect(wallet.backupDid).toBe(did); }); +}); - it('should recover over existing wallet', async () => { +describe('recoverWallet', () => { + it('should recover wallet from stored backup DID', async () => { await keymaster.createId('Bob'); - await keymaster.loadWallet(); await keymaster.backupWallet(); await keymaster.createId('Alice'); - // Recover over existing wallet const recovered = await keymaster.recoverWallet(); - expect(recovered).toEqual( - expect.objectContaining({ - version: 1, - counter: 1, - current: "Bob", - seed: expect.objectContaining({ - mnemonicEnc: expect.any(Object), - }), - ids: expect.objectContaining({ - Bob: expect.objectContaining({ - account: 0, - did: expect.any(String), - index: 0 - }), - }) - }) - ); + expect(Object.keys(recovered.ids)).toEqual(['Bob']); + expect(recovered.current).toBe('Bob'); }); - it('should recover augmented wallet from seed bank', async () => { + it('should recover augmented wallet from backup DID', async () => { await keymaster.createId('Bob'); const wallet = await keymaster.loadWallet(); - const mnemonic = await keymaster.decryptMnemonic(); - - wallet.ids['Bob'].icon = 'smiley'; + wallet.ids.Bob.icon = 'smiley'; wallet.metadata = { foo: 'bar' }; await keymaster.saveWallet(wallet, true); - await keymaster.backupWallet(); - - // Recover wallet from mnemonic - await keymaster.newWallet(mnemonic, true); - const recovered = await keymaster.recoverWallet(); - - expect(recovered).toEqual( - expect.objectContaining({ - counter: wallet.counter, - version: wallet.version, - seed: { - mnemonicEnc: expect.any(Object), - }, - current: wallet.current, - ids: wallet.ids - }) - ); - }); - - it('should recover v0 wallet from seed bank', async () => { - await keymaster.saveWallet(MOCK_WALLET_V0_WITH_IDS); - const mnemonic = await keymaster.decryptMnemonic(); - await keymaster.backupWallet(undefined, MOCK_WALLET_V0_WITH_IDS); - - // Recover wallet from mnemonic - await keymaster.newWallet(mnemonic, true); - const recovered = await keymaster.recoverWallet(); - - expect(recovered).toBeDefined(); - expect(recovered.ids).toStrictEqual(MOCK_WALLET_V0_WITH_IDS.ids); - }); - - it('should recover wallet from backup DID', async () => { - await keymaster.createId('Bob'); - const wallet = await keymaster.loadWallet(); - const mnemonic = await keymaster.decryptMnemonic(); const did = await keymaster.backupWallet(); - // Recover wallet from mnemonic and recovery DID - await keymaster.newWallet(mnemonic, true); + await keymaster.createId('Alice'); const recovered = await keymaster.recoverWallet(did); expect(recovered).toEqual( expect.objectContaining({ - counter: wallet.counter, - version: wallet.version, - seed: { - mnemonicEnc: expect.any(Object), - }, - current: wallet.current, - ids: wallet.ids + version: 2, + ids: expect.objectContaining({ + Bob: expect.objectContaining({ + did: wallet.ids.Bob.did, + keyRef: wallet.ids.Bob.keyRef, + icon: 'smiley', + }), + }), + metadata: { foo: 'bar' }, }) ); + expect(recovered.ids.Alice).toBeUndefined(); }); it('should do nothing if wallet was not backed up', async () => { await keymaster.createId('Bob'); - const mnemonic = await keymaster.decryptMnemonic(); + const current = await keymaster.loadWallet(); - // Recover wallet from mnemonic - await keymaster.newWallet(mnemonic, true); const recovered = await keymaster.recoverWallet(); - expect(recovered.ids).toStrictEqual({}); + expect(recovered).toStrictEqual(current); }); it('should do nothing if backup DID is invalid', async () => { - const agentDID = await keymaster.createId('Bob'); - const mnemonic = await keymaster.decryptMnemonic(); + await keymaster.createId('Bob'); + const current = await keymaster.loadWallet(); - // Recover wallet from mnemonic - await keymaster.newWallet(mnemonic, true); - const recovered = await keymaster.recoverWallet(agentDID); + const recovered = await keymaster.recoverWallet('did:test:invalid'); - expect(recovered.ids).toStrictEqual({}); + expect(recovered).toStrictEqual(current); }); }); @@ -682,7 +491,7 @@ describe('checkWallet', () => { expect(checked).toBe(16); expect(invalid).toBe(0); - expect(deleted).toBe(4); // 2 credentials mentioned both in held and name lists + expect(deleted).toBe(4); }); }); @@ -764,29 +573,19 @@ describe('fixWallet', () => { describe('no WebCrypto subtle', () => { let restore: () => void; - beforeAll(async () => { + beforeAll(() => { restore = disableSubtle(); }); - afterAll(async () => { + afterAll(() => { restore(); }); it('encMnemonic will throw without crypto subtle', async () => { - try { - await encMnemonic("", PASSPHRASE); - throw new ExpectedExceptionError(); - } catch (error: any) { - expect(error.message).toBe('Web Cryptography API not available'); - } + await expect(encMnemonic('', PASSPHRASE)).rejects.toThrow('Web Cryptography API not available'); }); it('decMnemonic will throw without crypto subtle', async () => { - try { - await decMnemonic(MOCK_WALLET_V0_ENCRYPTED, PASSPHRASE); - throw new ExpectedExceptionError(); - } catch (error: any) { - expect(error.message).toBe('Web Cryptography API not available'); - } + await expect(decMnemonic(MOCK_WALLET_V0_ENCRYPTED, PASSPHRASE)).rejects.toThrow('Web Cryptography API not available'); }); });