diff --git a/gateway/package-lock.json b/gateway/package-lock.json index fab326c..0d0b5e6 100644 --- a/gateway/package-lock.json +++ b/gateway/package-lock.json @@ -27,12 +27,16 @@ "@types/node": "^22.0.0", "@types/nodemailer": "^7.0.9", "@types/qrcode": "^1.5.6", + "@types/supertest": "^6.0.3", "@types/uuid": "^9.0.7", + "@vitest/coverage-v8": "^4.0.18", "eslint": "^9.0.0", "globals": "^15.0.0", + "supertest": "^7.2.2", "tsx": "^4.19.0", "typescript": "^5.7.0", - "typescript-eslint": "^8.0.0" + "typescript-eslint": "^8.0.0", + "vitest": "^4.0.18" }, "engines": { "node": ">=18.0.0" @@ -289,6 +293,66 @@ "zod": "^3.23.8" } }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/linux-x64": { "version": "0.27.1", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.1.tgz", @@ -521,12 +585,63 @@ "url": "https://github.com/sponsors/nzakas" } }, + "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/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "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/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", + "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, "node_modules/@peculiar/asn1-android": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/@peculiar/asn1-android/-/asn1-android-2.6.0.tgz", @@ -585,6 +700,356 @@ "tslib": "^2.8.1" } }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", + "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", + "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", + "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", + "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", + "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", + "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", + "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", + "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", + "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", + "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", + "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", + "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", + "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", + "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", + "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", + "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", + "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", + "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", + "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", + "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", + "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", + "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", + "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", + "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@simplewebauthn/server": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/@simplewebauthn/server/-/server-10.0.1.tgz", @@ -612,6 +1077,13 @@ "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", "license": "MIT" }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/better-sqlite3": { "version": "7.6.13", "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", @@ -633,6 +1105,17 @@ "@types/node": "*" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, "node_modules/@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", @@ -643,6 +1126,13 @@ "@types/node": "*" } }, + "node_modules/@types/cookiejar": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", + "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/cors": { "version": "2.8.19", "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", @@ -653,6 +1143,13 @@ "@types/node": "*" } }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -699,6 +1196,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/methods": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", + "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "22.19.3", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.3.tgz", @@ -764,6 +1268,30 @@ "@types/node": "*" } }, + "node_modules/@types/superagent": { + "version": "8.1.9", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.9.tgz", + "integrity": "sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cookiejar": "^2.1.5", + "@types/methods": "^1.1.4", + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/supertest": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-6.0.3.tgz", + "integrity": "sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/methods": "^1.1.4", + "@types/superagent": "^8.1.0" + } + }, "node_modules/@types/uuid": { "version": "9.0.8", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", @@ -1027,6 +1555,148 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@vitest/coverage-v8": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.18.tgz", + "integrity": "sha512-7i+N2i0+ME+2JFZhfuz7Tg/FqKtilHjGyGvoHYQ6iLV0zahbsJ9sljC9OcFcPDbhYKCet+sG8SsVqlyGvPflZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.0.18", + "ast-v8-to-istanbul": "^0.3.10", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.1", + "obug": "^2.1.1", + "std-env": "^3.10.0", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "4.0.18", + "vitest": "4.0.18" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", + "integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "chai": "^6.2.1", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz", + "integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.0.18", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz", + "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz", + "integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.0.18", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz", + "integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz", + "integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz", + "integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", @@ -1111,6 +1781,13 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT" + }, "node_modules/asn1js": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.7.tgz", @@ -1125,6 +1802,35 @@ "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", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-v8-to-istanbul": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.11.tgz", + "integrity": "sha512-Qya9fkoofMjCBNVdWINMjB5KZvkYfaO9/anwkWnjxibpWUxo5iHl2sOdP7/uAqaRuUYuoo8rDwnbaaKVFxoUvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1299,6 +2005,16 @@ "node": ">=6" } }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -1351,6 +2067,29 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1398,6 +2137,13 @@ "node": ">=6.6.0" } }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true, + "license": "MIT" + }, "node_modules/core-js": { "version": "3.47.0", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.47.0.tgz", @@ -1503,6 +2249,16 @@ "dev": true, "license": "MIT" }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -1521,6 +2277,17 @@ "node": ">=8" } }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, "node_modules/dijkstrajs": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", @@ -1595,6 +2362,13 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -1607,6 +2381,22 @@ "node": ">= 0.4" } }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/esbuild": { "version": "0.27.1", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.1.tgz", @@ -1812,6 +2602,16 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -1840,6 +2640,16 @@ "node": ">=6" } }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/express": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", @@ -1904,6 +2714,13 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true, + "license": "MIT" + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -2000,6 +2817,64 @@ "dev": true, "license": "ISC" }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data/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/form-data/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" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/formidable": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", + "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", + "dezalgo": "^1.0.4", + "once": "^1.4.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -2024,6 +2899,21 @@ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", "license": "MIT" }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -2158,6 +3048,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -2179,6 +3085,13 @@ "node": ">=18.0.0" } }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, "node_modules/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", @@ -2344,6 +3257,45 @@ "integrity": "sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA==", "license": "MIT" }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/jose": { "version": "5.10.0", "resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz", @@ -2353,6 +3305,13 @@ "url": "https://github.com/sponsors/panva" } }, + "node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/js-yaml": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", @@ -2449,6 +3408,44 @@ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "license": "ISC" }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magicast": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.2.tgz", + "integrity": "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "source-map-js": "^1.2.1" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -2479,6 +3476,29 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/mime-db": { "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", @@ -2556,6 +3576,25 @@ "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==", "license": "(Apache-2.0 AND MIT)" }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/napi-build-utils": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", @@ -2640,6 +3679,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -2783,6 +3833,20 @@ "url": "https://opencollective.com/express" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "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, + "license": "ISC" + }, "node_modules/picomatch": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", @@ -2805,6 +3869,35 @@ "node": ">=10.13.0" } }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/prebuild-install": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", @@ -2920,9 +4013,9 @@ } }, "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -3031,6 +4124,51 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, + "node_modules/rollup": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", + "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.57.1", + "@rollup/rollup-android-arm64": "4.57.1", + "@rollup/rollup-darwin-arm64": "4.57.1", + "@rollup/rollup-darwin-x64": "4.57.1", + "@rollup/rollup-freebsd-arm64": "4.57.1", + "@rollup/rollup-freebsd-x64": "4.57.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", + "@rollup/rollup-linux-arm64-gnu": "4.57.1", + "@rollup/rollup-linux-arm64-musl": "4.57.1", + "@rollup/rollup-linux-loong64-gnu": "4.57.1", + "@rollup/rollup-linux-loong64-musl": "4.57.1", + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", + "@rollup/rollup-linux-ppc64-musl": "4.57.1", + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", + "@rollup/rollup-linux-riscv64-musl": "4.57.1", + "@rollup/rollup-linux-s390x-gnu": "4.57.1", + "@rollup/rollup-linux-x64-gnu": "4.57.1", + "@rollup/rollup-linux-x64-musl": "4.57.1", + "@rollup/rollup-openbsd-x64": "4.57.1", + "@rollup/rollup-openharmony-arm64": "4.57.1", + "@rollup/rollup-win32-arm64-msvc": "4.57.1", + "@rollup/rollup-win32-ia32-msvc": "4.57.1", + "@rollup/rollup-win32-x64-gnu": "4.57.1", + "@rollup/rollup-win32-x64-msvc": "4.57.1", + "fsevents": "~2.3.2" + } + }, "node_modules/router": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", @@ -3229,6 +4367,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/simple-concat": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", @@ -3274,6 +4419,23 @@ "simple-concat": "^1.0.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/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -3283,6 +4445,13 @@ "node": ">= 0.8" } }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -3331,6 +4500,42 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/superagent": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.3.0.tgz", + "integrity": "sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "component-emitter": "^1.3.1", + "cookiejar": "^2.1.4", + "debug": "^4.3.7", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.5", + "formidable": "^3.5.4", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.14.1" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/supertest": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.2.2.tgz", + "integrity": "sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cookie-signature": "^1.2.2", + "methods": "^1.1.2", + "superagent": "^10.3.0" + }, + "engines": { + "node": ">=14.18.0" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -3372,6 +4577,23 @@ "node": ">=6" } }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -3389,6 +4611,16 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinyrainbow": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -3598,6 +4830,159 @@ "node": ">= 0.8" } }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz", + "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.0.18", + "@vitest/mocker": "4.0.18", + "@vitest/pretty-format": "4.0.18", + "@vitest/runner": "4.0.18", + "@vitest/snapshot": "4.0.18", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "es-module-lexer": "^1.7.0", + "expect-type": "^1.2.2", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^3.10.0", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.0.18", + "@vitest/browser-preview": "4.0.18", + "@vitest/browser-webdriverio": "4.0.18", + "@vitest/ui": "4.0.18", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -3636,6 +5021,23 @@ "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", "license": "ISC" }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", diff --git a/gateway/package.json b/gateway/package.json index f62a8ec..dca83e4 100644 --- a/gateway/package.json +++ b/gateway/package.json @@ -10,7 +10,12 @@ "start": "node dist/index.js", "dev": "tsx watch src/index.ts", "lint": "eslint src/", - "typecheck": "tsc --noEmit" + "typecheck": "tsc --noEmit", + "test": "vitest", + "test:run": "vitest run", + "test:coverage": "vitest run --coverage", + "test:unit": "vitest run src/", + "test:e2e": "vitest run tests/" }, "keywords": [ "atproto", @@ -44,12 +49,16 @@ "@types/node": "^22.0.0", "@types/nodemailer": "^7.0.9", "@types/qrcode": "^1.5.6", + "@types/supertest": "^6.0.3", "@types/uuid": "^9.0.7", + "@vitest/coverage-v8": "^4.0.18", "eslint": "^9.0.0", "globals": "^15.0.0", + "supertest": "^7.2.2", "tsx": "^4.19.0", "typescript": "^5.7.0", - "typescript-eslint": "^8.0.0" + "typescript-eslint": "^8.0.0", + "vitest": "^4.0.18" }, "engines": { "node": ">=18.0.0" diff --git a/gateway/src/services/oidc/claims.test.ts b/gateway/src/services/oidc/claims.test.ts new file mode 100644 index 0000000..c861976 --- /dev/null +++ b/gateway/src/services/oidc/claims.test.ts @@ -0,0 +1,204 @@ +/** + * OIDC Claims Tests + */ +import { describe, it, expect } from 'vitest'; +import { + SUPPORTED_SCOPES, + isSupportedScope, + parseScopes, + filterSupportedScopes, + hasOpenIdScope, + hasOfflineAccessScope, + buildUserInfo, + validateScopes, + getDefaultScopes, +} from './claims.js'; + +describe('OIDC Claims', () => { + describe('SUPPORTED_SCOPES', () => { + it('should include required OIDC scopes', () => { + expect(SUPPORTED_SCOPES).toContain('openid'); + expect(SUPPORTED_SCOPES).toContain('profile'); + expect(SUPPORTED_SCOPES).toContain('email'); + expect(SUPPORTED_SCOPES).toContain('offline_access'); + }); + }); + + describe('isSupportedScope', () => { + it('should accept supported scopes', () => { + expect(isSupportedScope('openid')).toBe(true); + expect(isSupportedScope('profile')).toBe(true); + expect(isSupportedScope('email')).toBe(true); + expect(isSupportedScope('offline_access')).toBe(true); + }); + + it('should reject unsupported scopes', () => { + expect(isSupportedScope('address')).toBe(false); + expect(isSupportedScope('phone')).toBe(false); + expect(isSupportedScope('custom')).toBe(false); + expect(isSupportedScope('')).toBe(false); + }); + }); + + describe('parseScopes', () => { + it('should parse space-separated scopes', () => { + expect(parseScopes('openid profile')).toEqual(['openid', 'profile']); + expect(parseScopes('openid profile email')).toEqual(['openid', 'profile', 'email']); + }); + + it('should handle single scope', () => { + expect(parseScopes('openid')).toEqual(['openid']); + }); + + it('should handle empty string', () => { + expect(parseScopes('')).toEqual([]); + }); + + it('should filter out empty strings from multiple spaces', () => { + expect(parseScopes('openid profile')).toEqual(['openid', 'profile']); + }); + }); + + describe('filterSupportedScopes', () => { + it('should filter to only supported scopes', () => { + const scopes = ['openid', 'profile', 'custom', 'email', 'unknown']; + expect(filterSupportedScopes(scopes)).toEqual(['openid', 'profile', 'email']); + }); + + it('should return empty array for all unsupported scopes', () => { + expect(filterSupportedScopes(['custom', 'unknown'])).toEqual([]); + }); + + it('should preserve all supported scopes', () => { + const scopes = ['openid', 'profile', 'email', 'offline_access']; + expect(filterSupportedScopes(scopes)).toEqual(scopes); + }); + }); + + describe('hasOpenIdScope', () => { + it('should return true when openid is present', () => { + expect(hasOpenIdScope(['openid'])).toBe(true); + expect(hasOpenIdScope(['openid', 'profile'])).toBe(true); + expect(hasOpenIdScope(['profile', 'openid', 'email'])).toBe(true); + }); + + it('should return false when openid is missing', () => { + expect(hasOpenIdScope([])).toBe(false); + expect(hasOpenIdScope(['profile'])).toBe(false); + expect(hasOpenIdScope(['profile', 'email'])).toBe(false); + }); + }); + + describe('hasOfflineAccessScope', () => { + it('should return true when offline_access is present', () => { + expect(hasOfflineAccessScope(['offline_access'])).toBe(true); + expect(hasOfflineAccessScope(['openid', 'offline_access'])).toBe(true); + }); + + it('should return false when offline_access is missing', () => { + expect(hasOfflineAccessScope([])).toBe(false); + expect(hasOfflineAccessScope(['openid', 'profile'])).toBe(false); + }); + }); + + describe('buildUserInfo', () => { + const testUser = { + did: 'did:plc:abc123', + handle: 'alice.bsky.social', + }; + + it('should always include sub claim', () => { + const info = buildUserInfo(testUser, []); + expect(info.sub).toBe(testUser.did); + }); + + it('should include profile claims when profile scope is present', () => { + const info = buildUserInfo(testUser, ['profile']); + expect(info.sub).toBe(testUser.did); + expect(info.handle).toBe(testUser.handle); + expect(info.did).toBe(testUser.did); + expect(info.preferred_username).toBe(testUser.handle); + expect(info.name).toBe('alice'); + }); + + it('should extract name from handle correctly', () => { + const user = { did: 'did:plc:xyz', handle: 'bob.example.com' }; + const info = buildUserInfo(user, ['profile']); + expect(info.name).toBe('bob'); + }); + + it('should not include profile claims without profile scope', () => { + const info = buildUserInfo(testUser, ['openid']); + expect(info.handle).toBeUndefined(); + expect(info.preferred_username).toBeUndefined(); + expect(info.name).toBeUndefined(); + }); + + it('should only include sub for openid-only scope', () => { + const info = buildUserInfo(testUser, ['openid']); + expect(Object.keys(info)).toEqual(['sub']); + }); + }); + + describe('validateScopes', () => { + const allowedScopes = ['openid', 'profile', 'email', 'offline_access']; + + it('should validate scopes when openid is included', () => { + const result = validateScopes(['openid', 'profile'], allowedScopes); + expect(result.valid).toBe(true); + expect(result.scopes).toContain('openid'); + expect(result.scopes).toContain('profile'); + }); + + it('should fail when openid is missing', () => { + const result = validateScopes(['profile'], allowedScopes); + expect(result.valid).toBe(false); + expect(result.error).toBe('openid scope is required'); + }); + + it('should filter out disallowed scopes', () => { + const result = validateScopes(['openid', 'profile', 'custom'], allowedScopes); + expect(result.valid).toBe(true); + expect(result.scopes).toContain('openid'); + expect(result.scopes).toContain('profile'); + expect(result.scopes).not.toContain('custom'); + }); + + it('should filter out unsupported scopes', () => { + const result = validateScopes(['openid', 'address'], allowedScopes); + expect(result.valid).toBe(true); + expect(result.scopes).toContain('openid'); + expect(result.scopes).not.toContain('address'); + }); + + it('should respect client allowed scopes', () => { + const limitedAllowed = ['openid', 'profile']; + const result = validateScopes(['openid', 'profile', 'email'], limitedAllowed); + expect(result.valid).toBe(true); + expect(result.scopes).toContain('openid'); + expect(result.scopes).toContain('profile'); + expect(result.scopes).not.toContain('email'); + }); + }); + + describe('getDefaultScopes', () => { + it('should always include openid', () => { + expect(getDefaultScopes([])).toContain('openid'); + expect(getDefaultScopes(['openid'])).toContain('openid'); + }); + + it('should include profile if allowed', () => { + expect(getDefaultScopes(['openid', 'profile'])).toContain('profile'); + }); + + it('should not include profile if not allowed', () => { + expect(getDefaultScopes(['openid', 'email'])).not.toContain('profile'); + }); + + it('should return correct defaults for full scope set', () => { + const allowed = ['openid', 'profile', 'email', 'offline_access']; + const defaults = getDefaultScopes(allowed); + expect(defaults).toEqual(['openid', 'profile']); + }); + }); +}); diff --git a/gateway/src/services/oidc/pkce.test.ts b/gateway/src/services/oidc/pkce.test.ts new file mode 100644 index 0000000..f2f7efc --- /dev/null +++ b/gateway/src/services/oidc/pkce.test.ts @@ -0,0 +1,137 @@ +/** + * PKCE (Proof Key for Code Exchange) Tests + */ +import { describe, it, expect } from 'vitest'; +import { + verifyCodeChallenge, + generateCodeVerifier, + generateCodeChallenge, + isValidCodeVerifier, + isValidCodeChallengeMethod, +} from './pkce.js'; + +describe('PKCE', () => { + describe('generateCodeVerifier', () => { + it('should generate a valid code verifier', () => { + const verifier = generateCodeVerifier(); + expect(verifier).toBeDefined(); + expect(verifier.length).toBeGreaterThanOrEqual(43); + expect(isValidCodeVerifier(verifier)).toBe(true); + }); + + it('should generate unique verifiers', () => { + const verifier1 = generateCodeVerifier(); + const verifier2 = generateCodeVerifier(); + expect(verifier1).not.toBe(verifier2); + }); + }); + + describe('generateCodeChallenge', () => { + it('should generate S256 challenge correctly', () => { + const verifier = 'dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk'; + const expectedChallenge = 'E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM'; + + const challenge = generateCodeChallenge(verifier, 'S256'); + expect(challenge).toBe(expectedChallenge); + }); + + it('should return verifier as challenge for plain method', () => { + const verifier = generateCodeVerifier(); + const challenge = generateCodeChallenge(verifier, 'plain'); + expect(challenge).toBe(verifier); + }); + + it('should default to S256', () => { + const verifier = generateCodeVerifier(); + const challenge1 = generateCodeChallenge(verifier); + const challenge2 = generateCodeChallenge(verifier, 'S256'); + expect(challenge1).toBe(challenge2); + }); + }); + + describe('verifyCodeChallenge', () => { + it('should verify S256 challenge correctly', () => { + const verifier = generateCodeVerifier(); + const challenge = generateCodeChallenge(verifier, 'S256'); + + expect(verifyCodeChallenge(verifier, challenge, 'S256')).toBe(true); + }); + + it('should reject invalid S256 verifier', () => { + const verifier = generateCodeVerifier(); + const challenge = generateCodeChallenge(verifier, 'S256'); + const wrongVerifier = generateCodeVerifier(); + + expect(verifyCodeChallenge(wrongVerifier, challenge, 'S256')).toBe(false); + }); + + it('should verify plain challenge correctly', () => { + const verifier = generateCodeVerifier(); + const challenge = generateCodeChallenge(verifier, 'plain'); + + expect(verifyCodeChallenge(verifier, challenge, 'plain')).toBe(true); + }); + + it('should reject invalid plain verifier', () => { + const verifier = generateCodeVerifier(); + const wrongVerifier = generateCodeVerifier(); + + expect(verifyCodeChallenge(wrongVerifier, verifier, 'plain')).toBe(false); + }); + + it('should default to S256 method', () => { + const verifier = generateCodeVerifier(); + const challenge = generateCodeChallenge(verifier, 'S256'); + + expect(verifyCodeChallenge(verifier, challenge)).toBe(true); + }); + + // RFC 7636 test vector + it('should pass RFC 7636 test vector', () => { + const verifier = 'dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk'; + const challenge = 'E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM'; + + expect(verifyCodeChallenge(verifier, challenge, 'S256')).toBe(true); + }); + }); + + describe('isValidCodeVerifier', () => { + it('should accept valid verifiers', () => { + expect(isValidCodeVerifier('a'.repeat(43))).toBe(true); + expect(isValidCodeVerifier('a'.repeat(128))).toBe(true); + expect(isValidCodeVerifier('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopq')).toBe(true); + expect(isValidCodeVerifier('0123456789-._~abcdefghijklmnopqrstuvwxyzABC')).toBe(true); + }); + + it('should reject too short verifiers', () => { + expect(isValidCodeVerifier('a'.repeat(42))).toBe(false); + expect(isValidCodeVerifier('')).toBe(false); + }); + + it('should reject too long verifiers', () => { + expect(isValidCodeVerifier('a'.repeat(129))).toBe(false); + }); + + it('should reject invalid characters', () => { + expect(isValidCodeVerifier('a'.repeat(42) + '!')).toBe(false); + expect(isValidCodeVerifier('a'.repeat(42) + ' ')).toBe(false); + expect(isValidCodeVerifier('a'.repeat(42) + '+')).toBe(false); + expect(isValidCodeVerifier('a'.repeat(42) + '/')).toBe(false); + }); + }); + + describe('isValidCodeChallengeMethod', () => { + it('should accept valid methods', () => { + expect(isValidCodeChallengeMethod('S256')).toBe(true); + expect(isValidCodeChallengeMethod('plain')).toBe(true); + }); + + it('should reject invalid methods', () => { + expect(isValidCodeChallengeMethod('SHA256')).toBe(false); + expect(isValidCodeChallengeMethod('s256')).toBe(false); + expect(isValidCodeChallengeMethod('PLAIN')).toBe(false); + expect(isValidCodeChallengeMethod('')).toBe(false); + expect(isValidCodeChallengeMethod('RS256')).toBe(false); + }); + }); +}); diff --git a/gateway/src/services/oidc/tokens.test.ts b/gateway/src/services/oidc/tokens.test.ts new file mode 100644 index 0000000..1237511 --- /dev/null +++ b/gateway/src/services/oidc/tokens.test.ts @@ -0,0 +1,324 @@ +/** + * OIDC Token Service Tests + */ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import crypto from 'crypto'; +import { TokenService } from './tokens.js'; +import type { KeyManager } from './keys.js'; + +// Create a real key pair for testing +function createTestKeyPair() { + const { privateKey } = crypto.generateKeyPairSync('ec', { + namedCurve: 'prime256v1', + }); + return privateKey; +} + +describe('TokenService', () => { + let tokenService: TokenService; + let mockKeyManager: KeyManager; + let testPrivateKey: crypto.KeyObject; + + beforeEach(() => { + testPrivateKey = createTestKeyPair(); + + mockKeyManager = { + getSigningKey: vi.fn().mockReturnValue({ + kid: 'test-key-1', + privateKey: testPrivateKey, + algorithm: 'ES256' as const, + }), + getKeyByKid: vi.fn().mockReturnValue({ + privateKey: testPrivateKey, + algorithm: 'ES256' as const, + }), + } as unknown as KeyManager; + + tokenService = new TokenService(mockKeyManager, 'https://auth.example.com'); + }); + + describe('createAccessToken', () => { + it('should create a valid access token', () => { + const token = tokenService.createAccessToken({ + sub: 'did:plc:abc123', + clientId: 'test-client', + scope: 'openid profile', + }); + + expect(token).toBeDefined(); + expect(token.split('.')).toHaveLength(3); + }); + + it('should include required claims', () => { + const token = tokenService.createAccessToken({ + sub: 'did:plc:abc123', + clientId: 'test-client', + scope: 'openid profile', + expiresIn: 3600, + }); + + const verified = tokenService.verifyAccessToken(token); + expect(verified).not.toBeNull(); + expect(verified?.iss).toBe('https://auth.example.com'); + expect(verified?.sub).toBe('did:plc:abc123'); + expect(verified?.client_id).toBe('test-client'); + expect(verified?.scope).toBe('openid profile'); + expect(verified?.jti).toBeDefined(); + }); + + it('should set correct expiration', () => { + const expiresIn = 7200; + const before = Math.floor(Date.now() / 1000); + + const token = tokenService.createAccessToken({ + sub: 'did:plc:abc123', + clientId: 'test-client', + scope: 'openid', + expiresIn, + }); + + const after = Math.floor(Date.now() / 1000); + const verified = tokenService.verifyAccessToken(token); + + expect(verified?.exp).toBeGreaterThanOrEqual(before + expiresIn); + expect(verified?.exp).toBeLessThanOrEqual(after + expiresIn); + }); + }); + + describe('createIdToken', () => { + it('should create a valid ID token', () => { + const token = tokenService.createIdToken({ + sub: 'did:plc:abc123', + aud: 'test-client', + did: 'did:plc:abc123', + handle: 'alice.bsky.social', + }); + + expect(token).toBeDefined(); + expect(token.split('.')).toHaveLength(3); + }); + + it('should include AT Protocol specific claims', () => { + const token = tokenService.createIdToken({ + sub: 'did:plc:abc123', + aud: 'test-client', + did: 'did:plc:abc123', + handle: 'alice.bsky.social', + }); + + const verified = tokenService.verifyIdToken(token, 'test-client'); + expect(verified).not.toBeNull(); + expect(verified?.did).toBe('did:plc:abc123'); + expect(verified?.handle).toBe('alice.bsky.social'); + }); + + it('should include nonce when provided', () => { + const nonce = 'test-nonce-123'; + const token = tokenService.createIdToken({ + sub: 'did:plc:abc123', + aud: 'test-client', + nonce, + did: 'did:plc:abc123', + handle: 'alice.bsky.social', + }); + + const verified = tokenService.verifyIdToken(token, 'test-client', nonce); + expect(verified?.nonce).toBe(nonce); + }); + + it('should include at_hash when access token provided', () => { + const accessToken = 'test-access-token'; + const token = tokenService.createIdToken({ + sub: 'did:plc:abc123', + aud: 'test-client', + did: 'did:plc:abc123', + handle: 'alice.bsky.social', + accessToken, + }); + + const verified = tokenService.verifyIdToken(token, 'test-client'); + expect(verified?.at_hash).toBeDefined(); + }); + }); + + describe('createTokenResponse', () => { + it('should create a complete token response', () => { + const response = tokenService.createTokenResponse({ + sub: 'did:plc:abc123', + clientId: 'test-client', + scope: 'openid profile', + did: 'did:plc:abc123', + handle: 'alice.bsky.social', + }); + + expect(response.access_token).toBeDefined(); + expect(response.token_type).toBe('Bearer'); + expect(response.expires_in).toBeDefined(); + expect(response.id_token).toBeDefined(); + expect(response.scope).toBe('openid profile'); + }); + + it('should not include id_token without openid scope', () => { + const response = tokenService.createTokenResponse({ + sub: 'did:plc:abc123', + clientId: 'test-client', + scope: 'profile', + did: 'did:plc:abc123', + handle: 'alice.bsky.social', + }); + + expect(response.access_token).toBeDefined(); + expect(response.id_token).toBeUndefined(); + }); + + it('should include refresh token when requested', () => { + const response = tokenService.createTokenResponse({ + sub: 'did:plc:abc123', + clientId: 'test-client', + scope: 'openid offline_access', + did: 'did:plc:abc123', + handle: 'alice.bsky.social', + includeRefreshToken: true, + refreshToken: 'test-refresh-token', + }); + + expect(response.refresh_token).toBe('test-refresh-token'); + }); + }); + + describe('verifyAccessToken', () => { + it('should verify valid access tokens', () => { + const token = tokenService.createAccessToken({ + sub: 'did:plc:abc123', + clientId: 'test-client', + scope: 'openid', + }); + + const verified = tokenService.verifyAccessToken(token); + expect(verified).not.toBeNull(); + }); + + it('should reject tampered tokens', () => { + const token = tokenService.createAccessToken({ + sub: 'did:plc:abc123', + clientId: 'test-client', + scope: 'openid', + }); + + // Tamper with the token + const parts = token.split('.'); + parts[1] = parts[1].slice(0, -5) + 'XXXXX'; + const tamperedToken = parts.join('.'); + + const verified = tokenService.verifyAccessToken(tamperedToken); + expect(verified).toBeNull(); + }); + + it('should reject expired tokens', () => { + const token = tokenService.createAccessToken({ + sub: 'did:plc:abc123', + clientId: 'test-client', + scope: 'openid', + expiresIn: -1, // Already expired + }); + + const verified = tokenService.verifyAccessToken(token); + expect(verified).toBeNull(); + }); + + it('should reject malformed tokens', () => { + expect(tokenService.verifyAccessToken('not.a.valid.token')).toBeNull(); + expect(tokenService.verifyAccessToken('invalid')).toBeNull(); + expect(tokenService.verifyAccessToken('')).toBeNull(); + }); + }); + + describe('verifyIdToken', () => { + it('should verify valid ID tokens', () => { + const token = tokenService.createIdToken({ + sub: 'did:plc:abc123', + aud: 'test-client', + did: 'did:plc:abc123', + handle: 'alice.bsky.social', + }); + + const verified = tokenService.verifyIdToken(token); + expect(verified).not.toBeNull(); + }); + + it('should validate audience when provided', () => { + const token = tokenService.createIdToken({ + sub: 'did:plc:abc123', + aud: 'test-client', + did: 'did:plc:abc123', + handle: 'alice.bsky.social', + }); + + expect(tokenService.verifyIdToken(token, 'test-client')).not.toBeNull(); + expect(tokenService.verifyIdToken(token, 'wrong-client')).toBeNull(); + }); + + it('should validate nonce when provided', () => { + const nonce = 'test-nonce'; + const token = tokenService.createIdToken({ + sub: 'did:plc:abc123', + aud: 'test-client', + nonce, + did: 'did:plc:abc123', + handle: 'alice.bsky.social', + }); + + expect(tokenService.verifyIdToken(token, 'test-client', nonce)).not.toBeNull(); + expect(tokenService.verifyIdToken(token, 'test-client', 'wrong-nonce')).toBeNull(); + }); + + it('should reject tokens with wrong issuer', () => { + // Create a token with our service + const token = tokenService.createIdToken({ + sub: 'did:plc:abc123', + aud: 'test-client', + did: 'did:plc:abc123', + handle: 'alice.bsky.social', + }); + + // Create a new service with different issuer + const differentIssuerService = new TokenService(mockKeyManager, 'https://different.issuer.com'); + expect(differentIssuerService.verifyIdToken(token)).toBeNull(); + }); + }); + + describe('JWT structure', () => { + it('should have correct header structure', () => { + const token = tokenService.createAccessToken({ + sub: 'did:plc:abc123', + clientId: 'test-client', + scope: 'openid', + }); + + const [headerB64] = token.split('.'); + const header = JSON.parse(Buffer.from(headerB64, 'base64url').toString()); + + expect(header.alg).toBe('ES256'); + expect(header.typ).toBe('JWT'); + expect(header.kid).toBe('test-key-1'); + }); + }); + + describe('error handling', () => { + it('should throw when no signing key available', () => { + const noKeyManager = { + getSigningKey: vi.fn().mockReturnValue(null), + } as unknown as KeyManager; + + const service = new TokenService(noKeyManager, 'https://auth.example.com'); + + expect(() => + service.createAccessToken({ + sub: 'did:plc:abc123', + clientId: 'test-client', + scope: 'openid', + }) + ).toThrow('No signing key available'); + }); + }); +}); diff --git a/gateway/tests/oidc-flow.test.ts b/gateway/tests/oidc-flow.test.ts new file mode 100644 index 0000000..436bb9c --- /dev/null +++ b/gateway/tests/oidc-flow.test.ts @@ -0,0 +1,585 @@ +/** + * OIDC Flow E2E Tests + * + * Tests the complete OIDC authorization code flow + */ +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import request from 'supertest'; +import express from 'express'; +import { DatabaseService } from '../src/services/database.js'; +import { OIDCService } from '../src/services/oidc/index.js'; +import { createOIDCRouter } from '../src/routes/oidc/index.js'; +import { generateCodeVerifier, generateCodeChallenge } from '../src/services/oidc/pkce.js'; +import type { OAuthService } from '../src/services/oauth.js'; +import crypto from 'crypto'; +import fs from 'fs'; +import path from 'path'; + +// Test configuration +const TEST_ISSUER = 'https://auth.test.example.com'; +const TEST_CLIENT_ID = 'test-oidc-client'; +const TEST_REDIRECT_URI = 'https://app.test.example.com/callback'; +const TEST_DB_PATH = path.join(process.cwd(), 'test-oidc.db'); + +describe('OIDC Flow E2E', () => { + let app: express.Application; + let db: DatabaseService; + let oidcService: OIDCService; + let mockOAuthService: OAuthService; + + beforeAll(async () => { + // Clean up any existing test database + if (fs.existsSync(TEST_DB_PATH)) { + fs.unlinkSync(TEST_DB_PATH); + } + + // Initialize database + db = new DatabaseService(TEST_DB_PATH); + + // Create mock OAuth service + mockOAuthService = { + generateAuthUrl: async () => ({ + url: 'https://pds.example.com/oauth/authorize?...', + state: 'atproto-state-123', + }), + handleCallback: async () => ({ + did: 'did:plc:testuser123', + handle: 'testuser.bsky.social', + }), + } as unknown as OAuthService; + + // Initialize OIDC service + oidcService = new OIDCService(db, { + issuer: TEST_ISSUER, + keySecret: crypto.randomBytes(32).toString('hex'), + }); + + // Ensure signing key exists + await oidcService.initialize(); + + // Register test OIDC client + db.upsertOIDCClient({ + id: TEST_CLIENT_ID, + name: 'Test OIDC App', + client_type: 'oidc', + hmac_secret: crypto.randomBytes(32).toString('hex'), + redirect_uris: [TEST_REDIRECT_URI], + allowed_scopes: ['openid', 'profile', 'email', 'offline_access'], + grant_types: ['authorization_code', 'refresh_token'], + require_pkce: true, + token_endpoint_auth_method: 'none', + token_ttl_seconds: 3600, + id_token_ttl_seconds: 3600, + access_token_ttl_seconds: 3600, + refresh_token_ttl_seconds: 604800, + }); + + // Create Express app + app = express(); + app.use(express.json()); + app.use(express.urlencoded({ extended: true })); + + // Mount OIDC routes + const { wellKnownRouter, oauthRouter } = createOIDCRouter(db, oidcService, mockOAuthService); + app.use('/.well-known', wellKnownRouter); + app.use('/oauth', oauthRouter); + }); + + afterAll(() => { + // Clean up test database + if (fs.existsSync(TEST_DB_PATH)) { + fs.unlinkSync(TEST_DB_PATH); + } + }); + + describe('Discovery Endpoints', () => { + it('should return valid OpenID configuration', async () => { + const response = await request(app) + .get('/.well-known/openid-configuration') + .expect(200); + + expect(response.body.issuer).toBe(TEST_ISSUER); + expect(response.body.authorization_endpoint).toBe(`${TEST_ISSUER}/oauth/authorize`); + expect(response.body.token_endpoint).toBe(`${TEST_ISSUER}/oauth/token`); + expect(response.body.userinfo_endpoint).toBe(`${TEST_ISSUER}/oauth/userinfo`); + expect(response.body.jwks_uri).toBe(`${TEST_ISSUER}/.well-known/jwks.json`); + expect(response.body.scopes_supported).toContain('openid'); + expect(response.body.response_types_supported).toContain('code'); + expect(response.body.grant_types_supported).toContain('authorization_code'); + expect(response.body.id_token_signing_alg_values_supported).toContain('ES256'); + expect(response.body.code_challenge_methods_supported).toContain('S256'); + }); + + it('should return valid JWKS', async () => { + const response = await request(app) + .get('/.well-known/jwks.json') + .expect(200); + + expect(response.body.keys).toBeDefined(); + expect(Array.isArray(response.body.keys)).toBe(true); + expect(response.body.keys.length).toBeGreaterThan(0); + + const key = response.body.keys[0]; + expect(key.kty).toBeDefined(); + expect(key.alg).toBeDefined(); + expect(key.kid).toBeDefined(); + expect(key.use).toBe('sig'); + }); + }); + + describe('Authorization Endpoint', () => { + it('should require all mandatory parameters', async () => { + const response = await request(app) + .get('/oauth/authorize') + .expect(400); + + expect(response.body.error).toBe('invalid_request'); + expect(response.body.error_description).toContain('Missing required parameters'); + }); + + it('should reject unsupported response_type', async () => { + const response = await request(app) + .get('/oauth/authorize') + .query({ + response_type: 'token', + client_id: TEST_CLIENT_ID, + redirect_uri: TEST_REDIRECT_URI, + scope: 'openid', + state: 'test-state', + }) + .expect(400); + + expect(response.body.error).toBe('unsupported_response_type'); + }); + + it('should reject unknown client_id', async () => { + const response = await request(app) + .get('/oauth/authorize') + .query({ + response_type: 'code', + client_id: 'unknown-client', + redirect_uri: TEST_REDIRECT_URI, + scope: 'openid', + state: 'test-state', + }) + .expect(400); + + expect(response.body.error).toBe('invalid_client'); + }); + + it('should reject invalid redirect_uri', async () => { + const response = await request(app) + .get('/oauth/authorize') + .query({ + response_type: 'code', + client_id: TEST_CLIENT_ID, + redirect_uri: 'https://malicious.example.com/callback', + scope: 'openid', + state: 'test-state', + }) + .expect(400); + + expect(response.body.error).toBe('invalid_request'); + expect(response.body.error_description).toContain('redirect_uri'); + }); + + it('should require PKCE code_challenge when client requires it', async () => { + const response = await request(app) + .get('/oauth/authorize') + .query({ + response_type: 'code', + client_id: TEST_CLIENT_ID, + redirect_uri: TEST_REDIRECT_URI, + scope: 'openid', + state: 'test-state', + }); + + // Should redirect with error + expect(response.status).toBe(302); + expect(response.headers.location).toContain('error=invalid_request'); + expect(response.headers.location).toContain('code_challenge'); + }); + + it('should return login page with valid parameters', async () => { + const codeVerifier = generateCodeVerifier(); + const codeChallenge = generateCodeChallenge(codeVerifier); + + const response = await request(app) + .get('/oauth/authorize') + .query({ + response_type: 'code', + client_id: TEST_CLIENT_ID, + redirect_uri: TEST_REDIRECT_URI, + scope: 'openid profile', + state: 'test-state-123', + code_challenge: codeChallenge, + code_challenge_method: 'S256', + }) + .expect(200); + + expect(response.type).toBe('text/html'); + expect(response.text).toContain('Sign in'); + expect(response.text).toContain('handle'); + }); + + it('should redirect with error for missing openid scope', async () => { + const codeVerifier = generateCodeVerifier(); + const codeChallenge = generateCodeChallenge(codeVerifier); + + const response = await request(app) + .get('/oauth/authorize') + .query({ + response_type: 'code', + client_id: TEST_CLIENT_ID, + redirect_uri: TEST_REDIRECT_URI, + scope: 'profile', + state: 'test-state', + code_challenge: codeChallenge, + code_challenge_method: 'S256', + }); + + expect(response.status).toBe(302); + expect(response.headers.location).toContain('error=invalid_scope'); + expect(response.headers.location).toContain('openid'); + }); + }); + + describe('Token Endpoint', () => { + let authorizationCode: string; + let codeVerifier: string; + + beforeAll(async () => { + // Create an authorization code for testing + codeVerifier = generateCodeVerifier(); + const codeChallenge = generateCodeChallenge(codeVerifier); + authorizationCode = crypto.randomBytes(32).toString('base64url'); + + db.saveAuthorizationCode({ + code: authorizationCode, + client_id: TEST_CLIENT_ID, + redirect_uri: TEST_REDIRECT_URI, + scope: 'openid profile', + state: 'test-state', + nonce: 'test-nonce', + code_challenge: codeChallenge, + code_challenge_method: 'S256', + did: 'did:plc:testuser123', + handle: 'testuser.bsky.social', + created_at: Math.floor(Date.now() / 1000), + expires_at: Math.floor(Date.now() / 1000) + 600, + used: false, + }); + }); + + it('should reject missing grant_type', async () => { + const response = await request(app) + .post('/oauth/token') + .send({ + code: authorizationCode, + redirect_uri: TEST_REDIRECT_URI, + client_id: TEST_CLIENT_ID, + }) + .expect(400); + + expect(response.body.error).toBe('invalid_request'); + }); + + it('should reject unsupported grant_type', async () => { + const response = await request(app) + .post('/oauth/token') + .send({ + grant_type: 'password', + code: authorizationCode, + redirect_uri: TEST_REDIRECT_URI, + client_id: TEST_CLIENT_ID, + }) + .expect(400); + + expect(response.body.error).toBe('unsupported_grant_type'); + }); + + it('should reject invalid authorization code', async () => { + const response = await request(app) + .post('/oauth/token') + .send({ + grant_type: 'authorization_code', + code: 'invalid-code', + redirect_uri: TEST_REDIRECT_URI, + client_id: TEST_CLIENT_ID, + code_verifier: codeVerifier, + }) + .expect(400); + + expect(response.body.error).toBe('invalid_grant'); + }); + + it('should reject invalid PKCE verifier', async () => { + // Create a fresh auth code + const freshCode = crypto.randomBytes(32).toString('base64url'); + const freshVerifier = generateCodeVerifier(); + const freshChallenge = generateCodeChallenge(freshVerifier); + + db.saveAuthorizationCode({ + code: freshCode, + client_id: TEST_CLIENT_ID, + redirect_uri: TEST_REDIRECT_URI, + scope: 'openid', + code_challenge: freshChallenge, + code_challenge_method: 'S256', + did: 'did:plc:testuser123', + handle: 'testuser.bsky.social', + created_at: Math.floor(Date.now() / 1000), + expires_at: Math.floor(Date.now() / 1000) + 600, + used: false, + }); + + const response = await request(app) + .post('/oauth/token') + .send({ + grant_type: 'authorization_code', + code: freshCode, + redirect_uri: TEST_REDIRECT_URI, + client_id: TEST_CLIENT_ID, + code_verifier: 'wrong-verifier-that-is-at-least-43-chars-long-for-validity', + }) + .expect(400); + + expect(response.body.error).toBe('invalid_grant'); + expect(response.body.error_description).toContain('code_verifier'); + }); + + it('should exchange valid code for tokens', async () => { + // Create a fresh auth code + const freshCode = crypto.randomBytes(32).toString('base64url'); + const freshVerifier = generateCodeVerifier(); + const freshChallenge = generateCodeChallenge(freshVerifier); + + db.saveAuthorizationCode({ + code: freshCode, + client_id: TEST_CLIENT_ID, + redirect_uri: TEST_REDIRECT_URI, + scope: 'openid profile', + nonce: 'test-nonce', + code_challenge: freshChallenge, + code_challenge_method: 'S256', + did: 'did:plc:testuser123', + handle: 'testuser.bsky.social', + created_at: Math.floor(Date.now() / 1000), + expires_at: Math.floor(Date.now() / 1000) + 600, + used: false, + }); + + const response = await request(app) + .post('/oauth/token') + .send({ + grant_type: 'authorization_code', + code: freshCode, + redirect_uri: TEST_REDIRECT_URI, + client_id: TEST_CLIENT_ID, + code_verifier: freshVerifier, + }) + .expect(200); + + expect(response.body.access_token).toBeDefined(); + expect(response.body.token_type).toBe('Bearer'); + expect(response.body.expires_in).toBeDefined(); + expect(response.body.id_token).toBeDefined(); + + // Verify tokens are valid JWTs + expect(response.body.access_token.split('.')).toHaveLength(3); + expect(response.body.id_token.split('.')).toHaveLength(3); + }); + + it('should not allow code reuse', async () => { + // Create a fresh auth code + const freshCode = crypto.randomBytes(32).toString('base64url'); + const freshVerifier = generateCodeVerifier(); + const freshChallenge = generateCodeChallenge(freshVerifier); + + db.saveAuthorizationCode({ + code: freshCode, + client_id: TEST_CLIENT_ID, + redirect_uri: TEST_REDIRECT_URI, + scope: 'openid', + code_challenge: freshChallenge, + code_challenge_method: 'S256', + did: 'did:plc:testuser123', + handle: 'testuser.bsky.social', + created_at: Math.floor(Date.now() / 1000), + expires_at: Math.floor(Date.now() / 1000) + 600, + used: false, + }); + + // First request should succeed + await request(app) + .post('/oauth/token') + .send({ + grant_type: 'authorization_code', + code: freshCode, + redirect_uri: TEST_REDIRECT_URI, + client_id: TEST_CLIENT_ID, + code_verifier: freshVerifier, + }) + .expect(200); + + // Second request should fail + const response = await request(app) + .post('/oauth/token') + .send({ + grant_type: 'authorization_code', + code: freshCode, + redirect_uri: TEST_REDIRECT_URI, + client_id: TEST_CLIENT_ID, + code_verifier: freshVerifier, + }) + .expect(400); + + expect(response.body.error).toBe('invalid_grant'); + }); + }); + + describe('UserInfo Endpoint', () => { + let accessToken: string; + + beforeAll(async () => { + // Get a valid access token + const freshCode = crypto.randomBytes(32).toString('base64url'); + const freshVerifier = generateCodeVerifier(); + const freshChallenge = generateCodeChallenge(freshVerifier); + + db.saveAuthorizationCode({ + code: freshCode, + client_id: TEST_CLIENT_ID, + redirect_uri: TEST_REDIRECT_URI, + scope: 'openid profile', + code_challenge: freshChallenge, + code_challenge_method: 'S256', + did: 'did:plc:userinfotest', + handle: 'userinfotest.bsky.social', + created_at: Math.floor(Date.now() / 1000), + expires_at: Math.floor(Date.now() / 1000) + 600, + used: false, + }); + + const response = await request(app) + .post('/oauth/token') + .send({ + grant_type: 'authorization_code', + code: freshCode, + redirect_uri: TEST_REDIRECT_URI, + client_id: TEST_CLIENT_ID, + code_verifier: freshVerifier, + }); + + accessToken = response.body.access_token; + }); + + it('should require authorization', async () => { + const response = await request(app) + .get('/oauth/userinfo') + .expect(401); + + expect(response.body.error).toBe('invalid_token'); + }); + + it('should reject invalid tokens', async () => { + const response = await request(app) + .get('/oauth/userinfo') + .set('Authorization', 'Bearer invalid-token') + .expect(401); + + expect(response.body.error).toBe('invalid_token'); + }); + + it('should return user info with valid token', async () => { + const response = await request(app) + .get('/oauth/userinfo') + .set('Authorization', `Bearer ${accessToken}`) + .expect(200); + + // sub is always returned + expect(response.body.sub).toBe('did:plc:userinfotest'); + // Note: handle/preferred_username require user mapping which isn't set up in this test + }); + + it('should support POST method', async () => { + const response = await request(app) + .post('/oauth/userinfo') + .set('Authorization', `Bearer ${accessToken}`) + .expect(200); + + expect(response.body.sub).toBe('did:plc:userinfotest'); + }); + }); + + describe('Token Revocation', () => { + let accessToken: string; + + beforeAll(async () => { + const freshCode = crypto.randomBytes(32).toString('base64url'); + const freshVerifier = generateCodeVerifier(); + const freshChallenge = generateCodeChallenge(freshVerifier); + + db.saveAuthorizationCode({ + code: freshCode, + client_id: TEST_CLIENT_ID, + redirect_uri: TEST_REDIRECT_URI, + scope: 'openid', + code_challenge: freshChallenge, + code_challenge_method: 'S256', + did: 'did:plc:revoketest', + handle: 'revoketest.bsky.social', + created_at: Math.floor(Date.now() / 1000), + expires_at: Math.floor(Date.now() / 1000) + 600, + used: false, + }); + + const response = await request(app) + .post('/oauth/token') + .send({ + grant_type: 'authorization_code', + code: freshCode, + redirect_uri: TEST_REDIRECT_URI, + client_id: TEST_CLIENT_ID, + code_verifier: freshVerifier, + }); + + accessToken = response.body.access_token; + }); + + it('should accept revocation request', async () => { + await request(app) + .post('/oauth/revoke') + .send({ + token: accessToken, + client_id: TEST_CLIENT_ID, + }) + .expect(200); + }); + + it('should accept revocation of already revoked/invalid token', async () => { + // Per RFC 7009, revocation endpoint should always return 200 + await request(app) + .post('/oauth/revoke') + .send({ + token: 'already-revoked-or-invalid', + client_id: TEST_CLIENT_ID, + }) + .expect(200); + }); + }); + + describe('End Session / Logout', () => { + it('should handle logout request', async () => { + const response = await request(app) + .get('/oauth/end_session') + .query({ + post_logout_redirect_uri: TEST_REDIRECT_URI, + state: 'logout-state', + }); + + // Should redirect or return success + expect([200, 302]).toContain(response.status); + }); + }); +}); diff --git a/gateway/vitest.config.ts b/gateway/vitest.config.ts new file mode 100644 index 0000000..5631b7c --- /dev/null +++ b/gateway/vitest.config.ts @@ -0,0 +1,16 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + include: ['src/**/*.test.ts', 'tests/**/*.test.ts'], + coverage: { + provider: 'v8', + reporter: ['text', 'json', 'html'], + include: ['src/**/*.ts'], + exclude: ['src/**/*.test.ts', 'src/types/**'], + }, + testTimeout: 30000, + }, +});