diff --git a/package-lock.json b/package-lock.json index a144e852d9..1ca16879d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -859,6 +859,10 @@ "resolved": "packages/client", "link": true }, + "node_modules/@redis/entraid": { + "resolved": "packages/entraid", + "link": true + }, "node_modules/@redis/graph": { "resolved": "packages/graph", "link": true @@ -956,11 +960,82 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", + "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/express-session": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/@types/express-session/-/express-session-1.18.1.tgz", + "integrity": "sha512-S6TkD/lljxDlQ2u/4A70luD8/ZxZcrU5pQwI1rVXCiaVIywoFgbA+PIUNDjPhQpPdK0dGleLtYc/y7XWBfclBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, "node_modules/@types/http-cache-semantics": { "version": "4.0.4", "dev": true, "license": "MIT" }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/mocha": { "version": "10.0.6", "dev": true, @@ -974,6 +1049,43 @@ "undici-types": "~5.26.4" } }, + "node_modules/@types/qs": { + "version": "6.9.17", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.17.tgz", + "integrity": "sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, "node_modules/@types/sinon": { "version": "17.0.3", "dev": true, @@ -1000,6 +1112,20 @@ "dev": true, "license": "MIT" }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/agent-base": { "version": "7.1.0", "dev": true, @@ -1139,6 +1265,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "dev": true, + "license": "MIT" + }, "node_modules/array-union": { "version": "1.0.2", "dev": true, @@ -1287,6 +1420,48 @@ "readable-stream": "^3.4.0" } }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, "node_modules/boxen": { "version": "7.1.1", "dev": true, @@ -1513,6 +1688,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/cacheable-lookup": { "version": "7.0.0", "dev": true, @@ -1564,18 +1749,38 @@ } }, "node_modules/call-bind": { - "version": "1.0.5", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", "dev": true, "license": "MIT", "dependencies": { - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.1", - "set-function-length": "^1.1.1" + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", + "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/callsites": { "version": "3.1.0", "dev": true, @@ -1838,11 +2043,51 @@ "url": "https://github.com/yeoman/configstore?sponsor=1" } }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/convert-source-map": { "version": "1.9.0", "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "dev": true, + "license": "MIT" + }, "node_modules/cosmiconfig": { "version": "9.0.0", "dev": true, @@ -2036,16 +2281,21 @@ } }, "node_modules/define-data-property": { - "version": "1.1.1", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "dev": true, "license": "MIT", "dependencies": { - "get-intrinsic": "^1.2.1", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/define-lazy-prop": { @@ -2088,11 +2338,32 @@ "node": ">= 14" } }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/deprecation": { "version": "2.3.1", "dev": true, "license": "ISC" }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, "node_modules/diff": { "version": "5.0.0", "dev": true, @@ -2115,6 +2386,34 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/dotenv": { + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.0.tgz", + "integrity": "sha512-9+Sj30DIu+4KvHqMfLUGLFYL2PkURSYMVXJyXe92nFRvlYq5hBjLEhblKB+vkd/WVlUYMWigiY07T91Fkk0+4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "dev": true, @@ -2129,6 +2428,13 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, + "license": "MIT" + }, "node_modules/electron-to-chromium": { "version": "1.4.656", "dev": true, @@ -2144,6 +2450,16 @@ "dev": true, "license": "MIT" }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/env-paths": { "version": "2.2.1", "dev": true, @@ -2217,6 +2533,16 @@ "dev": true, "license": "MIT" }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-errors": { "version": "1.3.0", "dev": true, @@ -2334,6 +2660,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, + "license": "MIT" + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "dev": true, @@ -2393,6 +2726,16 @@ "node": ">=0.10.0" } }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/execa": { "version": "8.0.1", "dev": true, @@ -2437,6 +2780,131 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-session": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.1.tgz", + "integrity": "sha512-a5mtTqEaZvBCL9A9aqkrtfz+3SMDhOVUnjafjo+s7A9Txkq+SVX2DLvSp1Zrv4uCXa3lMSK3viWnh9Gg07PBUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.7", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.0.2", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/express-session/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express-session/node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/express-session/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express-session/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/express/node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "dev": true, + "license": "MIT" + }, "node_modules/external-editor": { "version": "3.1.0", "dev": true, @@ -2567,6 +3035,42 @@ "node": ">=8" } }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, "node_modules/find-cache-dir": { "version": "3.3.2", "dev": true, @@ -2645,6 +3149,26 @@ "node": ">=12.20.0" } }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/fromentries": { "version": "1.3.2", "dev": true, @@ -2743,15 +3267,20 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.3", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.5.tgz", + "integrity": "sha512-Y4+pKa7XeRUPWFNvOOYHkRYrfzW07oraURSvjDmRVOJ748OrVmeXtpE4+GCEHncjCjkTxPNRt8kEbxDhsn6VTg==", "dev": true, "license": "MIT", "dependencies": { - "es-errors": "^1.0.0", + "call-bind-apply-helpers": "^1.0.0", + "dunder-proto": "^1.0.0", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -2976,11 +3505,13 @@ } }, "node_modules/gopd": { - "version": "1.0.1", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "dev": true, "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.1.3" + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3043,11 +3574,13 @@ } }, "node_modules/has-property-descriptors": { - "version": "1.0.1", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dev": true, "license": "MIT", "dependencies": { - "get-intrinsic": "^1.2.2" + "es-define-property": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3065,7 +3598,9 @@ } }, "node_modules/has-symbols": { - "version": "1.0.3", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "dev": true, "license": "MIT", "engines": { @@ -3105,7 +3640,9 @@ } }, "node_modules/hasown": { - "version": "2.0.0", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3133,6 +3670,23 @@ "dev": true, "license": "BSD-2-Clause" }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/http-proxy-agent": { "version": "7.0.0", "dev": true, @@ -3402,6 +3956,16 @@ "dev": true, "license": "MIT" }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/is-arguments": { "version": "1.1.1", "dev": true, @@ -4334,6 +4898,26 @@ "node": ">= 12" } }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "dev": true, @@ -4347,6 +4931,16 @@ "node": ">= 8" } }, + "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/micromatch": { "version": "4.0.5", "dev": true, @@ -4359,6 +4953,19 @@ "node": ">=8.6" } }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/mime-db": { "version": "1.52.0", "dev": true, @@ -4530,6 +5137,16 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/netmask": { "version": "2.0.2", "dev": true, @@ -4847,6 +5464,29 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "dev": true, @@ -5302,6 +5942,16 @@ "parse-path": "^7.0.0" } }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/path-exists": { "version": "4.0.0", "dev": true, @@ -5489,6 +6139,20 @@ "dev": true, "license": "MIT" }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/proxy-agent": { "version": "6.3.1", "dev": true, @@ -5534,6 +6198,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "dev": true, @@ -5564,6 +6244,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/randombytes": { "version": "2.1.0", "dev": true, @@ -5572,6 +6262,32 @@ "safe-buffer": "^5.1.0" } }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/rc": { "version": "1.2.8", "dev": true, @@ -6093,6 +6809,58 @@ "dev": true, "license": "ISC" }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/serialize-javascript": { "version": "6.0.0", "dev": true, @@ -6101,21 +6869,40 @@ "randombytes": "^2.1.0" } }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/set-blocking": { "version": "2.0.0", "dev": true, "license": "ISC" }, "node_modules/set-function-length": { - "version": "1.2.0", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "dev": true, "license": "MIT", "dependencies": { - "define-data-property": "^1.1.1", + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.2", + "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.1" + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -6134,6 +6921,13 @@ "node": ">= 0.4" } }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "license": "ISC" + }, "node_modules/shebang-command": { "version": "2.0.0", "dev": true, @@ -6181,13 +6975,19 @@ } }, "node_modules/side-channel": { - "version": "1.0.4", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -6314,6 +7114,16 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/stdin-discarder": { "version": "0.2.2", "dev": true, @@ -6527,6 +7337,16 @@ "node": ">=8.0" } }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, "node_modules/trim-repeated": { "version": "1.0.0", "dev": true, @@ -6585,6 +7405,20 @@ "node": ">=8" } }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/typed-array-buffer": { "version": "1.0.0", "dev": true, @@ -6708,6 +7542,19 @@ "node": ">=14.17" } }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "dev": true, + "license": "MIT", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/unbox-primitive": { "version": "1.0.2", "dev": true, @@ -6765,6 +7612,16 @@ "node": ">= 10.0.0" } }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/update-browserslist-db": { "version": "1.0.13", "dev": true, @@ -6873,6 +7730,16 @@ "dev": true, "license": "MIT" }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/uuid": { "version": "8.3.2", "license": "MIT", @@ -6880,6 +7747,16 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/vscode-oniguruma": { "version": "1.7.0", "dev": true, @@ -7297,6 +8174,46 @@ "@redis/authx": "^5.0.0-next.5" } }, + "packages/entraid": { + "name": "@redis/entraid", + "version": "5.0.0-next.5", + "license": "MIT", + "dependencies": { + "@azure/msal-node": "^2.16.1" + }, + "devDependencies": { + "@types/express": "^4.17.21", + "@types/express-session": "^1.18.0", + "@types/node": "^22.9.0", + "dotenv": "^16.3.1", + "express": "^4.21.1", + "express-session": "^1.18.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@redis/authx": "^5.0.0-next.5", + "@redis/client": "^5.0.0-next.5" + } + }, + "packages/entraid/node_modules/@types/node": { + "version": "22.10.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz", + "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "packages/entraid/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "packages/graph": { "name": "@redis/graph", "version": "5.0.0-next.5", diff --git a/packages/authx/lib/token-manager.ts b/packages/authx/lib/token-manager.ts index b91a4c5c12..fc780f1c4e 100644 --- a/packages/authx/lib/token-manager.ts +++ b/packages/authx/lib/token-manager.ts @@ -35,7 +35,6 @@ export interface TokenManagerConfig { retry?: RetryPolicy; } - /** * IDPError is an error that occurs while calling the underlying IdentityProvider. * @@ -88,7 +87,7 @@ export class TokenManager { constructor( private readonly identityProvider: IdentityProvider, - private readonly config: TokenManagerConfig, + private readonly config: TokenManagerConfig ) { if (this.config.expirationRefreshRatio > 1) { throw new Error('expirationRefreshRatio must be less than or equal to 1'); @@ -186,25 +185,29 @@ export class TokenManager { if (!this.listener) { throw new Error('TokenManager is not running, but a new token was received'); } - const token = this.wrapNativeToken(nativeToken, ttlMs); - this.currentToken = token; + const token = this.wrapAndSetCurrentToken(nativeToken, ttlMs); this.listener.onNext(token); this.scheduleNextRefresh(this.calculateRefreshTime(token)); } /** - * Wraps a native token obtained from identity provider. - * @param nativeToken - * @param ttlMs + * Creates a Token object from a native token and sets it as the current token. + * + * @param nativeToken - The raw token received from the identity provider + * @param ttlMs - Time-to-live in milliseconds for the token + * + * @returns A new Token instance containing the wrapped native token and expiration details + * */ - public wrapNativeToken(nativeToken: T, ttlMs: number): Token { + public wrapAndSetCurrentToken(nativeToken: T, ttlMs: number): Token { const now = Date.now(); const token = new Token( nativeToken, now + ttlMs, now ); + this.currentToken = token; return token; } @@ -228,7 +231,7 @@ export class TokenManager { * @param token The token to calculate the refresh time for. * @param now The current time in milliseconds. Defaults to Date.now(). */ - public calculateRefreshTime(token: Token, now:number = Date.now()): number { + public calculateRefreshTime(token: Token, now: number = Date.now()): number { const ttlMs = token.getTtlMs(now); return Math.floor(ttlMs * this.config.expirationRefreshRatio); } diff --git a/packages/client/lib/client/index.ts b/packages/client/lib/client/index.ts index 380f136062..76009946e2 100644 --- a/packages/client/lib/client/index.ts +++ b/packages/client/lib/client/index.ts @@ -380,11 +380,18 @@ export default class RedisClient< } /** - * TODO: Implement re-authentication to support refreshing credentials without reconnecting * @param credentials */ private reAuthenticate = async (credentials: BasicAuth) => { - throw new Error('Not implemented'); + // Re-authentication is not supported on RESP2 with PubSub active + if (!(this.isPubSubActive && !this.#options?.RESP)) { + await this.sendCommand( + parseArgs(COMMANDS.AUTH, { + username: credentials.username, + password: credentials.password ?? '' + }) + ); + } } private subscribeForStreamingCredentials(cp: StreamingCredentialsProvider): Promise<[BasicAuth, Disposable]> { diff --git a/packages/entraid/README.md b/packages/entraid/README.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/entraid/lib/entra-id-credentials-provider-factory.ts b/packages/entraid/lib/entra-id-credentials-provider-factory.ts new file mode 100644 index 0000000000..1eb32c2195 --- /dev/null +++ b/packages/entraid/lib/entra-id-credentials-provider-factory.ts @@ -0,0 +1,357 @@ +import { NetworkError } from '@azure/msal-common'; +import { + LogLevel, + ManagedIdentityApplication, + ManagedIdentityConfiguration, + AuthenticationResult, + PublicClientApplication, + ConfidentialClientApplication, AuthorizationUrlRequest, AuthorizationCodeRequest, CryptoProvider, Configuration, NodeAuthOptions, AccountInfo +} from '@azure/msal-node'; +import { RetryPolicy, TokenManager, TokenManagerConfig, ReAuthenticationError } from '@redis/authx'; +import { EntraidCredentialsProvider } from './entraid-credentials-provider'; +import { MSALIdentityProvider } from './msal-identity-provider'; + + +/** + * This class is used to create credentials providers for different types of authentication flows. + */ +export class EntraIdCredentialsProviderFactory { + + /** + * This method is used to create a ManagedIdentityProvider for both system-assigned and user-assigned managed identities. + * + * @param params + * @param userAssignedClientId For user-assigned managed identities, the developer needs to pass either the client ID, + * full resource identifier, or the object ID of the managed identity when creating ManagedIdentityApplication. + * + */ + public static createManagedIdentityProvider( + params: CredentialParams, userAssignedClientId?: string + ): EntraidCredentialsProvider { + const config: ManagedIdentityConfiguration = { + // For user-assigned identity, include the client ID + ...(userAssignedClientId && { + managedIdentityIdParams: { + userAssignedClientId + } + }), + system: { + loggerOptions + } + }; + + const client = new ManagedIdentityApplication(config); + + const idp = new MSALIdentityProvider( + () => client.acquireToken({ + resource: params.scopes?.[0] ?? FALLBACK_SCOPE, + forceRefresh: true + }).then(x => x === null ? Promise.reject('Token is null') : x) + ); + + return new EntraidCredentialsProvider( + new TokenManager(idp, params.tokenManagerConfig), + idp, + { onReAuthenticationError: params.onReAuthenticationError } + ); + } + + /** + * This method is used to create a credentials provider for system-assigned managed identities. + * @param params + */ + static createForSystemAssignedManagedIdentity( + params: CredentialParams + ): EntraidCredentialsProvider { + return this.createManagedIdentityProvider(params); + } + + /** + * This method is used to create a credentials provider for user-assigned managed identities. + * It will include the client ID as the userAssignedClientId in the ManagedIdentityConfiguration. + * @param params + */ + static createForUserAssignedManagedIdentity( + params: CredentialParams + ): EntraidCredentialsProvider { + return this.createManagedIdentityProvider(params, params.clientId); + } + + private static _createForClientCredentials( + authConfig: NodeAuthOptions, + params: CredentialParams + ): EntraidCredentialsProvider { + const config: Configuration = { + auth: { + ...authConfig, + authority: this.getAuthority(params.authorityConfig ?? { type: 'default' }) + }, + system: { + loggerOptions + } + }; + + const client = new ConfidentialClientApplication(config); + + const idp = new MSALIdentityProvider( + () => client.acquireTokenByClientCredential({ + skipCache: true, + scopes: params.scopes ?? [FALLBACK_SCOPE] + }).then(x => x === null ? Promise.reject('Token is null') : x) + ); + + return new EntraidCredentialsProvider(new TokenManager(idp, params.tokenManagerConfig), idp, + { onReAuthenticationError: params.onReAuthenticationError }); + } + + /** + * This method is used to create a credentials provider for service principals using certificate. + * @param params + */ + static createForClientCredentialsWithCertificate( + params: ClientCredentialsWithCertificateParams + ): EntraidCredentialsProvider { + return this._createForClientCredentials( + { + clientId: params.clientId, + clientCertificate: params.certificate + }, + params + ); + } + + /** + * This method is used to create a credentials provider for service principals using client secret. + * @param params + */ + static createForClientCredentials( + params: ClientSecretCredentialsParams + ): EntraidCredentialsProvider { + return this._createForClientCredentials( + { + clientId: params.clientId, + clientSecret: params.clientSecret + }, + params + ); + } + + /** + * This method is used to create a credentials provider for the Authorization Code Flow with PKCE. + * @param params + */ + static createForAuthorizationCodeWithPKCE( + params: AuthCodePKCEParams + ): { + getPKCECodes: () => Promise<{ + verifier: string; + challenge: string; + challengeMethod: string; + }>; + getAuthCodeUrl: ( + pkceCodes: { challenge: string; challengeMethod: string } + ) => Promise; + createCredentialsProvider: ( + params: PKCEParams + ) => EntraidCredentialsProvider; + } { + + const requiredScopes = ['user.read', 'offline_access']; + const scopes = [...new Set([...(params.scopes || []), ...requiredScopes])]; + + const authFlow = AuthCodeFlowHelper.create({ + clientId: params.clientId, + redirectUri: params.redirectUri, + scopes: scopes, + authorityConfig: params.authorityConfig + }); + + return { + getPKCECodes: AuthCodeFlowHelper.generatePKCE, + getAuthCodeUrl: (pkceCodes) => authFlow.getAuthCodeUrl(pkceCodes), + createCredentialsProvider: (pkceParams) => { + + // This is used to store the initial credentials account to be used + // for silent token acquisition after the initial token acquisition. + let initialCredentialsAccount: AccountInfo | null = null; + + const idp = new MSALIdentityProvider( + async () => { + if (!initialCredentialsAccount) { + let authResult = await authFlow.acquireTokenByCode(pkceParams); + initialCredentialsAccount = authResult.account; + return authResult; + } else { + return authFlow.client.acquireTokenSilent({ + forceRefresh: true, + account: initialCredentialsAccount, + scopes + }); + } + + } + ); + const tm = new TokenManager(idp, params.tokenManagerConfig); + return new EntraidCredentialsProvider(tm, idp, { onReAuthenticationError: params.onReAuthenticationError }); + } + }; + } + + static getAuthority(config: AuthorityConfig): string { + switch (config.type) { + case 'multi-tenant': + return `https://login.microsoftonline.com/${config.tenantId}`; + case 'custom': + return config.authorityUrl; + case 'default': + return 'https://login.microsoftonline.com/common'; + default: + throw new Error('Invalid authority configuration'); + } + } + + +} + + +const FALLBACK_SCOPE = 'https://redis.azure.com/.default'; + +export type AuthorityConfig = + | { type: 'multi-tenant'; tenantId: string } + | { type: 'custom'; authorityUrl: string } + | { type: 'default' }; + +export type PKCEParams = { + code: string; + verifier: string; + clientInfo?: string; +} + +export type CredentialParams = { + clientId: string; + scopes?: string[]; + authorityConfig?: AuthorityConfig; + + tokenManagerConfig: TokenManagerConfig + onReAuthenticationError?: (error: ReAuthenticationError) => void; +} + +export type AuthCodePKCEParams = CredentialParams & { + redirectUri: string; +}; + +export type ClientSecretCredentialsParams = CredentialParams & { + clientSecret: string; +}; + +export type ClientCredentialsWithCertificateParams = CredentialParams & { + certificate: { + thumbprint: string; + privateKey: string; + x5c?: string; + }; +}; + +const loggerOptions = { + loggerCallback(loglevel: LogLevel, message: string, containsPii: boolean) { + if (!containsPii) console.log(message); + }, + piiLoggingEnabled: false, + logLevel: LogLevel.Verbose +} + +/** + * The most imporant part of the RetryPolicy is the shouldRetry function. This function is used to determine if a request should be retried based + * on the error returned from the identity provider. The defaultRetryPolicy is used to retry on network errors only. + */ +export const DEFAULT_RETRY_POLICY: RetryPolicy = { + // currently only retry on network errors + shouldRetry: (error: unknown) => error instanceof NetworkError, + maxAttempts: 10, + initialDelayMs: 100, + maxDelayMs: 100000, + backoffMultiplier: 2, + jitterPercentage: 0.1 + +}; + +export const DEFAULT_TOKEN_MANAGER_CONFIG: TokenManagerConfig = { + retry: DEFAULT_RETRY_POLICY, + expirationRefreshRatio: 0.7 // Refresh token when 70% of the token has expired +} + +/** + * This class is used to help with the Authorization Code Flow with PKCE. + * It provides methods to generate PKCE codes, get the authorization URL, and create the credential provider. + */ +export class AuthCodeFlowHelper { + private constructor( + readonly client: PublicClientApplication, + readonly scopes: string[], + readonly redirectUri: string + ) {} + + async getAuthCodeUrl(pkceCodes: { + challenge: string; + challengeMethod: string; + }): Promise { + const authCodeUrlParameters: AuthorizationUrlRequest = { + scopes: this.scopes, + redirectUri: this.redirectUri, + codeChallenge: pkceCodes.challenge, + codeChallengeMethod: pkceCodes.challengeMethod + }; + + return this.client.getAuthCodeUrl(authCodeUrlParameters); + } + + async acquireTokenByCode(params: PKCEParams): Promise { + const tokenRequest: AuthorizationCodeRequest = { + code: params.code, + scopes: this.scopes, + redirectUri: this.redirectUri, + codeVerifier: params.verifier, + clientInfo: params.clientInfo + }; + + return this.client.acquireTokenByCode(tokenRequest); + } + + static async generatePKCE(): Promise<{ + verifier: string; + challenge: string; + challengeMethod: string; + }> { + const cryptoProvider = new CryptoProvider(); + const { verifier, challenge } = await cryptoProvider.generatePkceCodes(); + return { + verifier, + challenge, + challengeMethod: 'S256' + }; + } + + static create(params: { + clientId: string; + redirectUri: string; + scopes?: string[]; + authorityConfig?: AuthorityConfig; + }): AuthCodeFlowHelper { + const config = { + auth: { + clientId: params.clientId, + authority: EntraIdCredentialsProviderFactory.getAuthority(params.authorityConfig ?? { type: 'default' }) + }, + system: { + loggerOptions + } + }; + + return new AuthCodeFlowHelper( + new PublicClientApplication(config), + params.scopes ?? ['user.read'], + params.redirectUri + ); + } +} + diff --git a/packages/entraid/lib/entraid-credentials-provider.spec.ts b/packages/entraid/lib/entraid-credentials-provider.spec.ts new file mode 100644 index 0000000000..f94cbcc9ad --- /dev/null +++ b/packages/entraid/lib/entraid-credentials-provider.spec.ts @@ -0,0 +1,187 @@ +import { IdentityProvider, TokenManager, TokenResponse, TokenManagerConfig, BasicAuth } from '@redis/authx'; +import { EntraidCredentialsProvider } from './entraid-credentials-provider'; +import { strict as assert } from 'node:assert'; +import { setTimeout } from 'timers/promises'; +import { AuthenticationResult } from '@azure/msal-common/node'; + +describe('EntraID CredentialsProvider Subscription Behavior', () => { + + it('should properly handle token refresh sequence for multiple subscribers', async () => { + const networkDelay = 20; + const tokenTTL = 100; + const refreshRatio = 0.5; // Refresh at 50% of TTL + + const idp = new SequenceEntraIDProvider(tokenTTL, networkDelay); + const tokenManager = new TokenManager(idp, { + expirationRefreshRatio: refreshRatio + }); + const entraid = new EntraidCredentialsProvider(tokenManager, idp); + + // Create two initial subscribers + const subscriber1 = new TestSubscriber('subscriber1'); + const subscriber2 = new TestSubscriber('subscriber2'); + + assert.equal(entraid.hasActiveSubscriptions(), false, 'There should be no active subscriptions'); + assert.equal(entraid.getSubscriptionsCount(), 0, 'There should be 0 subscriptions'); + + // Start the first two subscriptions almost simultaneously + const [sub1Initial, sub2Initial] = await Promise.all([ + entraid.subscribe(subscriber1), + entraid.subscribe(subscriber2)] + ); + + assertCredentials(sub1Initial[0], 'initial-token', 'Subscriber 1 should receive initial token'); + assertCredentials(sub2Initial[0], 'initial-token', 'Subscriber 2 should receive initial token'); + + assert.equal(entraid.hasActiveSubscriptions(), true, 'There should be active subscriptions'); + assert.equal(entraid.getSubscriptionsCount(), 2, 'There should be 2 subscriptions'); + + // add a third subscriber after a very short delay + const subscriber3 = new TestSubscriber('subscriber3'); + await setTimeout(1); + const sub3Initial = await entraid.subscribe(subscriber3) + + assert.equal(entraid.hasActiveSubscriptions(), true, 'There should be active subscriptions'); + assert.equal(entraid.getSubscriptionsCount(), 3, 'There should be 3 subscriptions'); + + // make sure the third subscriber gets the initial token as well + assertCredentials(sub3Initial[0], 'initial-token', 'Subscriber 3 should receive initial token'); + + // Wait for first refresh (50% of TTL + network delay + small buffer) + await setTimeout((tokenTTL * refreshRatio) + networkDelay + 15); + + // All 3 subscribers should receive refresh-token-1 + assertCredentials(subscriber1.credentials[0], 'refresh-token-1', 'Subscriber 1 should receive first refresh token'); + assertCredentials(subscriber2.credentials[0], 'refresh-token-1', 'Subscriber 2 should receive first refresh token'); + assertCredentials(subscriber3.credentials[0], 'refresh-token-1', 'Subscriber 3 should receive first refresh token'); + + // Add a late subscriber - should immediately get refresh-token-1 + const subscriber4 = new TestSubscriber('subscriber4'); + const sub4Initial = await entraid.subscribe(subscriber4); + + assert.equal(entraid.hasActiveSubscriptions(), true, 'There should be active subscriptions'); + assert.equal(entraid.getSubscriptionsCount(), 4, 'There should be 4 subscriptions'); + + assertCredentials(sub4Initial[0], 'refresh-token-1', 'Late subscriber should receive refresh-token-1'); + + // Wait for second refresh + await setTimeout((tokenTTL * refreshRatio) + networkDelay + 15); + + assertCredentials(subscriber1.credentials[1], 'refresh-token-2', 'Subscriber 1 should receive second refresh token'); + assertCredentials(subscriber2.credentials[1], 'refresh-token-2', 'Subscriber 2 should receive second refresh token'); + assertCredentials(subscriber3.credentials[1], 'refresh-token-2', 'Subscriber 3 should receive second refresh token'); + + assertCredentials(subscriber4.credentials[0], 'refresh-token-2', 'Subscriber 4 should receive second refresh token'); + + // Verify refreshes happen after minimum expected time + const minimumRefreshInterval = tokenTTL * 0.4; // 40% of TTL as safety margin + + verifyRefreshTiming(subscriber1, minimumRefreshInterval); + verifyRefreshTiming(subscriber2, minimumRefreshInterval); + verifyRefreshTiming(subscriber3, minimumRefreshInterval); + verifyRefreshTiming(subscriber4, minimumRefreshInterval); + + // Cleanup + + assert.equal(tokenManager.isRunning(), true); + sub1Initial[1][Symbol.dispose](); + sub2Initial[1][Symbol.dispose](); + sub3Initial[1][Symbol.dispose](); + assert.equal(entraid.hasActiveSubscriptions(), true, 'There should be active subscriptions'); + assert.equal(entraid.getSubscriptionsCount(), 1, 'There should be 1 subscriptions'); + sub4Initial[1][Symbol.dispose](); + assert.equal(entraid.hasActiveSubscriptions(), false, 'There should be no active subscriptions'); + assert.equal(entraid.getSubscriptionsCount(), 0, 'There should be 0 subscriptions'); + assert.equal(tokenManager.isRunning(), false) + }); + + const verifyRefreshTiming = ( + subscriber: TestSubscriber, + expectedMinimumInterval: number, + message?: string + ) => { + const intervals = []; + for (let i = 1; i < subscriber.timestamps.length; i++) { + intervals.push(subscriber.timestamps[i] - subscriber.timestamps[i - 1]); + } + + intervals.forEach((interval, index) => { + assert.ok( + interval > expectedMinimumInterval, + message || `Refresh ${index + 1} for ${subscriber.name} should happen after minimum interval of ${expectedMinimumInterval}ms` + ); + }); + }; + + class SequenceEntraIDProvider implements IdentityProvider { + private currentIndex = 0; + + constructor( + private readonly tokenTTL: number = 100, + private tokenDeliveryDelayMs: number = 0, + private readonly tokenSequence: AuthenticationResult[] = [ + { + accessToken: 'initial-token', + account: { username: 'test-user' } + } as AuthenticationResult, + { + accessToken: 'refresh-token-1', + account: { username: 'test-user' } + } as AuthenticationResult, + { + accessToken: 'refresh-token-2', + account: { username: 'test-user' } + } as AuthenticationResult + ] + ) {} + + setTokenDeliveryDelay(delayMs: number): void { + this.tokenDeliveryDelayMs = delayMs; + } + + async requestToken(): Promise> { + if (this.tokenDeliveryDelayMs > 0) { + await setTimeout(this.tokenDeliveryDelayMs); + } + + if (this.currentIndex >= this.tokenSequence.length) { + throw new Error('No more tokens in sequence'); + } + + return { + token: this.tokenSequence[this.currentIndex++], + ttlMs: this.tokenTTL + }; + } + } + + class TestSubscriber { + public readonly credentials: Array = []; + public readonly errors: Error[] = []; + public readonly timestamps: number[] = []; + + constructor(public readonly name: string = 'unnamed') {} + + onNext = (creds: BasicAuth) => { + this.credentials.push(creds); + this.timestamps.push(Date.now()); + } + + onError = (error: Error) => { + this.errors.push(error); + } + } + + /** + * Assert that the actual credentials match the expected token + * @param actual + * @param expectedToken + * @param message + */ + const assertCredentials = (actual: BasicAuth, expectedToken: string, message: string) => { + assert.deepEqual(actual, { + username: 'test-user', + password: expectedToken + }, message); + }; +}); \ No newline at end of file diff --git a/packages/entraid/lib/entraid-credentials-provider.ts b/packages/entraid/lib/entraid-credentials-provider.ts new file mode 100644 index 0000000000..aa638d5fb9 --- /dev/null +++ b/packages/entraid/lib/entraid-credentials-provider.ts @@ -0,0 +1,128 @@ +import { AuthenticationResult } from '@azure/msal-common/node'; +import { + BasicAuth, StreamingCredentialsProvider, IdentityProvider, TokenManager, + ReAuthenticationError, StreamingCredentialsListener, IDPError, Token +} from '@redis/authx'; + +/** + * A streaming credentials provider that uses the Entraid identity provider to provide credentials. + * Please use one of the factory functions in `entraid-credetfactories.ts` to create an instance of this class for the different + * type of authentication flows. + */ +export class EntraidCredentialsProvider implements StreamingCredentialsProvider { + readonly type = 'streaming-credentials-provider'; + + private readonly listeners: Set> = new Set(); + + private tokenManagerDisposable: Disposable | null = null; + private isStarting: boolean = false; + + private pendingSubscribers: Array<{ + resolve: (value: [BasicAuth, Disposable]) => void; + reject: (error: Error) => void; + listener: StreamingCredentialsListener; + }> = []; + + constructor( + private readonly tokenManager: TokenManager, + private readonly idp: IdentityProvider, + options: { + onReAuthenticationError?: (error: ReAuthenticationError) => void + } = {} + ) { + this.onReAuthenticationError = options.onReAuthenticationError ?? + ((error) => console.error('ReAuthenticationError', error)); + } + + async subscribe( + listener: StreamingCredentialsListener + ): Promise<[BasicAuth, Disposable]> { + + const currentToken = this.tokenManager.getCurrentToken(); + + if (currentToken) { + this.listeners.add(listener); + return [this.toBasicAuth(currentToken.value), this.createDisposable(listener)]; + } + + if (this.isStarting) { + return new Promise((resolve, reject) => { + this.pendingSubscribers.push({ resolve, reject, listener }); + }); + } + + this.isStarting = true; + try { + const initialToken = await this.startTokenManagerAndObtainInitialToken(); + + this.pendingSubscribers.forEach(({ resolve, listener }) => { + this.listeners.add(listener); + resolve([this.toBasicAuth(initialToken.value), this.createDisposable(listener)]); + }); + + this.pendingSubscribers = []; + this.listeners.add(listener); + + return [this.toBasicAuth(initialToken.value), this.createDisposable(listener)]; + } finally { + this.isStarting = false; + } + } + + onReAuthenticationError: (error: ReAuthenticationError) => void; + + private toBasicAuth(token: AuthenticationResult): BasicAuth { + return { + username: token.account?.username ?? undefined, + password: token.accessToken + }; + } + + private createTokenManagerListener(subscribers: Set>) { + return { + onError: (error: IDPError): void => { + if (error.isFatal) { + subscribers.forEach(listener => listener.onError(error)); + } else { + console.log('Transient identity provider error', error); + } + }, + onNext: (token: { value: AuthenticationResult }): void => { + const credentials = this.toBasicAuth(token.value); + subscribers.forEach(listener => listener.onNext(credentials)); + } + }; + } + + private createDisposable(listener: StreamingCredentialsListener): Disposable { + return { + [Symbol.dispose]: () => { + this.listeners.delete(listener); + if (this.listeners.size === 0 && this.tokenManagerDisposable) { + this.tokenManagerDisposable[Symbol.dispose](); + this.tokenManagerDisposable = null; + } + } + }; + } + + private async startTokenManagerAndObtainInitialToken(): Promise> { + const initialResponse = await this.idp.requestToken(); + const token = this.tokenManager.wrapAndSetCurrentToken(initialResponse.token, initialResponse.ttlMs); + + this.tokenManagerDisposable = this.tokenManager.start( + this.createTokenManagerListener(this.listeners), + this.tokenManager.calculateRefreshTime(token) + ); + return token; + } + + public hasActiveSubscriptions(): boolean { + return this.tokenManagerDisposable !== null && this.listeners.size > 0; + } + + public getSubscriptionsCount(): number { + return this.listeners.size; + } + +} \ No newline at end of file diff --git a/packages/entraid/lib/msal-identity-provider.ts b/packages/entraid/lib/msal-identity-provider.ts new file mode 100644 index 0000000000..1b44549fe3 --- /dev/null +++ b/packages/entraid/lib/msal-identity-provider.ts @@ -0,0 +1,32 @@ +import { + AuthenticationResult +} from '@azure/msal-node'; +import { IdentityProvider, TokenResponse } from '@redis/authx'; + +export class MSALIdentityProvider implements IdentityProvider { + private readonly getToken: () => Promise; + + constructor(getToken: () => Promise) { + this.getToken = getToken; + } + + async requestToken(): Promise> { + try { + const result = await this.getToken(); + + if (!result?.accessToken || !result?.expiresOn) { + throw new Error('Invalid token response'); + } + return { + token: result, + ttlMs: result.expiresOn.getTime() - Date.now() + }; + } catch (error) { + console.error('Error acquiring token:', error); + throw error; + } + } + +} + + diff --git a/packages/entraid/package.json b/packages/entraid/package.json new file mode 100644 index 0000000000..867772b24c --- /dev/null +++ b/packages/entraid/package.json @@ -0,0 +1,47 @@ +{ + "name": "@redis/entraid", + "version": "5.0.0-next.5", + "license": "MIT", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist/", + "!dist/tsconfig.tsbuildinfo" + ], + "scripts": { + "clean": "rimraf dist", + "build": "npm run clean && tsc", + "start:auth-pkce": "npm run build && node dist/samples/auth-code-pkce/index.js", + "test": "nyc -r text-summary -r lcov mocha -r tsx './lib/**/*.spec.ts'" + }, + "dependencies": { + "@azure/msal-node": "^2.16.1" + }, + "peerDependencies": { + "@redis/authx": "^5.0.0-next.5", + "@redis/client": "^5.0.0-next.5" + }, + "devDependencies": { + "@types/express": "^4.17.21", + "@types/express-session": "^1.18.0", + "@types/node": "^22.9.0", + "dotenv": "^16.3.1", + "express": "^4.21.1", + "express-session": "^1.18.1" + }, + "engines": { + "node": ">= 18" + }, + "repository": { + "type": "git", + "url": "git://github.com/redis/node-redis.git" + }, + "bugs": { + "url": "https://github.com/redis/node-redis/issues" + }, + "homepage": "https://github.com/redis/node-redis/tree/master/packages/entraid", + "keywords": [ + "redis", + "entraid" + ] +} diff --git a/packages/entraid/samples/auth-code-pkce/index.ts b/packages/entraid/samples/auth-code-pkce/index.ts new file mode 100644 index 0000000000..797358e0e2 --- /dev/null +++ b/packages/entraid/samples/auth-code-pkce/index.ts @@ -0,0 +1,157 @@ +import express, { Request, Response } from 'express'; +import session from 'express-session'; +import dotenv from 'dotenv'; +import { DEFAULT_TOKEN_MANAGER_CONFIG, EntraIdCredentialsProviderFactory } from '../../lib/entra-id-credentials-provider-factory'; + +dotenv.config(); + +if (!process.env.SESSION_SECRET) { + throw new Error('SESSION_SECRET environment variable must be set'); +} + +interface PKCESession extends session.Session { + pkceCodes?: { + verifier: string; + challenge: string; + challengeMethod: string; + }; +} + +interface AuthRequest extends Request { + session: PKCESession; +} + +const app = express(); + +const sessionConfig = { + secret: process.env.SESSION_SECRET, + resave: false, + saveUninitialized: false, + cookie: { + secure: process.env.NODE_ENV === 'production', // Only use secure in production + httpOnly: true, + sameSite: 'lax', + maxAge: 3600000 // 1 hour + } +} as const; + +app.use(session(sessionConfig)); + +if (!process.env.MSAL_CLIENT_ID || !process.env.MSAL_TENANT_ID) { + throw new Error('MSAL_CLIENT_ID and MSAL_TENANT_ID environment variables must be set'); +} + +// Initialize MSAL provider with authorization code PKCE flow +const { + getPKCECodes, + createCredentialsProvider, + getAuthCodeUrl +} = EntraIdCredentialsProviderFactory.createForAuthorizationCodeWithPKCE({ + clientId: process.env.MSAL_CLIENT_ID, + redirectUri: process.env.REDIRECT_URI || 'http://localhost:3000/redirect', + authorityConfig: { type: 'multi-tenant', tenantId: process.env.MSAL_TENANT_ID }, + tokenManagerConfig: DEFAULT_TOKEN_MANAGER_CONFIG +}); + +app.get('/login', async (req: AuthRequest, res: Response) => { + try { + // Generate PKCE Codes before starting the authorization flow + const pkceCodes = await getPKCECodes(); + + // Store PKCE codes in session + req.session.pkceCodes = pkceCodes + + await new Promise((resolve, reject) => { + req.session.save((err) => { + if (err) reject(err); + else resolve(); + }); + }); + + const authUrl = await getAuthCodeUrl({ + challenge: pkceCodes.challenge, + challengeMethod: pkceCodes.challengeMethod + }); + + res.redirect(authUrl); + } catch (error) { + console.error('Login flow failed:', error); + res.status(500).send('Authentication failed'); + } +}); + +app.get('/redirect', async (req: AuthRequest, res: Response) => { + try { + // Debug log to see exactly what we're receiving + console.log('Full request query:', req.query); + console.log('Code from request:', req.query.code); + console.log('Session state:', req.session); + + // The authorization code is in req.query.code + const { code, client_info } = req.query; + const { pkceCodes } = req.session; + + if (!pkceCodes) { + console.error('Session state:', { + hasSession: !!req.session, + sessionID: req.sessionID, + pkceCodes: req.session.pkceCodes + }); + return res.status(400).send('PKCE codes not found in session'); + } + + // Check both possible error scenarios + if (req.query.error) { + console.error('OAuth error:', req.query.error, req.query.error_description); + return res.status(400).send(`OAuth error: ${req.query.error_description || req.query.error}`); + } + + if (!code) { + console.error('Missing authorization code. Query parameters received:', req.query); + return res.status(400).send('Authorization code not found in request. Query params: ' + JSON.stringify(req.query)); + } + + // Configure with the received code + const entraidCredentialsProvider = createCredentialsProvider( + { + code: code as string, + verifier: pkceCodes.verifier, + clientInfo: client_info as string | undefined + }, + ); + + const initialCredentials = entraidCredentialsProvider.subscribe({ + onNext: (token) => { + console.log('Token acquired:', token); + }, + onError: (error) => { + console.error('Token acquisition failed:', error); + } + }); + + const [credentials] = await initialCredentials; + + console.log('Credentials acquired:', credentials) + + // Clear sensitive data + delete req.session.pkceCodes; + + await new Promise((resolve, reject) => { + req.session.save((err) => { + if (err) reject(err); + else resolve(); + }); + }); + + res.json({ message: 'Authentication successful' }); + } catch (error) { + console.error('Token acquisition failed:', error); + res.status(500).send('Failed to acquire token'); + } +}); + +const PORT = process.env.PORT || 3000; +app.listen(PORT, () => { + console.log(`Server running on port ${PORT}`); + console.log(`Login URL: http://localhost:${PORT}/login`); +}); \ No newline at end of file diff --git a/packages/entraid/tsconfig.json b/packages/entraid/tsconfig.json new file mode 100644 index 0000000000..3efd3ae061 --- /dev/null +++ b/packages/entraid/tsconfig.json @@ -0,0 +1,23 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist" + }, + "include": [ + "./samples/**/*.ts", + "./lib/**/*.ts" + ], + "exclude": [ + "./lib/test-utils.ts", + "./lib/**/*.spec.ts", + "./lib/sentinel/test-util.ts" + ], + "typedocOptions": { + "entryPoints": [ + "./index.ts", + "./lib" + ], + "entryPointStrategy": "expand", + "out": "../../documentation/entraid" + } +} diff --git a/tsconfig.json b/tsconfig.json index 2c2cb84270..ec2b133ac2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -27,6 +27,9 @@ }, { "path": "./packages/authx" + }, + { + "path": "./packages/entraid" } ]