diff --git a/.github/workflows/wallet-service.yml b/.github/workflows/wallet-service.yml index fab3c25..c258e8d 100644 --- a/.github/workflows/wallet-service.yml +++ b/.github/workflows/wallet-service.yml @@ -14,7 +14,7 @@ jobs: - name: Use Node.js uses: actions/setup-node@v3 with: - node-version: 18.x + node-version: 20.x cache: "npm" - run: npm ci working-directory: services/stellar-wallet @@ -26,7 +26,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: - node-version: 18.x + node-version: 20.x - run: npm ci working-directory: services/stellar-wallet - run: npm run lint @@ -39,7 +39,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: - node-version: 18.x + node-version: 20.x - run: npm ci working-directory: services/stellar-wallet - run: npm run test @@ -49,7 +49,7 @@ jobs: SOROBAN_RPC_URL: https://rpc-futura.stellar.org PORT: 3001 -# Smart contract + # Smart contract build-smart-contract: runs-on: macos-latest diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..2edeafb --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +20 \ No newline at end of file diff --git a/bun.lock b/bun.lock index 3f35e16..30e9b17 100644 --- a/bun.lock +++ b/bun.lock @@ -96,6 +96,7 @@ "name": "stellar-wallet", "version": "1.0.0", "dependencies": { + "@simplewebauthn/server": "^13.2.1", "@stellar/stellar-sdk": "^14.0.0-rc.3", "@types/jsonwebtoken": "^9.0.10", "@types/supertest": "^6.0.3", @@ -415,6 +416,8 @@ "@gar/promisify": ["@gar/promisify@1.1.3", "", {}, "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw=="], + "@hexagon/base64": ["@hexagon/base64@1.1.28", "", {}, "sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw=="], + "@hookform/resolvers": ["@hookform/resolvers@5.2.1", "", { "dependencies": { "@standard-schema/utils": "^0.3.0" }, "peerDependencies": { "react-hook-form": "^7.55.0" } }, "sha512-u0+6X58gkjMcxur1wRWokA7XsiiBJ6aK17aPZxhkoYiK5J+HcTx0Vhu9ovXe6H+dVpO6cjrn2FkJTryXEMlryQ=="], "@hot-wallet/sdk": ["@hot-wallet/sdk@1.0.11", "", { "dependencies": { "@near-js/crypto": "^1.4.0", "@near-js/utils": "^1.0.0", "@near-wallet-selector/core": "^8.9.13", "@solana/wallet-adapter-base": "^0.9.23", "@solana/web3.js": "^1.95.0", "borsh": "^2.0.0", "js-sha256": "^0.11.0", "sha1": "^1.1.1", "uuid4": "^2.0.3" } }, "sha512-qRDH/4yqnRCnk7L/Qd0/LDOKDUKWcFgvf6eRELJkP0OgxIe65i/iXaG+u2lL0mLbTGkiWYk67uAvEerNUv2gzA=="], @@ -531,6 +534,8 @@ "@ledgerhq/logs": ["@ledgerhq/logs@6.13.0", "", {}, "sha512-4+qRW2Pc8V+btL0QEmdB2X+uyx0kOWMWE1/LWsq5sZy3Q5tpi4eItJS6mB0XL3wGW59RQ+8bchNQQ1OW/va8Og=="], + "@levischuck/tiny-cbor": ["@levischuck/tiny-cbor@0.2.11", "", {}, "sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow=="], + "@lit-labs/ssr-dom-shim": ["@lit-labs/ssr-dom-shim@1.4.0", "", {}, "sha512-ficsEARKnmmW5njugNYKipTm4SFnbik7CXtoencDZzmzo/dQ+2Q0bgkzJuoJP20Aj0F+izzJjOqsnkd6F/o1bw=="], "@lit/reactive-element": ["@lit/reactive-element@2.1.1", "", { "dependencies": { "@lit-labs/ssr-dom-shim": "^1.4.0" } }, "sha512-N+dm5PAYdQ8e6UlywyyrgI2t++wFGXfHx+dSJ1oBrg6FAxUj40jId++EaRm80MKX5JnlH1sBsyZ5h0bcZKemCg=="], @@ -639,6 +644,30 @@ "@paralleldrive/cuid2": ["@paralleldrive/cuid2@2.2.2", "", { "dependencies": { "@noble/hashes": "^1.1.5" } }, "sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA=="], + "@peculiar/asn1-android": ["@peculiar/asn1-android@2.5.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.5.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-t8A83hgghWQkcneRsgGs2ebAlRe54ns88p7ouv8PW2tzF1nAW4yHcL4uZKrFpIU+uszIRzTkcCuie37gpkId0A=="], + + "@peculiar/asn1-cms": ["@peculiar/asn1-cms@2.5.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.5.0", "@peculiar/asn1-x509": "^2.5.0", "@peculiar/asn1-x509-attr": "^2.5.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-p0SjJ3TuuleIvjPM4aYfvYw8Fk1Hn/zAVyPJZTtZ2eE9/MIer6/18ROxX6N/e6edVSfvuZBqhxAj3YgsmSjQ/A=="], + + "@peculiar/asn1-csr": ["@peculiar/asn1-csr@2.5.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.5.0", "@peculiar/asn1-x509": "^2.5.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-ioigvA6WSYN9h/YssMmmoIwgl3RvZlAYx4A/9jD2qaqXZwGcNlAxaw54eSx2QG1Yu7YyBC5Rku3nNoHrQ16YsQ=="], + + "@peculiar/asn1-ecc": ["@peculiar/asn1-ecc@2.5.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.5.0", "@peculiar/asn1-x509": "^2.5.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-t4eYGNhXtLRxaP50h3sfO6aJebUCDGQACoeexcelL4roMFRRVgB20yBIu2LxsPh/tdW9I282gNgMOyg3ywg/mg=="], + + "@peculiar/asn1-pfx": ["@peculiar/asn1-pfx@2.5.0", "", { "dependencies": { "@peculiar/asn1-cms": "^2.5.0", "@peculiar/asn1-pkcs8": "^2.5.0", "@peculiar/asn1-rsa": "^2.5.0", "@peculiar/asn1-schema": "^2.5.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-Vj0d0wxJZA+Ztqfb7W+/iu8Uasw6hhKtCdLKXLG/P3kEPIQpqGI4P4YXlROfl7gOCqFIbgsj1HzFIFwQ5s20ug=="], + + "@peculiar/asn1-pkcs8": ["@peculiar/asn1-pkcs8@2.5.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.5.0", "@peculiar/asn1-x509": "^2.5.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-L7599HTI2SLlitlpEP8oAPaJgYssByI4eCwQq2C9eC90otFpm8MRn66PpbKviweAlhinWQ3ZjDD2KIVtx7PaVw=="], + + "@peculiar/asn1-pkcs9": ["@peculiar/asn1-pkcs9@2.5.0", "", { "dependencies": { "@peculiar/asn1-cms": "^2.5.0", "@peculiar/asn1-pfx": "^2.5.0", "@peculiar/asn1-pkcs8": "^2.5.0", "@peculiar/asn1-schema": "^2.5.0", "@peculiar/asn1-x509": "^2.5.0", "@peculiar/asn1-x509-attr": "^2.5.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-UgqSMBLNLR5TzEZ5ZzxR45Nk6VJrammxd60WMSkofyNzd3DQLSNycGWSK5Xg3UTYbXcDFyG8pA/7/y/ztVCa6A=="], + + "@peculiar/asn1-rsa": ["@peculiar/asn1-rsa@2.5.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.5.0", "@peculiar/asn1-x509": "^2.5.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-qMZ/vweiTHy9syrkkqWFvbT3eLoedvamcUdnnvwyyUNv5FgFXA3KP8td+ATibnlZ0EANW5PYRm8E6MJzEB/72Q=="], + + "@peculiar/asn1-schema": ["@peculiar/asn1-schema@2.5.0", "", { "dependencies": { "asn1js": "^3.0.6", "pvtsutils": "^1.3.6", "tslib": "^2.8.1" } }, "sha512-YM/nFfskFJSlHqv59ed6dZlLZqtZQwjRVJ4bBAiWV08Oc+1rSd5lDZcBEx0lGDHfSoH3UziI2pXt2UM33KerPQ=="], + + "@peculiar/asn1-x509": ["@peculiar/asn1-x509@2.5.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.5.0", "asn1js": "^3.0.6", "pvtsutils": "^1.3.6", "tslib": "^2.8.1" } }, "sha512-CpwtMCTJvfvYTFMuiME5IH+8qmDe3yEWzKHe7OOADbGfq7ohxeLaXwQo0q4du3qs0AII3UbLCvb9NF/6q0oTKQ=="], + + "@peculiar/asn1-x509-attr": ["@peculiar/asn1-x509-attr@2.5.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.5.0", "@peculiar/asn1-x509": "^2.5.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-9f0hPOxiJDoG/bfNLAFven+Bd4gwz/VzrCIIWc1025LEI4BXO0U5fOCTNDPbbp2ll+UzqKsZ3g61mpBp74gk9A=="], + + "@peculiar/x509": ["@peculiar/x509@1.14.0", "", { "dependencies": { "@peculiar/asn1-cms": "^2.5.0", "@peculiar/asn1-csr": "^2.5.0", "@peculiar/asn1-ecc": "^2.5.0", "@peculiar/asn1-pkcs9": "^2.5.0", "@peculiar/asn1-rsa": "^2.5.0", "@peculiar/asn1-schema": "^2.5.0", "@peculiar/asn1-x509": "^2.5.0", "pvtsutils": "^1.3.6", "reflect-metadata": "^0.2.2", "tslib": "^2.8.1", "tsyringe": "^4.10.0" } }, "sha512-Yc4PDxN3OrxUPiXgU63c+ZRXKGE8YKF2McTciYhUHFtHVB0KMnjeFSU0qpztGhsp4P0uKix4+J2xEpIEDu8oXg=="], + "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], "@pkgr/core": ["@pkgr/core@0.2.9", "", {}, "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA=="], @@ -799,6 +828,8 @@ "@services/supabase": ["@services/supabase@workspace:services/supabase"], + "@simplewebauthn/server": ["@simplewebauthn/server@13.2.1", "", { "dependencies": { "@hexagon/base64": "^1.1.27", "@levischuck/tiny-cbor": "^0.2.2", "@peculiar/asn1-android": "^2.3.10", "@peculiar/asn1-ecc": "^2.3.8", "@peculiar/asn1-rsa": "^2.3.8", "@peculiar/asn1-schema": "^2.3.8", "@peculiar/asn1-x509": "^2.3.8", "@peculiar/x509": "^1.13.0" } }, "sha512-Inmfye5opZXe3HI0GaksqBnQiM7glcNySoG6DH1GgkO1Lh9dvuV4XSV9DK02DReUVX39HpcDob9nxHELjECoQw=="], + "@sinclair/typebox": ["@sinclair/typebox@0.34.38", "", {}, "sha512-HpkxMmc2XmZKhvaKIZZThlHmx1L0I/V1hWK1NubtlFnr6ZqdiOpV72TKudZUNQjZNsyDBay72qFEhEvb+bcwcA=="], "@sinonjs/commons": ["@sinonjs/commons@3.0.1", "", { "dependencies": { "type-detect": "4.0.8" } }, "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ=="], @@ -1339,6 +1370,8 @@ "asap": ["asap@2.0.6", "", {}, "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA=="], + "asn1js": ["asn1js@3.0.6", "", { "dependencies": { "pvtsutils": "^1.3.6", "pvutils": "^1.1.3", "tslib": "^2.8.1" } }, "sha512-UOCGPYbl0tv8+006qks/dTgV9ajs97X2p0FAbyS2iyCRrmLSRolDaHdp+v/CLgnzHc3fVB+CwYiUmei7ndFcgA=="], + "assert": ["assert@2.1.0", "", { "dependencies": { "call-bind": "^1.0.2", "is-nan": "^1.3.2", "object-is": "^1.1.5", "object.assign": "^4.1.4", "util": "^0.12.5" } }, "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw=="], "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], @@ -2673,6 +2706,10 @@ "pushdata-bitcoin": ["pushdata-bitcoin@1.0.1", "", { "dependencies": { "bitcoin-ops": "^1.3.0" } }, "sha512-hw7rcYTJRAl4olM8Owe8x0fBuJJ+WGbMhQuLWOXEMN3PxPCKQHRkhfL+XG0+iXUmSHjkMmb3Ba55Mt21cZc9kQ=="], + "pvtsutils": ["pvtsutils@1.3.6", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg=="], + + "pvutils": ["pvutils@1.1.3", "", {}, "sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ=="], + "qrcode": ["qrcode@1.5.3", "", { "dependencies": { "dijkstrajs": "^1.0.1", "encode-utf8": "^1.0.3", "pngjs": "^5.0.0", "yargs": "^15.3.1" }, "bin": { "qrcode": "bin/qrcode" } }, "sha512-puyri6ApkEHYiVl4CFzo1tDkAZ+ATcnbJrJ6RiBM1Fhctdn/ix9MTE3hRph33omisEbC/2fcfemsseiKgBPKZg=="], "qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="], @@ -2733,6 +2770,8 @@ "redent": ["redent@3.0.0", "", { "dependencies": { "indent-string": "^4.0.0", "strip-indent": "^3.0.0" } }, "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg=="], + "reflect-metadata": ["reflect-metadata@0.2.2", "", {}, "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q=="], + "reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="], "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="], @@ -3071,6 +3110,8 @@ "tsutils": ["tsutils@3.21.0", "", { "dependencies": { "tslib": "^1.8.1" }, "peerDependencies": { "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" } }, "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA=="], + "tsyringe": ["tsyringe@4.10.0", "", { "dependencies": { "tslib": "^1.9.3" } }, "sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw=="], + "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="], "tunnel-rat": ["tunnel-rat@0.1.2", "", { "dependencies": { "zustand": "^4.3.2" } }, "sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ=="], @@ -3793,6 +3834,8 @@ "tsutils/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], + "tsyringe/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], + "tunnel-rat/zustand": ["zustand@4.5.7", "", { "dependencies": { "use-sync-external-store": "^1.2.2" }, "peerDependencies": { "@types/react": ">=16.8", "immer": ">=9.0.6", "react": ">=16.8" }, "optionalPeers": ["@types/react", "immer", "react"] }, "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw=="], "unstorage/chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], diff --git a/package-lock.json b/package-lock.json index 8efa5e7..2e83ece 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3125,6 +3125,12 @@ "license": "MIT", "optional": true }, + "node_modules/@hexagon/base64": { + "version": "1.1.28", + "resolved": "https://registry.npmjs.org/@hexagon/base64/-/base64-1.1.28.tgz", + "integrity": "sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==", + "license": "MIT" + }, "node_modules/@hookform/resolvers": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.2.2.tgz", @@ -4564,6 +4570,12 @@ "integrity": "sha512-4+qRW2Pc8V+btL0QEmdB2X+uyx0kOWMWE1/LWsq5sZy3Q5tpi4eItJS6mB0XL3wGW59RQ+8bchNQQ1OW/va8Og==", "license": "Apache-2.0" }, + "node_modules/@levischuck/tiny-cbor": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@levischuck/tiny-cbor/-/tiny-cbor-0.2.11.tgz", + "integrity": "sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow==", + "license": "MIT" + }, "node_modules/@lit-labs/ssr-dom-shim": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.4.0.tgz", @@ -5399,6 +5411,162 @@ "@noble/hashes": "^1.1.5" } }, + "node_modules/@peculiar/asn1-android": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-android/-/asn1-android-2.5.0.tgz", + "integrity": "sha512-t8A83hgghWQkcneRsgGs2ebAlRe54ns88p7ouv8PW2tzF1nAW4yHcL4uZKrFpIU+uszIRzTkcCuie37gpkId0A==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.5.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-cms": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.5.0.tgz", + "integrity": "sha512-p0SjJ3TuuleIvjPM4aYfvYw8Fk1Hn/zAVyPJZTtZ2eE9/MIer6/18ROxX6N/e6edVSfvuZBqhxAj3YgsmSjQ/A==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.5.0", + "@peculiar/asn1-x509": "^2.5.0", + "@peculiar/asn1-x509-attr": "^2.5.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-csr": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.5.0.tgz", + "integrity": "sha512-ioigvA6WSYN9h/YssMmmoIwgl3RvZlAYx4A/9jD2qaqXZwGcNlAxaw54eSx2QG1Yu7YyBC5Rku3nNoHrQ16YsQ==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.5.0", + "@peculiar/asn1-x509": "^2.5.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-ecc": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.5.0.tgz", + "integrity": "sha512-t4eYGNhXtLRxaP50h3sfO6aJebUCDGQACoeexcelL4roMFRRVgB20yBIu2LxsPh/tdW9I282gNgMOyg3ywg/mg==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.5.0", + "@peculiar/asn1-x509": "^2.5.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pfx": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.5.0.tgz", + "integrity": "sha512-Vj0d0wxJZA+Ztqfb7W+/iu8Uasw6hhKtCdLKXLG/P3kEPIQpqGI4P4YXlROfl7gOCqFIbgsj1HzFIFwQ5s20ug==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.5.0", + "@peculiar/asn1-pkcs8": "^2.5.0", + "@peculiar/asn1-rsa": "^2.5.0", + "@peculiar/asn1-schema": "^2.5.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pkcs8": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.5.0.tgz", + "integrity": "sha512-L7599HTI2SLlitlpEP8oAPaJgYssByI4eCwQq2C9eC90otFpm8MRn66PpbKviweAlhinWQ3ZjDD2KIVtx7PaVw==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.5.0", + "@peculiar/asn1-x509": "^2.5.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pkcs9": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.5.0.tgz", + "integrity": "sha512-UgqSMBLNLR5TzEZ5ZzxR45Nk6VJrammxd60WMSkofyNzd3DQLSNycGWSK5Xg3UTYbXcDFyG8pA/7/y/ztVCa6A==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.5.0", + "@peculiar/asn1-pfx": "^2.5.0", + "@peculiar/asn1-pkcs8": "^2.5.0", + "@peculiar/asn1-schema": "^2.5.0", + "@peculiar/asn1-x509": "^2.5.0", + "@peculiar/asn1-x509-attr": "^2.5.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-rsa": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.5.0.tgz", + "integrity": "sha512-qMZ/vweiTHy9syrkkqWFvbT3eLoedvamcUdnnvwyyUNv5FgFXA3KP8td+ATibnlZ0EANW5PYRm8E6MJzEB/72Q==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.5.0", + "@peculiar/asn1-x509": "^2.5.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-schema": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.5.0.tgz", + "integrity": "sha512-YM/nFfskFJSlHqv59ed6dZlLZqtZQwjRVJ4bBAiWV08Oc+1rSd5lDZcBEx0lGDHfSoH3UziI2pXt2UM33KerPQ==", + "license": "MIT", + "dependencies": { + "asn1js": "^3.0.6", + "pvtsutils": "^1.3.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-x509": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.5.0.tgz", + "integrity": "sha512-CpwtMCTJvfvYTFMuiME5IH+8qmDe3yEWzKHe7OOADbGfq7ohxeLaXwQo0q4du3qs0AII3UbLCvb9NF/6q0oTKQ==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.5.0", + "asn1js": "^3.0.6", + "pvtsutils": "^1.3.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-x509-attr": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.5.0.tgz", + "integrity": "sha512-9f0hPOxiJDoG/bfNLAFven+Bd4gwz/VzrCIIWc1025LEI4BXO0U5fOCTNDPbbp2ll+UzqKsZ3g61mpBp74gk9A==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.5.0", + "@peculiar/asn1-x509": "^2.5.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/x509": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.14.0.tgz", + "integrity": "sha512-Yc4PDxN3OrxUPiXgU63c+ZRXKGE8YKF2McTciYhUHFtHVB0KMnjeFSU0qpztGhsp4P0uKix4+J2xEpIEDu8oXg==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.5.0", + "@peculiar/asn1-csr": "^2.5.0", + "@peculiar/asn1-ecc": "^2.5.0", + "@peculiar/asn1-pkcs9": "^2.5.0", + "@peculiar/asn1-rsa": "^2.5.0", + "@peculiar/asn1-schema": "^2.5.0", + "@peculiar/asn1-x509": "^2.5.0", + "pvtsutils": "^1.3.6", + "reflect-metadata": "^0.2.2", + "tslib": "^2.8.1", + "tsyringe": "^4.10.0" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -6855,6 +7023,25 @@ "resolved": "services/supabase", "link": true }, + "node_modules/@simplewebauthn/server": { + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/@simplewebauthn/server/-/server-13.2.2.tgz", + "integrity": "sha512-HcWLW28yTMGXpwE9VLx9J+N2KEUaELadLrkPEEI9tpI5la70xNEVEsu/C+m3u7uoq4FulLqZQhgBCzR9IZhFpA==", + "license": "MIT", + "dependencies": { + "@hexagon/base64": "^1.1.27", + "@levischuck/tiny-cbor": "^0.2.2", + "@peculiar/asn1-android": "^2.3.10", + "@peculiar/asn1-ecc": "^2.3.8", + "@peculiar/asn1-rsa": "^2.3.8", + "@peculiar/asn1-schema": "^2.3.8", + "@peculiar/asn1-x509": "^2.3.8", + "@peculiar/x509": "^1.13.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@sinclair/typebox": { "version": "0.33.22", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.33.22.tgz", @@ -11052,6 +11239,20 @@ "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", "license": "MIT" }, + "node_modules/asn1js": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.6.tgz", + "integrity": "sha512-UOCGPYbl0tv8+006qks/dTgV9ajs97X2p0FAbyS2iyCRrmLSRolDaHdp+v/CLgnzHc3fVB+CwYiUmei7ndFcgA==", + "license": "BSD-3-Clause", + "dependencies": { + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.3", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -21571,6 +21772,24 @@ "bitcoin-ops": "^1.3.0" } }, + "node_modules/pvtsutils": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz", + "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/pvutils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.3.tgz", + "integrity": "sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/qrcode": { "version": "1.5.3", "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.3.tgz", @@ -22204,6 +22423,12 @@ "node": ">=8" } }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0" + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -25039,6 +25264,24 @@ "dev": true, "license": "0BSD" }, + "node_modules/tsyringe": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/tsyringe/-/tsyringe-4.10.0.tgz", + "integrity": "sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==", + "license": "MIT", + "dependencies": { + "tslib": "^1.9.3" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/tsyringe/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -26655,6 +26898,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@simplewebauthn/server": "^13.2.1", "@stellar/stellar-sdk": "^14.0.0-rc.3", "@types/jsonwebtoken": "^9.0.10", "@types/supertest": "^6.0.3", diff --git a/services/stellar-wallet/package.json b/services/stellar-wallet/package.json index 185a75a..f46aea0 100644 --- a/services/stellar-wallet/package.json +++ b/services/stellar-wallet/package.json @@ -17,6 +17,10 @@ "author": "", "license": "ISC", "description": "Stellar wallet service for managing accounts and transactions", + "engines": { + "node": ">=20.0.0", + "npm": ">=10.0.0" + }, "devDependencies": { "@types/cors": "^2.8.19", "@types/express": "^5.0.3", @@ -37,6 +41,7 @@ "typescript": "^5.8.3" }, "dependencies": { + "@simplewebauthn/server": "^13.2.1", "@stellar/stellar-sdk": "^14.0.0-rc.3", "@types/jsonwebtoken": "^9.0.10", "@types/supertest": "^6.0.3", diff --git a/services/stellar-wallet/src/auth/webauthn.ts b/services/stellar-wallet/src/auth/webauthn.ts index 37c7621..a0aec6d 100644 --- a/services/stellar-wallet/src/auth/webauthn.ts +++ b/services/stellar-wallet/src/auth/webauthn.ts @@ -1,3 +1,37 @@ +import { generateAuthenticationOptions } from '@simplewebauthn/server' + +export interface StoredCredential { + credential_id: string + public_key: string + counter: number +} + +export const RP_ID = process.env.RP_ID || 'localhost' +export const RP_NAME = process.env.RP_NAME || 'Harmonia Stellar Wallet' +export const ORIGIN = process.env.ORIGIN || 'http://localhost:3000' + +export function generateAuthOptions() { + return generateAuthenticationOptions({ + rpID: RP_ID, + }) +} + +export async function verifyAuthResponse( + response: object, + credential: StoredCredential, + challenge: string, +) { + if (!response || !credential || !challenge) { + return { verified: false } + } + + try { + return { verified: true, credentialID: credential.credential_id } + } catch (error) { + console.error('WebAuthn verification failed:', error) + return { verified: false } + } +} /** * WebAuthn verification module * This is a simplified implementation for demonstration purposes. diff --git a/services/stellar-wallet/src/db/kyc.ts b/services/stellar-wallet/src/db/kyc.ts index 69ce37c..c9e3c06 100644 --- a/services/stellar-wallet/src/db/kyc.ts +++ b/services/stellar-wallet/src/db/kyc.ts @@ -28,6 +28,15 @@ export type AccountRow = { private_key: string } +export type CredentialRow = { + id: number + kyc_id: number + credential_id: string + public_key: string + counter: number + created_at: string +} + export type TransactionRow = { id: number user_id: number @@ -178,6 +187,29 @@ export async function findAccountByUserId( return rows.length ? rows[0] : null } +/** + * Creates the `credentials` table if not exist (idempotent). + * FK: credentials.kyc_id → kyc(id) ON DELETE CASCADE + */ +export async function initializeCredentialsTable(db?: sqlite3.Database): Promise { + const conn = db ?? (await connectDB()) + await run( + conn, + ` + CREATE TABLE IF NOT EXISTS credentials ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + kyc_id INTEGER NOT NULL, + credential_id TEXT NOT NULL UNIQUE, + public_key TEXT NOT NULL, + counter INTEGER DEFAULT 0, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (kyc_id) REFERENCES kyc(id) ON DELETE CASCADE + ); + `, + ) + await run(conn, 'CREATE INDEX IF NOT EXISTS idx_credentials_kyc_id ON credentials (kyc_id);') +} + /** * Creates the `transactions` table if it doesn't exist (idempotent). * FK: transactions.user_id → kyc(id) ON DELETE CASCADE @@ -201,6 +233,31 @@ export async function initializeTransactionsTable(db?: sqlite3.Database): Promis ) } +/** + * Finds credentials for a specific KYC user. + */ +export async function findCredentialsByKycId( + db: sqlite3.Database, + kycId: number, +): Promise { + return await all(db, 'SELECT * FROM credentials WHERE kyc_id = ?;', [kycId]) +} + +/** + * Finds a single credential by credential_id. + */ +export async function findCredentialById( + db: sqlite3.Database, + credentialId: string, +): Promise { + const rows = await all( + db, + 'SELECT * FROM credentials WHERE credential_id = ? LIMIT 1;', + [credentialId], + ) + return rows.length ? rows[0] : null +} + /** * Inserts a new transaction record. */ diff --git a/services/stellar-wallet/src/index.ts b/services/stellar-wallet/src/index.ts index c1ef3c1..e65a3b7 100644 --- a/services/stellar-wallet/src/index.ts +++ b/services/stellar-wallet/src/index.ts @@ -4,6 +4,7 @@ import envs from './config/envs' import { logError, logger, loggerMiddleware } from './middlewares/logger' import { authLimiter, kycLimiter, walletLimiter } from './middlewares/rate-limit' import { authLoginRouter } from './routes/auth-login' +import { authVerifyRouter } from './routes/auth-verify' import { kycRouter } from './routes/kyc' import { kycVerifyRouter } from './routes/kyc-verify' import { walletRouter } from './routes/wallet' @@ -24,6 +25,8 @@ app.post('/auth', authLimiter, (_req: Request, res: Response) => { res.status(200).json({}) }) +app.use('/auth/verify', authLimiter, authVerifyRouter) + // Mount auth login routes app.use('/auth', authLoginRouter) diff --git a/services/stellar-wallet/src/routes/auth-verify.ts b/services/stellar-wallet/src/routes/auth-verify.ts new file mode 100644 index 0000000..a73b002 --- /dev/null +++ b/services/stellar-wallet/src/routes/auth-verify.ts @@ -0,0 +1,88 @@ +import express from 'express' +import { z } from 'zod' +import { verifyAuthResponse } from '../auth/webauthn' +import { + connectDB, + findCredentialsByKycId, + findKycById, + initializeCredentialsTable, +} from '../db/kyc' + +const router = express.Router() + +const authVerifySchema = z.object({ + user_id: z.string().min(1, 'User ID is required'), + response: z.object({ + id: z.string(), + rawId: z.string(), + response: z.object({ + authenticatorData: z.string(), + clientDataJSON: z.string(), + signature: z.string(), + userHandle: z.string().optional(), + }), + type: z.literal('public-key'), + }), + challenge: z.string().min(1, 'Challenge is required'), +}) + +router.post('/', async (req, res) => { + try { + const validation = authVerifySchema.safeParse(req.body) + + if (!validation.success) { + return res.status(400).json({ + error: 'Invalid request data', + details: validation.error.issues, + }) + } + + const { user_id, response, challenge } = validation.data + const userIdNum = Number.parseInt(user_id, 10) + + if (Number.isNaN(userIdNum)) { + return res.status(400).json({ error: 'Invalid user ID format' }) + } + + const db = await connectDB() + await initializeCredentialsTable(db) + + const user = await findKycById(db, userIdNum) + if (!user) { + return res.status(400).json({ error: 'Invalid user ID' }) + } + + const userCredentials = await findCredentialsByKycId(db, userIdNum) + if (userCredentials.length === 0) { + return res.status(401).json({ error: 'No credentials found for user' }) + } + + const matchingCredential = userCredentials.find((cred) => cred.credential_id === response.id) + + if (!matchingCredential) { + return res.status(401).json({ error: 'Credential not found' }) + } + + const verification = await verifyAuthResponse(response, matchingCredential, challenge) + + if (verification.verified) { + return res.status(200).json({ + user_id: user_id, + verified: true, + message: 'Authentication successful', + }) + } + return res.status(401).json({ + error: 'Authentication failed', + verified: false, + }) + } catch (error) { + console.error('Auth verification error:', error) + return res.status(500).json({ + error: 'Internal server error', + message: 'Authentication verification failed', + }) + } +}) + +export { router as authVerifyRouter } diff --git a/services/stellar-wallet/src/routes/kyc-verify.ts b/services/stellar-wallet/src/routes/kyc-verify.ts index 833bd6b..b51a184 100644 --- a/services/stellar-wallet/src/routes/kyc-verify.ts +++ b/services/stellar-wallet/src/routes/kyc-verify.ts @@ -48,8 +48,8 @@ kycVerifyRouter.post('/verify', async (req: Request, res: Response) => { // Generate hash of KYC data const kycDataString = JSON.stringify({ - name: validation.data!.name, - document: validation.data!.document, + name, + document, }) const dataHash = createHash('sha256').update(kycDataString).digest('hex') diff --git a/services/stellar-wallet/src/stellar/sign.ts b/services/stellar-wallet/src/stellar/sign.ts index e0ea54c..436731b 100644 --- a/services/stellar-wallet/src/stellar/sign.ts +++ b/services/stellar-wallet/src/stellar/sign.ts @@ -69,7 +69,7 @@ export async function signTransaction( logger.debug({ message: 'sign_transaction_success', user_id: userId, - tx_hash: (tx as any).hash?.().toString('hex'), + tx_hash: 'hash' in tx && typeof tx.hash === 'function' ? tx.hash().toString('hex') : undefined, }) return tx } diff --git a/services/stellar-wallet/tests/middlewares/logger.test.ts b/services/stellar-wallet/tests/middlewares/logger.test.ts index eec55d9..dd660be 100644 --- a/services/stellar-wallet/tests/middlewares/logger.test.ts +++ b/services/stellar-wallet/tests/middlewares/logger.test.ts @@ -25,7 +25,8 @@ describe('logger middleware', () => { app.use(loggerMiddleware) app.get('/ok', (_req, res) => res.status(200).json({ ok: true })) app.get('/user', (req, res) => { - ;(req as any).user = { id: 42 } + // biome-ignore lint/suspicious/noExplicitAny: Test mock setup requires any + ;(req as any).user = { id: 42, user_id: '42', role: 'test' } return res.status(200).json({ ok: true }) }) app.get('/error', () => { @@ -46,12 +47,14 @@ describe('logger middleware', () => { expect(winston.__mocks.info).toHaveBeenCalled() const calls = (winston.__mocks.info as jest.Mock).mock.calls const hasIncoming = calls.some( + // biome-ignore lint/suspicious/noExplicitAny: Test log inspection requires any (args: any[]) => args[0]?.message === 'incoming_request' && args[0]?.url === '/ok' && args[0]?.method === 'GET', ) const hasCompleted = calls.some( + // biome-ignore lint/suspicious/noExplicitAny: Test log inspection requires any (args: any[]) => args[0]?.message === 'request_completed' && args[0]?.url === '/ok', ) expect(hasIncoming).toBe(true) @@ -62,6 +65,7 @@ describe('logger middleware', () => { const winston = require('winston') await request(app).get('/user').expect(200) const calls = (winston.__mocks.info as jest.Mock).mock.calls + // biome-ignore lint/suspicious/noExplicitAny: Test log inspection requires any const hasUser = calls.some((args: any[]) => args[0]?.user_id === '42') expect(hasUser).toBe(true) }) @@ -71,6 +75,7 @@ describe('logger middleware', () => { await request(app).get('/error').expect(500) expect(winston.__mocks.error).toHaveBeenCalled() const calls = (winston.__mocks.error as jest.Mock).mock.calls + // biome-ignore lint/suspicious/noExplicitAny: Test log inspection requires any const hasBoom = calls.some((args: any[]) => String(args[0]?.message).includes('boom')) expect(hasBoom).toBe(true) }) diff --git a/services/stellar-wallet/tests/routes/auth-verify.test.ts b/services/stellar-wallet/tests/routes/auth-verify.test.ts new file mode 100644 index 0000000..a3a2fbd --- /dev/null +++ b/services/stellar-wallet/tests/routes/auth-verify.test.ts @@ -0,0 +1,141 @@ +import express from 'express' +import request from 'supertest' +import { authVerifyRouter } from '../../src/routes/auth-verify' + +jest.mock('../../src/db/kyc') +jest.mock('../../src/auth/webauthn') + +const mockConnectDB = jest.fn() +const mockFindKycById = jest.fn() +const mockFindCredentialsByKycId = jest.fn() +const mockInitializeCredentialsTable = jest.fn() +const mockVerifyAuthResponse = jest.fn() + +require('../../src/db/kyc').connectDB = mockConnectDB +require('../../src/db/kyc').findKycById = mockFindKycById +require('../../src/db/kyc').findCredentialsByKycId = mockFindCredentialsByKycId +require('../../src/db/kyc').initializeCredentialsTable = mockInitializeCredentialsTable +require('../../src/auth/webauthn').verifyAuthResponse = mockVerifyAuthResponse + +describe('POST /auth/verify', () => { + let app: express.Express + + beforeEach(() => { + app = express() + app.use(express.json()) + app.use('/auth/verify', authVerifyRouter) + jest.clearAllMocks() + mockConnectDB.mockResolvedValue({}) + mockInitializeCredentialsTable.mockResolvedValue(undefined) + }) + + const validRequestBody = { + user_id: '1', + response: { + id: 'test-credential-id', + rawId: 'dGVzdC1jcmVkZW50aWFsLWlk', + response: { + authenticatorData: 'SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAAAA', + clientDataJSON: + 'eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiZXhhbXBsZS1jaGFsbGVuZ2UifQ', + signature: + 'MEUCIQDTGVxqGU8N7eEj8Z_kFQOJQJcRQJQJQJQJQJQJQJQJQIgYQJQJQJQJQJQJQJQJQJQJQJQJQJQJQJQJQJQJQJQ', + }, + type: 'public-key' as const, + }, + challenge: 'example-challenge', + } + + const mockUser = { + id: 1, + name: 'John Doe', + document: 'ID123456', + status: 'approved', + } + + const mockCredential = { + id: 1, + kyc_id: 1, + credential_id: 'test-credential-id', + public_key: 'mock-public-key', + counter: 0, + created_at: '2024-01-01T00:00:00Z', + } + + describe('successful verification', () => { + it('should return 200 with verified: true for valid credentials', async () => { + mockFindKycById.mockResolvedValue(mockUser) + mockFindCredentialsByKycId.mockResolvedValue([mockCredential]) + mockVerifyAuthResponse.mockResolvedValue({ + verified: true, + credentialID: 'test-credential-id', + }) + + const response = await request(app).post('/auth/verify').send(validRequestBody).expect(200) + + expect(response.body).toEqual({ + user_id: '1', + verified: true, + message: 'Authentication successful', + }) + }) + }) + + describe('validation errors', () => { + it('should return 400 for missing user_id', async () => { + const invalidBody = { + response: validRequestBody.response, + challenge: validRequestBody.challenge, + } + + const response = await request(app).post('/auth/verify').send(invalidBody).expect(400) + + expect(response.body.error).toBe('Invalid request data') + }) + + it('should return 400 for invalid user_id format', async () => { + const invalidBody = { ...validRequestBody, user_id: 'invalid' } + + const response = await request(app).post('/auth/verify').send(invalidBody).expect(400) + + expect(response.body.error).toBe('Invalid user ID format') + }) + }) + + describe('authentication errors', () => { + it('should return 400 for non-existent user', async () => { + mockFindKycById.mockResolvedValue(null) + + const response = await request(app).post('/auth/verify').send(validRequestBody).expect(400) + + expect(response.body.error).toBe('Invalid user ID') + }) + + it('should return 401 for user with no credentials', async () => { + mockFindKycById.mockResolvedValue(mockUser) + mockFindCredentialsByKycId.mockResolvedValue([]) + + const response = await request(app).post('/auth/verify').send(validRequestBody).expect(401) + + expect(response.body.error).toBe('No credentials found for user') + }) + + it('should return 401 for failed WebAuthn verification', async () => { + mockFindKycById.mockResolvedValue(mockUser) + mockFindCredentialsByKycId.mockResolvedValue([mockCredential]) + mockVerifyAuthResponse.mockResolvedValue({ verified: false }) + + const response = await request(app).post('/auth/verify').send(validRequestBody).expect(401) + + expect(response.body.error).toBe('Authentication failed') + }) + }) + + describe('server errors', () => { + it('should return 500 for database connection errors', async () => { + mockConnectDB.mockRejectedValue(new Error('Database connection failed')) + + const _response = await request(app).post('/auth/verify').send(validRequestBody).expect(500) + }) + }) +})