diff --git a/.dockerignore b/.dockerignore index a19600a3..55949ae2 100644 --- a/.dockerignore +++ b/.dockerignore @@ -46,6 +46,7 @@ build .vscode .env .env.example +.envrc LICENSE README.md .dockerignore diff --git a/.env.example b/.env.example index bca50ca5..f53549d4 100644 --- a/.env.example +++ b/.env.example @@ -3,11 +3,18 @@ PORT= LOG_MODE= LOG_LEVEL= LOG_FILE= -TURBO_TOKEN= STORAGE_PROVIDER= STORAGE_PATH= BODY_LIMIT= STORAGE_PATH_USE_TMP_FOLDER= +# Auth +TURBO_TOKEN= +# ...or +JWKS_URL= +JWT_ISSUER= +JWT_AUDIENCE= +JWT_READ_SCOPES= +JWT_WRITE_SCOPES= # AWS S3 Storage Provider AWS_ACCESS_KEY_ID= AWS_SECRET_ACCESS_KEY= diff --git a/.gitignore b/.gitignore index 90d93efd..5405cfc3 100644 --- a/.gitignore +++ b/.gitignore @@ -64,3 +64,4 @@ junit-testresults.xml # Misc .DS_Store build +.envrc diff --git a/docs/environment-variables.md b/docs/environment-variables.md index 1ba2069e..4eda97eb 100644 --- a/docs/environment-variables.md +++ b/docs/environment-variables.md @@ -10,11 +10,17 @@ nav_order: 2 | -- | -- | -- | -- | -- | | `NODE_ENV` | string | optional | `production` | Possible values are `development` or `production`| | `PORT` | number | optional | `3000` | | -| `TURBO_TOKEN` | string | mandatory | | Secret token used for the authentication. You can specify multiple tokens separated by comma (e.g. `TURBO_TOKEN=token1,token2,token3`). The value must be the same one provided for the `token` parameter of the `build` script. See enable [custom remote caching](https://ducktors.github.io/turborepo-remote-cache/custom-remote-caching) in a Turborepo monorepo | -| `LOG_LEVEL` | string | optional | Possibile values are [one of these](https://github.com/ducktors/turborepo-remote-cache/blob/main/src/logger.ts#L3) | `'info'` | -| `LOG_MODE` | string | optional | Setting it to 'file' enables writing logs to file | `stdout` | -| `LOG_FILE` | string | optional | Path and file name where save .log file (e.g. /path/to/my/file.log) | `server.log` | -| `STORAGE_PROVIDER` | string | optional | Possible values are `local`, `s3`, `google-cloud-storage` or `azure-blob-storage`. Use this var to choose the storage provider. | `local` | +| `TURBO_TOKEN` | string | optional | | Secret token used for the authentication. Required if `AUTH_MODE` is undefined or `static`. You can specify multiple tokens separated by comma (e.g. `TURBO_TOKEN=token1,token2,token3`). The value must be the same one provided for the `token` parameter of the `build` script. See enable [custom remote caching](https://ducktors.github.io/turborepo-remote-cache/custom-remote-caching) in a Turborepo monorepo | +| `AUTH_MODE` | string | optional | `static` | Which authentication mode to use, possible values are `static` or `jwt`| +| `JWKS_URL` | string | optional | | JWKS metadata url for retrieving public keys for verifying JWTs| +| `JWT_ISSUER` | string | optional | | JWT Issuer, optional even if using JWT authentication, to match `iss` field in JWT. +| `JWT_AUDIENCE` | string | optional | | JWT Audience, optional even if using JWT authentication, to match `aud` field in JWT. +| `JWT_READ_SCOPES` | string | optional | | If specified, one of the scopes listed here must be present in order to read from the cache. You can specify multiple options with a comma-delimited string of scopes. +| `JWT_WRITE_SCOPES` | string | optional | | If specified, one of the scopes listed here must be present in order to write to the cache. You can specify multiple options with a comma-delimited string of scopes. +| `LOG_LEVEL` | string | optional | `'info'` | Possibile values are [one of these](https://github.com/ducktors/turborepo-remote-cache/blob/main/src/logger.ts#L3) | +| `LOG_MODE` | string | optional | `stdout` | Setting it to 'file' enables writing logs to file | +| `LOG_FILE` | string | optional | `server.log` | Path and file name where save .log file (e.g. /path/to/my/file.log) | +| `STORAGE_PROVIDER` | string | optional | `local` | Possible values are `local`, `s3`, `google-cloud-storage` or `azure-blob-storage`. Use this var to choose the storage provider. | | `STORAGE_PATH` | string | optional | | Caching folder under `/tmp` if `STORAGE_PROVIDER` is set to `local`. If `STORAGE_PROVIDER` is set to `s3`, `google-cloud-storage` or `azure-blob-storage`, this will be the name of the bucket. | | `STORAGE_PATH_USE_TMP_FOLDER` | boolean | optional | `true` | Uses the system tmp folder as a prefix to `STORAGE_PATH` | | `BODY_LIMIT` | number | optional | `104857600` | The limit for artifact upload size | diff --git a/package.json b/package.json index a7516c34..d7256a0a 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "dependencies": { "@azure/storage-blob": "^12.23.0", "@fastify/aws-lambda": "^5.0.0", + "@fastify/jwt": "9.0.1", "@google-cloud/storage": "6.9.2", "@hapi/boom": "10.0.0", "@sinclair/typebox": "0.25.21", @@ -55,6 +56,7 @@ "close-with-grace": "1.1.0", "env-schema": "5.2.0", "fastify": "5.1.0", + "fastify-jwt-jwks": "^2.0.0", "fastify-plugin": "5.0.1", "fs-blob-store": "6.0.0", "hyperid": "3.1.1", @@ -75,6 +77,7 @@ "@semantic-release/npm": "^12.0.1", "@semantic-release/release-notes-generator": "^14.0.1", "@types/node": "^20.6.3", + "@types/s3rver": "^3.7.4", "c8": "^9.0.0", "commitizen": "^4.3.1", "commitlint-config-cz": "^0.13.3", @@ -82,11 +85,12 @@ "cz-conventional-changelog": "^3.3.0", "fastify-tsconfig": "^2.0.0", "husky": "^8.0.3", + "mock-jwks": "^3.2.2", "npm-run-all": "^4.1.5", "rimraf": "^4.1.2", "s3rver": "^3.7.1", "semantic-release": "^22.0.12", - "tsx": "^4.7.0", + "tsx": "^4.19.2", "typescript": "^5.2.2" }, "bugs": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e4af2e71..05428040 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: '@fastify/aws-lambda': specifier: ^5.0.0 version: 5.0.0 + '@fastify/jwt': + specifier: 9.0.1 + version: 9.0.1 '@google-cloud/storage': specifier: 6.9.2 version: 6.9.2 @@ -38,6 +41,9 @@ importers: fastify: specifier: 5.1.0 version: 5.1.0 + fastify-jwt-jwks: + specifier: ^2.0.0 + version: 2.0.0 fastify-plugin: specifier: 5.0.1 version: 5.0.1 @@ -93,6 +99,9 @@ importers: '@types/node': specifier: ^20.6.3 version: 20.6.3 + '@types/s3rver': + specifier: ^3.7.4 + version: 3.7.4 c8: specifier: ^9.0.0 version: 9.0.0 @@ -114,6 +123,9 @@ importers: husky: specifier: ^8.0.3 version: 8.0.3 + mock-jwks: + specifier: ^3.2.2 + version: 3.2.2(@types/node@20.6.3)(typescript@5.2.2) npm-run-all: specifier: ^4.1.5 version: 4.1.5 @@ -127,8 +139,8 @@ importers: specifier: ^22.0.12 version: 22.0.12 tsx: - specifier: ^4.7.0 - version: 4.7.0 + specifier: ^4.19.2 + version: 4.19.2 typescript: specifier: ^5.2.2 version: 5.2.2 @@ -255,6 +267,15 @@ packages: cpu: [x64] os: [win32] + '@bundled-es-modules/cookie@2.0.1': + resolution: {integrity: sha512-8o+5fRPLNbjbdGRRmJj3h6Hh1AQJf2dk3qQ/5ZFb+PXkRNiSoMGGUKlsgLfrxneb72axVJyIYji64E2+nNfYyw==} + + '@bundled-es-modules/statuses@1.0.1': + resolution: {integrity: sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg==} + + '@bundled-es-modules/tough-cookie@0.1.6': + resolution: {integrity: sha512-dvMHbL464C0zI+Yqxbz6kZ5TOEp7GLW+pry/RWndAR8MJQAXZ2rPmIs8tziTZjeIyhSNZgZbCePtfSbdWqStJw==} + '@colors/colors@1.5.0': resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} engines: {node: '>=0.1.90'} @@ -364,141 +385,147 @@ packages: peerDependencies: typescript: ^5.2.2 - '@esbuild/aix-ppc64@0.19.11': - resolution: {integrity: sha512-FnzU0LyE3ySQk7UntJO4+qIiQgI7KoODnZg5xzXIrFJlKd2P2gwHsHY4927xj9y5PJmJSzULiUCWmv7iWnNa7g==} - engines: {node: '>=12'} + '@esbuild/aix-ppc64@0.23.1': + resolution: {integrity: sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==} + engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.19.11': - resolution: {integrity: sha512-aiu7K/5JnLj//KOnOfEZ0D90obUkRzDMyqd/wNAUQ34m4YUPVhRZpnqKV9uqDGxT7cToSDnIHsGooyIczu9T+Q==} - engines: {node: '>=12'} + '@esbuild/android-arm64@0.23.1': + resolution: {integrity: sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==} + engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.19.11': - resolution: {integrity: sha512-5OVapq0ClabvKvQ58Bws8+wkLCV+Rxg7tUVbo9xu034Nm536QTII4YzhaFriQ7rMrorfnFKUsArD2lqKbFY4vw==} - engines: {node: '>=12'} + '@esbuild/android-arm@0.23.1': + resolution: {integrity: sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==} + engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.19.11': - resolution: {integrity: sha512-eccxjlfGw43WYoY9QgB82SgGgDbibcqyDTlk3l3C0jOVHKxrjdc9CTwDUQd0vkvYg5um0OH+GpxYvp39r+IPOg==} - engines: {node: '>=12'} + '@esbuild/android-x64@0.23.1': + resolution: {integrity: sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==} + engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.19.11': - resolution: {integrity: sha512-ETp87DRWuSt9KdDVkqSoKoLFHYTrkyz2+65fj9nfXsaV3bMhTCjtQfw3y+um88vGRKRiF7erPrh/ZuIdLUIVxQ==} - engines: {node: '>=12'} + '@esbuild/darwin-arm64@0.23.1': + resolution: {integrity: sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==} + engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.19.11': - resolution: {integrity: sha512-fkFUiS6IUK9WYUO/+22omwetaSNl5/A8giXvQlcinLIjVkxwTLSktbF5f/kJMftM2MJp9+fXqZ5ezS7+SALp4g==} - engines: {node: '>=12'} + '@esbuild/darwin-x64@0.23.1': + resolution: {integrity: sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==} + engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.19.11': - resolution: {integrity: sha512-lhoSp5K6bxKRNdXUtHoNc5HhbXVCS8V0iZmDvyWvYq9S5WSfTIHU2UGjcGt7UeS6iEYp9eeymIl5mJBn0yiuxA==} - engines: {node: '>=12'} + '@esbuild/freebsd-arm64@0.23.1': + resolution: {integrity: sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==} + engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.19.11': - resolution: {integrity: sha512-JkUqn44AffGXitVI6/AbQdoYAq0TEullFdqcMY/PCUZ36xJ9ZJRtQabzMA+Vi7r78+25ZIBosLTOKnUXBSi1Kw==} - engines: {node: '>=12'} + '@esbuild/freebsd-x64@0.23.1': + resolution: {integrity: sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==} + engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.19.11': - resolution: {integrity: sha512-LneLg3ypEeveBSMuoa0kwMpCGmpu8XQUh+mL8XXwoYZ6Be2qBnVtcDI5azSvh7vioMDhoJFZzp9GWp9IWpYoUg==} - engines: {node: '>=12'} + '@esbuild/linux-arm64@0.23.1': + resolution: {integrity: sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==} + engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.19.11': - resolution: {integrity: sha512-3CRkr9+vCV2XJbjwgzjPtO8T0SZUmRZla+UL1jw+XqHZPkPgZiyWvbDvl9rqAN8Zl7qJF0O/9ycMtjU67HN9/Q==} - engines: {node: '>=12'} + '@esbuild/linux-arm@0.23.1': + resolution: {integrity: sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==} + engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.19.11': - resolution: {integrity: sha512-caHy++CsD8Bgq2V5CodbJjFPEiDPq8JJmBdeyZ8GWVQMjRD0sU548nNdwPNvKjVpamYYVL40AORekgfIubwHoA==} - engines: {node: '>=12'} + '@esbuild/linux-ia32@0.23.1': + resolution: {integrity: sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==} + engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.19.11': - resolution: {integrity: sha512-ppZSSLVpPrwHccvC6nQVZaSHlFsvCQyjnvirnVjbKSHuE5N24Yl8F3UwYUUR1UEPaFObGD2tSvVKbvR+uT1Nrg==} - engines: {node: '>=12'} + '@esbuild/linux-loong64@0.23.1': + resolution: {integrity: sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==} + engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.19.11': - resolution: {integrity: sha512-B5x9j0OgjG+v1dF2DkH34lr+7Gmv0kzX6/V0afF41FkPMMqaQ77pH7CrhWeR22aEeHKaeZVtZ6yFwlxOKPVFyg==} - engines: {node: '>=12'} + '@esbuild/linux-mips64el@0.23.1': + resolution: {integrity: sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==} + engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.19.11': - resolution: {integrity: sha512-MHrZYLeCG8vXblMetWyttkdVRjQlQUb/oMgBNurVEnhj4YWOr4G5lmBfZjHYQHHN0g6yDmCAQRR8MUHldvvRDA==} - engines: {node: '>=12'} + '@esbuild/linux-ppc64@0.23.1': + resolution: {integrity: sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==} + engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.19.11': - resolution: {integrity: sha512-f3DY++t94uVg141dozDu4CCUkYW+09rWtaWfnb3bqe4w5NqmZd6nPVBm+qbz7WaHZCoqXqHz5p6CM6qv3qnSSQ==} - engines: {node: '>=12'} + '@esbuild/linux-riscv64@0.23.1': + resolution: {integrity: sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==} + engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.19.11': - resolution: {integrity: sha512-A5xdUoyWJHMMlcSMcPGVLzYzpcY8QP1RtYzX5/bS4dvjBGVxdhuiYyFwp7z74ocV7WDc0n1harxmpq2ePOjI0Q==} - engines: {node: '>=12'} + '@esbuild/linux-s390x@0.23.1': + resolution: {integrity: sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==} + engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.19.11': - resolution: {integrity: sha512-grbyMlVCvJSfxFQUndw5mCtWs5LO1gUlwP4CDi4iJBbVpZcqLVT29FxgGuBJGSzyOxotFG4LoO5X+M1350zmPA==} - engines: {node: '>=12'} + '@esbuild/linux-x64@0.23.1': + resolution: {integrity: sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==} + engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-x64@0.19.11': - resolution: {integrity: sha512-13jvrQZJc3P230OhU8xgwUnDeuC/9egsjTkXN49b3GcS5BKvJqZn86aGM8W9pd14Kd+u7HuFBMVtrNGhh6fHEQ==} - engines: {node: '>=12'} + '@esbuild/netbsd-x64@0.23.1': + resolution: {integrity: sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==} + engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-x64@0.19.11': - resolution: {integrity: sha512-ysyOGZuTp6SNKPE11INDUeFVVQFrhcNDVUgSQVDzqsqX38DjhPEPATpid04LCoUr2WXhQTEZ8ct/EgJCUDpyNw==} - engines: {node: '>=12'} + '@esbuild/openbsd-arm64@0.23.1': + resolution: {integrity: sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.23.1': + resolution: {integrity: sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==} + engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/sunos-x64@0.19.11': - resolution: {integrity: sha512-Hf+Sad9nVwvtxy4DXCZQqLpgmRTQqyFyhT3bZ4F2XlJCjxGmRFF0Shwn9rzhOYRB61w9VMXUkxlBy56dk9JJiQ==} - engines: {node: '>=12'} + '@esbuild/sunos-x64@0.23.1': + resolution: {integrity: sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==} + engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.19.11': - resolution: {integrity: sha512-0P58Sbi0LctOMOQbpEOvOL44Ne0sqbS0XWHMvvrg6NE5jQ1xguCSSw9jQeUk2lfrXYsKDdOe6K+oZiwKPilYPQ==} - engines: {node: '>=12'} + '@esbuild/win32-arm64@0.23.1': + resolution: {integrity: sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==} + engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.19.11': - resolution: {integrity: sha512-6YOrWS+sDJDmshdBIQU+Uoyh7pQKrdykdefC1avn76ss5c+RN6gut3LZA4E2cH5xUEp5/cA0+YxRaVtRAb0xBg==} - engines: {node: '>=12'} + '@esbuild/win32-ia32@0.23.1': + resolution: {integrity: sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==} + engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.19.11': - resolution: {integrity: sha512-vfkhltrjCAb603XaFhqhAF4LGDi2M4OrCRrFusyQ+iTLQ/o60QQXxc9cZC/FFpihBI9N1Grn6SMKVJ4KP7Fuiw==} - engines: {node: '>=12'} + '@esbuild/win32-x64@0.23.1': + resolution: {integrity: sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==} + engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -508,12 +535,18 @@ packages: '@fastify/aws-lambda@5.0.0': resolution: {integrity: sha512-yroOd+VkuNd+OsbcyT0NVgLWMsfsp1S2Uef7y5UomLMlrB5/vxi+BPwMz9hn0EZSHzNk2w4TFwMvu14iC6zX8g==} + '@fastify/cookie@11.0.1': + resolution: {integrity: sha512-n1Ooz4bgQ5LcOlJQboWPfsMNxIrGV0SgU85UkctdpTlCQE0mtA3rlspOPUdqk9ubiiZn053ucnia4DjTquI4/g==} + '@fastify/error@4.0.0': resolution: {integrity: sha512-OO/SA8As24JtT1usTUTKgGH7uLvhfwZPwlptRi2Dp5P4KKmJI3gvsZ8MIHnNwDs4sLf/aai5LzTyl66xr7qMxA==} '@fastify/fast-json-stringify-compiler@5.0.1': resolution: {integrity: sha512-f2d3JExJgFE3UbdFcpPwqNUEoHWmt8pAKf8f+9YuLESdefA0WgqxeT6DrGL4Yrf/9ihXNSKOqpjEmurV405meA==} + '@fastify/jwt@9.0.1': + resolution: {integrity: sha512-+vnlUi7Rwi5lihuPxCIqOzla7C+wk7rIzLf09xlxpwqRKXpun7kgIM6LLc+J1Iv0IidlxdOQmCiXmB52Q74MVA==} + '@fastify/merge-json-schemas@0.1.1': resolution: {integrity: sha512-fERDVz7topgNjtXsJTTW1JKLy0rhuLRcquYqNR9rF7OcVpCa2OVW49ZPDIhaRRCaUuvVxI+N416xUoF76HNSXA==} @@ -539,10 +572,30 @@ packages: '@hapi/hoek@10.0.1': resolution: {integrity: sha512-CvlW7jmOhWzuqOqiJQ3rQVLMcREh0eel4IBnxDx2FAcK8g7qoJRQK4L1CPBASoCY6y8e6zuCy3f2g+HWdkzcMw==} + '@inquirer/confirm@5.0.2': + resolution: {integrity: sha512-KJLUHOaKnNCYzwVbryj3TNBxyZIrr56fR5N45v6K9IPrbT6B7DcudBMfylkV1A8PUdJE15mybkEQyp2/ZUpxUA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + + '@inquirer/core@10.1.0': + resolution: {integrity: sha512-I+ETk2AL+yAVbvuKx5AJpQmoaWhpiTFOg/UJb7ZkMAK4blmtG8ATh5ct+T/8xNld0CZG/2UhtkdMwpgvld92XQ==} + engines: {node: '>=18'} + '@inquirer/figures@1.0.3': resolution: {integrity: sha512-ErXXzENMH5pJt5/ssXV0DfWUZqly8nGzf0UcBV9xTnP+KyffE2mqyxIMBrZ8ijQck2nU0TQm40EQB53YreyWHw==} engines: {node: '>=18'} + '@inquirer/figures@1.0.8': + resolution: {integrity: sha512-tKd+jsmhq21AP1LhexC0pPwsCxEhGgAkg28byjJAd+xhmIs8LUX8JbUc3vBf3PhLxWiB5EvyBE5X7JSPAqMAqg==} + engines: {node: '>=18'} + + '@inquirer/type@3.0.1': + resolution: {integrity: sha512-+ksJMIy92sOAiAccGpcKZUc3bYO07cADnscIxHBknEm3uNts3movSmBofc1908BNy5edKscxYeAdaX1NXkHS6A==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + '@istanbuljs/schema@0.1.3': resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} engines: {node: '>=8'} @@ -569,6 +622,14 @@ packages: resolution: {integrity: sha512-/gKJun8NNiWGZJkGzI/Ragc53cOdcLNdzjLaIa+GEjguQs0ulsurx8WN0jijdK9yPqDvziX995sMRLyLt1uZMQ==} engines: {node: '>= 0.4'} + '@lukeed/ms@2.0.2': + resolution: {integrity: sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==} + engines: {node: '>=8'} + + '@mswjs/interceptors@0.37.1': + resolution: {integrity: sha512-SvE+tSpcX884RJrPCskXxoS965Ky/pYABDEhWW6oeSRhpUDLrS5nTvT5n1LLSDVDYvty4imVmXsy+3/ROVuknA==} + engines: {node: '>=18'} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -683,6 +744,15 @@ packages: '@octokit/types@13.5.0': resolution: {integrity: sha512-HdqWTf5Z3qwDVlzCrP8UJquMwunpDiMPt5er+QjGzL4hqr/vBVY/MauQgS1xWxCDT1oMx1EULyqxncdCY/NVSQ==} + '@open-draft/deferred-promise@2.2.0': + resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==} + + '@open-draft/logger@0.3.0': + resolution: {integrity: sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==} + + '@open-draft/until@2.1.0': + resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} + '@pnpm/network.ca-file@1.0.2': resolution: {integrity: sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==} engines: {node: '>=12.22.0'} @@ -796,6 +866,9 @@ packages: '@types/conventional-commits-parser@5.0.1': resolution: {integrity: sha512-7uz5EHdzz2TqoMfV7ee61Egf5y6NkcO4FB/1iCCQnbeiI1F3xzv3vK5dBCXUCLQgGYS+mUeigK1iKQzvED+QnQ==} + '@types/cookie@0.6.0': + resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + '@types/istanbul-lib-coverage@2.0.4': resolution: {integrity: sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==} @@ -811,9 +884,18 @@ packages: '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} + '@types/s3rver@3.7.4': + resolution: {integrity: sha512-CMCmdNszxS2FsIznWvBMVCl6fpvr5ueaFCaY0iSoH7Ud5maGcLghukpDvsXBnIcp92cv2HeVnVqI1p8yPcab9Q==} + '@types/semver@7.5.8': resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==} + '@types/statuses@2.0.5': + resolution: {integrity: sha512-jmIUGWrAiwu3dZpxntxieC+1n/5c3mjrImkmOSQ2NC5uP6cYO4aAZDdSmRcI5C1oiTmqlZGHC+/NmJrKogbP5A==} + + '@types/tough-cookie@4.0.5': + resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} + JSONStream@1.3.5: resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==} hasBin: true @@ -911,6 +993,12 @@ packages: resolution: {integrity: sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==} engines: {node: '>=8'} + asn1.js@5.4.1: + resolution: {integrity: sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==} + + asn1@0.2.6: + resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==} + async-retry@1.3.3: resolution: {integrity: sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==} @@ -942,6 +1030,10 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + base64-url@2.3.3: + resolution: {integrity: sha512-dLMhIsK7OplcDauDH/tZLvK7JmUZK3A7KiQpjNzsBrM6Etw7hzNI1tLEywqJk9NnwkgWuFKSlx/IUO7vF6Mo8Q==} + engines: {node: '>=6'} + before-after-hook@2.2.3: resolution: {integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==} @@ -954,6 +1046,9 @@ packages: bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + bn.js@4.12.1: + resolution: {integrity: sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==} + bottleneck@2.19.5: resolution: {integrity: sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==} @@ -1191,6 +1286,10 @@ packages: convert-source-map@1.9.0: resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + cookie@1.0.2: resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==} engines: {node: '>=18'} @@ -1411,9 +1510,9 @@ packages: resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} engines: {node: '>= 0.4'} - esbuild@0.19.11: - resolution: {integrity: sha512-HJ96Hev2hX/6i5cDVwcqiJBBtuo9+FeIJOtZ9W1kA5M6AMJRHUZlpYZ1/SbEwtO0ioNAW8rUooVpC/WehY2SfA==} - engines: {node: '>=12'} + esbuild@0.23.1: + resolution: {integrity: sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==} + engines: {node: '>=18'} hasBin: true escalade@3.1.1: @@ -1487,6 +1586,10 @@ packages: fast-json-stringify@6.0.0: resolution: {integrity: sha512-FGMKZwniMTgZh7zQp9b6XnBVxUmKVahQLQeRQHqwYmPDqDhcEKZ3BaQsxelFFI5PY7nN71OEeiL47/zUWcYe1A==} + fast-jwt@4.0.5: + resolution: {integrity: sha512-QnpNdn0955GT7SlT8iMgYfhTsityUWysrQjM+Q7bGFijLp6+TNWzlbSMPvgalbrQGRg4ZaHZgMcns5fYOm5avg==} + engines: {node: '>=16'} + fast-querystring@1.1.1: resolution: {integrity: sha512-qR2r+e3HvhEFmpdHMv//U8FnFlnYjaC6QKDuaXALDkw2kvHO8WDjxH+f/rHGR4Me4pnk8p9JAkRNTjYHAKRn2Q==} @@ -1514,6 +1617,14 @@ packages: resolution: {integrity: sha512-kLY3jFlwIYwBNDojclKsNAC12sfD6NwW74QB2CoNGPvtVxjliYehVunB3HYyNi+n4Tt1dAcgwYvmKF/Z18flqg==} hasBin: true + fastfall@1.5.1: + resolution: {integrity: sha512-KH6p+Z8AKPXnmA7+Iz2Lh8ARCMr+8WNPVludm1LGkZoD2MjY6LVnRMtTKhkdzI+jr0RzQWXKzKyBJm1zoHEL4Q==} + engines: {node: '>=0.10.0'} + + fastify-jwt-jwks@2.0.0: + resolution: {integrity: sha512-RWoxwkEOZv/kEEM/Y10BlBi+KTxLx9DJiSvBO17BZuDXK53pMwrety0ESx+VFs1inICTXOOd68D0xf0d4ere9w==} + engines: {node: '>= 20'} + fastify-plugin@5.0.1: resolution: {integrity: sha512-HCxs+YnRaWzCl+cWRYFnHmeRFyR5GVnJTAaCJQiYzQSDwK9MgJdyAsuL3nh0EWRCYMgQ5MeziymvmAhUHYHDUQ==} @@ -1524,12 +1635,15 @@ packages: fastify@5.1.0: resolution: {integrity: sha512-0SdUC5AoiSgMSc2Vxwv3WyKzyGMDJRAW/PgNsK1kZrnkO6MeqUIW9ovVg9F2UGIqtIcclYMyeJa4rK6OZc7Jxg==} - fastq@1.15.0: - resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==} + fastparallel@2.4.1: + resolution: {integrity: sha512-qUmhxPgNHmvRjZKBFUNI0oZuuH9OlSIOXmJ98lhKPxMZZ7zS/Fi0wRHOihDSz0R1YiIOjxzOY4bq65YTcdBi2Q==} fastq@1.17.1: resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} + fastseries@1.7.2: + resolution: {integrity: sha512-dTPFrPGS8SNSzAt7u/CbMKCJ3s01N04s4JFbORHcmyvVfVKmbhMD1VtRbh5enGHxkaQDqWyLefiKOGGmohGDDQ==} + fecha@4.2.3: resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} @@ -1683,8 +1797,8 @@ packages: resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==} engines: {node: '>= 0.4'} - get-tsconfig@4.7.2: - resolution: {integrity: sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==} + get-tsconfig@4.8.1: + resolution: {integrity: sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==} git-log-parser@1.2.0: resolution: {integrity: sha512-rnCVNfkTL8tdNryFuaY0fYiBWEBcgF748O6ZI61rslBvr2o7U65c2/6npCRqH40vuAhtgtDiqLTJjBVdrejCzA==} @@ -1753,6 +1867,10 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + graphql@16.9.0: + resolution: {integrity: sha512-GGTKBX4SD7Wdb8mqeDLni2oaRGYQWjWHGKPQ24ZMnUtKfcsVoiv4uX8+LJr1K6U5VW2Lu1BwJnj7uiori0YtRw==} + engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + gtoken@6.1.2: resolution: {integrity: sha512-4ccGpzz7YAr7lxrT2neugmXQ3hP9ho2gcaityLVkiUecAiwiy60Ii8gRbZeOsXV19fYaRjgBSshs8kXw+NKCPQ==} engines: {node: '>=12.0.0'} @@ -1803,6 +1921,9 @@ packages: resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} hasBin: true + headers-polyfill@4.0.3: + resolution: {integrity: sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==} + help-me@5.0.0: resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==} @@ -1832,6 +1953,10 @@ packages: resolution: {integrity: sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==} engines: {node: '>= 0.6'} + http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + http-proxy-agent@5.0.0: resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} engines: {node: '>= 6'} @@ -2005,6 +2130,9 @@ packages: resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==} engines: {node: '>= 0.4'} + is-node-process@1.2.0: + resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==} + is-number-object@1.0.7: resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} engines: {node: '>= 0.4'} @@ -2159,9 +2287,19 @@ packages: resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} engines: {'0': node >= 0.2.0} + jsonwebtoken@9.0.2: + resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==} + engines: {node: '>=12', npm: '>=6'} + + jwa@1.4.1: + resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==} + jwa@2.0.0: resolution: {integrity: sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==} + jws@3.2.2: + resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} + jws@4.0.0: resolution: {integrity: sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==} @@ -2228,6 +2366,18 @@ packages: lodash.escaperegexp@4.1.2: resolution: {integrity: sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==} + lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + + lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + + lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + + lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + lodash.isplainobject@4.0.6: resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} @@ -2246,6 +2396,9 @@ packages: lodash.mergewith@4.6.2: resolution: {integrity: sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==} + lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + lodash.snakecase@4.1.1: resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==} @@ -2370,6 +2523,9 @@ packages: resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} engines: {node: '>=12'} + minimalistic-assert@1.0.1: + resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -2390,12 +2546,29 @@ packages: mkdirp-classic@0.5.3: resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + mnemonist@0.39.8: + resolution: {integrity: sha512-vyWo2K3fjrUw8YeeZ1zF0fy6Mu59RHokURlld8ymdUPjMlD9EC9ov1/YPqTgqRvUN9nTr3Gqfz29LYAmu0PHPQ==} + + mock-jwks@3.2.2: + resolution: {integrity: sha512-FEeBqYzO/pPuVe9LuaIn2oR6mzgxfQylLARMDcpQyHQOOQIrw53SEM3FdH26126RfwTfqO93kcr5hKpKxtF4rQ==} + engines: {node: '>=14.16'} + ms@2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + msw@2.6.5: + resolution: {integrity: sha512-PnlnTpUlOrj441kYQzzFhzMzMCGFT6a2jKUBG7zSpLkYS5oh8Arrbc0dL8/rNAtxaoBy0EVs2mFqj2qdmWK7lQ==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + typescript: '>= 4.8.x' + peerDependenciesMeta: + typescript: + optional: true + mute-stream@0.0.8: resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} @@ -2403,6 +2576,10 @@ packages: resolution: {integrity: sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + mute-stream@2.0.0: + resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} + engines: {node: ^18.17.0 || >=20.5.0} + negotiator@0.6.3: resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} engines: {node: '>= 0.6'} @@ -2416,6 +2593,10 @@ packages: nice-try@1.0.5: resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==} + node-cache@5.1.2: + resolution: {integrity: sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==} + engines: {node: '>= 8.0.0'} + node-emoji@2.1.3: resolution: {integrity: sha512-E2WEOVsgs7O16zsURJ/eH8BqhF029wGpEOnv7Urwdo2wmQanOACwJQh0devF9D9RhoZru0+9JXIS0dBXIAz+lA==} engines: {node: '>=18'} @@ -2433,6 +2614,9 @@ packages: resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==} engines: {node: '>= 6.13.0'} + node-rsa@1.1.1: + resolution: {integrity: sha512-Jd4cvbJMryN21r5HgxQOpMEqv+ooke/korixNNK3mGqfGJmy0M77WDDzo/05969+OkMy3XW1UuZsSmW9KQm7Fw==} + normalize-package-data@2.5.0: resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} @@ -2620,6 +2804,9 @@ packages: resolution: {integrity: sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==} engines: {node: '>= 0.4'} + obliterator@2.0.4: + resolution: {integrity: sha512-lgHwxlxV1qIg1Eap7LgIeoBWIMFibOjbrYPIPJZcI1mmGAI2m3lNYpK12Y+GBdPQ0U1hRwSord7GIaawz962qQ==} + on-exit-leak-free@2.1.0: resolution: {integrity: sha512-VuCaZZAjReZ3vUwgOB8LxAosIurDiAW0s13rI1YwmaP++jvcxP77AWoQvenZebpCA2m8WC1/EosPYPMjnRAp/w==} @@ -2652,6 +2839,9 @@ packages: resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} engines: {node: '>=0.10.0'} + outvariant@1.4.3: + resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} + p-each-series@3.0.0: resolution: {integrity: sha512-lastgtAdoH9YaLyDa5i5z64q+kzOcQHsQ5SsZJD3q0VEyI8mq872S3geuNbRUQLVAE9siMfgKrpj7MloKFHruw==} engines: {node: '>=12'} @@ -2837,6 +3027,9 @@ packages: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} + psl@1.11.0: + resolution: {integrity: sha512-pjFdcBXT4g061k/SQkzNCRnav+1RdIOgrcX8hs5eL3CEQcFZP9qT8T1RWYxGKT11rH1DdIW+kJRfCYykBJuerQ==} + pump@3.0.0: resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} @@ -2847,11 +3040,18 @@ packages: resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} engines: {node: '>=6'} + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + querystring@0.2.0: resolution: {integrity: sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==} engines: {node: '>=0.4.x'} deprecated: The querystring API is considered Legacy. new code should use the URLSearchParams API instead. + querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -2913,6 +3113,9 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} + requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + resolve-dir@1.0.1: resolution: {integrity: sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==} engines: {node: '>=0.10.0'} @@ -3147,6 +3350,9 @@ packages: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} + steed@1.1.3: + resolution: {integrity: sha512-EUkci0FAUiE4IvGTSKcDJIQ/eRUP2JJb56+fvZ4sdnguLTqIdKjSxUe138poW8mkvKWXW2sFPrgTsxqoISnmoA==} + stream-combiner2@1.1.1: resolution: {integrity: sha512-3PnJbYgS56AeWgtKF5jtJRT6uFJe56Z0Hc5Ngg/6sI6rIt8iiMBTa9cvdyFfpMQjaVHr8dusbNeFGIIonxOvKw==} @@ -3160,6 +3366,9 @@ packages: resolution: {integrity: sha512-jos8u++JKm0ARcSUTAZXOVC0mSox7Bhn6sBgty73P1f3JGf7yG2clTbBNHUdde/kdvP2FESam+vM6l8jBrNxHA==} engines: {node: '>=0.8.0'} + strict-event-emitter@0.5.1: + resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -3285,6 +3494,10 @@ packages: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} + tough-cookie@4.1.4: + resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} + engines: {node: '>=6'} + tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} @@ -3318,8 +3531,8 @@ packages: resolution: {integrity: sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==} engines: {node: '>=0.6.x'} - tsx@4.7.0: - resolution: {integrity: sha512-I+t79RYPlEYlHn9a+KzwrvEwhJg35h/1zHsLC2JXvhC2mdynMv6Zxzvhv5EMV6VF5qJlLlkSnMVvdZV3PSIGcg==} + tsx@4.19.2: + resolution: {integrity: sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g==} engines: {node: '>=18.0.0'} hasBin: true @@ -3343,6 +3556,10 @@ packages: resolution: {integrity: sha512-MBh+PHUHHisjXf4tlx0CFWoMdjx8zCMLJHOjnV1prABYZFHqtFOyauCIK2/7w4oIfwkF8iNhLtnJEfVY2vn3iw==} engines: {node: '>=16'} + type-fest@4.27.0: + resolution: {integrity: sha512-3IMSWgP7C5KSQqmo1wjhKrwsvXAtF33jO3QY+Uy++ia7hqvgSK6iXbbg5PbDBc1P2ZbNEDgejOrN4YooXvhwCw==} + engines: {node: '>=16'} + type-fest@4.29.0: resolution: {integrity: sha512-RPYt6dKyemXJe7I6oNstcH24myUGSReicxcHTvCLgzm4e0n8y05dGvcGB15/SoPRBmhlMthWQ9pvKyL81ko8nQ==} engines: {node: '>=16'} @@ -3389,6 +3606,10 @@ packages: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} + universalify@0.2.0: + resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} + engines: {node: '>= 4.0.0'} + universalify@2.0.0: resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==} engines: {node: '>= 10.0.0'} @@ -3400,6 +3621,9 @@ packages: resolution: {integrity: sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + url@0.10.3: resolution: {integrity: sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ==} @@ -3531,6 +3755,10 @@ packages: resolution: {integrity: sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==} engines: {node: '>=12.20'} + yoctocolors-cjs@2.1.2: + resolution: {integrity: sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==} + engines: {node: '>=18'} + yoctocolors@2.0.2: resolution: {integrity: sha512-Ct97huExsu7cWeEjmrXlofevF8CvzUglJ4iGUet5B8xn1oumtAZBpHU4GzYuoE6PVqcZ5hghtBrSlhwHuR1Jmw==} engines: {node: '>=18'} @@ -3685,6 +3913,19 @@ snapshots: '@biomejs/cli-win32-x64@1.2.2': optional: true + '@bundled-es-modules/cookie@2.0.1': + dependencies: + cookie: 0.7.2 + + '@bundled-es-modules/statuses@1.0.1': + dependencies: + statuses: 2.0.1 + + '@bundled-es-modules/tough-cookie@0.1.6': + dependencies: + '@types/tough-cookie': 4.0.5 + tough-cookie: 4.1.4 + '@colors/colors@1.5.0': {} '@commitlint/cli@19.6.0(@types/node@20.6.3)(typescript@5.2.2)': @@ -3869,73 +4110,76 @@ snapshots: fastify-tsconfig: 2.0.0 typescript: 5.2.2 - '@esbuild/aix-ppc64@0.19.11': + '@esbuild/aix-ppc64@0.23.1': optional: true - '@esbuild/android-arm64@0.19.11': + '@esbuild/android-arm64@0.23.1': optional: true - '@esbuild/android-arm@0.19.11': + '@esbuild/android-arm@0.23.1': optional: true - '@esbuild/android-x64@0.19.11': + '@esbuild/android-x64@0.23.1': optional: true - '@esbuild/darwin-arm64@0.19.11': + '@esbuild/darwin-arm64@0.23.1': optional: true - '@esbuild/darwin-x64@0.19.11': + '@esbuild/darwin-x64@0.23.1': optional: true - '@esbuild/freebsd-arm64@0.19.11': + '@esbuild/freebsd-arm64@0.23.1': optional: true - '@esbuild/freebsd-x64@0.19.11': + '@esbuild/freebsd-x64@0.23.1': optional: true - '@esbuild/linux-arm64@0.19.11': + '@esbuild/linux-arm64@0.23.1': optional: true - '@esbuild/linux-arm@0.19.11': + '@esbuild/linux-arm@0.23.1': optional: true - '@esbuild/linux-ia32@0.19.11': + '@esbuild/linux-ia32@0.23.1': optional: true - '@esbuild/linux-loong64@0.19.11': + '@esbuild/linux-loong64@0.23.1': optional: true - '@esbuild/linux-mips64el@0.19.11': + '@esbuild/linux-mips64el@0.23.1': optional: true - '@esbuild/linux-ppc64@0.19.11': + '@esbuild/linux-ppc64@0.23.1': optional: true - '@esbuild/linux-riscv64@0.19.11': + '@esbuild/linux-riscv64@0.23.1': optional: true - '@esbuild/linux-s390x@0.19.11': + '@esbuild/linux-s390x@0.23.1': optional: true - '@esbuild/linux-x64@0.19.11': + '@esbuild/linux-x64@0.23.1': optional: true - '@esbuild/netbsd-x64@0.19.11': + '@esbuild/netbsd-x64@0.23.1': optional: true - '@esbuild/openbsd-x64@0.19.11': + '@esbuild/openbsd-arm64@0.23.1': optional: true - '@esbuild/sunos-x64@0.19.11': + '@esbuild/openbsd-x64@0.23.1': optional: true - '@esbuild/win32-arm64@0.19.11': + '@esbuild/sunos-x64@0.23.1': optional: true - '@esbuild/win32-ia32@0.19.11': + '@esbuild/win32-arm64@0.23.1': optional: true - '@esbuild/win32-x64@0.19.11': + '@esbuild/win32-ia32@0.23.1': + optional: true + + '@esbuild/win32-x64@0.23.1': optional: true '@fastify/ajv-compiler@4.0.1': @@ -3946,12 +4190,25 @@ snapshots: '@fastify/aws-lambda@5.0.0': {} + '@fastify/cookie@11.0.1': + dependencies: + cookie: 1.0.2 + fastify-plugin: 5.0.1 + '@fastify/error@4.0.0': {} '@fastify/fast-json-stringify-compiler@5.0.1': dependencies: fast-json-stringify: 6.0.0 + '@fastify/jwt@9.0.1': + dependencies: + '@fastify/error': 4.0.0 + '@lukeed/ms': 2.0.2 + fast-jwt: 4.0.5 + fastify-plugin: 5.0.1 + steed: 1.1.3 + '@fastify/merge-json-schemas@0.1.1': dependencies: fast-deep-equal: 3.1.3 @@ -3994,8 +4251,34 @@ snapshots: '@hapi/hoek@10.0.1': {} + '@inquirer/confirm@5.0.2(@types/node@20.6.3)': + dependencies: + '@inquirer/core': 10.1.0(@types/node@20.6.3) + '@inquirer/type': 3.0.1(@types/node@20.6.3) + '@types/node': 20.6.3 + + '@inquirer/core@10.1.0(@types/node@20.6.3)': + dependencies: + '@inquirer/figures': 1.0.8 + '@inquirer/type': 3.0.1(@types/node@20.6.3) + ansi-escapes: 4.3.2 + cli-width: 4.1.0 + mute-stream: 2.0.0 + signal-exit: 4.1.0 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + yoctocolors-cjs: 2.1.2 + transitivePeerDependencies: + - '@types/node' + '@inquirer/figures@1.0.3': {} + '@inquirer/figures@1.0.8': {} + + '@inquirer/type@3.0.1(@types/node@20.6.3)': + dependencies: + '@types/node': 20.6.3 + '@istanbuljs/schema@0.1.3': {} '@jridgewell/resolve-uri@3.1.0': {} @@ -4027,6 +4310,17 @@ snapshots: dependencies: call-bind: 1.0.7 + '@lukeed/ms@2.0.2': {} + + '@mswjs/interceptors@0.37.1': + dependencies: + '@open-draft/deferred-promise': 2.2.0 + '@open-draft/logger': 0.3.0 + '@open-draft/until': 2.1.0 + is-node-process: 1.2.0 + outvariant: 1.4.3 + strict-event-emitter: 0.5.1 + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -4037,7 +4331,7 @@ snapshots: '@nodelib/fs.walk@1.2.8': dependencies: '@nodelib/fs.scandir': 2.1.5 - fastq: 1.15.0 + fastq: 1.17.1 '@octokit/auth-token@4.0.0': {} @@ -4165,6 +4459,15 @@ snapshots: dependencies: '@octokit/openapi-types': 22.2.0 + '@open-draft/deferred-promise@2.2.0': {} + + '@open-draft/logger@0.3.0': + dependencies: + is-node-process: 1.2.0 + outvariant: 1.4.3 + + '@open-draft/until@2.1.0': {} + '@pnpm/network.ca-file@1.0.2': dependencies: graceful-fs: 4.2.10 @@ -4365,6 +4668,8 @@ snapshots: dependencies: '@types/node': 20.6.3 + '@types/cookie@0.6.0': {} + '@types/istanbul-lib-coverage@2.0.4': {} '@types/node@20.4.7': @@ -4376,8 +4681,16 @@ snapshots: '@types/normalize-package-data@2.4.4': {} + '@types/s3rver@3.7.4': + dependencies: + '@types/node': 20.6.3 + '@types/semver@7.5.8': {} + '@types/statuses@2.0.5': {} + + '@types/tough-cookie@4.0.5': {} + JSONStream@1.3.5: dependencies: jsonparse: 1.3.1 @@ -4466,6 +4779,17 @@ snapshots: arrify@2.0.1: {} + asn1.js@5.4.1: + dependencies: + bn.js: 4.12.1 + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + safer-buffer: 2.1.2 + + asn1@0.2.6: + dependencies: + safer-buffer: 2.1.2 + async-retry@1.3.3: dependencies: retry: 0.13.1 @@ -4500,6 +4824,8 @@ snapshots: base64-js@1.5.1: {} + base64-url@2.3.3: {} + before-after-hook@2.2.3: {} before-after-hook@3.0.2: {} @@ -4512,6 +4838,8 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 + bn.js@4.12.1: {} + bottleneck@2.19.5: {} brace-expansion@1.1.11: @@ -4772,6 +5100,8 @@ snapshots: convert-source-map@1.9.0: {} + cookie@0.7.2: {} + cookie@1.0.2: {} cookies@0.8.0: @@ -5015,31 +5345,32 @@ snapshots: is-date-object: 1.0.5 is-symbol: 1.0.4 - esbuild@0.19.11: + esbuild@0.23.1: optionalDependencies: - '@esbuild/aix-ppc64': 0.19.11 - '@esbuild/android-arm': 0.19.11 - '@esbuild/android-arm64': 0.19.11 - '@esbuild/android-x64': 0.19.11 - '@esbuild/darwin-arm64': 0.19.11 - '@esbuild/darwin-x64': 0.19.11 - '@esbuild/freebsd-arm64': 0.19.11 - '@esbuild/freebsd-x64': 0.19.11 - '@esbuild/linux-arm': 0.19.11 - '@esbuild/linux-arm64': 0.19.11 - '@esbuild/linux-ia32': 0.19.11 - '@esbuild/linux-loong64': 0.19.11 - '@esbuild/linux-mips64el': 0.19.11 - '@esbuild/linux-ppc64': 0.19.11 - '@esbuild/linux-riscv64': 0.19.11 - '@esbuild/linux-s390x': 0.19.11 - '@esbuild/linux-x64': 0.19.11 - '@esbuild/netbsd-x64': 0.19.11 - '@esbuild/openbsd-x64': 0.19.11 - '@esbuild/sunos-x64': 0.19.11 - '@esbuild/win32-arm64': 0.19.11 - '@esbuild/win32-ia32': 0.19.11 - '@esbuild/win32-x64': 0.19.11 + '@esbuild/aix-ppc64': 0.23.1 + '@esbuild/android-arm': 0.23.1 + '@esbuild/android-arm64': 0.23.1 + '@esbuild/android-x64': 0.23.1 + '@esbuild/darwin-arm64': 0.23.1 + '@esbuild/darwin-x64': 0.23.1 + '@esbuild/freebsd-arm64': 0.23.1 + '@esbuild/freebsd-x64': 0.23.1 + '@esbuild/linux-arm': 0.23.1 + '@esbuild/linux-arm64': 0.23.1 + '@esbuild/linux-ia32': 0.23.1 + '@esbuild/linux-loong64': 0.23.1 + '@esbuild/linux-mips64el': 0.23.1 + '@esbuild/linux-ppc64': 0.23.1 + '@esbuild/linux-riscv64': 0.23.1 + '@esbuild/linux-s390x': 0.23.1 + '@esbuild/linux-x64': 0.23.1 + '@esbuild/netbsd-x64': 0.23.1 + '@esbuild/openbsd-arm64': 0.23.1 + '@esbuild/openbsd-x64': 0.23.1 + '@esbuild/sunos-x64': 0.23.1 + '@esbuild/win32-arm64': 0.23.1 + '@esbuild/win32-ia32': 0.23.1 + '@esbuild/win32-x64': 0.23.1 escalade@3.1.1: {} @@ -5132,6 +5463,13 @@ snapshots: json-schema-ref-resolver: 1.0.1 rfdc: 1.4.1 + fast-jwt@4.0.5: + dependencies: + '@lukeed/ms': 2.0.2 + asn1.js: 5.4.1 + ecdsa-sig-formatter: 1.0.11 + mnemonist: 0.39.8 + fast-querystring@1.1.1: dependencies: fast-decode-uri-component: 1.0.1 @@ -5154,6 +5492,21 @@ snapshots: dependencies: strnum: 1.0.5 + fastfall@1.5.1: + dependencies: + reusify: 1.0.4 + + fastify-jwt-jwks@2.0.0: + dependencies: + '@fastify/cookie': 11.0.1 + '@fastify/jwt': 9.0.1 + fastify-plugin: 5.0.1 + http-errors: 2.0.0 + node-cache: 5.1.2 + node-fetch: 2.6.9 + transitivePeerDependencies: + - encoding + fastify-plugin@5.0.1: {} fastify-tsconfig@2.0.0: {} @@ -5176,14 +5529,20 @@ snapshots: semver: 7.6.2 toad-cache: 3.7.0 - fastq@1.15.0: + fastparallel@2.4.1: dependencies: reusify: 1.0.4 + xtend: 4.0.2 fastq@1.17.1: dependencies: reusify: 1.0.4 + fastseries@1.7.2: + dependencies: + reusify: 1.0.4 + xtend: 4.0.2 + fecha@4.2.3: {} figures@2.0.0: @@ -5363,7 +5722,7 @@ snapshots: call-bind: 1.0.2 get-intrinsic: 1.2.0 - get-tsconfig@4.7.2: + get-tsconfig@4.8.1: dependencies: resolve-pkg-maps: 1.0.0 @@ -5474,6 +5833,8 @@ snapshots: graceful-fs@4.2.11: {} + graphql@16.9.0: {} + gtoken@6.1.2: dependencies: gaxios: 5.1.0 @@ -5524,6 +5885,8 @@ snapshots: he@1.2.0: {} + headers-polyfill@4.0.3: {} + help-me@5.0.0: {} homedir-polyfill@1.0.3: @@ -5553,6 +5916,14 @@ snapshots: statuses: 1.5.0 toidentifier: 1.0.1 + http-errors@2.0.0: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + http-proxy-agent@5.0.0: dependencies: '@tootallnate/once': 2.0.0 @@ -5741,6 +6112,8 @@ snapshots: is-negative-zero@2.0.2: {} + is-node-process@1.2.0: {} + is-number-object@1.0.7: dependencies: has-tostringtag: 1.0.0 @@ -5877,12 +6250,36 @@ snapshots: jsonparse@1.3.1: {} + jsonwebtoken@9.0.2: + dependencies: + jws: 3.2.2 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 7.6.2 + + jwa@1.4.1: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + jwa@2.0.0: dependencies: buffer-equal-constant-time: 1.0.1 ecdsa-sig-formatter: 1.0.11 safe-buffer: 5.2.1 + jws@3.2.2: + dependencies: + jwa: 1.4.1 + safe-buffer: 5.2.1 + jws@4.0.0: dependencies: jwa: 2.0.0 @@ -5976,6 +6373,14 @@ snapshots: lodash.escaperegexp@4.1.2: {} + lodash.includes@4.3.0: {} + + lodash.isboolean@3.0.3: {} + + lodash.isinteger@4.0.4: {} + + lodash.isnumber@3.0.3: {} + lodash.isplainobject@4.0.6: {} lodash.isstring@4.0.1: {} @@ -5988,6 +6393,8 @@ snapshots: lodash.mergewith@4.6.2: {} + lodash.once@4.1.1: {} + lodash.snakecase@4.1.1: {} lodash.startcase@4.4.0: {} @@ -6079,6 +6486,8 @@ snapshots: mimic-fn@4.0.0: {} + minimalistic-assert@1.0.1: {} + minimatch@3.1.2: dependencies: brace-expansion: 1.1.11 @@ -6095,14 +6504,56 @@ snapshots: mkdirp-classic@0.5.3: {} + mnemonist@0.39.8: + dependencies: + obliterator: 2.0.4 + + mock-jwks@3.2.2(@types/node@20.6.3)(typescript@5.2.2): + dependencies: + base64-url: 2.3.3 + jsonwebtoken: 9.0.2 + msw: 2.6.5(@types/node@20.6.3)(typescript@5.2.2) + node-forge: 1.3.1 + node-rsa: 1.1.1 + transitivePeerDependencies: + - '@types/node' + - typescript + ms@2.1.2: {} ms@2.1.3: {} + msw@2.6.5(@types/node@20.6.3)(typescript@5.2.2): + dependencies: + '@bundled-es-modules/cookie': 2.0.1 + '@bundled-es-modules/statuses': 1.0.1 + '@bundled-es-modules/tough-cookie': 0.1.6 + '@inquirer/confirm': 5.0.2(@types/node@20.6.3) + '@mswjs/interceptors': 0.37.1 + '@open-draft/deferred-promise': 2.2.0 + '@open-draft/until': 2.1.0 + '@types/cookie': 0.6.0 + '@types/statuses': 2.0.5 + chalk: 4.1.2 + graphql: 16.9.0 + headers-polyfill: 4.0.3 + is-node-process: 1.2.0 + outvariant: 1.4.3 + path-to-regexp: 6.3.0 + strict-event-emitter: 0.5.1 + type-fest: 4.27.0 + yargs: 17.7.2 + optionalDependencies: + typescript: 5.2.2 + transitivePeerDependencies: + - '@types/node' + mute-stream@0.0.8: {} mute-stream@1.0.0: {} + mute-stream@2.0.0: {} + negotiator@0.6.3: {} neo-async@2.6.2: {} @@ -6111,6 +6562,10 @@ snapshots: nice-try@1.0.5: {} + node-cache@5.1.2: + dependencies: + clone: 2.1.2 + node-emoji@2.1.3: dependencies: '@sindresorhus/is': 4.6.0 @@ -6124,6 +6579,10 @@ snapshots: node-forge@1.3.1: {} + node-rsa@1.1.1: + dependencies: + asn1: 0.2.6 + normalize-package-data@2.5.0: dependencies: hosted-git-info: 2.8.9 @@ -6179,6 +6638,8 @@ snapshots: has-symbols: 1.0.3 object-keys: 1.1.1 + obliterator@2.0.4: {} + on-exit-leak-free@2.1.0: {} on-finished@2.4.1: @@ -6217,6 +6678,8 @@ snapshots: os-tmpdir@1.0.2: {} + outvariant@1.4.3: {} + p-each-series@3.0.0: {} p-filter@4.1.0: @@ -6388,6 +6851,10 @@ snapshots: forwarded: 0.2.0 ipaddr.js: 1.9.1 + psl@1.11.0: + dependencies: + punycode: 2.3.1 + pump@3.0.0: dependencies: end-of-stream: 1.4.4 @@ -6397,8 +6864,12 @@ snapshots: punycode@2.3.0: {} + punycode@2.3.1: {} + querystring@0.2.0: {} + querystringify@2.2.0: {} + queue-microtask@1.2.3: {} quick-format-unescaped@4.0.4: {} @@ -6479,6 +6950,8 @@ snapshots: require-from-string@2.0.2: {} + requires-port@1.0.0: {} + resolve-dir@1.0.1: dependencies: expand-tilde: 2.0.2 @@ -6738,6 +7211,14 @@ snapshots: statuses@2.0.1: {} + steed@1.1.3: + dependencies: + fastfall: 1.5.1 + fastparallel: 2.4.1 + fastq: 1.17.1 + fastseries: 1.7.2 + reusify: 1.0.4 + stream-combiner2@1.1.1: dependencies: duplexer2: 0.1.4 @@ -6751,6 +7232,8 @@ snapshots: streamsearch@0.1.2: {} + strict-event-emitter@0.5.1: {} + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -6875,6 +7358,13 @@ snapshots: toidentifier@1.0.1: {} + tough-cookie@4.1.4: + dependencies: + psl: 1.11.0 + punycode: 2.3.0 + universalify: 0.2.0 + url-parse: 1.5.10 + tr46@0.0.3: {} traverse@0.6.7: {} @@ -6925,10 +7415,10 @@ snapshots: tsscmp@1.0.6: {} - tsx@4.7.0: + tsx@4.19.2: dependencies: - esbuild: 0.19.11 - get-tsconfig: 4.7.2 + esbuild: 0.23.1 + get-tsconfig: 4.8.1 optionalDependencies: fsevents: 2.3.3 @@ -6942,6 +7432,8 @@ snapshots: type-fest@4.20.0: {} + type-fest@4.27.0: {} + type-fest@4.29.0: {} type-is@1.6.18: @@ -6981,6 +7473,8 @@ snapshots: universalify@0.1.2: {} + universalify@0.2.0: {} + universalify@2.0.0: {} uri-js@4.4.1: @@ -6989,6 +7483,11 @@ snapshots: url-join@5.0.0: {} + url-parse@1.5.10: + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + url@0.10.3: dependencies: punycode: 1.3.2 @@ -7136,4 +7635,6 @@ snapshots: yocto-queue@1.1.1: {} + yoctocolors-cjs@2.1.2: {} + yoctocolors@2.0.2: {} diff --git a/src/app.ts b/src/app.ts index f8fdf950..093910ae 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,22 +1,24 @@ import { isBoom } from '@hapi/boom' import Fastify, { FastifyInstance, FastifyServerOptions } from 'fastify' import hyperid from 'hyperid' +import { Config } from './env.js' import { logger } from './logger.js' import config from './plugins/config.js' import remoteCache from './plugins/remote-cache/index.js' const uuid = hyperid({ urlSafe: true }) -export function createApp(options: FastifyServerOptions = {}): FastifyInstance { +export function createApp( + options: FastifyServerOptions & { configOverrides?: Partial } = {}, +): FastifyInstance { const app = Fastify({ loggerInstance: logger, genReqId: () => uuid(), ...options, }) - app.register(config).after(() => { + app.register(config, { overrides: options.configOverrides }).after(() => { app.register(remoteCache, { - allowedTokens: [...app.config.TURBO_TOKEN], provider: app.config.STORAGE_PROVIDER, }) }) diff --git a/src/env.ts b/src/env.ts index 10ec60b8..61afbe8e 100644 --- a/src/env.ts +++ b/src/env.ts @@ -25,7 +25,13 @@ const schema = Type.Object( NODE_ENV: Type.Optional( Type.Enum(NODE_ENVS, { default: NODE_ENVS.PRODUCTION }), ), - TURBO_TOKEN: Type.String({ separator: ',' }), + AUTH_MODE: Type.Optional(Type.Enum({ static: 'static', jwt: 'jwt' })), + JWT_AUDIENCE: Type.Optional(Type.String({ separator: ',' })), + JWT_ISSUER: Type.Optional(Type.String()), + JWKS_URL: Type.Optional(Type.String()), + JWT_READ_SCOPES: Type.Optional(Type.String({ separator: ',' })), + JWT_WRITE_SCOPES: Type.Optional(Type.String({ separator: ',' })), + TURBO_TOKEN: Type.Optional(Type.String({ separator: ',' })), PORT: Type.Number({ default: 3000 }), LOG_LEVEL: Type.Optional(Type.String({ default: 'info' })), LOG_MODE: Type.Optional(Type.String({ default: 'stdout' })), @@ -59,17 +65,25 @@ const schema = Type.Object( additionalProperties: false, }, ) -const _env = envSchema>({ - // we call the default method because Ajv provides wrong types. ref https://github.com/ajv-validator/ajv/issues/2132 - ajv: new Ajv.default({ - removeAdditional: true, - useDefaults: true, - coerceTypes: true, - keywords: ['kind', 'RegExp', 'modifier', envSchema.keywords.separator], - }), - dotenv: process.env.NODE_ENV === NODE_ENVS.DEVELOPMENT ? true : false, - schema, -}) + +export type Config = Static +let _env: Config +export function load(overrides?: Partial) { + _env = envSchema>({ + // we call the default method because Ajv provides wrong types. ref https://github.com/ajv-validator/ajv/issues/2132 + ajv: new Ajv.default({ + removeAdditional: true, + useDefaults: true, + coerceTypes: true, + keywords: ['kind', 'RegExp', 'modifier', envSchema.keywords.separator], + }), + data: overrides, + dotenv: process.env.NODE_ENV === NODE_ENVS.DEVELOPMENT ? true : false, + schema, + }) + return _env +} +_env = load() // we export an object so we can mock the env value while testing. In fact exported vars in are not mockable in ESM export const env = { diff --git a/src/plugins/config.ts b/src/plugins/config.ts index 778bcd35..f3c6a0c6 100644 --- a/src/plugins/config.ts +++ b/src/plugins/config.ts @@ -1,15 +1,15 @@ import fp from 'fastify-plugin' -import { env } from '../env.js' +import { Config, load } from '../env.js' -export default fp( - async function (fastify) { - fastify.decorate('config', env.get()) +export default fp<{ overrides?: Partial }>( + async function (fastify, { overrides }) { + fastify.decorate('config', load(overrides)) }, { name: 'config' }, ) declare module 'fastify' { interface FastifyInstance { - config: ReturnType + config: Config } } diff --git a/src/plugins/remote-cache/auth/index.ts b/src/plugins/remote-cache/auth/index.ts new file mode 100644 index 00000000..60a0d717 --- /dev/null +++ b/src/plugins/remote-cache/auth/index.ts @@ -0,0 +1,25 @@ +import { FastifyInstance } from 'fastify' +import fp from 'fastify-plugin' +import jwtAuth from './jwt.js' +import staticAuth from './static.js' + +declare module 'fastify' { + interface RouteOptions { + readonly authorization?: 'read' | 'write' + } +} + +export default fp(async (fastify: FastifyInstance) => { + if (fastify.config.AUTH_MODE === 'jwt') { + fastify.register(jwtAuth) + fastify.log.info('Registered JWT auth') + } else if ( + fastify.config.AUTH_MODE === 'static' || + fastify.config.AUTH_MODE === undefined + ) { + fastify.register(staticAuth) + fastify.log.info('Registered static auth') + } else { + throw new Error('Invalid AUTH_MODE, select either "jwt" or "static"') + } +}) diff --git a/src/plugins/remote-cache/auth/jwt.ts b/src/plugins/remote-cache/auth/jwt.ts new file mode 100644 index 00000000..b800b2b9 --- /dev/null +++ b/src/plugins/remote-cache/auth/jwt.ts @@ -0,0 +1,66 @@ +import { Boom, forbidden, isBoom, unauthorized } from '@hapi/boom' +import { FastifyRequest } from 'fastify' +import { fastifyJwtJwks } from 'fastify-jwt-jwks' +import fp from 'fastify-plugin' + +declare module '@fastify/jwt' { + interface FastifyJWT { + payload: { scope?: string } + user: { scope: Set } + } +} + +export default fp(async (fastify) => { + if (!fastify.config.JWKS_URL) { + throw new Error('Must provide JWKS url when using JWT authentication') + } + + await fastify.register(fastifyJwtJwks, { + audience: fastify.config.JWT_AUDIENCE, + issuer: fastify.config.JWT_ISSUER, + jwksUrl: fastify.config.JWKS_URL, + formatUser(payload) { + return { scope: new Set(payload.scope?.split(' ')) } + }, + }) + const readScopes = [fastify.config.JWT_READ_SCOPES || []].flat() + const writeScopes = [fastify.config.JWT_WRITE_SCOPES || []].flat() + fastify.addHook('onRequest', fastify.authenticate) + fastify.addHook('onRoute', async (route) => { + if ( + route.authorization && + route.authorization === 'read' && + readScopes.length > 0 + ) { + async function authorizeRead(request: FastifyRequest) { + if (!readScopes.some((scope) => request.user.scope.has(scope))) + throw forbidden() + } + route.onRequest = [...[route.onRequest ?? []].flat(), authorizeRead] + } + + if ( + route.authorization && + route.authorization === 'write' && + writeScopes.length > 0 + ) { + async function authorizeWrite(request: FastifyRequest) { + if (!writeScopes.some((scope) => request.user.scope.has(scope))) + throw forbidden() + } + route.onRequest = [...[route.onRequest ?? []].flat(), authorizeWrite] + } + }) + + fastify.setErrorHandler(async (error, req, res) => { + if (isBoom(error)) { + throw error + } else if (error.code?.startsWith('FST_JWT_')) { + throw new Boom(error.message, { + statusCode: error.statusCode || 500, + }) + } else { + throw unauthorized() + } + }) +}) diff --git a/src/plugins/remote-cache/auth/static.ts b/src/plugins/remote-cache/auth/static.ts new file mode 100644 index 00000000..e7514296 --- /dev/null +++ b/src/plugins/remote-cache/auth/static.ts @@ -0,0 +1,26 @@ +import { badRequest, unauthorized } from '@hapi/boom' +import { FastifyInstance } from 'fastify' +import fp from 'fastify-plugin' + +export default fp(async (fastify: FastifyInstance) => { + const allowedTokens = fastify.config.TURBO_TOKEN + if (!(Array.isArray(allowedTokens) && allowedTokens.length)) { + throw new Error( + `'allowedTokens' options must be a string[], ${typeof allowedTokens} provided instead`, + ) + } + const tokens = new Set(allowedTokens) + + fastify.addHook('onRequest', async function (request) { + let authHeader = request.headers.authorization + authHeader = Array.isArray(authHeader) ? authHeader.join() : authHeader + + if (!authHeader) { + throw badRequest('Missing Authorization header') + } + const [, token] = authHeader.split('Bearer ') + if (!tokens.has(token)) { + throw unauthorized('Invalid authorization token') + } + }) +}) diff --git a/src/plugins/remote-cache/index.ts b/src/plugins/remote-cache/index.ts index 539fcfa4..d379fdd8 100644 --- a/src/plugins/remote-cache/index.ts +++ b/src/plugins/remote-cache/index.ts @@ -1,6 +1,6 @@ -import { badRequest, unauthorized } from '@hapi/boom' import { FastifyInstance } from 'fastify' import { STORAGE_PROVIDERS } from '../../env.js' +import auth from './auth/index.js' import { artifactsEvents, getArtifact, @@ -13,22 +13,12 @@ import { createLocation } from './storage/index.js' async function turboRemoteCache( instance: FastifyInstance, options: { - allowedTokens: string[] apiVersion?: `v${number}` provider?: STORAGE_PROVIDERS }, ) { const bodyLimit = instance.config.BODY_LIMIT - const { - allowedTokens, - apiVersion = 'v8', - provider = STORAGE_PROVIDERS.LOCAL, - } = options - if (!(Array.isArray(allowedTokens) && allowedTokens.length)) { - throw new Error( - `'allowedTokens' options must be a string[], ${typeof allowedTokens} provided instead`, - ) - } + const { apiVersion = 'v8', provider = STORAGE_PROVIDERS.LOCAL } = options instance.addContentTypeParser( 'application/octet-stream', @@ -54,23 +44,9 @@ async function turboRemoteCache( }), ) - await instance.register( + instance.register( async function (i) { - const tokens = new Set(allowedTokens) - - i.addHook('onRequest', async function (request) { - let authHeader = request.headers.authorization - authHeader = Array.isArray(authHeader) ? authHeader.join() : authHeader - - if (!authHeader) { - throw badRequest('Missing Authorization header') - } - const [, token] = authHeader.split('Bearer ') - if (!tokens.has(token)) { - throw unauthorized('Invalid authorization token') - } - }) - + await i.register(auth) i.route(getArtifact) i.route(headArtifact) i.route(putArtifact) diff --git a/src/plugins/remote-cache/routes/artifacts-events.ts b/src/plugins/remote-cache/routes/artifacts-events.ts index 258f1313..c0abca33 100644 --- a/src/plugins/remote-cache/routes/artifacts-events.ts +++ b/src/plugins/remote-cache/routes/artifacts-events.ts @@ -12,6 +12,7 @@ export const artifactsEvents: RouteOptions< > = { method: 'POST', url: '/artifacts/events', + authorization: 'read', async handler(_req, reply) { reply.code(200).send({}) }, diff --git a/src/plugins/remote-cache/routes/get-artifact.ts b/src/plugins/remote-cache/routes/get-artifact.ts index d90b4f6b..73066d06 100644 --- a/src/plugins/remote-cache/routes/get-artifact.ts +++ b/src/plugins/remote-cache/routes/get-artifact.ts @@ -24,6 +24,7 @@ export const getArtifact: RouteOptions< exposeHeadRoute: false, url: '/artifacts/:id', schema: artifactsRouteSchema, + authorization: 'read', async handler(req, reply) { const artifactId = req.params.id const team = req.query.teamId ?? req.query.team ?? req.query.slug // turborepo client passes team as slug when --team cli option is used diff --git a/src/plugins/remote-cache/routes/get-status.ts b/src/plugins/remote-cache/routes/get-status.ts index 4e4bdaa8..65d18acd 100644 --- a/src/plugins/remote-cache/routes/get-status.ts +++ b/src/plugins/remote-cache/routes/get-status.ts @@ -30,6 +30,7 @@ export const getStatus: RouteOptions< url: '/artifacts/status', schema: statusRouteSchema, logLevel: 'error', + authorization: 'read', async handler(req, reply) { reply.send({ status: 'enabled', diff --git a/src/plugins/remote-cache/routes/head-artifact.ts b/src/plugins/remote-cache/routes/head-artifact.ts index fb3d7168..fb42be27 100644 --- a/src/plugins/remote-cache/routes/head-artifact.ts +++ b/src/plugins/remote-cache/routes/head-artifact.ts @@ -23,6 +23,7 @@ export const headArtifact: RouteOptions< method: 'HEAD', url: '/artifacts/:id', schema: artifactsRouteSchema, + authorization: 'read', async handler(req, reply) { const artifactId = req.params.id const team = req.query.teamId ?? req.query.team ?? req.query.slug // turborepo client passes team as slug when --team cli option is used diff --git a/src/plugins/remote-cache/routes/put-artifact.ts b/src/plugins/remote-cache/routes/put-artifact.ts index 2d3cc188..9834304c 100644 --- a/src/plugins/remote-cache/routes/put-artifact.ts +++ b/src/plugins/remote-cache/routes/put-artifact.ts @@ -25,6 +25,7 @@ export const putArtifact: RouteOptions< url: '/artifacts/:id', method: 'PUT', schema: artifactsRouteSchema, + authorization: 'write', async handler(req, reply) { const artifactId = req.params.id const team = req.query.teamId ?? req.query.team ?? req.query.slug // turborepo client passes team as slug when --team cli option is used diff --git a/test/google-cloud-storage.ts b/test/google-cloud-storage.ts index 4a3a7872..7de46da9 100644 --- a/test/google-cloud-storage.ts +++ b/test/google-cloud-storage.ts @@ -42,23 +42,19 @@ const commonTestEnv = { STORAGE_PATH: 'turborepo-remote-cache-test', } +Object.assign(process.env, commonTestEnv) + test('Google Cloud Storage', async (t) => { /** * MOCKS */ const testEnv = { - ...commonTestEnv, GCS_PROJECT_ID: 'some-storage', GCS_CLIENT_EMAIL: 'service-account@some-storage.iam.gserviceaccount.com', GCS_PRIVATE_KEY: '-----BEGIN PRIVATE KEY-----\nFooBarKey\n-----END PRIVATE KEY-----\n', } - Object.assign(process.env, testEnv) - const { env } = await import('../src/env.js') - t.mock.method(env, 'get', () => { - return testEnv - }) - const { default: GCS } = await import('@google-cloud/storage') + const GCS = await import('@google-cloud/storage') const mockedGCS = t.mock.getter(GCS, 'Storage', function () { return GCSMock }) @@ -70,13 +66,12 @@ test('Google Cloud Storage', async (t) => { const team = 'superteam' const { createApp } = await import('../src/app.js') - - const app = createApp({ logger: false }) + const app = createApp({ logger: false, configOverrides: testEnv }) await app.ready() await t.test('loads correct env vars', async () => { - assert.equal(app.config.STORAGE_PROVIDER, testEnv.STORAGE_PROVIDER) - assert.equal(app.config.STORAGE_PATH, testEnv.STORAGE_PATH) + assert.equal(app.config.STORAGE_PROVIDER, commonTestEnv.STORAGE_PROVIDER) + assert.equal(app.config.STORAGE_PATH, commonTestEnv.STORAGE_PATH) assert.equal(app.config.GCS_PROJECT_ID, testEnv.GCS_PROJECT_ID) assert.equal(app.config.GCS_CLIENT_EMAIL, testEnv.GCS_CLIENT_EMAIL) assert.equal(app.config.GCS_PRIVATE_KEY, testEnv.GCS_PRIVATE_KEY) @@ -237,23 +232,18 @@ test('Google Cloud Storage', async (t) => { test('Google Cloud Storage ADC', async (t) => { const testEnv = { - ...commonTestEnv, GCS_PROJECT_ID: '', GCS_CLIENT_EMAIL: '', GCS_PRIVATE_KEY: '', } - Object.assign(process.env, testEnv) /** * MOCKS */ - const { default: GCS } = await import('@google-cloud/storage') + + const GCS = await import('@google-cloud/storage') const mockedGCS = t.mock.getter(GCS, 'Storage', function () { return GCSMock }) - const { env } = await import('../src/env.js') - t.mock.method(env, 'get', () => { - return testEnv - }) /** * END MOCKS */ @@ -262,12 +252,12 @@ test('Google Cloud Storage ADC', async (t) => { const team = 'superteam2' const { createApp } = await import('../src/app.js') - const app = createApp({ logger: false }) + const app = createApp({ logger: false, configOverrides: testEnv }) await app.ready() await t.test('loads correct env vars', async () => { - assert.equal(app.config.STORAGE_PROVIDER, testEnv.STORAGE_PROVIDER) - assert.equal(app.config.STORAGE_PATH, testEnv.STORAGE_PATH) + assert.equal(app.config.STORAGE_PROVIDER, commonTestEnv.STORAGE_PROVIDER) + assert.equal(app.config.STORAGE_PATH, commonTestEnv.STORAGE_PATH) assert.equal(app.config.GCS_PROJECT_ID, testEnv.GCS_PROJECT_ID) assert.equal(app.config.GCS_CLIENT_EMAIL, testEnv.GCS_CLIENT_EMAIL) assert.equal(app.config.GCS_PRIVATE_KEY, testEnv.GCS_PRIVATE_KEY) diff --git a/test/jwt-auth.ts b/test/jwt-auth.ts new file mode 100644 index 00000000..4f2e613d --- /dev/null +++ b/test/jwt-auth.ts @@ -0,0 +1,163 @@ +import assert from 'node:assert/strict' +import { randomUUID } from 'node:crypto' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { after, before, describe, test } from 'node:test' +import { createJWKSMock } from 'mock-jwks' + +const jwksUrl = 'http://test.com/.well-known/jwks.json' +const jwksMock = createJWKSMock('http://test.com') +const invalidJwksMock = createJWKSMock('http://other.com') + +const testEnv = { + NODE_ENV: 'test', + PORT: 3000, + LOG_LEVEL: 'info', + LOG_MODE: 'stdout', + LOG_FILE: 'server.log', + STORAGE_PROVIDER: 'local', + STORAGE_PATH: join(tmpdir(), 'turborepo-remote-cache-test'), + AUTH_MODE: 'jwt', + JWKS_URL: jwksUrl, +} + +Object.assign(process.env, testEnv) + +let stopJwks +before(() => { + const stopValid = jwksMock.start() + const stopInvalid = invalidJwksMock.start() + stopJwks = () => { + stopValid() + stopInvalid() + } +}) +after(() => stopJwks?.()) + +describe('JWT auth', async () => { + await test('without authorization scopes configured', async (t) => { + const { createApp } = await import('../src/app.js') + const app = createApp({ logger: false }) + await app.ready() + await t.test('Fails for static/malformed token', async () => { + const resp = await app.inject({ + method: 'GET', + url: '/v8/artifacts/123', + headers: { + authorization: 'Bearer changeme', + }, + query: { + team: 'asd', + }, + }) + assert.equal(resp.statusCode, 401) + }) + await t.test('Fails for invalid token', async () => { + const token = invalidJwksMock.token() + const resp = await app.inject({ + method: 'GET', + url: '/v8/artifacts/123', + headers: { + authorization: `Bearer ${token}`, + }, + query: { + team: 'asd', + }, + }) + assert.equal(resp.statusCode, 401) + }) + await t.test('Valid token', async (t1) => { + const artifactId = randomUUID() + const team = randomUUID() + const token = jwksMock.token() + await t1.test('creates cache entry', async () => { + const response = await app.inject({ + method: 'PUT', + url: `/v8/artifacts/${artifactId}`, + headers: { + authorization: `Bearer ${token}`, + 'content-type': 'application/octet-stream', + }, + query: { + team, + }, + payload: Buffer.from('test cache data'), + }) + assert.equal(response.statusCode, 200) + }) + + await t1.test('fetches artifact', async () => { + const resp = await app.inject({ + method: 'GET', + url: `/v8/artifacts/${artifactId}`, + headers: { + authorization: `Bearer ${token}`, + }, + query: { + team, + }, + }) + assert.equal(resp.statusCode, 200) + }) + }) + }) + await test('with authorization scopes defined', async (t) => { + const { createApp } = await import('../src/app.js') + const app = createApp({ + logger: false, + configOverrides: { + JWT_READ_SCOPES: 'artifacts:read,artifacts:write', + JWT_WRITE_SCOPES: 'artifacts:write', + }, + }) + await app.ready() + const artifactId = randomUUID() + const team = randomUUID() + const token = jwksMock.token({ scope: 'artifacts:write' }) + + await t.test('creates cache entry', async () => { + const response = await app.inject({ + method: 'PUT', + url: `/v8/artifacts/${artifactId}`, + headers: { + authorization: `Bearer ${token}`, + 'content-type': 'application/octet-stream', + }, + query: { + team, + }, + payload: Buffer.from('test cache data'), + }) + assert.equal(response.statusCode, 200) + }) + + await t.test('fetches artifact', async () => { + const resp = await app.inject({ + method: 'GET', + url: `/v8/artifacts/${artifactId}`, + headers: { + authorization: `Bearer ${token}`, + }, + query: { + team, + }, + }) + assert.equal(resp.statusCode, 200) + }) + + await t.test('forbidden with token without scope', async () => { + const token = jwksMock.token() + const resp = await app.inject({ + method: 'GET', + url: `/v8/artifacts/${artifactId}`, + headers: { + authorization: `Bearer ${token}`, + }, + query: { + team, + }, + }) + assert.equal(resp.statusCode, 403) + }) + }) +}) diff --git a/test/minio.ts b/test/minio.ts index 406aca27..bee49cb0 100644 --- a/test/minio.ts +++ b/test/minio.ts @@ -1,8 +1,8 @@ import assert from 'node:assert/strict' import crypto from 'node:crypto' import { tmpdir } from 'node:os' -import { test } from 'node:test' -import S3erver from 's3rver' +import { after, before, describe, test } from 'node:test' +import S3rver from 's3rver' const testEnv = { NODE_ENV: 'test', @@ -16,167 +16,160 @@ const testEnv = { AWS_ACCESS_KEY_ID: 'S3RVER', AWS_SECRET_ACCESS_KEY: 'S3RVER', AWS_REGION: '', - S3_ENDPOINT: 'http://localhost:4568', } Object.assign(process.env, testEnv) -const server = new S3erver({ +const server = new S3rver({ directory: tmpdir(), silent: true, + port: 0, configureBuckets: [ { - name: process.env.STORAGE_PATH, + name: process.env.STORAGE_PATH || '', + configs: [], }, ], }) +before(async (ctx) => { + const address = await server.run() + Object.assign(process.env, { + S3_ENDPOINT: `http://localhost:${address.port}`, + }) +}) + +after((ctx, done) => { + server.close(done) +}) +describe('Minio', async () => { + const artifactId = crypto.randomBytes(20).toString('hex') + const team = 'superteam' -server.run((err) => { - assert.equal(err, null) - test('Minio', async (t) => { - const artifactId = crypto.randomBytes(20).toString('hex') - const team = 'superteam' + const { createApp } = await import('../src/app.js') + const app = createApp({ logger: false }) + await app.ready() - const { createApp } = await import('../src/app.js') - const app = createApp({ logger: false }) - await app.ready() + await test('loads correct env vars', async () => { + assert.equal(app.config.STORAGE_PROVIDER, testEnv.STORAGE_PROVIDER) + assert.equal(app.config.STORAGE_PATH, testEnv.STORAGE_PATH) + assert.equal(app.config.AWS_ACCESS_KEY_ID, testEnv.AWS_ACCESS_KEY_ID) + assert.equal( + app.config.AWS_SECRET_ACCESS_KEY, + testEnv.AWS_SECRET_ACCESS_KEY, + ) + assert.equal(app.config.AWS_REGION, testEnv.AWS_REGION) + }) - await t.test('loads correct env vars', async () => { - assert.equal(app.config.STORAGE_PROVIDER, testEnv.STORAGE_PROVIDER) - assert.equal(app.config.STORAGE_PATH, testEnv.STORAGE_PATH) - assert.equal(app.config.AWS_ACCESS_KEY_ID, testEnv.AWS_ACCESS_KEY_ID) - assert.equal( - app.config.AWS_SECRET_ACCESS_KEY, - testEnv.AWS_SECRET_ACCESS_KEY, - ) - assert.equal(app.config.AWS_REGION, testEnv.AWS_REGION) - assert.equal(app.config.S3_ENDPOINT, testEnv.S3_ENDPOINT) + await test('should return 400 when missing authorization header', async () => { + const response = await app.inject({ + method: 'GET', + url: '/v8/artifacts/not-found', + headers: {}, }) + assert.equal(response.statusCode, 400) + assert.equal(response.json().message, 'Missing Authorization header') + }) - await t.test( - 'should return 400 when missing authorization header', - async () => { - const response = await app.inject({ - method: 'GET', - url: '/v8/artifacts/not-found', - headers: {}, - }) - assert.equal(response.statusCode, 400) - assert.equal(response.json().message, 'Missing Authorization header') + await test('should return 401 when wrong authorization token is provided', async () => { + const response = await app.inject({ + method: 'GET', + url: '/v8/artifacts/not-found', + headers: { + authorization: 'wrong token', }, - ) + }) + assert.equal(response.statusCode, 401) + assert.equal(response.json().message, 'Invalid authorization token') + }) - await t.test( - 'should return 401 when wrong authorization token is provided', - async () => { - const response = await app.inject({ - method: 'GET', - url: '/v8/artifacts/not-found', - headers: { - authorization: 'wrong token', - }, - }) - assert.equal(response.statusCode, 401) - assert.equal(response.json().message, 'Invalid authorization token') + await test('should return 400 when missing team query parameter', async () => { + const response = await app.inject({ + method: 'GET', + url: '/v8/artifacts/not-found', + headers: { + authorization: 'Bearer changeme', }, + }) + assert.equal(response.statusCode, 400) + assert.equal( + response.json().message, + "querystring should have required property 'team'", ) + }) - await t.test( - 'should return 400 when missing team query parameter', - async () => { - const response = await app.inject({ - method: 'GET', - url: '/v8/artifacts/not-found', - headers: { - authorization: 'Bearer changeme', - }, - }) - assert.equal(response.statusCode, 400) - assert.equal( - response.json().message, - "querystring should have required property 'team'", - ) + await test('should return 404 on cache miss', async () => { + const response = await app.inject({ + method: 'GET', + url: '/v8/artifacts/not-found', + headers: { + authorization: 'Bearer changeme', + }, + query: { + team: 'superteam', }, - ) - - await t.test('should return 404 on cache miss', async () => { - const response = await app.inject({ - method: 'GET', - url: '/v8/artifacts/not-found', - headers: { - authorization: 'Bearer changeme', - }, - query: { - team: 'superteam', - }, - }) - assert.equal(response.statusCode, 404) - assert.equal(response.json().message, 'Artifact not found') - }) - - await t.test('should upload an artifact', async () => { - const response = await app.inject({ - method: 'PUT', - url: `/v8/artifacts/${artifactId}`, - headers: { - authorization: 'Bearer changeme', - 'content-type': 'application/octet-stream', - }, - query: { - team, - }, - payload: Buffer.from('test cache data'), - }) - assert.equal(response.statusCode, 200) - assert.deepEqual(response.json(), { urls: [`${team}/${artifactId}`] }) }) + assert.equal(response.statusCode, 404) + assert.equal(response.json().message, 'Artifact not found') + }) - await t.test('should download an artifact', async () => { - const response = await app.inject({ - method: 'GET', - url: `/v8/artifacts/${artifactId}`, - headers: { - authorization: 'Bearer changeme', - }, - query: { - team, - }, - }) - assert.equal(response.statusCode, 200) - assert.deepEqual(response.body, 'test cache data') + await test('should upload an artifact', async () => { + const response = await app.inject({ + method: 'PUT', + url: `/v8/artifacts/${artifactId}`, + headers: { + authorization: 'Bearer changeme', + 'content-type': 'application/octet-stream', + }, + query: { + team, + }, + payload: Buffer.from('test cache data'), }) + assert.equal(response.statusCode, 200) + assert.deepEqual(response.json(), { urls: [`${team}/${artifactId}`] }) + }) - await t.test('should verify artifact exists', async () => { - const response = await app.inject({ - method: 'HEAD', - url: `/v8/artifacts/${artifactId}`, - headers: { - authorization: 'Bearer changeme', - }, - query: { - team, - }, - }) - assert.equal(response.statusCode, 200) - assert.deepEqual(response.body, '') + await test('should download an artifact', async () => { + const response = await app.inject({ + method: 'GET', + url: `/v8/artifacts/${artifactId}`, + headers: { + authorization: 'Bearer changeme', + }, + query: { + team, + }, }) + assert.equal(response.statusCode, 200) + assert.deepEqual(response.body, 'test cache data') + }) - await t.test('should verify artifact does not exist', async () => { - const response = await app.inject({ - method: 'HEAD', - url: '/v8/artifacts/not-found', - headers: { - authorization: 'Bearer changeme', - }, - query: { - team, - }, - }) - assert.equal(response.statusCode, 404) - assert.equal(response.json().message, 'Artifact not found') + await test('should verify artifact exists', async () => { + const response = await app.inject({ + method: 'HEAD', + url: `/v8/artifacts/${artifactId}`, + headers: { + authorization: 'Bearer changeme', + }, + query: { + team, + }, }) + assert.equal(response.statusCode, 200) + assert.deepEqual(response.body, '') + }) - t.after(() => { - server.close() + await test('should verify artifact does not exist', async () => { + const response = await app.inject({ + method: 'HEAD', + url: '/v8/artifacts/not-found', + headers: { + authorization: 'Bearer changeme', + }, + query: { + team, + }, }) + assert.equal(response.statusCode, 404) + assert.equal(response.json().message, 'Artifact not found') }) }) diff --git a/test/s3.ts b/test/s3.ts index 0e81b8fa..21ded003 100644 --- a/test/s3.ts +++ b/test/s3.ts @@ -1,7 +1,7 @@ import assert from 'node:assert/strict' import crypto from 'node:crypto' import { tmpdir } from 'node:os' -import { test } from 'node:test' +import { after, before, describe, test } from 'node:test' import S3erver from 's3rver' const testEnv = { @@ -16,167 +16,160 @@ const testEnv = { AWS_ACCESS_KEY_ID: 'S3RVER', AWS_SECRET_ACCESS_KEY: 'S3RVER', AWS_REGION: '', - S3_ENDPOINT: 'http://localhost:4568', } Object.assign(process.env, testEnv) const server = new S3erver({ directory: tmpdir(), silent: true, + port: 0, configureBuckets: [ { - name: process.env.STORAGE_PATH, + name: process.env.STORAGE_PATH || '', + configs: [], }, ], }) +before(async (ctx) => { + const address = await server.run() + Object.assign(process.env, { + S3_ENDPOINT: `http://localhost:${address.port}`, + }) +}) + +after((ctx, done) => { + server.close(done) +}) +describe('Amazon S3', async (t) => { + const artifactId = crypto.randomBytes(20).toString('hex') + const team = 'superteam' -server.run((err) => { - assert.equal(err, null) - test('Amazon S3', async (t) => { - const artifactId = crypto.randomBytes(20).toString('hex') - const team = 'superteam' + const { createApp } = await import('../src/app.js') + const app = createApp({ logger: false }) + await app.ready() - const { createApp } = await import('../src/app.js') - const app = createApp({ logger: false }) - await app.ready() + await test('loads correct env vars', async () => { + assert.equal(app.config.STORAGE_PROVIDER, testEnv.STORAGE_PROVIDER) + assert.equal(app.config.STORAGE_PATH, testEnv.STORAGE_PATH) + assert.equal(app.config.AWS_ACCESS_KEY_ID, testEnv.AWS_ACCESS_KEY_ID) + assert.equal( + app.config.AWS_SECRET_ACCESS_KEY, + testEnv.AWS_SECRET_ACCESS_KEY, + ) + assert.equal(app.config.AWS_REGION, testEnv.AWS_REGION) + }) - await t.test('loads correct env vars', async () => { - assert.equal(app.config.STORAGE_PROVIDER, testEnv.STORAGE_PROVIDER) - assert.equal(app.config.STORAGE_PATH, testEnv.STORAGE_PATH) - assert.equal(app.config.AWS_ACCESS_KEY_ID, testEnv.AWS_ACCESS_KEY_ID) - assert.equal( - app.config.AWS_SECRET_ACCESS_KEY, - testEnv.AWS_SECRET_ACCESS_KEY, - ) - assert.equal(app.config.AWS_REGION, testEnv.AWS_REGION) - assert.equal(app.config.S3_ENDPOINT, testEnv.S3_ENDPOINT) + await test('should return 400 when missing authorization header', async () => { + const response = await app.inject({ + method: 'GET', + url: '/v8/artifacts/not-found', + headers: {}, }) + assert.equal(response.statusCode, 400) + assert.equal(response.json().message, 'Missing Authorization header') + }) - await t.test( - 'should return 400 when missing authorization header', - async () => { - const response = await app.inject({ - method: 'GET', - url: '/v8/artifacts/not-found', - headers: {}, - }) - assert.equal(response.statusCode, 400) - assert.equal(response.json().message, 'Missing Authorization header') + await test('should return 401 when wrong authorization token is provided', async () => { + const response = await app.inject({ + method: 'GET', + url: '/v8/artifacts/not-found', + headers: { + authorization: 'wrong token', }, - ) + }) + assert.equal(response.statusCode, 401) + assert.equal(response.json().message, 'Invalid authorization token') + }) - await t.test( - 'should return 401 when wrong authorization token is provided', - async () => { - const response = await app.inject({ - method: 'GET', - url: '/v8/artifacts/not-found', - headers: { - authorization: 'wrong token', - }, - }) - assert.equal(response.statusCode, 401) - assert.equal(response.json().message, 'Invalid authorization token') + await test('should return 400 when missing team query parameter', async () => { + const response = await app.inject({ + method: 'GET', + url: '/v8/artifacts/not-found', + headers: { + authorization: 'Bearer changeme', }, + }) + assert.equal(response.statusCode, 400) + assert.equal( + response.json().message, + "querystring should have required property 'team'", ) + }) - await t.test( - 'should return 400 when missing team query parameter', - async () => { - const response = await app.inject({ - method: 'GET', - url: '/v8/artifacts/not-found', - headers: { - authorization: 'Bearer changeme', - }, - }) - assert.equal(response.statusCode, 400) - assert.equal( - response.json().message, - "querystring should have required property 'team'", - ) + await test('should return 404 on cache miss', async () => { + const response = await app.inject({ + method: 'GET', + url: '/v8/artifacts/not-found', + headers: { + authorization: 'Bearer changeme', + }, + query: { + team: 'superteam', }, - ) - - await t.test('should return 404 on cache miss', async () => { - const response = await app.inject({ - method: 'GET', - url: '/v8/artifacts/not-found', - headers: { - authorization: 'Bearer changeme', - }, - query: { - team: 'superteam', - }, - }) - assert.equal(response.statusCode, 404) - assert.equal(response.json().message, 'Artifact not found') - }) - - await t.test('should upload an artifact', async () => { - const response = await app.inject({ - method: 'PUT', - url: `/v8/artifacts/${artifactId}`, - headers: { - authorization: 'Bearer changeme', - 'content-type': 'application/octet-stream', - }, - query: { - team, - }, - payload: Buffer.from('test cache data'), - }) - assert.equal(response.statusCode, 200) - assert.deepEqual(response.json(), { urls: [`${team}/${artifactId}`] }) }) + assert.equal(response.statusCode, 404) + assert.equal(response.json().message, 'Artifact not found') + }) - await t.test('should download an artifact', async () => { - const response = await app.inject({ - method: 'GET', - url: `/v8/artifacts/${artifactId}`, - headers: { - authorization: 'Bearer changeme', - }, - query: { - team, - }, - }) - assert.equal(response.statusCode, 200) - assert.deepEqual(response.body, 'test cache data') + await test('should upload an artifact', async () => { + const response = await app.inject({ + method: 'PUT', + url: `/v8/artifacts/${artifactId}`, + headers: { + authorization: 'Bearer changeme', + 'content-type': 'application/octet-stream', + }, + query: { + team, + }, + payload: Buffer.from('test cache data'), }) + assert.equal(response.statusCode, 200) + assert.deepEqual(response.json(), { urls: [`${team}/${artifactId}`] }) + }) - await t.test('should verify artifact exists', async () => { - const response = await app.inject({ - method: 'HEAD', - url: `/v8/artifacts/${artifactId}`, - headers: { - authorization: 'Bearer changeme', - }, - query: { - team, - }, - }) - assert.equal(response.statusCode, 200) - assert.deepEqual(response.body, '') + await test('should download an artifact', async () => { + const response = await app.inject({ + method: 'GET', + url: `/v8/artifacts/${artifactId}`, + headers: { + authorization: 'Bearer changeme', + }, + query: { + team, + }, }) + assert.equal(response.statusCode, 200) + assert.deepEqual(response.body, 'test cache data') + }) - await t.test('should verify artifact does not exist', async () => { - const response = await app.inject({ - method: 'HEAD', - url: '/v8/artifacts/not-found', - headers: { - authorization: 'Bearer changeme', - }, - query: { - team, - }, - }) - assert.equal(response.statusCode, 404) - assert.equal(response.json().message, 'Artifact not found') + await test('should verify artifact exists', async () => { + const response = await app.inject({ + method: 'HEAD', + url: `/v8/artifacts/${artifactId}`, + headers: { + authorization: 'Bearer changeme', + }, + query: { + team, + }, }) + assert.equal(response.statusCode, 200) + assert.deepEqual(response.body, '') + }) - t.after(() => { - server.close() + await test('should verify artifact does not exist', async () => { + const response = await app.inject({ + method: 'HEAD', + url: '/v8/artifacts/not-found', + headers: { + authorization: 'Bearer changeme', + }, + query: { + team, + }, }) + assert.equal(response.statusCode, 404) + assert.equal(response.json().message, 'Artifact not found') }) })