diff --git a/.dockerignore b/.dockerignore index 5698c61..e29725d 100644 --- a/.dockerignore +++ b/.dockerignore @@ -3,4 +3,3 @@ dist node_modules samples -yarn.lock \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9ca917b..3ec229f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,6 +7,8 @@ on: paths: - "src/**" - "package.json" + - "yarn.lock" + - "Dockerfile" - "biome.json" - "tsconfig.json" - "vitest.config.ts" @@ -15,6 +17,8 @@ on: paths: - "src/**" - "package.json" + - "yarn.lock" + - "Dockerfile" - "biome.json" - "tsconfig.json" - "vitest.config.ts" @@ -34,7 +38,8 @@ jobs: with: node-version: ${{ matrix.node-version }} cache: "yarn" - - run: yarn + - run: corepack enable + - run: yarn install --frozen-lockfile - run: yarn lint build: @@ -50,8 +55,9 @@ jobs: with: node-version: ${{ matrix.node-version }} cache: "yarn" + - run: corepack enable - name: Install dependencies - run: yarn + run: yarn install --frozen-lockfile - name: Build source code run: yarn build @@ -68,7 +74,8 @@ jobs: with: node-version: ${{ matrix.node-version }} cache: "yarn" - - run: yarn + - run: corepack enable + - run: yarn install --frozen-lockfile - run: yarn test:coverage - name: Coveralls uses: coverallsapp/github-action@v2 diff --git a/.gitignore b/.gitignore index c6445ec..520497e 100644 --- a/.gitignore +++ b/.gitignore @@ -108,4 +108,5 @@ typings/ .AppleDouble .LSOverride bin/ -dist/ \ No newline at end of file +dist/ +.slim/deepwork/ diff --git a/.husky/pre-commit b/.husky/pre-commit index 5e5b976..a6a0450 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,2 +1,2 @@ -npm run lint -npm run build \ No newline at end of file +yarn lint +yarn build diff --git a/AGENTS.md b/AGENTS.md index f5cbf6c..6aeadf5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -41,7 +41,7 @@ npm run format # Format code with Biome ``` ### Git Hooks -Pre-commit: Runs `npm run lint` and `npm run build` +Pre-commit: Runs `yarn lint` and `yarn build` Commit-msg: Validates commit messages using commitlint (conventional commits) ### Release @@ -183,7 +183,9 @@ try { ### Configuration - Support CLI flags and environment variables - Use `commander` for CLI parsing with `.env()` for env var binding -- Validate required options (e.g., tokens must be 40 characters) +- Validate supported GitHub credential formats: legacy 40-character credentials, `ghp_`, `gho_`, + `ghu_`, `ghs_`, and `ghr_` credentials with 36-character suffixes, or `github_pat_` credentials + with an 82-character suffix ## Common Gotchas diff --git a/Dockerfile b/Dockerfile index a3a9ad5..2a7f80c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,11 @@ # ---- Base Node ---- FROM node:24 AS base WORKDIR /app -COPY package*.json ./ +COPY package.json yarn.lock ./ # ---- Dependencies ---- FROM base AS dependencies -RUN npm install --force +RUN corepack enable && yarn install --frozen-lockfile --ignore-scripts # ---- Build ---- FROM dependencies AS build @@ -21,8 +21,8 @@ WORKDIR /app RUN apk add --no-cache curl tini # Install app dependencies -COPY --from=dependencies /app/package*.json ./ -RUN npm ci --omit=dev --ignore-scripts --force +COPY --from=dependencies /app/package.json /app/yarn.lock ./ +RUN corepack enable && yarn install --frozen-lockfile --production=true --ignore-scripts # Bundle app source COPY --from=build /app/dist ./dist @@ -33,4 +33,4 @@ EXPOSE ${PORT:-3000} HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ CMD curl -f http://localhost:${PORT:-3000}/status || exit 1 -ENTRYPOINT ["/sbin/tini", "--", "node", "dist/cli.js"] \ No newline at end of file +ENTRYPOINT ["/sbin/tini", "--", "node", "dist/cli.js"] diff --git a/README.md b/README.md index 02a81f0..63d3e56 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,23 @@ # 🖥️ GitHub Proxy Server -[![Build + S3 sync](https://github.com/gittrends-app/github-proxy-server/actions/workflows/build.yml/badge.svg?branch=master)](https://github.com/gittrends-app/github-proxy-server/actions/workflows/build.yml) +[![CI](https://github.com/gittrends-app/github-proxy-server/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/gittrends-app/github-proxy-server/actions/workflows/ci.yml) [![Coverage Status](https://coveralls.io/repos/github/gittrends-app/github-proxy-server/badge.svg)](https://coveralls.io/github/gittrends-app/github-proxy-server) [![GitHub version](https://badge.fury.io/gh/gittrends-app%2Fgithub-proxy-server.svg)](https://badge.fury.io/gh/gittrends-app%2Fgithub-proxy-server) ![GitHub](https://img.shields.io/github/license/gittrends-app/github-proxy-server)
-> GitHub Proxy Server is a tool to support developers and researchers collect massive amount of data from GitHub API (REST or GraphQL) by automatically managing access tokens and client requests to avoid triggering the GitHub API abuse detection mechanisms. +> GitHub Proxy Server is a tool to support developers and researchers collecting massive amounts of data from the GitHub API (REST or GraphQL) by automatically managing access tokens and client requests to avoid triggering GitHub API abuse-detection mechanisms.
-**Why should I use it?** GitHub API has a limited number of requests per client and implements several mechanisms to detect user abuses. Thus, users must handle these restrictions in their applications. GitHub Proxy Server is a tool that abstracts these problems by means of a proxy server. +**Why should I use it?** The GitHub API has a limited number of requests per client and implements several mechanisms to detect abuse. Thus, users must handle these restrictions in their applications. GitHub Proxy Server is a tool that abstracts these problems by means of a proxy server. -**When should I use it?** This tool is intended to be used by developers and researches that need to perform massive data collection of public repositories using both REST and GraphQL APIs. +**When should I use it?** This tool is intended to be used by developers and researchers who need to perform massive data collection from public repositories using both REST and GraphQL APIs. **When should I not use it?** If you need to deal with private information of users and repositories this tool is not for you (see [limitations section](#limitations)). -**Can I use it with other libs?** Yes, as long they allow the users setup the proxy server as base url (see [samples](samples)). +**Can I use it with other libs?** Yes, as long as they allow users to set up the proxy server as a base URL (see [samples](samples)). **How it works?** @@ -28,7 +28,7 @@ ## Features -- Support to multiple access tokens +- Support for multiple access tokens - Load balancing @@ -46,10 +46,10 @@ First, you need to clone the repository: git clone https://github.com/gittrends-app/github-proxy-server.git ``` -Then, install dependencies, build files, and run the server: +Node.js >=24 and Yarn 1.22.22 are required. Then, install dependencies, build files, and run the server: ```bash -yarn install +yarn install --frozen-lockfile yarn build yarn start --help ``` @@ -69,7 +69,7 @@ To use this tool you need to provide at least one GitHub access token: github-proxy-server -p 3000 -t ``` -Or provide a file with several access token (one per line): +Or provide a file with several access tokens (one per line): ```bash github-proxy-server -p 3000 --tokens @@ -101,34 +101,66 @@ Then make authenticated requests: curl -s -u myuser:mypass http://localhost:3000/users/gittrends-app 2>&1 ``` -**Note:** The `/status` monitoring endpoint is excluded from authentication to allow health checks. - -To more usage information, use the option `--help`. +**Note:** `/status` and `/status/` are small public health endpoints and return `{"status":"ok"}`. +Unknown `/status/*` paths return `404` and never fall through to the proxy. Detailed swagger-stats +monitoring, when enabled, is isolated under `/metrics` (`/metrics/stats` and `/metrics/metrics`) +and is protected by Basic Authentication whenever proxy authentication is configured. Monitoring +paths return `404` when disabled; similarly prefixed routes such as `/status-other` still require +authentication. + +When the proxy is deployed behind a trusted public URL, set `--external-base-url` or +`GPS_EXTERNAL_BASE_URL` to an absolute `http://` or `https://` URL. Redirect and `Link` headers are +rewritten to that base; if it is omitted, upstream links are preserved unchanged and the untrusted +inbound `Host` header is never used for external URLs. + +Request bodies are limited to 1 MiB by default and can be configured with +`--max-request-body-bytes` or `GPS_MAX_REQUEST_BODY_BYTES` (1–16 MiB). Each live worker contributes +50 queue slots by default; configure this with `--max-queue-depth` or `GPS_MAX_QUEUE_DEPTH`. +Queued requests expire after 30 seconds by default (`--queue-wait-timeout` or +`GPS_QUEUE_WAIT_TIMEOUT`), and the total request lifetime is limited to 120 seconds by default +(`--request-lifetime-timeout` or `GPS_REQUEST_LIFETIME_TIMEOUT`). Body overflow returns `413`, a +full queue returns `503` with `Retry-After: 1`, and queue/lifetime expiry returns `504`. + +### Deployment and TLS + +The server listens for plain HTTP and binds to all interfaces when started by the CLI. Do not expose +that listener directly to an untrusted network: Basic Authentication credentials and proxied traffic +are not encrypted by this process. Put the server behind a trusted HTTPS/TLS termination boundary +when credentials or traffic cross an untrusted network. A local or private-network Docker health +check may continue to use `http://localhost:3000/status`; external health checks should use the +trusted HTTPS endpoint. + +For more information, use the option `--help`. ```bash -Usage: index [options] +Usage: cli [options] Options: - -p, --port [port] Port to start the proxy server (default: 3000, env: PORT) - -t, --token [token] GitHub token to be used (default: []) - --tokens [file] File containing a list of tokens (env: GPS_TOKENS_FILE) - --request-timeout [timeout] Request timeout (ms) (default: 30000, env: GPS_REQUEST_TIMEOUT) - --min-remaining Stop using token on a minimum of (default: 100, env: GPS_MIN_REMAINING) - --time-budget-multiplier [multiplier] Time budget multiplier (>= 1.0) (default: 1, env: GPS_TIME_BUDGET_MULTIPLIER) - --silent Dont show requests outputs (env: GPS_SILENT) - --no-override-authorization By default, the authorization header is overrided with a configured token - --auth-username [username] Proxy authentication username (env: GPS_AUTH_USERNAME) - --auth-password [password] Proxy authentication password (env: GPS_AUTH_PASSWORD) - --no-status-monitor Disable requests monitoring on /status - -v, --version output the current version - -h, --help display help for command + -p, --port [port] Port to start the proxy server (default: 3000, env: PORT) + -t, --token [token] GitHub token to be used (default: []) + --tokens [file] File containing a list of tokens (env: GPS_TOKENS_FILE) + --request-timeout [timeout] Request timeout (ms) (default: 30000, env: GPS_REQUEST_TIMEOUT) + --min-remaining Stop using token on a minimum of (default: 100, env: GPS_MIN_REMAINING) + --max-request-body-bytes [bytes] Maximum request body size (bytes) (default: 1048576, env: GPS_MAX_REQUEST_BODY_BYTES) + --max-queue-depth [depth] Maximum queued requests per worker (default: 50, env: GPS_MAX_QUEUE_DEPTH) + --queue-wait-timeout [timeout] Maximum queue wait (ms) (default: 30000, env: GPS_QUEUE_WAIT_TIMEOUT) + --request-lifetime-timeout [timeout] Maximum request lifetime (ms) (default: 120000, env: GPS_REQUEST_LIFETIME_TIMEOUT) + --time-budget-multiplier [multiplier] Time budget multiplier (>= 1.0) (default: 1, env: GPS_TIME_BUDGET_MULTIPLIER) + --external-base-url Trusted external HTTP(S) base URL (env: GPS_EXTERNAL_BASE_URL) + --silent Don't show request output (env: GPS_SILENT) + --no-override-authorization By default, the authorization header is overridden with a configured token + --auth-username [username] Proxy authentication username (env: GPS_AUTH_USERNAME) + --auth-password [password] Proxy authentication password (env: GPS_AUTH_PASSWORD) + --no-status-monitor Disable requests monitoring on /metrics + -v, --version output the current version + -h, --help display help for command ``` ## Limitations -GitHub Proxy Server was primarly intended to be a tool to support massive data collection of public repositories and users. To this purpose, we use a pool of access tokens to proxy requests to GitHub servers. For each request, we select the token with the lowest queue size and with more requests available. +GitHub Proxy Server was primarily intended to support massive data collection from public repositories and users. For this purpose, we use a pool of access tokens to proxy requests to GitHub servers. Requests are routed to a per-resource FIFO queue, then an event-driven dispatcher assigns them in round-robin order among eligible workers for that resource. -Besides that, **we do not perform any verification on the clients requests, which may implies in security issues for the users who provided their tokens**. +Besides that, **we do not perform any verification on clients' requests, which may imply security issues for users who provided their tokens**. To mitigate this problem, you can: @@ -138,12 +170,28 @@ To mitigate this problem, you can: ## Integrations -As mentioned, this tool can be used with serveral other libraries. You can find several examples in [samples](samples) folder. +As mentioned, this tool can be used with several other libraries. You can find several examples in the [samples](samples) folder. ## Contributing Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. +### Development checks + +Use Yarn 1.22.22 with Node.js >=24: + +```bash +yarn install --frozen-lockfile +yarn test +yarn lint +npx tsc --noEmit +yarn build +``` + +The `.husky/pre-commit` hook runs `yarn lint` and `yarn build` automatically. Biome intentionally +enables only the current limited `noConsole` and `noExplicitAny` checks; other recommended rules +are not enabled by this project. + ## License [MIT](https://choosealicense.com/licenses/mit/) diff --git a/biome.json b/biome.json index 4b003fe..899a13b 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.3.10/schema.json", + "$schema": "https://biomejs.dev/schemas/2.3.11/schema.json", "vcs": { "enabled": true, "clientKind": "git", diff --git a/package.json b/package.json index ef8708b..eb980f4 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "engines": { "node": ">=24" }, + "packageManager": "yarn@1.22.22", "type": "module", "repository": "git@github.com:gittrends-app/github-proxy-server.git", "author": "Hudson Silva Borges ", @@ -23,7 +24,7 @@ "build": "shx rm -rf dist && tsup-node src/cli.ts --format esm --sourcemap --minify", "prepare": "husky", "release": "standard-version", - "np": "np --no-publish --yarn --contents dist" + "np": "np --no-publish --contents dist" }, "bin": { "github-proxy-server": "./dist/cli.js" @@ -36,54 +37,53 @@ "architecture.png" ], "dependencies": { - "basic-auth": "^2.0.1", - "chalk": "5.6.2", - "commander": "^14.0.2", + "basic-auth": "^3.0.0", + "chalk": "6.0.0", + "commander": "^15.0.0", "compression": "^1.8.1", "consola": "^3.4.2", - "dayjs": "^1.11.19", - "dotenv-override-true": "^6.2.2", + "dayjs": "^1.11.23", "express": "^5.2.1", "http-status-codes": "^2.3.0", - "https-proxy-agent": "^7.0.6", "ip": "^2.0.1", - "lodash": "^4.17.21", - "p-limit": "^7.2.0", - "p-queue": "^8.0.1", - "pino": "^10.2.0", + "lodash": "^4.18.1", + "p-limit": "^7.3.1", + "p-queue": "^9.3.3", + "pino": "^10.3.1", "pino-http": "^11.0.0", "pino-pretty": "^13.1.3", + "prom-client": "^14.2.0", "swagger-stats": "^0.99.7", "table": "^6.9.0", - "undici": "^7.18.2" + "undici": "^8.10.0" }, "devDependencies": { - "@biomejs/biome": "^2.3.11", - "@commitlint/cli": "^20.3.1", - "@commitlint/config-conventional": "^20.3.1", - "@tsconfig/node20": "^20.1.8", + "@biomejs/biome": "^2.5.11", + "@commitlint/cli": "^20.5.3", + "@commitlint/config-conventional": "^20.5.3", + "@tsconfig/node20": "^20.1.10", "@types/async": "^3.2.25", "@types/basic-auth": "^1.1.8", "@types/compression": "^1.8.1", "@types/ip": "^1.1.3", - "@types/lodash": "^4.17.23", - "@types/node": "^25.0.9", - "@types/supertest": "^6.0.3", + "@types/lodash": "^4.17.25", + "@types/node": "^26.4.0", + "@types/supertest": "^7.2.1", "@types/swagger-stats": "^0.95.11", - "@vitest/coverage-v8": "^4.0.17", - "commitizen": "^4.3.1", + "@vitest/coverage-v8": "^4.1.11", + "commitizen": "^4.3.2", "cz-conventional-changelog": "3.3.0", "husky": "^9.1.7", - "nock": "^14.0.10", - "np": "^10.2.0", + "nock": "^14.0.17", + "np": "^12.0.1", "shx": "^0.4.0", "standard-version": "^9.5.0", "supertest": "^7.2.2", "tmp-promise": "^3.0.3", "tsup": "^8.5.1", - "tsx": "^4.21.0", - "typescript": "^5.9.3", - "vitest": "^4.0.17" + "tsx": "^4.23.12", + "typescript": "7.0.2", + "vitest": "^4.1.11" }, "config": { "commitizen": { diff --git a/src/cli.spec.ts b/src/cli.spec.ts index b5a12b5..1af18c9 100644 --- a/src/cli.spec.ts +++ b/src/cli.spec.ts @@ -1,14 +1,45 @@ -import { exec } from 'node:child_process'; +import { exec, spawn } from 'node:child_process'; import { unlinkSync, writeFileSync } from 'node:fs'; +import { createServer } from 'node:net'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { Command } from 'commander'; +import repeat from 'lodash/repeat.js'; import { afterEach, beforeEach, describe, expect, test } from 'vitest'; -import { createCli } from './cli.js'; +import { createAuthConfiguration, createCli, PARTIAL_AUTHENTICATION_ERROR } from './cli.js'; +import { + parseExternalBaseUrl, + parseMaxQueueDepthPerWorker, + parseMaxRequestBodyBytes, + parseMinRemaining, + parsePort, + parseQueueWaitTimeout, + parseRequestLifetimeTimeout, + parseRequestTimeout, + parseTimeBudgetMultiplier +} from './router.js'; import { concatTokens, parseTokens, readTokensFile } from './server.js'; +type BooleanCliOptions = { + overrideAuthorization: boolean; + statusMonitor: boolean; +}; + +async function parseBooleanOptions(args: string[]): Promise { + const program = createCli(); + let options: BooleanCliOptions | undefined; + program.action((parsedOptions: BooleanCliOptions) => { + options = parsedOptions; + }); + + await program.parseAsync(['node', 'test', ...args]); + + if (!options) throw new Error('CLI options were not parsed'); + return options; +} + export type CliCmdResult = { code: number; error?: Error | null; @@ -16,17 +47,40 @@ export type CliCmdResult = { stderr?: string | null; }; -export async function cli(args: string[], cwd: string): Promise { +export async function cli( + args: string[], + cwd: string, + environment: NodeJS.ProcessEnv = {} +): Promise { return new Promise((resolve) => { exec( - `npm run dev-no-reload --no-status-monitor ${args.join(' ')}`, - { cwd }, + `npm run dev-no-reload -- --no-status-monitor ${args.join(' ')}`, + { cwd, env: { ...process.env, ...environment } }, (error, stdout, stderr) => resolve({ code: error?.code ?? 0, error, stdout, stderr }) ); }); } describe('Test cli app', () => { + test.each([ + ['max request body bytes', parseMaxRequestBodyBytes, 1024 * 1024, 16 * 1024 * 1024], + ['max queue depth', parseMaxQueueDepthPerWorker, 1, 1000], + ['queue wait timeout', parseQueueWaitTimeout, 1, 120000], + ['request lifetime timeout', parseRequestLifetimeTimeout, 1, 600000] + ])('should parse %s within its configured range', (_name, parser, min, max) => { + expect(parser(min)).toBe(min); + expect(parser(max)).toBe(max); + }); + + test.each([ + [parseMaxRequestBodyBytes, 1024 * 1024 - 1], + [parseMaxQueueDepthPerWorker, 0], + [parseQueueWaitTimeout, 0], + [parseRequestLifetimeTimeout, 0] + ])('should reject an invalid enhancement limit', (parser, value) => { + expect(() => parser(value)).toThrowError(); + }); + test('it should thrown an error if token/tokens is not provided', async () => { const result = await cli([], '.'); expect(result.code).toEqual(1); @@ -36,6 +90,135 @@ describe('Test cli app', () => { const result = await cli(['-t', 'invalid'], '.'); expect(result.code).toEqual(1); }); + + test('it should reject username-only authentication before starting', async () => { + const username = 'only-user'; + const result = await cli(['-t', '1234567890123456789012345678901234567890'], '.', { + GPS_AUTH_USERNAME: username + }); + const output = `${result.stdout ?? ''}${result.stderr ?? ''}`; + + expect(result.code).toEqual(1); + expect(output).toContain(PARTIAL_AUTHENTICATION_ERROR); + expect(output).not.toContain(username); + }); + + test('it should reject password-only authentication before starting', async () => { + const password = 'only-password'; + const result = await cli(['-t', '1234567890123456789012345678901234567890'], '.', { + GPS_AUTH_PASSWORD: password + }); + const output = `${result.stdout ?? ''}${result.stderr ?? ''}`; + + expect(result.code).toEqual(1); + expect(output).toContain(PARTIAL_AUTHENTICATION_ERROR); + expect(output).not.toContain(password); + }); + + test('it should destroy the router when a child receives SIGTERM', async () => { + const child = spawn( + process.execPath, + [ + '--import', + 'tsx/esm', + 'src/cli.ts', + '--no-status-monitor', + '-t', + '1234567890123456789012345678901234567890', + '-p', + '0' + ], + { cwd: process.cwd(), env: { ...process.env, FORCE_COLOR: '0' } } + ); + + const result = await new Promise<{ code: number | null; signal: NodeJS.Signals | null }>( + (resolve, reject) => { + let settled = false; + const timeout = setTimeout(() => { + if (!settled) { + child.kill('SIGKILL'); + reject(new Error('CLI did not shut down in time')); + } + }, 10000); + const signalTimer = setTimeout(() => child.kill('SIGTERM'), 3000); + child.once('error', (error) => { + clearTimeout(timeout); + clearTimeout(signalTimer); + if (!settled) { + settled = true; + reject(error); + } + }); + child.once('close', (code, signal) => { + clearTimeout(timeout); + clearTimeout(signalTimer); + if (!settled) { + settled = true; + resolve({ code, signal }); + } + }); + } + ); + + expect(result.code).toBe(0); + expect(result.signal).toBeNull(); + }, 15000); + + test('it should clean up when the listen server emits an error', async () => { + const blocker = createServer(); + await new Promise((resolve, reject) => { + blocker.once('error', reject); + blocker.listen({ host: '0.0.0.0', port: 0 }, resolve); + }); + + try { + const address = blocker.address(); + if (!address || typeof address === 'string') throw new Error('Unable to determine test port'); + + const result = await cli( + ['-t', '1234567890123456789012345678901234567890', '-p', `${address.port}`], + '.' + ); + expect(result.code).toBe(1); + } finally { + await new Promise((resolve) => blocker.close(() => resolve())); + } + }, 15000); +}); + +describe('CLI authentication configuration', () => { + test('should leave authentication disabled when neither credential is supplied', () => { + expect(createAuthConfiguration(undefined, undefined)).toBeUndefined(); + }); + + test('should configure authentication when both credentials are supplied', () => { + expect(createAuthConfiguration('testuser', 'testpass')).toEqual({ + username: 'testuser', + password: 'testpass' + }); + }); + + test('should reject username-only configuration without logging the username', () => { + const username = 'username-secret'; + + expect(() => createAuthConfiguration(username, undefined)).toThrowError( + PARTIAL_AUTHENTICATION_ERROR + ); + expect(() => createAuthConfiguration(username, undefined)).toThrowError( + expect.not.objectContaining({ message: expect.stringContaining(username) }) + ); + }); + + test('should reject password-only configuration without logging the password', () => { + const password = 'password-secret'; + + expect(() => createAuthConfiguration(undefined, password)).toThrowError( + PARTIAL_AUTHENTICATION_ERROR + ); + expect(() => createAuthConfiguration(undefined, password)).toThrowError( + expect.not.objectContaining({ message: expect.stringContaining(password) }) + ); + }); }); describe('createCli command structure', () => { @@ -79,6 +262,15 @@ describe('createCli command structure', () => { expect(minRemainingOption?.defaultValue).toBe(100); }); + test.each([ + ['--max-request-body-bytes', 1024 * 1024], + ['--max-queue-depth', 50], + ['--queue-wait-timeout', 30000], + ['--request-lifetime-timeout', 120000] + ])('should have %s with default %s', (name, value) => { + expect(program.options.find((option) => option.long === name)?.defaultValue).toBe(value); + }); + test('should have --silent option', () => { const silentOption = program.options.find((opt) => opt.long === '--silent'); expect(silentOption).toBeDefined(); @@ -104,6 +296,28 @@ describe('createCli command structure', () => { expect(statusMonitorOption).toBeDefined(); }); + test.each([ + [[], true, true], + [['--no-override-authorization'], false, true], + [['--no-status-monitor'], true, false], + [['--no-override-authorization', '--no-status-monitor'], false, false] + ])( + 'should parse boolean defaults and explicit negative values %#', + async (args, overrideAuthorization, statusMonitor) => { + await expect(parseBooleanOptions(args)).resolves.toMatchObject({ + overrideAuthorization, + statusMonitor + }); + } + ); + + test('should have trusted external base URL option', () => { + const externalBaseUrlOption = program.options.find((opt) => opt.long === '--external-base-url'); + expect(externalBaseUrlOption).toBeDefined(); + expect(externalBaseUrlOption?.parseArg).toBeDefined(); + expect(externalBaseUrlOption?.required).toBe(true); + }); + test('should have version option', () => { const versionOption = program.options.find((opt) => opt.long === '--version'); expect(versionOption).toBeDefined(); @@ -162,6 +376,29 @@ describe('CLI option parsing', () => { }); }); +describe('Numeric configuration validation', () => { + test.each([ + ['port', parsePort, 0, 65535], + ['requestTimeout', parseRequestTimeout, 1, 120000], + ['minRemaining', parseMinRemaining, 0, 5000], + ['timeBudgetMultiplier', parseTimeBudgetMultiplier, 1, 10] + ])('should accept %s boundaries', (_name, parse, minimum, maximum) => { + expect(parse(minimum)).toBe(minimum); + expect(parse(maximum)).toBe(maximum); + }); + + test.each([ + ['port', parsePort, [-1, 65536, 1.5, '1e3', '']], + ['requestTimeout', parseRequestTimeout, [0, 120001, 1.5, 'Infinity', '']], + ['minRemaining', parseMinRemaining, [-1, 5001, 1.5, 'NaN', '']], + ['timeBudgetMultiplier', parseTimeBudgetMultiplier, [0, 10.1, 'Infinity', '1e2', '']] + ])('should reject invalid %s values', (name, parse, values) => { + for (const value of values) { + expect(() => parse(value)).toThrow(`Invalid ${name}`); + } + }); +}); + describe('CLI environment variables', () => { test('should support PORT environment variable', () => { const program = createCli(); @@ -187,6 +424,15 @@ describe('CLI environment variables', () => { expect(minRemainingOption?.envVar).toBe('GPS_MIN_REMAINING'); }); + test.each([ + ['--max-request-body-bytes', 'GPS_MAX_REQUEST_BODY_BYTES'], + ['--max-queue-depth', 'GPS_MAX_QUEUE_DEPTH'], + ['--queue-wait-timeout', 'GPS_QUEUE_WAIT_TIMEOUT'], + ['--request-lifetime-timeout', 'GPS_REQUEST_LIFETIME_TIMEOUT'] + ])('should support %s environment variable %s', (name, envVar) => { + expect(createCli().options.find((option) => option.long === name)?.envVar).toBe(envVar); + }); + test('should support GPS_AUTH_USERNAME environment variable', () => { const program = createCli(); const authUsernameOption = program.options.find((opt) => opt.long === '--auth-username'); @@ -198,6 +444,27 @@ describe('CLI environment variables', () => { const authPasswordOption = program.options.find((opt) => opt.long === '--auth-password'); expect(authPasswordOption?.envVar).toBe('GPS_AUTH_PASSWORD'); }); + + test('should support GPS_EXTERNAL_BASE_URL environment variable', () => { + const program = createCli(); + const externalBaseUrlOption = program.options.find((opt) => opt.long === '--external-base-url'); + expect(externalBaseUrlOption?.envVar).toBe('GPS_EXTERNAL_BASE_URL'); + }); +}); + +describe('External base URL validation', () => { + test('should accept and normalize absolute HTTP(S) URLs', () => { + expect(parseExternalBaseUrl('http://proxy.example/base/')).toBe('http://proxy.example/base'); + expect(parseExternalBaseUrl('https://proxy.example')).toBe('https://proxy.example'); + expect(parseExternalBaseUrl(undefined)).toBeUndefined(); + }); + + test.each(['ftp://proxy.example', '//proxy.example', 'not-a-url', 'https://proxy.example/?x=1'])( + 'should reject invalid external base URL %s', + (value) => { + expect(() => parseExternalBaseUrl(value)).toThrow('Invalid externalBaseUrl'); + } + ); }); describe('Helper Functions - concatTokens', () => { @@ -208,6 +475,21 @@ describe('Helper Functions - concatTokens', () => { expect(result).toHaveLength(1); }); + test('should accept supported prefixed GitHub credential formats', () => { + const credentials = [ + `ghp_${repeat('a', 36)}`, + `gho_${repeat('b', 36)}`, + `ghu_${repeat('c', 36)}`, + `ghs_${repeat('d', 36)}`, + `ghr_${repeat('e', 36)}`, + `github_pat_${repeat('f', 82)}` + ]; + + expect( + credentials.reduce((list, credential) => concatTokens(credential, list), []) + ).toEqual(credentials); + }); + test('should add valid token to existing list', () => { const token1 = '1234567890123456789012345678901234567890'; const token2 = '0987654321098765432109876543210987654321'; @@ -236,6 +518,14 @@ describe('Helper Functions - concatTokens', () => { test('should throw error for empty token', () => { expect(() => concatTokens('', [])).toThrow('Invalid access token detected'); }); + + test('should reject unsupported credential formats without exposing them', () => { + const invalidToken = `github_pat_${repeat('secret', 20)}`; + + expect(() => concatTokens(invalidToken, [])).toThrow( + expect.not.objectContaining({ message: expect.stringContaining(invalidToken) }) + ); + }); }); describe('Helper Functions - parseTokens', () => { @@ -398,4 +688,4 @@ describe('CLI flag combinations', () => { const versionOption = program.options.find((opt) => opt.long === '--version'); expect(versionOption?.short).toBe('-v'); }); -}); +}); \ No newline at end of file diff --git a/src/cli.ts b/src/cli.ts index d684691..136925e 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,6 +1,5 @@ #!/usr/bin/env node /* Author: Hudson S. Borges */ -import EventEmitter from 'node:events'; import { pathToFileURL } from 'node:url'; import chalk from 'chalk'; @@ -13,14 +12,34 @@ import omit from 'lodash/omit.js'; import omitBy from 'lodash/omitBy.js'; import packageJson from '../package.json' with { type: 'json' }; +import { + parseExternalBaseUrl, + parseMaxQueueDepthPerWorker, + parseMaxRequestBodyBytes, + parseMinRemaining, + parsePort, + parseQueueWaitTimeout, + parseRequestLifetimeTimeout, + parseRequestTimeout, + parseTimeBudgetMultiplier +} from './router.js'; import { type CliOpts, concatTokens, createProxyServer, readTokensFile } from './server.js'; -function parseTimeBudgetMultiplier(value: string): number { - const num = Number(value); - if (isNaN(num) || num < 1) { - throw new Error('Time budget multiplier must be >= 1.0 (use 1.0 for 100%, 1.5 for 150%, etc.)'); +export const PARTIAL_AUTHENTICATION_ERROR = + 'Authentication requires both username and password when configured.'; + +export function createAuthConfiguration( + username: string | undefined, + password: string | undefined +): CliOpts['auth'] { + const hasUsername = username !== undefined; + const hasPassword = password !== undefined; + + if (hasUsername !== hasPassword) { + throw new Error(PARTIAL_AUTHENTICATION_ERROR); } - return num; + + return hasUsername && hasPassword ? { username, password } : undefined; } export function createCli(): Command { @@ -29,7 +48,7 @@ export function createCli(): Command { return program .addOption( new Option('-p, --port [port]', 'Port to start the proxy server') - .argParser(Number) + .argParser(parsePort) .default(3000) .env('PORT') ) @@ -45,27 +64,56 @@ export function createCli(): Command { ) .addOption( new Option('--request-timeout [timeout]', 'Request timeout (ms)') - .argParser(Number) + .argParser(parseRequestTimeout) .default(30000) .env('GPS_REQUEST_TIMEOUT') ) .addOption( new Option('--min-remaining ', 'Stop using token on a minimum of') - .argParser(Number) + .argParser(parseMinRemaining) .default(100) .env('GPS_MIN_REMAINING') ) + .addOption( + new Option('--max-request-body-bytes [bytes]', 'Maximum request body size (bytes)') + .argParser(parseMaxRequestBodyBytes) + .default(1024 * 1024) + .env('GPS_MAX_REQUEST_BODY_BYTES') + ) + .addOption( + new Option('--max-queue-depth [depth]', 'Maximum queued requests per worker') + .argParser(parseMaxQueueDepthPerWorker) + .default(50) + .env('GPS_MAX_QUEUE_DEPTH') + ) + .addOption( + new Option('--queue-wait-timeout [timeout]', 'Maximum queue wait (ms)') + .argParser(parseQueueWaitTimeout) + .default(30000) + .env('GPS_QUEUE_WAIT_TIMEOUT') + ) + .addOption( + new Option('--request-lifetime-timeout [timeout]', 'Maximum request lifetime (ms)') + .argParser(parseRequestLifetimeTimeout) + .default(120000) + .env('GPS_REQUEST_LIFETIME_TIMEOUT') + ) .addOption( new Option('--time-budget-multiplier [multiplier]', 'Time budget multiplier (>= 1.0)') .argParser(parseTimeBudgetMultiplier) .default(1) .env('GPS_TIME_BUDGET_MULTIPLIER') ) - .addOption(new Option('--silent', 'Dont show requests outputs').env('GPS_SILENT')) + .addOption( + new Option('--external-base-url ', 'Trusted external HTTP(S) base URL') + .argParser(parseExternalBaseUrl) + .env('GPS_EXTERNAL_BASE_URL') + ) + .addOption(new Option('--silent', "Don't show request output").env('GPS_SILENT')) .addOption( new Option( '--no-override-authorization', - 'By default, the authorization header is overrided with a configured token' + 'By default, the authorization header is overridden with a configured token' ) ) .addOption( @@ -78,37 +126,51 @@ export function createCli(): Command { 'GPS_AUTH_PASSWORD' ) ) - .addOption(new Option('--no-status-monitor', 'Disable requests monitoring on /status')) + .addOption(new Option('--no-status-monitor', 'Disable requests monitoring on /metrics')) .version(packageJson.version || '?', '-v, --version', 'output the current version') .action(async (options) => { + let auth: CliOpts['auth']; + try { + auth = createAuthConfiguration(options.authUsername, options.authPassword); + } catch (error) { + consola.error(error instanceof Error ? error.message : PARTIAL_AUTHENTICATION_ERROR); + process.exit(1); + } + if (!options.token.length && !options.tokens?.length) { consola.info(`${program.helpInformation()}`); consola.error(`Arguments missing ("--token" or "--tokens" is mandatory).\n\n`); process.exit(1); } - EventEmitter.defaultMaxListeners = Number.MAX_SAFE_INTEGER; - const tokens = [...options.token, ...(options.tokens || [])].reduce( (memo: string[], token: string) => concatTokens(token, memo), [] ); + const port = parsePort(options.port); + const requestTimeout = parseRequestTimeout(options.requestTimeout); + const minRemaining = parseMinRemaining(options.minRemaining); + const maxRequestBodyBytes = parseMaxRequestBodyBytes(options.maxRequestBodyBytes); + const maxQueueDepthPerWorker = parseMaxQueueDepthPerWorker(options.maxQueueDepth); + const queueWaitTimeout = parseQueueWaitTimeout(options.queueWaitTimeout); + const requestLifetimeTimeout = parseRequestLifetimeTimeout(options.requestLifetimeTimeout); + const timeBudgetMultiplier = parseTimeBudgetMultiplier(options.timeBudgetMultiplier); + const externalBaseUrl = parseExternalBaseUrl(options.externalBaseUrl); const appOptions: CliOpts = { - requestTimeout: options.requestTimeout, + requestTimeout, silent: options.silent, overrideAuthorization: options.overrideAuthorization, tokens: tokens, - minRemaining: options.minRemaining, - timeBudgetMultiplier: options.timeBudgetMultiplier, + minRemaining, + maxRequestBodyBytes, + maxQueueDepthPerWorker, + queueWaitTimeout, + requestLifetimeTimeout, + timeBudgetMultiplier, + externalBaseUrl, statusMonitor: options.statusMonitor, - auth: - options.authUsername && options.authPassword - ? { - username: options.authUsername, - password: options.authPassword - } - : undefined + auth }; const app = createProxyServer(appOptions); @@ -118,13 +180,11 @@ export function createCli(): Command { .on('warn', consola.warn) .on('error', consola.error); - const server = app.listen({ host: '0.0.0.0', port: options.port }, (error?: Error) => { - if (error) { - consola.error(error); - process.exit(1); - } - - const host = `http://${ip.address()}:${options.port}`; + let startupReady = false; + const server = app.listen({ host: '0.0.0.0', port }, () => { + if (!server.listening) return; + startupReady = true; + const host = `http://${ip.address()}:${port}`; consola.success( `Proxy server running on ${host} (tokens: ${chalk.greenBright(tokens.length)})` ); @@ -149,28 +209,87 @@ export function createCli(): Command { ); }); - const shutdown = async () => { - server.close((err?: Error) => { - if (err) { - consola.error(err); - process.exit(1); - } - - consola.success('Server closed'); - process.exit(0); + const closeServer = (): Promise => { + return new Promise((resolve, reject) => { + server.close((error?: Error) => { + const code = (error as NodeJS.ErrnoException | undefined)?.code; + if (error && code !== 'ERR_SERVER_NOT_RUNNING') { + reject(error); + } else { + resolve(); + } + }); }); }; - ['SIGTERM', 'SIGINT'].forEach((signal) => { - process.on(signal, async () => { - consola.info(`${signal} signal received: closing HTTP server`); - await shutdown(); - }); - }); + let shutdownPromise: Promise | undefined; + let handleSigterm: () => void; + let handleSigint: () => void; + + const removeSignalHandlers = (): void => { + process.off('SIGTERM', handleSigterm); + process.off('SIGINT', handleSigint); + }; + + const dispose = async (exitCode: number, announce: boolean): Promise => { + // Start both operations before awaiting either so active requests can drain or abort. + const serverClose = closeServer(); + const routerDestroy = app.destroy(); + const results = await Promise.allSettled([serverClose, routerDestroy]); + const errors = results + .filter((result): result is PromiseRejectedResult => result.status === 'rejected') + .map((result) => result.reason); + + removeSignalHandlers(); + if (errors.length) { + consola.error(new AggregateError(errors, 'Proxy server shutdown failed')); + process.exit(1); + return; + } + + if (announce) consola.success('Server closed'); + process.exit(exitCode); + }; + + const shutdown = (): Promise => { + if (shutdownPromise) return shutdownPromise; + + shutdownPromise = dispose(0, true); + + return shutdownPromise; + }; + + const cleanupStartupFailure = async (startupError: Error): Promise => { + consola.error(startupError); + if (!shutdownPromise) shutdownPromise = dispose(1, false); + await shutdownPromise; + }; + + handleSigterm = (): void => { + consola.info('SIGTERM signal received: closing HTTP server'); + void shutdown(); + }; + + handleSigint = (): void => { + consola.info('SIGINT signal received: closing HTTP server'); + void shutdown(); + }; + + const handleServerError = (error: Error): void => { + if (startupReady && server.listening) { + consola.error(error); + return; + } + void cleanupStartupFailure(error); + }; + + server.on('error', handleServerError); + process.once('SIGTERM', handleSigterm); + process.once('SIGINT', handleSigint); }); } // parse arguments from command line if (import.meta.url === pathToFileURL(process.argv[1]).href) { createCli().parse(process.argv); -} +} \ No newline at end of file diff --git a/src/proxy-client.spec.ts b/src/proxy-client.spec.ts index 647f3e9..fb2ef42 100644 --- a/src/proxy-client.spec.ts +++ b/src/proxy-client.spec.ts @@ -1,33 +1,56 @@ /* Author: Hudson S. Borges */ +import EventEmitter from 'node:events'; import { IncomingMessage, ServerResponse } from 'node:http'; -import { Agent as HttpsAgent } from 'node:https'; import { StatusCodes } from 'http-status-codes'; -import nock from 'nock'; -import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test, vi } from 'vitest'; +import { + Agent, + MockAgent, + type MockPool, + Headers as UndiciHeaders, + type Response as UndiciResponse, + fetch as undiciFetch +} from 'undici'; +import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; + +vi.mock('undici', async (importOriginal) => { + const actual = await importOriginal(); + return { ...actual, fetch: vi.fn(actual.fetch) }; +}); import { ProxyClient } from './proxy-client'; describe('ProxyClient', () => { let client: ProxyClient; - let scope: nock.Scope; + let mockAgent: MockAgent | undefined; + let mockPool!: MockPool; const TARGET = 'https://api.example.com'; const TIMEOUT = 5000; - beforeAll(() => { - nock.disableNetConnect(); - }); + function intercept(method: string, path: string, body?: unknown) { + return mockPool.intercept({ + method, + path, + ...(body === undefined + ? {} + : { + body: (value: string): boolean => + Buffer.from(value).toString() === + (typeof body === 'string' ? body : JSON.stringify(body)) + }) + }); + } - afterAll(() => { - nock.cleanAll(); - nock.restore(); - nock.enableNetConnect(); - }); + function normalizeHeaders( + headers: UndiciHeaders | Record | undefined + ): Record { + return Object.fromEntries(new UndiciHeaders(headers).entries()); + } - afterEach(() => { - nock.cleanAll(); + afterEach(async () => { + await mockAgent?.close(); }); describe('constructor', () => { @@ -40,29 +63,33 @@ describe('ProxyClient', () => { expect(proxyClient).toBeInstanceOf(ProxyClient); }); - test('should create client with custom agent', () => { - const agent = new HttpsAgent({ keepAlive: true }); + test('should create client with custom agent', async () => { + const agent = new Agent({ keepAliveTimeout: 60000 }); const proxyClient = new ProxyClient({ target: TARGET, timeout: TIMEOUT, - agent + dispatcher: agent }); expect(proxyClient).toBeInstanceOf(ProxyClient); + await agent.close(); }); }); describe('proxy method', () => { beforeEach(() => { + mockAgent = new MockAgent(); + mockAgent.disableNetConnect(); + mockPool = mockAgent.get(TARGET); client = new ProxyClient({ target: TARGET, - timeout: TIMEOUT + timeout: TIMEOUT, + dispatcher: mockAgent }); - scope = nock(TARGET); }); test('should proxy GET request successfully', async () => { - scope.get('/test').reply(StatusCodes.OK, { success: true }); + intercept('GET', '/test').reply(StatusCodes.OK, { success: true }); const { req, res } = createMockRequestResponse('GET', '/test'); @@ -75,7 +102,7 @@ describe('ProxyClient', () => { test('should proxy POST request with body', async () => { const requestBody = { data: 'test' }; - scope.post('/test', requestBody).reply(StatusCodes.CREATED, { id: 123 }); + intercept('POST', '/test', requestBody).reply(StatusCodes.CREATED, { id: 123 }); const { req, res } = createMockRequestResponse('POST', '/test', requestBody); @@ -85,12 +112,61 @@ describe('ProxyClient', () => { expect(res.writableFinished).toBe(true); }); + test('should accept a request body exactly at the configured limit', async () => { + client = new ProxyClient({ + target: TARGET, + timeout: TIMEOUT, + maxRequestBodyBytes: 7, + dispatcher: mockAgent + }); + intercept('POST', '/at-limit', '"12345"').reply(StatusCodes.OK, 'ok'); + const { req, res } = createMockRequestResponse('POST', '/at-limit', '12345', { + 'content-length': '7' + }); + + await expect(client.proxy(req, res)).resolves.toBeUndefined(); + expect(res.writableFinished).toBe(true); + }); + + test('should reject a declared body larger than the configured limit before buffering', async () => { + client = new ProxyClient({ target: TARGET, timeout: TIMEOUT, maxRequestBodyBytes: 4 }); + const { req, res } = createMockRequestResponse('POST', '/too-large', '12345', { + 'content-length': '5' + }); + + await expect(client.proxy(req, res)).rejects.toMatchObject({ code: 'PAYLOAD_TOO_LARGE' }); + expect(req.resume).toHaveBeenCalledTimes(1); + }); + + test('should reject a chunked body when it exceeds the configured limit', async () => { + client = new ProxyClient({ target: TARGET, timeout: TIMEOUT, maxRequestBodyBytes: 4 }); + const { req, res } = createMockRequestResponse('POST', '/too-large', '12345'); + + await expect(client.proxy(req, res)).rejects.toMatchObject({ code: 'PAYLOAD_TOO_LARGE' }); + }); + + test('should abort and clean up listeners while waiting for a slow upload', async () => { + const timeoutClient = new ProxyClient({ target: TARGET, timeout: 10 }); + const req = Object.assign(new EventEmitter(), { + method: 'POST', + url: '/slow-upload', + headers: { host: 'localhost:3000' }, + socket: { remoteAddress: '127.0.0.1', encrypted: false } + }) as unknown as IncomingMessage; + const { res } = createMockRequestResponse('POST', '/slow-upload'); + + await expect(timeoutClient.proxy(req, res)).rejects.toMatchObject({ code: 'ETIMEDOUT' }); + expect(req.listenerCount('data')).toBe(0); + expect(req.listenerCount('end')).toBe(0); + expect(req.listenerCount('error')).toBe(0); + }); + test('should copy request headers', async () => { let receivedHeaders: Record = {}; - scope.get('/test').reply(function () { - receivedHeaders = this.req.headers as Record; - return [StatusCodes.OK, { success: true }]; + intercept('GET', '/test').reply(({ headers }) => { + receivedHeaders = normalizeHeaders(headers); + return { statusCode: StatusCodes.OK, data: { success: true } }; }); const { req, res } = createMockRequestResponse('GET', '/test', undefined, { @@ -104,29 +180,156 @@ describe('ProxyClient', () => { expect(receivedHeaders['x-custom-header']).toBe('custom-value'); }); - test('should add x-forwarded headers', async () => { + test('should filter hop-by-hop request headers and connection tokens', async () => { + let receivedHeaders: Record = {}; + + intercept('GET', '/hop-by-hop').reply(({ headers }) => { + receivedHeaders = normalizeHeaders(headers); + return { statusCode: StatusCodes.OK, data: 'ok' }; + }); + + const { req, res } = createMockRequestResponse('GET', '/hop-by-hop', undefined, { + connection: 'keep-alive, x-request-hop', + 'keep-alive': 'timeout=5', + 'x-request-hop': 'remove-me', + te: 'trailers', + 'x-end-to-end': 'preserve-me' + }); + + await client.proxy(req, res); + + expect(receivedHeaders.connection).toBeUndefined(); + expect(receivedHeaders['keep-alive']).toBeUndefined(); + expect(receivedHeaders['x-request-hop']).toBeUndefined(); + expect(receivedHeaders.te).toBeUndefined(); + expect(receivedHeaders['x-end-to-end']).toBe('preserve-me'); + }); + + test('should preserve trusted forwarded headers named by inbound Connection', async () => { + let receivedHeaders: Record = {}; + + intercept('GET', '/forwarded-connection').reply(({ headers }) => { + receivedHeaders = normalizeHeaders(headers); + return { statusCode: StatusCodes.OK, data: 'ok' }; + }); + + const { req, res } = createMockRequestResponse('GET', '/forwarded-connection', undefined, { + connection: 'x-forwarded-for, x-forwarded-host, x-forwarded-proto', + 'x-forwarded-for': 'spoofed-for', + 'x-forwarded-host': 'spoofed-host', + 'x-forwarded-proto': 'spoofed-proto' + }); + + await client.proxy(req, res); + + expect(receivedHeaders['x-forwarded-for']).toBe('127.0.0.1'); + expect(receivedHeaders['x-forwarded-host']).toBe('localhost:3000'); + expect(receivedHeaders['x-forwarded-proto']).toBe('http'); + }); + + test('should preserve modified authorization despite an inbound Connection token', async () => { + let receivedHeaders: Record = {}; + + intercept('GET', '/authorization').reply(({ headers }) => { + receivedHeaders = normalizeHeaders(headers); + return { statusCode: StatusCodes.OK, data: 'ok' }; + }); + + const { req, res } = createMockRequestResponse('GET', '/authorization', undefined, { + connection: 'authorization' + }); + + await client.proxy(req, res, { + modifyHeaders: (headers) => ({ ...headers, authorization: 'token injected' }) + }); + + expect(receivedHeaders.authorization).toBe('token injected'); + }); + + test('should add forwarded metadata from the immediate request', async () => { + let receivedHeaders: Record = {}; + + intercept('GET', '/test').reply(({ headers }) => { + receivedHeaders = normalizeHeaders(headers); + return { statusCode: StatusCodes.OK, data: { success: true } }; + }); + + const { req, res } = createMockRequestResponse('GET', '/test', undefined, { + host: 'incoming.example.com' + }); + + await client.proxy(req, res); + + expect(receivedHeaders['x-forwarded-for']).toBe('127.0.0.1'); + expect(receivedHeaders['x-forwarded-proto']).toBe('http'); + expect(receivedHeaders['x-forwarded-host']).toBe('incoming.example.com'); + }); + + test('should derive HTTPS protocol from the request socket', async () => { + let receivedHeaders: Record = {}; + + intercept('GET', '/test').reply(({ headers }) => { + receivedHeaders = normalizeHeaders(headers); + return { statusCode: StatusCodes.OK, data: { success: true } }; + }); + + const { req, res } = createMockRequestResponse( + 'GET', + '/test', + undefined, + {}, + { encrypted: true } + ); + + await client.proxy(req, res); + + expect(receivedHeaders['x-forwarded-proto']).toBe('https'); + }); + + test('should replace spoofed forwarded headers with immediate request metadata', async () => { let receivedHeaders: Record = {}; - scope.get('/test').reply(function () { - receivedHeaders = this.req.headers as Record; - return [StatusCodes.OK, { success: true }]; + intercept('GET', '/test').reply(({ headers }) => { + receivedHeaders = normalizeHeaders(headers); + return { statusCode: StatusCodes.OK, data: { success: true } }; + }); + + const { req, res } = createMockRequestResponse('GET', '/test', undefined, { + host: 'incoming.example.com', + 'x-forwarded-for': 'spoofed-client', + 'x-forwarded-host': 'spoofed.example.com', + 'x-forwarded-proto': 'https' + }); + + await client.proxy(req, res); + + expect(receivedHeaders['x-forwarded-for']).toBe('127.0.0.1'); + expect(receivedHeaders['x-forwarded-host']).toBe('incoming.example.com'); + expect(receivedHeaders['x-forwarded-proto']).toBe('http'); + }); + + test('should send an empty forwarded host when the request has no host', async () => { + let receivedHeaders: Record = {}; + + intercept('GET', '/test').reply(({ headers }) => { + receivedHeaders = normalizeHeaders(headers); + return { statusCode: StatusCodes.OK, data: { success: true } }; }); const { req, res } = createMockRequestResponse('GET', '/test'); + delete req.headers.host; await client.proxy(req, res); - expect(receivedHeaders['x-forwarded-for']).toBeDefined(); - expect(receivedHeaders['x-forwarded-proto']).toBeDefined(); - expect(receivedHeaders['x-forwarded-host']).toBeDefined(); + expect(receivedHeaders['x-forwarded-host']).toBe(''); }); test('should modify headers via modifyHeaders callback', async () => { let receivedHeaders: Record = {}; - scope.get('/test').reply(function () { - receivedHeaders = this.req.headers as Record; - return [StatusCodes.OK, { success: true }]; + intercept('GET', '/test').reply(({ headers }) => { + receivedHeaders = normalizeHeaders(headers); + return { statusCode: StatusCodes.OK, data: { success: true } }; }); const { req, res } = createMockRequestResponse('GET', '/test'); @@ -142,7 +345,7 @@ describe('ProxyClient', () => { }); test('should call onResponse callback', async () => { - scope.get('/test').reply(StatusCodes.OK, { success: true }); + intercept('GET', '/test').reply(StatusCodes.OK, { success: true }); const { req, res } = createMockRequestResponse('GET', '/test'); @@ -161,7 +364,13 @@ describe('ProxyClient', () => { }); test('should allow header manipulation in onResponse', async () => { - scope.get('/test').reply(StatusCodes.OK, { success: true }, { 'x-rate-limit': '100' }); + intercept('GET', '/test').reply( + StatusCodes.OK, + { success: true }, + { + headers: { 'x-rate-limit': '100' } + } + ); const { req, res } = createMockRequestResponse('GET', '/test'); @@ -177,13 +386,10 @@ describe('ProxyClient', () => { }); test('should copy response headers', async () => { - scope.get('/test').reply( + intercept('GET', '/test').reply( StatusCodes.OK, { success: true }, - { - 'content-type': 'application/json', - 'x-custom-header': 'custom-value' - } + { headers: { 'content-type': 'application/json', 'x-custom-header': 'custom-value' } } ); const { req, res } = createMockRequestResponse('GET', '/test'); @@ -194,8 +400,38 @@ describe('ProxyClient', () => { expect(res.getHeader('x-custom-header')).toBe('custom-value'); }); + test('should filter hop-by-hop response headers and preserve repeated cookies', async () => { + const headers = new Headers({ + connection: 'x-response-hop', + 'x-response-hop': 'remove-me', + 'x-end-to-end': 'preserve-me' + }); + Object.defineProperty(headers, 'getSetCookie', { + value: () => ['first=1; Path=/', 'second=2; Path=/'] + }); + const fetch = vi.mocked(undiciFetch).mockResolvedValue({ + status: StatusCodes.OK, + statusText: 'OK', + headers, + body: null + } as unknown as UndiciResponse); + + const { req, res } = createMockRequestResponse('GET', '/response-headers'); + + try { + await client.proxy(req, res); + } finally { + fetch.mockRestore(); + } + + expect(res.getHeader('connection')).toBeUndefined(); + expect(res.getHeader('x-response-hop')).toBeUndefined(); + expect(res.getHeader('x-end-to-end')).toBe('preserve-me'); + expect(res.getHeader('set-cookie')).toEqual(['first=1; Path=/', 'second=2; Path=/']); + }); + test('should handle empty response body', async () => { - scope.get('/test').reply(StatusCodes.NO_CONTENT); + intercept('GET', '/test').reply(StatusCodes.NO_CONTENT); const { req, res } = createMockRequestResponse('GET', '/test'); @@ -207,7 +443,7 @@ describe('ProxyClient', () => { test('should handle large response bodies with streaming', async () => { const largeBody = 'x'.repeat(1024 * 1024); // 1MB - scope.get('/large').reply(StatusCodes.OK, largeBody); + intercept('GET', '/large').reply(StatusCodes.OK, largeBody); const { req, res } = createMockRequestResponse('GET', '/large'); @@ -221,13 +457,13 @@ describe('ProxyClient', () => { const shortTimeout = 100; const timeoutClient = new ProxyClient({ target: TARGET, - timeout: shortTimeout + timeout: shortTimeout, + dispatcher: mockAgent }); - scope - .get('/slow') - .delay(shortTimeout * 2) - .reply(StatusCodes.OK); + intercept('GET', '/slow') + .reply(StatusCodes.OK) + .delay(shortTimeout * 2); const { req, res } = createMockRequestResponse('GET', '/slow'); @@ -236,16 +472,131 @@ describe('ProxyClient', () => { }); }); + test('should cancel an active upstream request with the caller controller', async () => { + const controller = new AbortController(); + intercept('GET', '/cancel') + .reply(StatusCodes.OK) + .delay(TIMEOUT * 2); + + const { req, res } = createMockRequestResponse('GET', '/cancel'); + const proxy = client.proxy(req, res, { abortController: controller }); + controller.abort(); + + await expect(proxy).rejects.toMatchObject({ code: 'ETIMEDOUT' }); + }); + + test('should not mutate the response after cancellation during onResponse', async () => { + const controller = new AbortController(); + let releaseResponse!: () => void; + const responseHandling = new Promise((resolve) => { + releaseResponse = resolve; + }); + let onResponseStarted!: () => void; + const responseStarted = new Promise((resolve) => { + onResponseStarted = resolve; + }); + intercept('GET', '/cancel-on-response').reply(StatusCodes.OK); + + const { req, res } = createMockRequestResponse('GET', '/cancel-on-response'); + const proxy = client.proxy(req, res, { + abortController: controller, + onResponse: async () => { + onResponseStarted(); + await responseHandling; + } + }); + + await responseStarted; + controller.abort(); + releaseResponse(); + + await expect(proxy).rejects.toMatchObject({ code: 'ETIMEDOUT' }); + expect(res.statusCode).toBe(0); + expect(res.writableFinished).toBe(false); + }); + + test('should stop streamed writes after downstream cancellation', async () => { + const controller = new AbortController(); + const read = vi.fn(() => new Promise>(() => undefined)); + const cancel = vi.fn().mockResolvedValue(undefined); + const releaseLock = vi.fn(); + const fetch = vi.mocked(undiciFetch).mockResolvedValue({ + status: StatusCodes.OK, + statusText: 'OK', + headers: new Headers(), + body: { getReader: () => ({ read, cancel, releaseLock }) } + } as unknown as UndiciResponse); + const { req, res } = createMockRequestResponse('GET', '/stream-cancel'); + + try { + const proxy = client.proxy(req, res, { abortController: controller }); + await vi.waitFor(() => expect(read).toHaveBeenCalled()); + controller.abort(); + + await expect(proxy).rejects.toMatchObject({ code: 'ETIMEDOUT' }); + expect(res.writableFinished).toBe(false); + expect(res.write).not.toHaveBeenCalled(); + expect(cancel).toHaveBeenCalled(); + expect(releaseLock).toHaveBeenCalled(); + } finally { + fetch.mockRestore(); + } + }); + + test('should remove drain listeners when backpressure is cancelled', async () => { + const controller = new AbortController(); + let drain!: () => void; + const read = vi + .fn() + .mockResolvedValueOnce({ done: false, value: new Uint8Array([1]) }) + .mockImplementation( + () => new Promise>(() => undefined) + ); + const cancel = vi.fn().mockResolvedValue(undefined); + const releaseLock = vi.fn(); + const fetch = vi.mocked(undiciFetch).mockResolvedValue({ + status: StatusCodes.OK, + statusText: 'OK', + headers: new Headers(), + body: { getReader: () => ({ read, cancel, releaseLock }) } + } as unknown as UndiciResponse); + const { req, res } = createMockRequestResponse('GET', '/backpressure-cancel'); + const removeListener = vi.fn(); + res.write = vi.fn(() => false); + res.once = vi.fn((event: string, callback: () => void) => { + if (event === 'drain') drain = callback; + return res; + }); + res.removeListener = removeListener; + + try { + const proxy = client.proxy(req, res, { abortController: controller }); + await vi.waitFor(() => expect(drain).toBeTypeOf('function')); + controller.abort(); + + await expect(proxy).rejects.toMatchObject({ code: 'ETIMEDOUT' }); + expect(res.writableFinished).toBe(false); + expect(removeListener).toHaveBeenCalledWith('drain', drain); + expect(removeListener).toHaveBeenCalledWith('close', expect.any(Function)); + expect(cancel).toHaveBeenCalled(); + expect(releaseLock).toHaveBeenCalled(); + } finally { + fetch.mockRestore(); + } + }); + test('should handle network errors', async () => { - scope.get('/error').replyWithError(new Error('Network error')); + intercept('GET', '/error').replyWithError(new Error('Network error')); const { req, res } = createMockRequestResponse('GET', '/error'); - await expect(client.proxy(req, res)).rejects.toThrow('Network error'); + await expect(client.proxy(req, res)).rejects.toMatchObject({ + cause: expect.objectContaining({ message: 'Network error' }) + }); }); test('should handle HTTP error responses', async () => { - scope.get('/not-found').reply(StatusCodes.NOT_FOUND, { error: 'Not Found' }); + intercept('GET', '/not-found').reply(StatusCodes.NOT_FOUND, { error: 'Not Found' }); const { req, res } = createMockRequestResponse('GET', '/not-found'); @@ -256,8 +607,8 @@ describe('ProxyClient', () => { }); test('should handle redirect responses with redirect: manual', async () => { - scope.get('/redirect').reply(StatusCodes.MOVED_PERMANENTLY, undefined, { - location: 'https://api.example.com/new-location' + intercept('GET', '/redirect').reply(StatusCodes.MOVED_PERMANENTLY, undefined, { + headers: { location: 'https://api.example.com/new-location' } }); const { req, res } = createMockRequestResponse('GET', '/redirect'); @@ -269,7 +620,7 @@ describe('ProxyClient', () => { }); test('should handle array headers', async () => { - scope.get('/test').reply(StatusCodes.OK, { success: true }); + intercept('GET', '/test').reply(StatusCodes.OK, { success: true }); const { req, res } = createMockRequestResponse('GET', '/test', undefined, { accept: ['application/json', 'text/html'] @@ -283,9 +634,9 @@ describe('ProxyClient', () => { test('should remove host header to avoid conflicts', async () => { let receivedHeaders: Record = {}; - scope.get('/test').reply(function () { - receivedHeaders = this.req.headers as Record; - return [StatusCodes.OK, { success: true }]; + intercept('GET', '/test').reply(({ headers }) => { + receivedHeaders = normalizeHeaders(headers); + return { statusCode: StatusCodes.OK, data: { success: true } }; }); const { req, res } = createMockRequestResponse('GET', '/test', undefined, { @@ -301,7 +652,7 @@ describe('ProxyClient', () => { test('should handle PUT requests', async () => { const requestBody = { updated: true }; - scope.put('/resource/123', requestBody).reply(StatusCodes.OK, { success: true }); + intercept('PUT', '/resource/123', requestBody).reply(StatusCodes.OK, { success: true }); const { req, res } = createMockRequestResponse('PUT', '/resource/123', requestBody); @@ -312,7 +663,7 @@ describe('ProxyClient', () => { }); test('should handle DELETE requests', async () => { - scope.delete('/resource/123').reply(StatusCodes.NO_CONTENT); + intercept('DELETE', '/resource/123').reply(StatusCodes.NO_CONTENT); const { req, res } = createMockRequestResponse('DELETE', '/resource/123'); @@ -325,7 +676,7 @@ describe('ProxyClient', () => { test('should handle PATCH requests', async () => { const requestBody = { field: 'new-value' }; - scope.patch('/resource/123', requestBody).reply(StatusCodes.OK, { success: true }); + intercept('PATCH', '/resource/123', requestBody).reply(StatusCodes.OK, { success: true }); const { req, res } = createMockRequestResponse('PATCH', '/resource/123', requestBody); @@ -336,7 +687,7 @@ describe('ProxyClient', () => { }); test('should handle streaming errors and release reader lock', async () => { - scope.get('/streaming-error').reply(StatusCodes.OK, 'test response'); + intercept('GET', '/streaming-error').reply(StatusCodes.OK, 'test response'); const { req, res } = createMockRequestResponse('GET', '/streaming-error'); @@ -349,13 +700,10 @@ describe('ProxyClient', () => { }); test('should allow header mutation in onResponse callback', async () => { - scope.get('/test').reply( + intercept('GET', '/test').reply( StatusCodes.OK, { success: true }, - { - 'x-rate-limit': '100', - 'x-scope': 'repo' - } + { headers: { 'x-rate-limit': '100', 'x-scope': 'repo' } } ); const { req, res } = createMockRequestResponse('GET', '/test'); @@ -377,8 +725,8 @@ describe('ProxyClient', () => { test('should remove content-encoding and content-length headers', async () => { // Mock a response that simulates having encoding headers // (fetch will have already decompressed, but headers remain) - scope.get('/api-response').reply(StatusCodes.OK, JSON.stringify({ success: true }), { - 'content-type': 'application/json' + intercept('GET', '/api-response').reply(StatusCodes.OK, JSON.stringify({ success: true }), { + headers: { 'content-type': 'application/json' } }); const { req, res } = createMockRequestResponse('GET', '/api-response'); @@ -407,7 +755,8 @@ function createMockRequestResponse( method: string, url: string, body?: unknown, - headers: Record = {} + headers: Record = {}, + socketOptions: { encrypted?: boolean; remoteAddress?: string } = {} ): { req: IncomingMessage; res: ServerResponse } { const req = { method, @@ -417,9 +766,10 @@ function createMockRequestResponse( host: headers.host || 'localhost:3000' }, socket: { - remoteAddress: '127.0.0.1', - encrypted: false + remoteAddress: socketOptions.remoteAddress || '127.0.0.1', + encrypted: socketOptions.encrypted || false }, + resume: vi.fn(), on: vi.fn((event: string, callback: (chunk?: Buffer) => void) => { if (event === 'data' && body) { // Emit body data @@ -468,4 +818,4 @@ function createMockRequestResponse( } as unknown as ServerResponse; return { req, res }; -} +} \ No newline at end of file diff --git a/src/proxy-client.ts b/src/proxy-client.ts index 224a6d5..d880098 100644 --- a/src/proxy-client.ts +++ b/src/proxy-client.ts @@ -1,22 +1,49 @@ /* Author: Hudson S. Borges */ import type { IncomingMessage, ServerResponse } from 'node:http'; -import type { Dispatcher } from 'undici'; +import { type Dispatcher, fetch as undiciFetch } from 'undici'; + +export type ProxyHeaderValue = string | string[]; +export type ProxyResponseHeaders = Record; + +const HOP_BY_HOP_HEADERS = new Set([ + 'connection', + 'keep-alive', + 'proxy-authenticate', + 'proxy-authorization', + 'te', + 'trailer', + 'transfer-encoding', + 'upgrade', + 'proxy-connection' +]); export interface ProxyClientOptions { target: string; timeout: number; + maxRequestBodyBytes?: number; dispatcher?: Dispatcher; } +export class PayloadTooLargeError extends Error { + readonly code = 'PAYLOAD_TOO_LARGE'; + + constructor() { + super('Request body too large'); + this.name = 'PayloadTooLargeError'; + } +} + export class ProxyClient { private readonly target: string; private readonly timeout: number; + private readonly maxRequestBodyBytes: number; private readonly dispatcher?: Dispatcher; constructor(options: ProxyClientOptions) { this.target = options.target; this.timeout = options.timeout; + this.maxRequestBodyBytes = options.maxRequestBodyBytes ?? 1024 * 1024; this.dispatcher = options.dispatcher; } @@ -31,14 +58,15 @@ export class ProxyClient { res: ServerResponse, options?: { modifyHeaders?: (headers: Record) => Record; + abortController?: AbortController; onResponse?: (data: { status: number; statusText: string; - headers: Record; + headers: ProxyResponseHeaders; }) => void | Promise; } ): Promise { - const controller = new AbortController(); + const controller = options?.abortController ?? new AbortController(); const timeoutId = setTimeout(() => controller.abort(), this.timeout); try { @@ -53,28 +81,43 @@ export class ProxyClient { } } + const connectionTokens = this.connectionTokens(headers); + const forwardedHost = headers.host || ''; + // Remove host header to avoid conflicts delete headers.host; // Apply header modifications if provided - const modifiedHeaders = options?.modifyHeaders ? options.modifyHeaders(headers) : headers; + const filteredHeaders = this.filterHopByHopHeaders(headers, connectionTokens) as Record< + string, + string + >; + const modifiedHeaders = options?.modifyHeaders + ? options.modifyHeaders(filteredHeaders) + : filteredHeaders; - // Add forwarded headers (x-forwarded-*) - const forwarded = headers['x-forwarded-for'] || req.socket.remoteAddress || ''; - modifiedHeaders['x-forwarded-for'] = forwarded; - modifiedHeaders['x-forwarded-proto'] = 'https' in req.socket ? 'https' : 'http'; - modifiedHeaders['x-forwarded-host'] = headers.host || ''; + // Do not trust inbound forwarded headers. The immediate connection is the only trusted hop + // until a trusted proxy policy is configured at the application boundary. + const socket = req.socket as IncomingMessage['socket'] & { encrypted?: boolean }; + const requestHeaders = this.filterHopByHopHeaders( + modifiedHeaders, + this.connectionTokens(modifiedHeaders) + ) as Record; + delete requestHeaders.host; + requestHeaders['x-forwarded-for'] = req.socket.remoteAddress || ''; + requestHeaders['x-forwarded-proto'] = socket.encrypted ? 'https' : 'http'; + requestHeaders['x-forwarded-host'] = forwardedHost; // Prepare request body if present let body: Buffer | undefined; if (req.method !== 'GET' && req.method !== 'HEAD') { - body = await this.readRequestBody(req); + body = await this.readRequestBody(req, controller.signal); } // Make the fetch request - const response = await fetch(targetUrl.toString(), { + const response = await undiciFetch(targetUrl.toString(), { method: req.method, - headers: modifiedHeaders, + headers: requestHeaders, body: body, signal: controller.signal, redirect: 'manual', @@ -84,52 +127,96 @@ export class ProxyClient { clearTimeout(timeoutId); // Convert immutable response headers to mutable object - const responseHeaders: Record = {}; + const responseHeaders: ProxyResponseHeaders = {}; response.headers.forEach((value, key) => { - responseHeaders[key] = value; + const current = responseHeaders[key]; + responseHeaders[key] = current + ? Array.isArray(current) + ? [...current, value] + : [current, value] + : value; }); + const setCookies = ( + response.headers as Headers & { getSetCookie?: () => string[] } + ).getSetCookie?.(); + if (setCookies?.length) responseHeaders['set-cookie'] = setCookies; + const filteredResponseHeaders = this.filterHopByHopHeaders( + responseHeaders, + this.connectionTokens(responseHeaders) + ); // Call onResponse callback if provided (with mutable headers) if (options?.onResponse) { + if (!this.canMutateResponse(res, controller.signal)) return; await options.onResponse({ status: response.status, statusText: response.statusText, - headers: responseHeaders + headers: filteredResponseHeaders }); } // Remove content-encoding headers since fetch automatically decompresses // Keeping them would cause ERR_CONTENT_DECODING_FAILED in browsers - delete responseHeaders['content-encoding']; - delete responseHeaders['content-length']; // Also remove as length changes after decompression + delete filteredResponseHeaders['content-encoding']; + delete filteredResponseHeaders['content-length']; // Also remove as length changes after decompression + + if (!this.canMutateResponse(res, controller.signal)) return; // Copy response status res.statusCode = response.status; + if (!this.canMutateResponse(res, controller.signal)) return; res.statusMessage = response.statusText; // Copy modified response headers - for (const [key, value] of Object.entries(responseHeaders)) { + for (const [key, value] of Object.entries(filteredResponseHeaders)) { + if (!this.canMutateResponse(res, controller.signal)) return; res.setHeader(key, value); } // Stream response body if (response.body) { + if (!this.canWriteResponse(res, controller.signal)) return; const reader = response.body.getReader(); try { while (true) { - const { done, value } = await reader.read(); - if (done) break; + if (!this.canWriteResponse(res, controller.signal)) { + void reader.cancel().catch(() => undefined); + return; + } + const { done, value } = await this.awaitAbort(reader.read(), controller.signal); + if (done) { + if (this.canWriteResponse(res, controller.signal)) res.end(); + return; + } + if (!this.canWriteResponse(res, controller.signal)) { + void reader.cancel().catch(() => undefined); + return; + } if (!res.write(value)) { // Backpressure: wait for drain event - await new Promise((resolve) => res.once('drain', resolve)); + if (!this.canWriteResponse(res, controller.signal)) { + void reader.cancel().catch(() => undefined); + return; + } + const drained = await this.waitForDrain(res, controller.signal); + if (!drained) { + void reader.cancel().catch(() => undefined); + return; + } } } - res.end(); } catch (error) { - reader.releaseLock(); + void reader.cancel().catch(() => undefined); throw error; + } finally { + try { + reader.releaseLock(); + } catch { + // The reader may already be released by the underlying stream. + } } } else { + if (!this.canWriteResponse(res, controller.signal)) return; res.end(); } } catch (error) { @@ -149,12 +236,142 @@ export class ProxyClient { /** * Read the full request body into a buffer */ - private readRequestBody(req: IncomingMessage): Promise { + private readRequestBody(req: IncomingMessage, signal: AbortSignal): Promise { return new Promise((resolve, reject) => { const chunks: Buffer[] = []; - req.on('data', (chunk) => chunks.push(chunk)); - req.on('end', () => resolve(Buffer.concat(chunks))); - req.on('error', reject); + let size = 0; + const contentLength = req.headers['content-length']; + const declaredLength = Array.isArray(contentLength) + ? Number(contentLength[0]) + : contentLength === undefined + ? undefined + : Number(contentLength); + const cleanup = (): void => { + req.removeListener?.('data', onData); + req.removeListener?.('end', onEnd); + req.removeListener?.('error', onError); + signal.removeEventListener('abort', onAbort); + }; + const onData = (chunk: Buffer): void => { + size += chunk.length; + if (size > this.maxRequestBodyBytes) { + cleanup(); + req.resume?.(); + reject(new PayloadTooLargeError()); + return; + } + chunks.push(chunk); + }; + + if (declaredLength !== undefined && declaredLength > this.maxRequestBodyBytes) { + reject(new PayloadTooLargeError()); + req.resume?.(); + return; + } + const onEnd = (): void => { + cleanup(); + resolve(Buffer.concat(chunks)); + }; + const onError = (error: Error): void => { + cleanup(); + reject(error); + }; + const onAbort = (): void => { + cleanup(); + reject(new DOMException('The operation was aborted', 'AbortError')); + }; + + if (signal.aborted) { + onAbort(); + return; + } + + req.on('data', onData); + req.on('end', onEnd); + req.on('error', onError); + signal.addEventListener('abort', onAbort, { once: true }); }); } -} + + private async awaitAbort(operation: Promise, signal: AbortSignal): Promise { + if (signal.aborted) throw new DOMException('The operation was aborted', 'AbortError'); + + let abort: (() => void) | undefined; + const aborted = new Promise((_resolve, reject) => { + abort = () => reject(new DOMException('The operation was aborted', 'AbortError')); + signal.addEventListener('abort', abort, { once: true }); + }); + + try { + return await Promise.race([operation, aborted]); + } finally { + if (abort) signal.removeEventListener('abort', abort); + } + } + + private connectionTokens(headers: Record): Set { + const connection = headers.connection; + const values = + connection === undefined ? [] : Array.isArray(connection) ? connection : [connection]; + return new Set( + values.flatMap((value) => value.split(',')).map((value) => value.trim().toLowerCase()) + ); + } + + private filterHopByHopHeaders( + headers: Record, + connectionTokens: Set + ): Record { + return Object.fromEntries( + Object.entries(headers).filter(([key]) => { + const normalizedKey = key.toLowerCase(); + return !HOP_BY_HOP_HEADERS.has(normalizedKey) && !connectionTokens.has(normalizedKey); + }) + ); + } + + private canWriteResponse(res: ServerResponse, signal: AbortSignal): boolean { + if (signal.aborted) throw new DOMException('The operation was aborted', 'AbortError'); + return !res.destroyed && !res.writableEnded; + } + + private canMutateResponse(res: ServerResponse, signal: AbortSignal): boolean { + return this.canWriteResponse(res, signal) && !res.headersSent; + } + + private waitForDrain(res: ServerResponse, signal: AbortSignal): Promise { + return new Promise((resolve, reject) => { + const cleanup = (): void => { + res.removeListener?.('drain', onDrain); + res.removeListener?.('close', onClose); + signal.removeEventListener('abort', onAbort); + }; + const onDrain = (): void => { + cleanup(); + resolve(true); + }; + const onClose = (): void => { + cleanup(); + resolve(false); + }; + const onAbort = (): void => { + cleanup(); + reject(new DOMException('The operation was aborted', 'AbortError')); + }; + + if (signal.aborted) { + onAbort(); + return; + } + + if (res.destroyed || res.writableEnded) { + resolve(false); + return; + } + + res.once('drain', onDrain); + res.once('close', onClose); + signal.addEventListener('abort', onAbort, { once: true }); + }); + } +} \ No newline at end of file diff --git a/src/router.spec.ts b/src/router.spec.ts index 122d35c..3b5b507 100644 --- a/src/router.spec.ts +++ b/src/router.spec.ts @@ -1,16 +1,38 @@ -import express, { type Express } from 'express'; +import EventEmitter from 'node:events'; + +import express, { type Express, type Request, type Response } from 'express'; import { StatusCodes } from 'http-status-codes'; import repeat from 'lodash/repeat.js'; import times from 'lodash/times.js'; import nock from 'nock'; import request from 'supertest'; -import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from 'vitest'; +import { MockAgent, type MockPool } from 'undici'; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test, vi } from 'vitest'; +import { PayloadTooLargeError } from './proxy-client'; import Middleware from './router'; let app: Express; const FAKE_TOKEN = repeat('t', 40); +const SECOND_TOKEN = `${repeat('u', 39)}1`; + +const RATE_LIMIT_RESOURCES = { + core: { limit: 5000, remaining: 4000, reset: 2000000000 }, + search: { limit: 30, remaining: 25, reset: 2000000000 }, + code_search: { limit: 10, remaining: 8, reset: 2000000000 }, + graphql: { limit: 5000, remaining: 4500, reset: 2000000000 } +}; + +function rateLimitResponse( + resources: unknown = RATE_LIMIT_RESOURCES, + status = StatusCodes.OK +): globalThis.Response { + return { + status, + json: async () => ({ resources }) + } as unknown as globalThis.Response; +} describe('Middleware constructor and methods', () => { beforeAll(() => { @@ -36,33 +58,1106 @@ describe('Middleware constructor and methods', () => { expect(() => new Middleware([])).toThrowError(); }); - test('it should remove/add tokens', () => { + test.each([ + ['requestTimeout', { requestTimeout: 0 }], + ['minRemaining', { minRemaining: -1 }], + ['timeBudgetMultiplier', { timeBudgetMultiplier: 11 }], + ['maxRequestBodyBytes', { maxRequestBodyBytes: 1024 * 1024 - 1 }], + ['maxQueueDepthPerWorker', { maxQueueDepthPerWorker: 0 }], + ['queueWaitTimeout', { queueWaitTimeout: 0 }], + ['requestLifetimeTimeout', { requestLifetimeTimeout: 0 }] + ])('it should reject invalid direct %s options', (_name, invalidOptions) => { + expect(() => new Middleware([FAKE_TOKEN], invalidOptions)).toThrow(`Invalid ${_name}`); + }); + + test('it should validate every token before allocating worker resources', () => { + const setInterval = vi.spyOn(globalThis, 'setInterval'); + + try { + expect(() => new Middleware([FAKE_TOKEN, 'invalid-token'])).toThrow( + 'unsupported GitHub credential format' + ); + expect(setInterval).not.toHaveBeenCalled(); + } finally { + setInterval.mockRestore(); + } + }); + + test('it should use token refresh intervals but no per-worker polling intervals', async () => { + vi.useFakeTimers(); + const setInterval = vi.spyOn(globalThis, 'setInterval'); + const middleware = new Middleware([FAKE_TOKEN, SECOND_TOKEN]); + + try { + expect(setInterval).toHaveBeenCalledTimes(2); + const workersByResource = ( + middleware as unknown as { + workersByResource: Record>>; + } + ).workersByResource; + for (const workers of Object.values(workersByResource)) { + for (const worker of workers) { + expect(worker).not.toHaveProperty('pullInterval'); + expect(worker).not.toHaveProperty('checkForWork'); + } + } + } finally { + setInterval.mockRestore(); + await middleware.destroy(); + vi.useRealTimers(); + } + }); + + test('it should remove/add tokens', async () => { const middleware = new Middleware([FAKE_TOKEN]); + const notifyDispatch = vi.spyOn( + middleware as unknown as { notifyDispatch: (resource: 'core') => void }, + 'notifyDispatch' + ); expect(middleware.tokens).toHaveLength(1); - middleware.removeToken(FAKE_TOKEN); + await middleware.removeToken(FAKE_TOKEN); expect(middleware.tokens).toHaveLength(0); + expect(notifyDispatch).toHaveBeenCalledTimes(4); middleware.addToken(FAKE_TOKEN); expect(middleware.tokens).toHaveLength(1); + expect(notifyDispatch).toHaveBeenCalledTimes(12); + return middleware.destroy(); }); - test('it should create only one client per token', () => { + test('it should create only one client per token', async () => { const middleware = new Middleware(times(2, () => FAKE_TOKEN)); expect(middleware.tokens).toHaveLength(1); + await middleware.destroy(); + }); + + test('it should destroy every token and its lifecycle timers exactly once', async () => { + vi.useFakeTimers(); + try { + const middleware = new Middleware(times(3, (index) => `${repeat('t', 39)}${index}`)); + + const timerCount = vi.getTimerCount(); + expect(timerCount).toBeGreaterThan(0); + + const destruction = middleware.destroy(); + expect(middleware.destroy()).toBe(destruction); + await destruction; + + expect(middleware.tokens).toEqual([]); + expect(vi.getTimerCount()).toBeLessThan(timerCount); + + middleware.addToken(FAKE_TOKEN); + expect(middleware.tokens).toEqual([]); + await middleware.removeToken('missing-token'); + } finally { + vi.useRealTimers(); + } + }); + + test('it should clear dispatcher timers synchronously during destruction', async () => { + vi.useFakeTimers(); + const middleware = new Middleware([FAKE_TOKEN], { minRemaining: 5000 }); + const requestResponse = createStateAwareRequestResponse({ + headersSent: false, + writableEnded: false, + destroyed: false + }); + const state = middleware as unknown as { + dispatchStates: Record; + budgetResetTimer?: NodeJS.Timeout; + dispatch: (resource: 'core') => void; + }; + const dispatch = vi.spyOn(state, 'dispatch'); + + try { + await middleware.schedule(requestResponse.req, requestResponse.res); + await Promise.resolve(); + await Promise.resolve(); + expect(state.dispatchStates.core.resetTimer).toBeDefined(); + + const dispatchCallsBeforeDestroy = dispatch.mock.calls.length; + const destruction = middleware.destroy(); + expect(state.budgetResetTimer).toBeUndefined(); + for (const resource of Object.values(state.dispatchStates)) { + expect(resource.resetTimer).toBeUndefined(); + expect(resource.notificationQueued).toBe(false); + } + await destruction; + vi.advanceTimersByTime(120000); + await Promise.resolve(); + expect(dispatch.mock.calls.length).toBe(dispatchCallsBeforeDestroy); + } finally { + await middleware.destroy(); + vi.useRealTimers(); + } + }); + + test('it should terminate queued responses and ignore work after destroy', async () => { + const middleware = new Middleware([FAKE_TOKEN], { minRemaining: 5000 }); + const response = { + destroyed: false, + destroy: vi.fn(), + writableEnded: false + } as unknown as Response; + + await middleware.schedule({ method: 'GET', path: '/' } as Request, response); + await middleware.destroy(); + + expect(response.destroy).toHaveBeenCalledTimes(1); + + await middleware.schedule({ method: 'GET', path: '/' } as Request, response); + expect(response.destroy).toHaveBeenCalledTimes(2); + }); + + test('it should settle worker schedule promises cleared during destruction', async () => { + const middleware = new Middleware([FAKE_TOKEN], { minRemaining: 0 }); + const worker = ( + middleware as unknown as { + workersByResource: { + core: Array<{ + remaining: number; + reset: number; + proxy: { proxy: (...args: never[]) => Promise }; + schedule: (req: Request, res: Response) => Promise; + }>; + }; + } + ).workersByResource.core[0]; + worker.remaining = 1000; + worker.reset = 0; + + vi.spyOn(worker.proxy, 'proxy').mockImplementation(() => new Promise(() => undefined)); + + const requests = times(11, () => { + const request = { + headers: {}, + method: 'GET', + path: '/', + socket: { destroyed: false, writableFinished: false } + } as unknown as Request; + const response = { + destroy: vi.fn(), + destroyed: false, + writableEnded: false + } as unknown as Response; + return { request, response }; + }); + + const schedules = requests.map(({ request, response }) => worker.schedule(request, response)); + const destruction = middleware.destroy(); + + await expect(Promise.all(schedules)).resolves.toHaveLength(11); + await destruction; + requests.forEach(({ response }) => expect(response.destroy).toHaveBeenCalled()); + }); + + test('it should await concurrent token removal during router destruction', async () => { + const firstToken = `${repeat('a', 39)}0`; + const secondToken = `${repeat('a', 39)}1`; + const middleware = new Middleware([firstToken, secondToken]); + const removal = middleware.removeToken(firstToken); + const destruction = middleware.destroy(); + + await Promise.all([removal, destruction]); + expect(middleware.tokens).toEqual([]); + }); + + test('it should aggregate Agent destruction failures after attempting every worker', async () => { + const middleware = new Middleware([FAKE_TOKEN]); + const worker = ( + middleware as unknown as { + workersByResource: { core: Array<{ agent: { destroy: () => Promise } }> }; + } + ).workersByResource.core[0]; + vi.spyOn(worker.agent, 'destroy').mockRejectedValue(new Error('agent cleanup failed')); + + await expect(middleware.destroy()).rejects.toThrow('agent cleanup failed'); + await expect(middleware.destroy()).rejects.toThrow('agent cleanup failed'); + }); + + test('manual refresh should reject and not emit ready when a worker refresh fails', async () => { + const fetch = vi.spyOn(globalThis, 'fetch').mockRejectedValue(new Error('refresh failed')); + const middleware = new Middleware([FAKE_TOKEN]); + const ready = vi.fn(); + middleware.on('ready', ready); + + try { + await expect(middleware.refreshRateLimits()).rejects.toThrow('refresh failed'); + expect(ready).not.toHaveBeenCalled(); + } finally { + await middleware.destroy(); + fetch.mockRestore(); + } + }); + + test('should not write or destroy an already-completed response after a proxy error', async () => { + const middleware = new Middleware([FAKE_TOKEN]); + const worker = ( + middleware as unknown as { + workersByResource: { + core: Array<{ + remaining: number; + reset: number; + proxy: { proxy: (...args: never[]) => Promise }; + schedule: (req: Request, res: Response) => Promise; + }>; + }; + } + ).workersByResource.core[0]; + worker.remaining = 5000; + worker.reset = 0; + const { req, res, send, status, destroy } = createStateAwareRequestResponse({ + headersSent: true, + writableEnded: true, + destroyed: false + }); + const proxy = vi.spyOn(worker.proxy, 'proxy').mockRejectedValue(new Error('upstream failed')); + + try { + await worker.schedule(req, res); + expect(status).not.toHaveBeenCalled(); + expect(send).not.toHaveBeenCalled(); + expect(destroy).not.toHaveBeenCalled(); + } finally { + proxy.mockRestore(); + await middleware.destroy(); + } + }); + + test('should destroy a connected partial response without sending a duplicate error', async () => { + const middleware = new Middleware([FAKE_TOKEN]); + const worker = ( + middleware as unknown as { + workersByResource: { + core: Array<{ + remaining: number; + reset: number; + proxy: { proxy: (...args: never[]) => Promise }; + schedule: (req: Request, res: Response) => Promise; + }>; + }; + } + ).workersByResource.core[0]; + worker.remaining = 5000; + worker.reset = 0; + const { req, res, send, status, destroy } = createStateAwareRequestResponse({ + headersSent: true, + writableEnded: false, + destroyed: false + }); + const proxy = vi.spyOn(worker.proxy, 'proxy').mockRejectedValue(new Error('upstream failed')); + + try { + await worker.schedule(req, res); + expect(status).not.toHaveBeenCalled(); + expect(send).not.toHaveBeenCalled(); + expect(destroy).toHaveBeenCalledTimes(1); + } finally { + proxy.mockRestore(); + await middleware.destroy(); + } + }); + + test('should propagate request cancellation to the active upstream operation', async () => { + const middleware = new Middleware([FAKE_TOKEN]); + const worker = ( + middleware as unknown as { + workersByResource: { + core: Array<{ + remaining: number; + reset: number; + proxy: { + proxy: ( + req: Request, + res: Response, + options?: { abortController?: AbortController } + ) => Promise; + }; + schedule: (req: Request, res: Response) => Promise; + }>; + }; + } + ).workersByResource.core[0]; + worker.remaining = 5000; + worker.reset = 0; + const { req, res, status, destroy } = createStateAwareRequestResponse({ + headersSent: false, + writableEnded: false, + destroyed: false + }); + let signal: AbortSignal | undefined; + let started!: () => void; + const operationStarted = new Promise((resolve) => { + started = resolve; + }); + const proxy = vi.spyOn(worker.proxy, 'proxy').mockImplementation((_req, _res, options) => { + signal = options?.abortController?.signal; + started(); + return new Promise((_resolve, reject) => { + signal?.addEventListener( + 'abort', + () => reject(new DOMException('The operation was aborted', 'AbortError')), + { once: true } + ); + }); + }); + + try { + const scheduled = worker.schedule(req, res); + await operationStarted; + req.aborted = true; + req.emit('aborted'); + await scheduled; + expect(signal?.aborted).toBe(true); + expect(status).not.toHaveBeenCalled(); + expect(destroy).not.toHaveBeenCalled(); + } finally { + proxy.mockRestore(); + await middleware.destroy(); + } + }); + + test('should not write a local rejection response after the request disconnects', async () => { + const middleware = new Middleware([FAKE_TOKEN]); + const worker = ( + middleware as unknown as { + workersByResource: { + core: Array<{ + remaining: number; + reset: number; + proxy: { proxy: (...args: never[]) => Promise }; + schedule: (req: Request, res: Response) => Promise; + }>; + }; + } + ).workersByResource.core[0]; + worker.remaining = 5000; + worker.reset = 0; + const { req, res, json } = createStateAwareRequestResponse({ + headersSent: false, + writableEnded: false, + destroyed: false + }); + const proxy = vi.spyOn(worker.proxy, 'proxy').mockImplementation(async () => { + req.aborted = true; + throw new PayloadTooLargeError(); + }); + + try { + await worker.schedule(req, res); + expect(json).not.toHaveBeenCalled(); + } finally { + proxy.mockRestore(); + await middleware.destroy(); + } + }); + + test('should destroy partial responses from the worker lifetime-error path', async () => { + const middleware = new Middleware([FAKE_TOKEN]); + const worker = ( + middleware as unknown as { + workersByResource: { + core: Array<{ + remaining: number; + reset: number; + proxy: { proxy: (...args: never[]) => Promise }; + schedule: (req: Request, res: Response) => Promise; + }>; + }; + } + ).workersByResource.core[0]; + worker.remaining = 5000; + worker.reset = 0; + const requestResponse = createStateAwareRequestResponse({ + headersSent: false, + writableEnded: false, + destroyed: false + }); + const proxy = vi.spyOn(worker.proxy, 'proxy').mockImplementation(async (req) => { + requestResponse.res.headersSent = true; + (req as unknown as { proxyContext?: { timeoutReason?: string } }) + .proxyContext!.timeoutReason = 'lifetime'; + throw new Error('lifetime expired'); + }); + + try { + await worker.schedule(requestResponse.req, requestResponse.res); + expect(requestResponse.destroy).toHaveBeenCalledTimes(1); + expect(requestResponse.json).not.toHaveBeenCalled(); + } finally { + proxy.mockRestore(); + await middleware.destroy(); + } + }); + + test('should abort the active proxy before settling a destroyed worker task', async () => { + const middleware = new Middleware([FAKE_TOKEN]); + const worker = ( + middleware as unknown as { + workersByResource: { + core: Array<{ + remaining: number; + reset: number; + proxy: { + proxy: ( + req: Request, + res: Response, + options?: { abortController?: AbortController } + ) => Promise; + }; + schedule: (req: Request, res: Response) => Promise; + destroy: () => Promise; + }>; + }; + } + ).workersByResource.core[0]; + worker.remaining = 5000; + worker.reset = 0; + const { req, res, status, send } = createStateAwareRequestResponse({ + headersSent: false, + writableEnded: false, + destroyed: false + }); + let signal: AbortSignal | undefined; + let started!: () => void; + const operationStarted = new Promise((resolve) => { + started = resolve; + }); + const proxy = vi.spyOn(worker.proxy, 'proxy').mockImplementation((_req, _res, options) => { + signal = options?.abortController?.signal; + started(); + return new Promise((_resolve, reject) => { + signal?.addEventListener( + 'abort', + () => reject(new DOMException('The operation was aborted', 'AbortError')), + { once: true } + ); + }); + }); + + try { + const scheduled = worker.schedule(req, res); + await operationStarted; + await Promise.all([scheduled, worker.destroy()]); + expect(signal?.aborted).toBe(true); + expect(status).not.toHaveBeenCalled(); + expect(send).not.toHaveBeenCalled(); + } finally { + proxy.mockRestore(); + await middleware.destroy(); + } + }); + + test('should retain the router while destroying an active request context', async () => { + const middleware = new Middleware([FAKE_TOKEN], { minRemaining: 0 }); + const worker = ( + middleware as unknown as { + workersByResource: { + core: Array<{ + schedule: (req: Request, res: Response) => Promise; + destroy: () => Promise; + }>; + }; + } + ).workersByResource.core[0]; + const { req, res } = createStateAwareRequestResponse({ + headersSent: false, + writableEnded: false, + destroyed: false + }); + (worker as unknown as { remaining: number; reset: number }).remaining = 5000; + (worker as unknown as { remaining: number; reset: number }).reset = 0; + const proxy = vi + .spyOn( + (worker as unknown as { proxy: { proxy: (...args: never[]) => Promise } }).proxy, + 'proxy' + ) + .mockImplementation(() => new Promise(() => undefined)); + + try { + await middleware.schedule(req, res); + await waitFor( + () => (worker as unknown as { ownedContexts: Set }).ownedContexts.size === 1 + ); + const context = (req as Request & { proxyContext?: { controller: AbortController } }) + .proxyContext; + expect(context).toBeDefined(); + expect((worker as unknown as { ownedContexts: Set }).ownedContexts.size).toBe(1); + await worker.destroy(); + expect(context?.controller.signal.aborted).toBe(true); + expect((req as Request & { proxyContext?: unknown }).proxyContext).toBeUndefined(); + } finally { + proxy.mockRestore(); + await middleware.destroy(); + } + }); + + test('should abort the shared controller and return one fresh 504 at the lifetime deadline', async () => { + const middleware = new Middleware([FAKE_TOKEN], { + minRemaining: 0, + requestLifetimeTimeout: 250, + queueWaitTimeout: 1000 + }); + const worker = ( + middleware as unknown as { + workersByResource: { + core: Array<{ + remaining: number; + reset: number; + proxy: { + proxy: ( + req: Request, + res: Response, + options?: { abortController?: AbortController } + ) => Promise; + }; + }>; + }; + } + ).workersByResource.core[0]; + worker.remaining = 5000; + worker.reset = 0; + const requestResponse = createStateAwareRequestResponse({ + headersSent: false, + writableEnded: false, + destroyed: false + }); + requestResponse.json.mockImplementation(() => { + requestResponse.res.headersSent = true; + }); + let observedSignal: AbortSignal | undefined; + let started!: () => void; + const startedPromise = new Promise((resolve) => { + started = resolve; + }); + const proxy = vi.spyOn(worker.proxy, 'proxy').mockImplementation((_req, _res, options) => { + observedSignal = options?.abortController?.signal; + started(); + return new Promise((_resolve, reject) => { + observedSignal?.addEventListener( + 'abort', + () => reject(new DOMException('The operation was aborted', 'AbortError')), + { once: true } + ); + }); + }); + + try { + await middleware.schedule(requestResponse.req, requestResponse.res); + await startedPromise; + const context = ( + requestResponse.req as Request & { + proxyContext?: { controller: AbortController }; + } + ).proxyContext; + expect(context).toBeDefined(); + await waitFor(() => requestResponse.json.mock.calls.length === 1); + expect(observedSignal).toBe(context?.controller.signal); + expect(observedSignal?.aborted).toBe(true); + expect(requestResponse.status).toHaveBeenCalledWith(StatusCodes.GATEWAY_TIMEOUT); + expect(requestResponse.json).toHaveBeenCalledWith({ message: 'Request lifetime exceeded' }); + } finally { + proxy.mockRestore(); + await middleware.destroy(); + } + }); + + test('should destroy a partial response on active lifetime expiry', async () => { + const middleware = new Middleware([FAKE_TOKEN], { + minRemaining: 0, + requestLifetimeTimeout: 250, + queueWaitTimeout: 1000 + }); + const worker = ( + middleware as unknown as { + workersByResource: { + core: Array<{ + remaining: number; + reset: number; + proxy: { + proxy: ( + req: Request, + res: Response, + options?: { abortController?: AbortController } + ) => Promise; + }; + }>; + }; + } + ).workersByResource.core[0]; + worker.remaining = 5000; + worker.reset = 0; + const requestResponse = createStateAwareRequestResponse({ + headersSent: false, + writableEnded: false, + destroyed: false + }); + requestResponse.destroy.mockImplementation(() => { + requestResponse.res.destroyed = true; + }); + let started!: () => void; + const startedPromise = new Promise((resolve) => { + started = resolve; + }); + const proxy = vi.spyOn(worker.proxy, 'proxy').mockImplementation((_req, res, options) => { + res.headersSent = true; + started(); + return new Promise((_resolve, reject) => { + options?.abortController?.signal.addEventListener( + 'abort', + () => reject(new DOMException('The operation was aborted', 'AbortError')), + { once: true } + ); + }); + }); + + try { + await middleware.schedule(requestResponse.req, requestResponse.res); + await startedPromise; + await waitFor(() => requestResponse.destroy.mock.calls.length === 1); + expect(requestResponse.json).not.toHaveBeenCalled(); + } finally { + proxy.mockRestore(); + await middleware.destroy(); + } + }); + + test.each([ + ['completed', { headersSent: false, writableEnded: true, destroyed: false }], + ['disconnected', { headersSent: false, writableEnded: false, destroyed: false }] + ])( + 'should not write or destroy a %s response on active lifetime expiry', + async (_name, state) => { + const middleware = new Middleware([FAKE_TOKEN], { + minRemaining: 0, + requestLifetimeTimeout: 250, + queueWaitTimeout: 1000 + }); + const worker = ( + middleware as unknown as { + workersByResource: { + core: Array<{ + remaining: number; + reset: number; + proxy: { + proxy: ( + req: Request, + res: Response, + options?: { abortController?: AbortController } + ) => Promise; + }; + }>; + }; + } + ).workersByResource.core[0]; + worker.remaining = 5000; + worker.reset = 0; + const requestResponse = createStateAwareRequestResponse({ + headersSent: false, + writableEnded: false, + destroyed: false + }); + let started!: () => void; + const startedPromise = new Promise((resolve) => { + started = resolve; + }); + const proxy = vi.spyOn(worker.proxy, 'proxy').mockImplementation((req, res, options) => { + if (_name === 'disconnected') req.aborted = true; + if (_name === 'completed') { + (res as unknown as { writableEnded: boolean }).writableEnded = true; + } + started(); + return new Promise((_resolve, reject) => { + options?.abortController?.signal.addEventListener( + 'abort', + () => reject(new DOMException('The operation was aborted', 'AbortError')), + { once: true } + ); + }); + }); + + try { + await middleware.schedule(requestResponse.req, requestResponse.res); + await startedPromise; + await new Promise((resolve) => setTimeout(resolve, 300)); + expect(requestResponse.json).not.toHaveBeenCalled(); + expect(requestResponse.destroy).not.toHaveBeenCalled(); + } finally { + proxy.mockRestore(); + await middleware.destroy(); + } + } + ); + + test('should destroy a partial response when active lifetime expires', async () => { + const middleware = new Middleware([FAKE_TOKEN]); + const requestResponse = createStateAwareRequestResponse({ + headersSent: false, + writableEnded: false, + destroyed: false + }); + await middleware.schedule(requestResponse.req, requestResponse.res); + const queue = ( + middleware as unknown as { + queues: { + core: { dequeue: () => { req: Request; res: Response } | undefined; size: number }; + }; + } + ).queues.core; + queue.dequeue(); + const context = ( + requestResponse.req as Request & { + proxyContext?: { state: string; timeoutReason?: string }; + } + ).proxyContext; + expect(context).toBeDefined(); + context!.state = 'active'; + context!.timeoutReason = 'lifetime'; + requestResponse.res.headersSent = true; + + try { + ( + middleware as unknown as { + expireLifetime: (value: typeof context) => void; + } + ).expireLifetime(context); + expect(requestResponse.destroy).toHaveBeenCalledTimes(1); + expect(requestResponse.json).not.toHaveBeenCalled(); + } finally { + await middleware.destroy(); + } + }); + + test.each([ + ['completed', { headersSent: false, writableEnded: true, destroyed: false }], + ['disconnected', { headersSent: false, writableEnded: false, destroyed: false }] + ])('should leave %s responses untouched on lifetime expiry', async (_name, state) => { + const middleware = new Middleware([FAKE_TOKEN]); + const requestResponse = createStateAwareRequestResponse(state); + await middleware.schedule(requestResponse.req, requestResponse.res); + const context = ( + requestResponse.req as Request & { + proxyContext?: { state: string }; + } + ).proxyContext; + expect(context).toBeDefined(); + if (_name === 'disconnected') requestResponse.req.aborted = true; + + try { + ( + middleware as unknown as { + expireLifetime: (value: typeof context) => void; + } + ).expireLifetime(context); + expect(requestResponse.json).not.toHaveBeenCalled(); + expect(requestResponse.destroy).not.toHaveBeenCalled(); + } finally { + await middleware.destroy(); + } }); }); +describe('Rate-limit refresh policy', () => { + test('should fetch once per token and fan out validated resources', async () => { + const fetch = vi.spyOn(globalThis, 'fetch').mockResolvedValue(rateLimitResponse()); + const middleware = new Middleware([FAKE_TOKEN]); + + try { + await new Promise((resolve) => middleware.once('ready', resolve)); + expect(fetch).toHaveBeenCalledTimes(1); + + const workers = ( + middleware as unknown as { + workersByResource: Record>; + } + ).workersByResource; + expect(workers.core[0]).toMatchObject({ remaining: 4000, reset: 2000000000 }); + expect(workers.search[0]).toMatchObject({ remaining: 25, reset: 2000000000 }); + expect(workers.code_search[0]).toMatchObject({ remaining: 8, reset: 2000000000 }); + expect(workers.graphql[0]).toMatchObject({ remaining: 4500, reset: 2000000000 }); + } finally { + await middleware.destroy(); + fetch.mockRestore(); + } + }); + + test('should coalesce concurrent refreshes for a token', async () => { + let resolveResponse!: (response: globalThis.Response) => void; + const pendingResponse = new Promise((resolve) => { + resolveResponse = resolve; + }); + const fetch = vi + .spyOn(globalThis, 'fetch') + .mockResolvedValueOnce(rateLimitResponse()) + .mockImplementation(() => pendingResponse); + const middleware = new Middleware([FAKE_TOKEN]); + + try { + await new Promise((resolve) => middleware.once('ready', resolve)); + const first = middleware.refreshRateLimits(); + const second = middleware.refreshRateLimits(); + expect(fetch).toHaveBeenCalledTimes(2); + + resolveResponse(rateLimitResponse()); + await Promise.all([first, second]); + expect(fetch).toHaveBeenCalledTimes(2); + } finally { + await middleware.destroy(); + fetch.mockRestore(); + } + }); + + test('should retry bounded refresh failures and preserve stale values', async () => { + vi.useFakeTimers(); + const fetch = vi.spyOn(globalThis, 'fetch').mockResolvedValue(rateLimitResponse()); + const middleware = new Middleware([FAKE_TOKEN]); + + try { + await new Promise((resolve) => middleware.once('ready', resolve)); + const worker = ( + middleware as unknown as { + workersByResource: { core: Array<{ remaining: number; reset: number }> }; + } + ).workersByResource.core[0]; + const previous = { remaining: worker.remaining, reset: worker.reset }; + fetch.mockRejectedValue(new Error('network unavailable')); + + const refresh = middleware.refreshRateLimits(); + const rejection = expect(refresh).rejects.toThrow('after 3 attempts'); + await vi.advanceTimersByTimeAsync(250); + await vi.advanceTimersByTimeAsync(500); + await rejection; + expect(fetch).toHaveBeenCalledTimes(4); + expect(worker).toMatchObject(previous); + } finally { + await middleware.destroy(); + fetch.mockRestore(); + vi.useRealTimers(); + } + }); + + test('should reject malformed HTTP, JSON, and resource responses', async () => { + const invalidResponses = [ + rateLimitResponse(undefined, StatusCodes.BAD_GATEWAY), + { + status: StatusCodes.OK, + json: async () => { + throw new Error('invalid json'); + } + } as unknown as globalThis.Response, + rateLimitResponse({ core: { remaining: 1, reset: 2000000000 } }) + ]; + + for (const invalidResponse of invalidResponses) { + const fetch = vi.spyOn(globalThis, 'fetch').mockResolvedValue(rateLimitResponse()); + const middleware = new Middleware([FAKE_TOKEN]); + try { + await new Promise((resolve) => middleware.once('ready', resolve)); + fetch.mockResolvedValue(invalidResponse); + const refresh = middleware.refreshRateLimits(); + await expect(refresh).rejects.toThrow('Rate-limit refresh failed'); + } finally { + await middleware.destroy(); + fetch.mockRestore(); + } + } + }); + + test('should contain refresh failures when destroyed during a retry', async () => { + vi.useFakeTimers(); + const fetch = vi.spyOn(globalThis, 'fetch').mockRejectedValue(new Error('network unavailable')); + const middleware = new Middleware([FAKE_TOKEN]); + + try { + const refresh = middleware.refreshRateLimits(); + await vi.advanceTimersByTimeAsync(250); + await middleware.destroy(); + await expect(refresh).resolves.toBeUndefined(); + } finally { + fetch.mockRestore(); + vi.useRealTimers(); + } + }); + + test('should abort a never-settling refresh when destroyed', async () => { + let signal: AbortSignal | undefined; + const fetch = vi.spyOn(globalThis, 'fetch').mockImplementation((_input, init) => { + signal = init?.signal ?? undefined; + return new Promise(() => undefined); + }); + const middleware = new Middleware([FAKE_TOKEN]); + + try { + await middleware.destroy(); + expect(signal?.aborted).toBe(true); + } finally { + fetch.mockRestore(); + } + }); + + test('should stop removed-token retries without later fetches or diagnostics', async () => { + vi.useFakeTimers(); + const fetch = vi + .spyOn(globalThis, 'fetch') + .mockResolvedValueOnce(rateLimitResponse()) + .mockRejectedValue(new Error('network unavailable')); + const middleware = new Middleware([FAKE_TOKEN]); + const errors = vi.fn(); + middleware.on('error', errors); + + try { + await new Promise((resolve) => middleware.once('ready', resolve)); + const refresh = middleware.refreshRateLimits(); + await Promise.resolve(); + await Promise.resolve(); + await middleware.removeToken(FAKE_TOKEN); + await expect(refresh).resolves.toBeUndefined(); + await vi.advanceTimersByTimeAsync(5000); + + expect(fetch).toHaveBeenCalledTimes(2); + expect(errors).not.toHaveBeenCalled(); + } finally { + await middleware.destroy(); + fetch.mockRestore(); + vi.useRealTimers(); + } + }); + + test('should recover after a retry succeeds', async () => { + vi.useFakeTimers(); + const recovered = rateLimitResponse({ + core: { limit: 5000, remaining: 1234, reset: 2000000100 }, + search: { limit: 30, remaining: 20, reset: 2000000100 }, + code_search: { limit: 10, remaining: 7, reset: 2000000100 }, + graphql: { limit: 5000, remaining: 4321, reset: 2000000100 } + }); + const fetch = vi + .spyOn(globalThis, 'fetch') + .mockResolvedValueOnce(rateLimitResponse()) + .mockRejectedValueOnce(new Error('temporary outage')) + .mockResolvedValueOnce(recovered); + const middleware = new Middleware([FAKE_TOKEN]); + + try { + await new Promise((resolve) => middleware.once('ready', resolve)); + const refresh = middleware.refreshRateLimits(); + const success = expect(refresh).resolves.toBeUndefined(); + await vi.advanceTimersByTimeAsync(250); + await success; + + const worker = ( + middleware as unknown as { + workersByResource: { core: Array<{ remaining: number; reset: number }> }; + } + ).workersByResource.core[0]; + expect(fetch).toHaveBeenCalledTimes(3); + expect(worker).toMatchObject({ remaining: 1234, reset: 2000000100 }); + } finally { + await middleware.destroy(); + fetch.mockRestore(); + vi.useRealTimers(); + } + }); + + test('should perform one refresh fetch per token across multiple tokens', async () => { + const firstToken = `${repeat('a', 39)}0`; + const secondToken = `${repeat('b', 39)}1`; + const fetch = vi.spyOn(globalThis, 'fetch').mockResolvedValue(rateLimitResponse()); + const middleware = new Middleware([firstToken, secondToken]); + + try { + await middleware.refreshRateLimits(); + expect(fetch).toHaveBeenCalledTimes(2); + expect(fetch.mock.calls.map(([, init]) => init?.headers)).toEqual([ + expect.objectContaining({ authorization: `token ${firstToken}` }), + expect.objectContaining({ authorization: `token ${secondToken}` }) + ]); + } finally { + await middleware.destroy(); + fetch.mockRestore(); + } + }); +}); + +async function waitFor(condition: () => boolean, timeout = 1000): Promise { + const deadline = Date.now() + timeout; + while (!condition()) { + if (Date.now() >= deadline) throw new Error('Timed out waiting for test condition'); + await new Promise((resolve) => setTimeout(resolve, 5)); + } +} + +function createStateAwareRequestResponse(state: { + headersSent: boolean; + writableEnded: boolean; + destroyed: boolean; +}): { + req: Request & { aborted: boolean }; + res: Response; + send: ReturnType; + json: ReturnType; + status: ReturnType; + setHeader: ReturnType; + destroy: ReturnType; + resume: ReturnType; +} { + const socket = Object.assign(new EventEmitter(), { destroyed: false }); + const resume = vi.fn(); + const req = Object.assign(new EventEmitter(), { + method: 'GET', + url: '/', + path: '/', + headers: { host: 'localhost:3000' }, + socket, + resume, + aborted: false, + destroyed: false + }) as unknown as Request & { aborted: boolean }; + const send = vi.fn(); + const json = vi.fn(); + const status = vi.fn(() => ({ send, json })); + const setHeader = vi.fn(); + const destroy = vi.fn(); + const res = Object.assign(new EventEmitter(), { + ...state, + status, + setHeader, + destroy + }) as unknown as Response; + + return { req, res, send, json, status, setHeader, destroy, resume }; +} + describe('Middleware core', () => { - let scope: nock.Scope; + let mockAgent: MockAgent; + let mockPool: MockPool; let middleware: Middleware; const requestTimeout = 1000; + function proxyReply( + method: string, + path: string, + status: number, + data?: Record | string, + headers: Record = {} + ) { + return mockPool.intercept({ path, method }).reply(status, data, { + headers: { + 'x-oauth-scopes': 'public_repo, read:org, read:user, user:emai', + 'x-ratelimit-remaining': '4999', + 'x-ratelimit-limit': '5000', + 'x-ratelimit-reset': `${Math.floor((Date.now() + 60 * 60 * 1000) / 1000)}`, + ...headers + } + }); + } + beforeEach(async () => { if (!nock.isActive()) nock.activate(); - scope = nock('https://api.github.com', { allowUnmocked: false }) + nock('https://api.github.com', { allowUnmocked: false }) .get('/rate_limit') .reply(StatusCodes.OK, { resources: { @@ -74,12 +1169,17 @@ describe('Middleware core', () => { }) .persist(); + mockAgent = new MockAgent(); + mockAgent.disableNetConnect(); + mockPool = mockAgent.get('https://api.github.com'); + app = express(); middleware = new Middleware([FAKE_TOKEN], { requestTimeout, minRemaining: 0, - overrideAuthorization: false + overrideAuthorization: false, + dispatcher: mockAgent }); await new Promise((resolve) => middleware.on('ready', resolve)); @@ -88,10 +1188,10 @@ describe('Middleware core', () => { }); afterEach(async () => { + await middleware.destroy(); + await mockAgent.close(); nock.cleanAll(); nock.restore(); - - middleware.destroy(); }); afterAll(() => { @@ -100,11 +1200,13 @@ describe('Middleware core', () => { describe('GitHub API is down or not reachable', () => { beforeEach(() => { - scope.get(/.*/).replyWithError({ + const connectionError = new Error('connect failed'); + Object.assign(connectionError, { code: 'ECONNREFUSED', errno: 'ECONNREFUSED', syscall: 'getaddrinfo' }); + mockPool.intercept({ path: '/', method: 'GET' }).replyWithError(connectionError); }); test(`it should respond with Bad Gateway (${StatusCodes.BAD_GATEWAY})`, async () => { @@ -112,34 +1214,589 @@ describe('Middleware core', () => { }); }); - describe('GitHub API is online', () => { - let scope: nock.Scope; - - beforeEach(async () => { - scope = nock('https://api.github.com') - .persist() - .defaultReplyHeaders({ - 'access-control-expose-headers': - 'ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset', - 'x-oauth-scopes': 'public_repo, read:org, read:user, user:emai', - 'x-ratelimit-remaining': '4999', - 'x-ratelimit-limit': '5000', - 'x-ratelimit-reset': `${Math.floor((Date.now() + 60 * 60 * 1000) / 1000)}` + test.each(['ftp://proxy.example', '//proxy.example', 'not-a-url', 'https://proxy.example/?x=1'])( + 'it should reject invalid external base URL %s', + (externalBaseUrl) => { + expect(() => new Middleware([FAKE_TOKEN], { externalBaseUrl })).toThrow( + 'Invalid externalBaseUrl' + ); + } + ); + + test('it should reject requests when the shared resource queue is full', async () => { + const limited = new Middleware([FAKE_TOKEN], { + minRemaining: 5000, + maxQueueDepthPerWorker: 1 + }); + const first = createStateAwareRequestResponse({ + headersSent: false, + writableEnded: false, + destroyed: false + }); + const second = createStateAwareRequestResponse({ + headersSent: false, + writableEnded: false, + destroyed: false + }); + + try { + await limited.schedule(first.req, first.res); + await limited.schedule(second.req, second.res); + expect(second.setHeader).toHaveBeenCalledWith('Retry-After', '1'); + expect(second.status).toHaveBeenCalledWith(StatusCodes.SERVICE_UNAVAILABLE); + expect(second.json).toHaveBeenCalledWith({ message: 'Proxy queue is full' }); + expect(second.resume).toHaveBeenCalledTimes(1); + + limited.addToken(SECOND_TOKEN); + const third = createStateAwareRequestResponse({ + headersSent: false, + writableEnded: false, + destroyed: false + }); + await limited.schedule(third.req, third.res); + expect(third.status).not.toHaveBeenCalled(); + + await limited.removeToken(SECOND_TOKEN); + expect(limited.tokens).toEqual([FAKE_TOKEN]); + } finally { + await limited.destroy(); + } + }); + + test('it should expire a queued request and remove it from the resource queue', async () => { + const limited = new Middleware([FAKE_TOKEN], { + minRemaining: 5000, + queueWaitTimeout: 1 + }); + const requestResponse = createStateAwareRequestResponse({ + headersSent: false, + writableEnded: false, + destroyed: false + }); + + try { + await limited.schedule(requestResponse.req, requestResponse.res); + await new Promise((resolve) => setTimeout(resolve, 10)); + expect(requestResponse.status).toHaveBeenCalledWith(StatusCodes.GATEWAY_TIMEOUT); + expect(requestResponse.json).toHaveBeenCalledWith({ + message: 'Request expired in proxy queue' + }); + expect(requestResponse.resume).toHaveBeenCalledTimes(1); + } finally { + await limited.destroy(); + } + }); + + test('it should abort the shared signal when queued lifetime expires', async () => { + vi.useFakeTimers(); + const limited = new Middleware([FAKE_TOKEN], { + minRemaining: 5000, + requestLifetimeTimeout: 10, + queueWaitTimeout: 100 + }); + const requestResponse = createStateAwareRequestResponse({ + headersSent: false, + writableEnded: false, + destroyed: false + }); + + try { + await limited.schedule(requestResponse.req, requestResponse.res); + const context = ( + requestResponse.req as Request & { + proxyContext?: { controller: AbortController }; + } + ).proxyContext; + expect(context).toBeDefined(); + vi.advanceTimersByTime(10); + expect(context?.controller.signal.aborted).toBe(true); + expect(requestResponse.json).toHaveBeenCalledWith({ message: 'Request lifetime exceeded' }); + } finally { + await limited.destroy(); + vi.useRealTimers(); + } + }); + + test('it should remove exactly one queued entry when a client disconnects', async () => { + const limited = new Middleware([FAKE_TOKEN], { + minRemaining: 5000, + maxQueueDepthPerWorker: 2 + }); + const first = createStateAwareRequestResponse({ + headersSent: false, + writableEnded: false, + destroyed: false + }); + const second = createStateAwareRequestResponse({ + headersSent: false, + writableEnded: false, + destroyed: false + }); + + try { + await limited.schedule(first.req, first.res); + await limited.schedule(second.req, second.res); + first.req.aborted = true; + first.req.emit('aborted'); + first.req.emit('aborted'); + const queue = ( + limited as unknown as { + queues: { core: { size: number; items: Array<{ req: Request }> } }; + } + ).queues.core; + expect(queue.size).toBe(1); + expect(queue.items[0]?.req).toBe(second.req); + expect((first.req as Request & { proxyContext?: unknown }).proxyContext).toBeUndefined(); + } finally { + await limited.destroy(); + } + }); + + test('should not spin on an exhausted worker before the budget reset', async () => { + vi.useFakeTimers(); + const limited = new Middleware([FAKE_TOKEN], { + minRemaining: 0, + queueWaitTimeout: 120000, + requestLifetimeTimeout: 120000 + }); + const worker = ( + limited as unknown as { + workersByResource: { + core: Array<{ + remaining: number; + reset: number; + timeBudget: number; + schedule: (req: Request, res: Response) => Promise; + }>; + }; + } + ).workersByResource.core[0]; + const queue = ( + limited as unknown as { + queues: { core: { size: number } }; + } + ).queues.core; + const requestResponse = createStateAwareRequestResponse({ + headersSent: false, + writableEnded: false, + destroyed: false + }); + const schedule = vi.spyOn(worker, 'schedule').mockResolvedValue(undefined); + const notifyDispatch = vi.spyOn( + limited as unknown as { notifyDispatch: (resource: 'core') => void }, + 'notifyDispatch' + ); + + try { + await limited.refreshRateLimits(); + worker.remaining = 5000; + worker.reset = 0; + worker.timeBudget = 0; + + await limited.schedule(requestResponse.req, requestResponse.res); + await Promise.resolve(); + await Promise.resolve(); + expect(schedule).not.toHaveBeenCalled(); + expect(queue.size).toBe(1); + const notificationsBeforeAdvance = notifyDispatch.mock.calls.length; + + vi.advanceTimersByTime(59999); + await Promise.resolve(); + expect(schedule).not.toHaveBeenCalled(); + expect(queue.size).toBe(1); + expect(notifyDispatch.mock.calls.length).toBe(notificationsBeforeAdvance); + + vi.advanceTimersByTime(1); + await Promise.resolve(); + await Promise.resolve(); + expect(schedule).toHaveBeenCalledTimes(1); + expect(queue.size).toBe(0); + } finally { + await limited.destroy(); + vi.useRealTimers(); + } + }); + + test('it should dispatch queued work immediately in exact round-robin order', async () => { + const middleware = new Middleware([FAKE_TOKEN, SECOND_TOKEN], { minRemaining: 0 }); + const workers = ( + middleware as unknown as { + workersByResource: { + core: Array<{ + remaining: number; + reset: number; + schedule: (req: Request, res: Response) => Promise; + }>; + }; + queues: { core: { enqueue: (req: Request, res: Response) => void; size: number } }; + dispatch: (resource: 'core') => void; + } + ).workersByResource.core; + const router = middleware as unknown as { + queues: { core: { enqueue: (req: Request, res: Response) => void; size: number } }; + dispatch: (resource: 'core') => void; + }; + const order: number[] = []; + workers.forEach((worker, index) => { + worker.remaining = 5000; + worker.reset = 0; + vi.spyOn(worker, 'schedule').mockImplementation(async () => { + order.push(index); + }); + }); + + try { + for (let index = 0; index < 4; index += 1) { + const requestResponse = createStateAwareRequestResponse({ + headersSent: false, + writableEnded: false, + destroyed: false }); + router.queues.core.enqueue(requestResponse.req, requestResponse.res); + } + router.dispatch('core'); + expect(order).toEqual([0, 1, 0, 1]); + expect(router.queues.core.size).toBe(0); + } finally { + await middleware.destroy(); + } + }); + + test('it should dispatch an enqueue notification without polling delay', async () => { + const middleware = new Middleware([FAKE_TOKEN], { minRemaining: 0 }); + const worker = ( + middleware as unknown as { + workersByResource: { + core: Array<{ + remaining: number; + reset: number; + schedule: (req: Request, res: Response) => Promise; + }>; + }; + } + ).workersByResource.core[0]; + worker.remaining = 5000; + worker.reset = 0; + const schedule = vi.spyOn(worker, 'schedule').mockResolvedValue(undefined); + const requestResponse = createStateAwareRequestResponse({ + headersSent: false, + writableEnded: false, + destroyed: false }); + try { + await middleware.schedule(requestResponse.req, requestResponse.res); + await Promise.resolve(); + await Promise.resolve(); + expect(schedule).toHaveBeenCalledTimes(1); + } finally { + await middleware.destroy(); + } + }); + + test('it should dispatch the next request immediately after a worker completes', async () => { + const middleware = new Middleware([FAKE_TOKEN], { minRemaining: 0 }); + const worker = ( + middleware as unknown as { + workersByResource: { + search: Array<{ + remaining: number; + reset: number; + proxy: { proxy: (...args: never[]) => Promise }; + }>; + }; + } + ).workersByResource.search[0]; + worker.remaining = 5000; + worker.reset = 0; + let releaseFirst!: () => void; + let markFirstStarted!: () => void; + let calls = 0; + const firstStarted = new Promise((resolve) => { + markFirstStarted = resolve; + }); + vi.spyOn(worker.proxy, 'proxy').mockImplementation(async () => { + calls += 1; + if (calls === 1) { + markFirstStarted(); + await new Promise((resolveFirst) => (releaseFirst = resolveFirst)); + } + }); + const first = createStateAwareRequestResponse({ + headersSent: false, + writableEnded: false, + destroyed: false + }); + Object.defineProperty(first.req, 'path', { value: '/search', configurable: true }); + const second = createStateAwareRequestResponse({ + headersSent: false, + writableEnded: false, + destroyed: false + }); + Object.defineProperty(second.req, 'path', { value: '/search', configurable: true }); + + try { + await middleware.schedule(first.req, first.res); + await middleware.schedule(second.req, second.res); + await firstStarted; + expect(calls).toBe(1); + releaseFirst(); + await waitFor(() => calls === 2); + } finally { + await middleware.destroy(); + } + }); + + test('it should keep resource dispatch queues independent', async () => { + const middleware = new Middleware([FAKE_TOKEN], { minRemaining: 0 }); + const workers = ( + middleware as unknown as { + workersByResource: { + core: Array<{ remaining: number; reset: number }>; + search: Array<{ + remaining: number; + reset: number; + schedule: (req: Request, res: Response) => Promise; + }>; + }; + } + ).workersByResource; + workers.core[0].remaining = 0; + workers.core[0].reset = Math.floor(Date.now() / 1000) + 60; + workers.search[0].remaining = 5000; + workers.search[0].reset = 0; + const schedule = vi.spyOn(workers.search[0], 'schedule').mockResolvedValue(undefined); + const core = createStateAwareRequestResponse({ + headersSent: false, + writableEnded: false, + destroyed: false + }); + const search = createStateAwareRequestResponse({ + headersSent: false, + writableEnded: false, + destroyed: false + }); + Object.defineProperty(search.req, 'path', { value: '/search', configurable: true }); + + try { + await middleware.schedule(core.req, core.res); + await middleware.schedule(search.req, search.res); + await Promise.resolve(); + await Promise.resolve(); + expect(schedule).toHaveBeenCalledTimes(1); + } finally { + await middleware.destroy(); + } + }); + + test('it should reserve atomic concurrency slots for each resource', async () => { + const middleware = new Middleware([FAKE_TOKEN], { minRemaining: 0 }); + const workers = ( + middleware as unknown as { + workersByResource: Record< + 'core' | 'search' | 'code_search' | 'graphql', + Array<{ + remaining: number; + reset: number; + timeBudget: number; + reserveWork: () => boolean; + releaseReservation: () => void; + }> + >; + } + ).workersByResource; + + try { + for (const resource of ['core', 'graphql', 'search', 'code_search'] as const) { + const worker = workers[resource][0]; + worker.remaining = 5000; + worker.reset = 0; + worker.timeBudget = 100000; + const limit = resource === 'core' || resource === 'graphql' ? 10 : 1; + const reservations = times(limit, () => worker.reserveWork()); + expect(reservations).toEqual(times(limit, () => true)); + expect(worker.reserveWork()).toBe(false); + times(limit, () => worker.releaseReservation()); + } + } finally { + await middleware.destroy(); + } + }); + + test('it should preserve absolute deadlines and clear worker ownership across retries', async () => { + const middleware = new Middleware([FAKE_TOKEN], { minRemaining: 0 }); + const worker = ( + middleware as unknown as { + workersByResource: { + core: Array<{ + remaining: number; + reset: number; + schedule: (req: Request, res: Response) => Promise; + }>; + }; + } + ).workersByResource.core[0]; + worker.remaining = 0; + worker.reset = Math.floor(Date.now() / 1000) + 60; + const requestResponse = createStateAwareRequestResponse({ + headersSent: false, + writableEnded: false, + destroyed: false + }); + const queue = ( + middleware as unknown as { + queues: { + core: { dequeue: () => { req: Request; res: Response } | undefined; size: number }; + }; + } + ).queues.core; + + try { + await middleware.schedule(requestResponse.req, requestResponse.res); + const context = ( + requestResponse.req as Request & { + proxyContext?: { queueDeadline: number; lifetimeDeadline: number; worker?: unknown }; + } + ).proxyContext; + const deadlines = [context?.queueDeadline, context?.lifetimeDeadline]; + const work = queue.dequeue(); + await worker.schedule(work!.req, work!.res); + expect([context?.queueDeadline, context?.lifetimeDeadline]).toEqual(deadlines); + expect(context?.worker).toBeUndefined(); + expect(queue.size).toBe(1); + } finally { + await middleware.destroy(); + } + }); + + test('should preserve a requeued context when worker A is destroyed before worker B claims it', async () => { + const middleware = new Middleware([FAKE_TOKEN, SECOND_TOKEN], { + minRemaining: 5000, + queueWaitTimeout: 1000, + requestLifetimeTimeout: 2000 + }); + await middleware.refreshRateLimits(); + const workerA = ( + middleware as unknown as { + workersByResource: { + core: Array<{ + remaining: number; + reset: number; + schedule: (req: Request, res: Response) => Promise; + destroy: () => Promise; + }>; + }; + } + ).workersByResource.core[0]; + workerA.remaining = 5000; + workerA.reset = 0; + const workerB = ( + middleware as unknown as { + workersByResource: { + core: Array<{ + remaining: number; + reset: number; + applyRateLimit: (limit: { remaining: number; reset: number }) => void; + proxy: { + proxy: ( + req: Request, + res: Response, + options?: { abortController?: AbortController } + ) => Promise; + }; + }>; + }; + } + ).workersByResource.core[1]; + workerB.remaining = 0; + workerB.reset = Math.floor(Date.now() / 1000) + 60; + const requestResponse = createStateAwareRequestResponse({ + headersSent: false, + writableEnded: false, + destroyed: false + }); + const queue = ( + middleware as unknown as { + queues: { + core: { dequeue: () => { req: Request; res: Response } | undefined; size: number }; + }; + } + ).queues.core; + let observedSignal: AbortSignal | undefined; + let started!: () => void; + const startedPromise = new Promise((resolve) => { + started = resolve; + }); + const originalSchedule = workerA.schedule; + const schedule = vi.spyOn(workerA, 'schedule').mockImplementation((req, res) => { + workerA.remaining = 0; + workerA.reset = Math.floor(Date.now() / 1000) + 60; + return originalSchedule(req, res); + }); + + try { + await middleware.schedule(requestResponse.req, requestResponse.res); + const context = ( + requestResponse.req as Request & { + proxyContext?: { + controller: AbortController; + queueDeadline: number; + lifetimeDeadline: number; + worker?: unknown; + }; + } + ).proxyContext; + expect(context).toBeDefined(); + const deadlines = [context!.queueDeadline, context!.lifetimeDeadline]; + await waitFor( + () => schedule.mock.calls.length === 1 && queue.size === 1 && context!.worker === undefined + ); + expect(queue.size).toBe(1); + expect(context!.controller.signal.aborted).toBe(false); + expect([context!.queueDeadline, context!.lifetimeDeadline]).toEqual(deadlines); + expect(context!.worker).toBeUndefined(); + + await workerA.destroy(); + expect(queue.size).toBe(1); + expect(requestResponse.destroy).not.toHaveBeenCalled(); + expect(context!.controller.signal.aborted).toBe(false); + + const proxy = vi.spyOn(workerB.proxy, 'proxy').mockImplementation((_req, _res, options) => { + observedSignal = options?.abortController?.signal; + started(); + return new Promise((_resolve, reject) => { + observedSignal?.addEventListener( + 'abort', + () => reject(new DOMException('The operation was aborted', 'AbortError')), + { once: true } + ); + }); + }); + + try { + workerB.applyRateLimit({ remaining: 5000, reset: 0 }); + await startedPromise; + expect(observedSignal).toBe(context!.controller.signal); + expect([context!.queueDeadline, context!.lifetimeDeadline]).toEqual(deadlines); + } finally { + proxy.mockRestore(); + } + } finally { + schedule.mockRestore(); + await middleware.destroy(); + } + }); + + describe('GitHub API is online', () => { test('it should wait if no requests available', async () => { const reset = Date.now() + 1000; // must be greather or equal to 1 - scope - .get('/reset') - .reply(StatusCodes.OK, '', { - 'x-ratelimit-remaining': '0', - 'x-ratelimit-limit': '5000', - 'x-ratelimit-reset': `${Math.floor(reset / 1000)}` - }) - .get('/') - .reply(StatusCodes.OK); + proxyReply('GET', '/reset', StatusCodes.OK, '', { + 'x-ratelimit-remaining': '0', + 'x-ratelimit-limit': '5000', + 'x-ratelimit-reset': `${Math.floor(reset / 1000)}` + }); + proxyReply('GET', '/', StatusCodes.OK); await request(app).get('/reset').expect(StatusCodes.OK); @@ -148,44 +1805,41 @@ describe('Middleware core', () => { }); test('it should forward responses received from GitHub', async () => { - scope.get('/').reply(200); + proxyReply('GET', '/', 200).persist(); await request(app) .get('/') .then(({ status }) => expect(status).toEqual(200)); - scope.get('/300').reply(300); + proxyReply('GET', '/300', 300); await request(app) .get('/300') .catch(({ response }) => expect(response.status).toEqual(300)); - scope.get('/400').reply(400); + proxyReply('GET', '/400', 400); await request(app) .get('/400') .catch(({ response }) => expect(response.status).toEqual(400)); - scope.get('/500').reply(500); + proxyReply('GET', '/500', 500); await request(app) .get('/500') .catch(({ response }) => expect(response.status).toEqual(500)); }); test('it should interrupt long requests', async () => { - scope - .get('/') - .delay(requestTimeout * 2) - .reply(StatusCodes.OK); + proxyReply('GET', '/', StatusCodes.OK).delay(requestTimeout * 2); return request(app).get('/').expect(StatusCodes.BAD_GATEWAY); }); test('it should respond to broken connections', async () => { - scope.get('/').replyWithError(new Error('Server Error')); + mockPool.intercept({ path: '/', method: 'GET' }).replyWithError(new Error('Server Error')); return request(app).get('/').expect(StatusCodes.BAD_GATEWAY); }); test('it should not break proxy when client disconnect', async () => { - scope.get('/').delay(500).reply(StatusCodes.OK); + proxyReply('GET', '/', StatusCodes.OK).persist().delay(500); await Promise.all( times(25, () => @@ -200,13 +1854,14 @@ describe('Middleware core', () => { }); test('it should balance the use of the tokens', async () => { - scope.get('/').delay(250).reply(200); + proxyReply('GET', '/', 200).persist().delay(250); - const tokens = times(5, (n) => `${FAKE_TOKEN}**${n}`) + const tokens = times(5, (n) => `${repeat('t', 39)}${n}`) .concat(FAKE_TOKEN) .reduce((memo: Record, token) => ({ ...memo, [token]: 0 }), {}); for (const token of Object.keys(tokens)) { + if (middleware.tokens.includes(token)) continue; middleware.addToken(token); await new Promise((resolve) => middleware.once('ready', resolve)); } @@ -220,11 +1875,11 @@ describe('Middleware core', () => { Object.values(tokens).forEach((calls) => expect(calls).toBeGreaterThan(0)); - Object.keys(tokens).forEach((token) => middleware.removeToken(token)); + await Promise.all(Object.keys(tokens).map((token) => middleware.removeToken(token))); }); test('it should not forward ratelimit and scope information', async () => { - scope.get('/').delay(250).reply(200); + proxyReply('GET', '/', 200).persist().delay(250); return request(app) .get('/') @@ -235,44 +1890,60 @@ describe('Middleware core', () => { }); test('it should handle unauthorized requests to API', async () => { - scope - .defaultReplyHeaders({ - 'x-ratelimit-remaining': '59', - 'x-ratelimit-reset': `${Math.floor((Date.now() + 60 * 60 * 1000) / 1000)}` - }) - .get('/user') - .matchHeader('authorization', `token ${repeat('i', 40)}`) - .reply(401, '', { 'x-ratelimit-limit': '60' }) - .get('/user') - .matchHeader('authorization', `token ${repeat('j', 40)}`) - .reply(401, '') - .intercept('/user', 'get') - .reply(200) - .intercept('/', 'get') - .reply(200); + const tokenI = `token ${repeat('i', 40)}`; + const tokenJ = `token ${repeat('j', 40)}`; + mockPool + .intercept({ path: '/user', method: 'GET', headers: { authorization: tokenI } }) + .reply(401, '', { + headers: { + 'x-ratelimit-remaining': '59', + 'x-ratelimit-reset': `${Math.floor((Date.now() + 60 * 60 * 1000) / 1000)}`, + 'x-ratelimit-limit': '60' + } + }); + mockPool + .intercept({ path: '/user', method: 'GET', headers: { authorization: tokenJ } }) + .reply(401, ''); + proxyReply('GET', '/user', 200); + proxyReply('GET', '/', 200).persist(); await request(app).get('/').expect(200); await request(app).get('/user').expect(200); - middleware.removeToken(FAKE_TOKEN); + await middleware.removeToken(FAKE_TOKEN); middleware.addToken(repeat('i', 40)); await request(app).get('/user').expect(401); - middleware.removeToken(repeat('i', 40)); + await middleware.removeToken(repeat('i', 40)); middleware.addToken(repeat('j', 40)); await request(app).get('/user').expect(401); await request(app).get('/').expect(200); }); + test('it should redact invalid tokens from rate-limit errors', async () => { + const errors: string[] = []; + middleware.on('error', (error) => errors.push(error.toString())); + + nock.cleanAll(); + nock('https://api.github.com', { allowUnmocked: false }) + .get('/rate_limit') + .times(4) + .reply(StatusCodes.UNAUTHORIZED); + + await expect(middleware.refreshRateLimits()).rejects.toThrow('Rate-limit refresh failed'); + + expect(errors.join('\n')).not.toContain(FAKE_TOKEN); + expect(errors.join('\n')).toContain(FAKE_TOKEN.slice(-4)); + }); + test('it should not update limits when "x-ratelimit-remaining" is not on header', async () => { - scope - .defaultReplyHeaders({ + mockPool.intercept({ path: '/', method: 'GET' }).reply(401, '', { + headers: { 'x-ratelimit-reset': `${Math.floor((Date.now() + 60 * 60 * 1000) / 1000)}` - }) - .get('/') - .reply(401); + } + }); await request(app).get('/').expect(401); }); @@ -281,32 +1952,108 @@ describe('Middleware core', () => { const token = repeat('i', 40); const tokenStr = `token ${token}`; - scope.get('/').matchHeader('authorization', tokenStr).reply(401).get('/').reply(200); + mockPool + .intercept({ path: '/', method: 'GET', headers: { authorization: tokenStr } }) + .reply(401); + proxyReply('GET', '/', 200).persist(); await request(app).get('/').set('Authorization', tokenStr).expect(401); await request(app).get('/').expect(200); - middleware.destroy(); + await middleware.destroy(); middleware = new Middleware([FAKE_TOKEN], { requestTimeout, minRemaining: 0, - overrideAuthorization: true + overrideAuthorization: true, + dispatcher: mockAgent }); await request(app).get('/').set('Authorization', tokenStr).expect(200); }); - test('it should replace base url on response header', async () => { + test('it should preserve upstream links when no external base URL is configured', async () => { const linkStr = '; rel="next", ; rel="last"'; - scope.get('/').reply(200, {}, { link: linkStr }); + proxyReply('GET', '/', 200, {}, { link: linkStr }); await request(app) .get('/') - .expect(({ headers, request }) => { - expect(headers.link).toEqual(linkStr.replace(/https:\/\/api.github.com\//g, request.url)); + .expect(({ headers }) => { + expect(headers.link).toEqual(linkStr); + }); + }); + + test.each([ + 'http://proxy.example/base', + 'https://proxy.example', + 'https://proxy.example/$edge' + ])('it should rewrite redirects and links using trusted external base %s', async (baseUrl) => { + await middleware.destroy(); + middleware = new Middleware([FAKE_TOKEN], { + requestTimeout, + minRemaining: 0, + externalBaseUrl: baseUrl, + dispatcher: mockAgent + }); + await new Promise((resolve) => middleware.once('ready', resolve)); + + const linkStr = + '; rel="next", ; rel="other"'; + proxyReply('GET', '/redirect', StatusCodes.MOVED_TEMPORARILY, '', { + location: 'https://api.github.com/repos/example', + link: linkStr + }); + proxyReply('GET', '/unrelated-location', StatusCodes.MOVED_TEMPORARILY, '', { + location: 'https://other.example/redirect?next=https://api.github.com/repos/example', + link: linkStr + }); + + await request(app) + .get('/redirect') + .expect(StatusCodes.MOVED_TEMPORARILY) + .expect(({ headers }) => { + expect(headers.location).toBe(`${baseUrl}/repos/example`); + expect(headers.link).toBe( + `<${baseUrl}/repos/example?page=2>; rel="next", ; rel="other"` + ); + }); + + await request(app) + .get('/unrelated-location') + .expect(StatusCodes.MOVED_TEMPORARILY) + .expect(({ headers }) => { + expect(headers.location).toBe( + 'https://other.example/redirect?next=https://api.github.com/repos/example' + ); + expect(headers.link).toBe( + `<${baseUrl}/repos/example?page=2>; rel="next", ; rel="other"` + ); + }); + }); + + test('should preserve origins when API and external paths begin with double slashes', async () => { + await middleware.destroy(); + middleware = new Middleware([FAKE_TOKEN], { + requestTimeout, + minRemaining: 0, + externalBaseUrl: 'https://proxy.example//edge', + dispatcher: mockAgent + }); + await new Promise((resolve) => middleware.once('ready', resolve)); + + proxyReply('GET', '/double-slash', StatusCodes.MOVED_TEMPORARILY, '', { + location: 'https://api.github.com//repos/example', + link: '; rel="next"' + }); + + await request(app) + .get('/double-slash') + .expect(StatusCodes.MOVED_TEMPORARILY) + .expect(({ headers }) => { + expect(headers.location).toBe('https://proxy.example//edge//repos/example'); + expect(headers.link).toBe('; rel="next"'); }); }); }); -}); +}); \ No newline at end of file diff --git a/src/router.ts b/src/router.ts index 1e3ee3e..6d09341 100644 --- a/src/router.ts +++ b/src/router.ts @@ -4,24 +4,400 @@ import EventEmitter from 'node:events'; import type { Request, Response } from 'express'; import { StatusCodes } from 'http-status-codes'; import PQueue from 'p-queue'; -import { Agent } from 'undici'; +import { Agent, type Dispatcher } from 'undici'; -import { ProxyClient } from './proxy-client.js'; +import { PayloadTooLargeError, ProxyClient, type ProxyHeaderValue } from './proxy-client.js'; export type ProxyRouterOpts = { requestTimeout: number; minRemaining: number; + maxRequestBodyBytes?: number; + maxQueueDepthPerWorker?: number; + queueWaitTimeout?: number; + requestLifetimeTimeout?: number; overrideAuthorization?: boolean; timeBudgetMultiplier?: number; + externalBaseUrl?: string; + dispatcher?: Dispatcher; }; type ExtendedRequest = Request & { startedAt?: Date; abortController?: AbortController; + proxyContext?: RequestContext; }; type APIResources = 'core' | 'search' | 'code_search' | 'graphql'; +type RateLimit = { + remaining: number; + reset: number; +}; + +type RateLimitResources = Record; + +const REFRESH_MAX_ATTEMPTS = 3; +const REFRESH_BACKOFF_BASE_MS = 250; +const REFRESH_BACKOFF_MAX_MS = 2000; +const GITHUB_API_ORIGIN = 'https://api.github.com'; +const REQUEST_BODY_DEFAULT = 1024 * 1024; +const QUEUE_DEPTH_DEFAULT = 50; +const QUEUE_WAIT_DEFAULT = 30000; +const REQUEST_LIFETIME_DEFAULT = 120000; + +type RequestContextState = 'queued' | 'active' | 'settled'; + +type RequestContext = { + req: ExtendedRequest; + res: Response; + controller: AbortController; + acceptedAt: number; + enqueuedAt: number; + queueDeadline: number; + lifetimeDeadline: number; + state: RequestContextState; + queueTimer?: NodeJS.Timeout; + lifetimeTimer?: NodeJS.Timeout; + worker?: ProxyWorker; + timeoutReason?: 'queue' | 'lifetime'; + onDisconnect: () => void; +}; + +type DispatchState = { + cursor: number; + notificationQueued: boolean; + resetTimer?: NodeJS.Timeout; +}; + +function headerString(value: ProxyHeaderValue | undefined): string | undefined { + if (value === undefined) return undefined; + return Array.isArray(value) ? value.join(', ') : value; +} + +function rebaseGitHubUrl(url: URL, externalBaseUrl: string): string { + const external = new URL(externalBaseUrl); + const basePath = external.pathname === '/' ? '' : external.pathname.replace(/\/$/, ''); + const rebased = new URL(external.origin); + rebased.pathname = `${basePath}${url.pathname}`; + rebased.search = url.search; + rebased.hash = url.hash; + return rebased.toString(); +} + +function rewriteLocation(value: ProxyHeaderValue, externalBaseUrl: string): ProxyHeaderValue { + const rewrite = (header: string): string => { + try { + const url = new URL(header); + return url.origin === GITHUB_API_ORIGIN ? rebaseGitHubUrl(url, externalBaseUrl) : header; + } catch { + return header; + } + }; + return Array.isArray(value) ? value.map(rewrite) : rewrite(value); +} + +function rewriteLink(value: ProxyHeaderValue, externalBaseUrl: string): ProxyHeaderValue { + const rewrite = (header: string): string => + header.replace(/<([^>]*)>/g, (reference, target: string) => { + try { + const url = new URL(target); + return url.origin === GITHUB_API_ORIGIN + ? `<${rebaseGitHubUrl(url, externalBaseUrl)}>` + : reference; + } catch { + return reference; + } + }); + return Array.isArray(value) ? value.map(rewrite) : rewrite(value); +} + +class RefreshFailure extends Error {} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null; +} + +function parseRateLimitResources(value: unknown): RateLimitResources { + if (!isRecord(value)) throw new RefreshFailure('invalid resource shape'); + + const resources = {} as RateLimitResources; + for (const resource of ['core', 'search', 'code_search', 'graphql'] as APIResources[]) { + const data = value[resource]; + if ( + !isRecord(data) || + typeof data.remaining !== 'number' || + !Number.isSafeInteger(data.remaining) || + data.remaining < 0 || + typeof data.reset !== 'number' || + !Number.isSafeInteger(data.reset) || + data.reset < 0 + ) { + throw new RefreshFailure('invalid resource shape'); + } + resources[resource] = { remaining: data.remaining, reset: data.reset }; + } + + return resources; +} + +export const NUMERIC_CONFIGURATION_LIMITS = { + port: { min: 0, max: 65535 }, + requestTimeout: { min: 1, max: 120000 }, + minRemaining: { min: 0, max: 5000 }, + timeBudgetMultiplier: { min: 1, max: 10 }, + maxRequestBodyBytes: { min: REQUEST_BODY_DEFAULT, max: REQUEST_BODY_DEFAULT * 16 }, + maxQueueDepthPerWorker: { min: 1, max: 1000 }, + queueWaitTimeout: { min: 1, max: 120000 }, + requestLifetimeTimeout: { min: 1, max: 600000 } +} as const; + +function parseNumericConfiguration( + value: unknown, + name: keyof typeof NUMERIC_CONFIGURATION_LIMITS, + integer: boolean +): number { + const limits = NUMERIC_CONFIGURATION_LIMITS[name]; + const pattern = integer ? /^\+?\d+$/ : /^\+?(?:\d+(?:\.\d+)?|\.\d+)$/; + const number = + typeof value === 'number' + ? value + : typeof value === 'string' && pattern.test(value) + ? Number(value) + : Number.NaN; + const isValid = + Number.isFinite(number) && + Math.abs(number) <= Number.MAX_SAFE_INTEGER && + (!integer || Number.isInteger(number)) && + number >= limits.min && + number <= limits.max; + + if (!isValid) { + const kind = integer ? 'a safe integer' : 'a finite safe number'; + throw new Error(`Invalid ${name}: expected ${kind} between ${limits.min} and ${limits.max}.`); + } + + return number; +} + +export function parsePort(value: unknown): number { + return parseNumericConfiguration(value, 'port', true); +} + +export function parseRequestTimeout(value: unknown): number { + return parseNumericConfiguration(value, 'requestTimeout', true); +} + +export function parseMinRemaining(value: unknown): number { + return parseNumericConfiguration(value, 'minRemaining', true); +} + +export function parseTimeBudgetMultiplier(value: unknown): number { + return parseNumericConfiguration(value, 'timeBudgetMultiplier', false); +} + +export function parseMaxRequestBodyBytes(value: unknown): number { + return parseNumericConfiguration(value, 'maxRequestBodyBytes', true); +} + +export function parseMaxQueueDepthPerWorker(value: unknown): number { + return parseNumericConfiguration(value, 'maxQueueDepthPerWorker', true); +} + +export const parseMaxQueueDepth = parseMaxQueueDepthPerWorker; + +export function parseQueueWaitTimeout(value: unknown): number { + return parseNumericConfiguration(value, 'queueWaitTimeout', true); +} + +export function parseRequestLifetimeTimeout(value: unknown): number { + return parseNumericConfiguration(value, 'requestLifetimeTimeout', true); +} + +export function parseExternalBaseUrl(value: unknown): string | undefined { + if (value === undefined) return undefined; + if (typeof value !== 'string') + throw new Error('Invalid externalBaseUrl: expected an absolute HTTP(S) URL.'); + + let url: URL; + try { + url = new URL(value); + } catch { + throw new Error('Invalid externalBaseUrl: expected an absolute HTTP(S) URL.'); + } + + if ( + !['http:', 'https:'].includes(url.protocol) || + !url.hostname || + url.username || + url.password || + url.search || + url.hash + ) { + throw new Error('Invalid externalBaseUrl: expected an absolute HTTP(S) URL.'); + } + + return url.toString().replace(/\/$/, ''); +} + +export function validateProxyRouterOptions(options: ProxyRouterOpts): ProxyRouterOpts { + const requestTimeout = parseRequestTimeout(options.requestTimeout); + const minRemaining = parseMinRemaining(options.minRemaining); + const timeBudgetMultiplier = + options.timeBudgetMultiplier === undefined + ? undefined + : parseTimeBudgetMultiplier(options.timeBudgetMultiplier); + const externalBaseUrl = parseExternalBaseUrl(options.externalBaseUrl); + const maxRequestBodyBytes = parseMaxRequestBodyBytes( + options.maxRequestBodyBytes ?? REQUEST_BODY_DEFAULT + ); + const maxQueueDepthPerWorker = parseMaxQueueDepthPerWorker( + options.maxQueueDepthPerWorker ?? QUEUE_DEPTH_DEFAULT + ); + const queueWaitTimeout = parseQueueWaitTimeout(options.queueWaitTimeout ?? QUEUE_WAIT_DEFAULT); + const requestLifetimeTimeout = parseRequestLifetimeTimeout( + options.requestLifetimeTimeout ?? REQUEST_LIFETIME_DEFAULT + ); + + return { + ...options, + requestTimeout, + minRemaining, + maxRequestBodyBytes, + maxQueueDepthPerWorker, + queueWaitTimeout, + requestLifetimeTimeout, + timeBudgetMultiplier, + externalBaseUrl + }; +} + +const GITHUB_TOKEN_PATTERNS = [ + /^[A-Za-z0-9]{40}$/, + /^gh[opusr]_[A-Za-z0-9]{36}$/, + /^github_pat_[A-Za-z0-9_]{82}$/ +]; + +export function validateGitHubToken(token: unknown): asserts token is string { + if (typeof token !== 'string' || !GITHUB_TOKEN_PATTERNS.some((pattern) => pattern.test(token))) { + throw new Error('Invalid access token detected (unsupported GitHub credential format).'); + } +} + +type ScheduledRequest = Omit & { + req: ExtendedRequest; + context: RequestContext; + settle: () => void; +}; + +function disposalError(errors: unknown[], message: string): Error | undefined { + if (!errors.length) return undefined; + if (errors.length === 1) { + return errors[0] instanceof Error ? errors[0] : new Error(String(errors[0])); + } + return new AggregateError(errors, message); +} + +function terminateResponse(res: Response): void { + if (!res.writableEnded && !res.destroyed) res.destroy(); +} + +function requestDisconnected(req: ExtendedRequest): boolean { + return Boolean(req.destroyed || req.aborted || req.socket?.destroyed); +} + +function responseUnavailable(res: Response): boolean { + return Boolean(res.headersSent || res.writableEnded || res.destroyed); +} + +function terminatePartialResponse(req: ExtendedRequest, res: Response): void { + if (requestDisconnected(req) || res.writableEnded || res.destroyed || !res.headersSent) return; + res.destroy(); +} + +function sendJsonResponse( + req: ExtendedRequest, + res: Response, + status: number, + body: Record +): void { + if (requestDisconnected(req) || responseUnavailable(res)) return; + res.status(status).json(body); +} + +function disposeUnreadRequest(req: ExtendedRequest): void { + if (requestDisconnected(req) || req.readableEnded || req.complete) return; + if (typeof req.resume === 'function') { + req.resume(); + } else if (typeof req.destroy === 'function') { + req.destroy(); + } +} + +function rejectRequest( + req: ExtendedRequest, + res: Response, + status: number, + body: Record, + retryAfter?: string +): void { + if (!requestDisconnected(req) && !res.writableEnded && !res.destroyed) { + if (res.headersSent) { + terminatePartialResponse(req, res); + } else { + if (retryAfter !== undefined) res.setHeader('Retry-After', retryAfter); + sendJsonResponse(req, res, status, body); + } + } + disposeUnreadRequest(req); +} + +function createRequestContext( + req: ExtendedRequest, + res: Response, + onDisconnect?: (context: RequestContext) => void +): RequestContext { + const controller = new AbortController(); + let context!: RequestContext; + const disconnect = (): void => { + if (context.state === 'settled') return; + controller.abort(); + onDisconnect?.(context); + }; + + context = { + req, + res, + controller, + acceptedAt: Date.now(), + enqueuedAt: Date.now(), + queueDeadline: 0, + lifetimeDeadline: 0, + state: 'queued', + onDisconnect: disconnect + }; + + req.once?.('aborted', disconnect); + req.once?.('error', disconnect); + req.socket?.once?.('close', disconnect); + res.once?.('close', disconnect); + req.proxyContext = context; + return context; +} + +function cleanupRequestContext(context: RequestContext): void { + if (context.queueTimer) clearTimeout(context.queueTimer); + if (context.lifetimeTimer) clearTimeout(context.lifetimeTimer); + context.queueTimer = undefined; + context.lifetimeTimer = undefined; + context.req.removeListener?.('aborted', context.onDisconnect); + context.req.removeListener?.('error', context.onDisconnect); + context.req.socket?.removeListener?.('close', context.onDisconnect); + context.res.removeListener?.('close', context.onDisconnect); + if (context.req.proxyContext === context) delete context.req.proxyContext; + context.worker = undefined; + context.state = 'settled'; +} + export interface WorkerLogger { resource: APIResources; token: string; @@ -42,6 +418,7 @@ export interface RequestQueue { items: QueuedRequest[]; enqueue(req: Request, res: Response): void; dequeue(): QueuedRequest | undefined; + remove(req: Request, res: Response): boolean; get size(): number; } @@ -53,11 +430,22 @@ class ProxyWorker extends EventEmitter { readonly schedule; private readonly opts: ProxyRouterOpts; + private readonly agent: Agent; + private readonly scheduledRequests = new Set(); + private readonly ownedContexts = new Set(); private router?: ProxyRouter; - private resourceQueue?: RequestQueue; - private pullInterval?: NodeJS.Timeout; - private _budgetResetInterval?: NodeJS.Timeout; - private checkForWork?: () => Promise; + private reservations = 0; + private destroyed = false; + private destroyPromise?: Promise; + + private emitError(error: unknown, token?: string): void { + if (!this.listenerCount('error')) return; + try { + this.emit('error', error, token); + } catch { + // Error listeners must not turn refresh failures into unhandled rejections. + } + } readonly defaults: { resource: APIResources; @@ -92,23 +480,21 @@ class ProxyWorker extends EventEmitter { }; } - // Initialize time budget tracking - this._budgetResetInterval = setInterval(() => { - this.timeBudget = - (this.defaults.resource === 'graphql' ? 60000 : 90000) * (opts.timeBudgetMultiplier || 1); - }, 60000).unref(); + this.agent = new Agent({ + connections: 20, + pipelining: 1, + allowH2: false, + keepAliveTimeout: 60000, + keepAliveMaxTimeout: 600000, + headersTimeout: opts.requestTimeout, + bodyTimeout: opts.requestTimeout + }); this.proxy = new ProxyClient({ target: 'https://api.github.com', timeout: opts.requestTimeout, - dispatcher: new Agent({ - connections: 20, - pipelining: 1, - keepAliveTimeout: 60000, - keepAliveMaxTimeout: 600000, - headersTimeout: opts.requestTimeout, - bodyTimeout: opts.requestTimeout - }) + maxRequestBodyBytes: opts.maxRequestBodyBytes, + dispatcher: opts.dispatcher ?? this.agent }); let maxConcurrent = 1; @@ -117,122 +503,205 @@ class ProxyWorker extends EventEmitter { this.queue = new PQueue({ concurrency: maxConcurrent }); this.schedule = async (req: ExtendedRequest, res: Response): Promise => { - return this.queue.add(async () => { - try { - if (req.socket.destroyed) { - this.log(); - return; - } - - const noTimeBudget = this.timeBudget < this.queue.pending * 1000; - const noRequests = - this.remaining <= opts.minRemaining && this.reset >= Math.floor(Date.now() / 1000); - - if (noTimeBudget || noRequests) { - this.emit('retry', req, res); - return; - } - - req.startedAt = new Date(); - this.remaining -= 1; - - const hasAuthorization = opts.overrideAuthorization ? false : !!req.headers.authorization; - - await this.proxy.proxy(req, res, { - modifyHeaders: (headers) => { - if (!hasAuthorization) headers.authorization = `token ${token}`; - return headers; - }, - onResponse: async (data) => { - const linkHeader = data.headers.link; - if (linkHeader && req.headers.host) { - data.headers.link = linkHeader.replaceAll( - 'https://api.github.com', - `http://${req.headers.host}` - ); - } + if (this.destroyed) { + terminateResponse(res); + return; + } - // Only update rate limits if we injected the token - if (!hasAuthorization) { - const status = data.status.toString(); - const rateLimitRemaining = data.headers['x-ratelimit-remaining']; - const rateLimitReset = data.headers['x-ratelimit-reset']; - const rateLimitLimit = data.headers['x-ratelimit-limit']; - - if (rateLimitRemaining) { - this.updateLimits({ - status, - 'x-ratelimit-remaining': rateLimitRemaining, - 'x-ratelimit-reset': rateLimitReset || '', - 'x-ratelimit-limit': rateLimitLimit || '' + const context = req.proxyContext ?? createRequestContext(req, res); + if (context.state === 'settled') return; + context.state = 'active'; + context.worker = this; + this.ownedContexts.add(context); + if (context.queueTimer) clearTimeout(context.queueTimer); + context.queueTimer = undefined; + + let settleTask!: () => void; + const completion = new Promise((resolve) => { + settleTask = resolve; + }); + const scheduledRequest = { req, res, context, settle: settleTask }; + this.scheduledRequests.add(scheduledRequest); + const abortController = context.controller; + let activeAbortController: AbortController | undefined = abortController; + + try { + void this.queue + .add(async () => { + try { + if ( + this.destroyed || + requestDisconnected(req) || + res.writableEnded || + res.destroyed || + abortController.signal.aborted + ) { + if (context.timeoutReason === 'lifetime') { + sendJsonResponse(req, res, StatusCodes.GATEWAY_TIMEOUT, { + message: 'Request lifetime exceeded' }); } + this.log(); + return; + } - this.timeBudget -= Date.now() - (req.startedAt?.getTime() || 1000); + if (context.state !== 'active') return; - this.log(data.status, req.startedAt); + const noTimeBudget = this.timeBudget < this.queue.pending * 1000; + const noRequests = + this.remaining <= opts.minRemaining && this.reset >= Math.floor(Date.now() / 1000); - // Remove rate limit and scope headers - for (const key of Object.keys(data.headers)) { - if (/(ratelimit|scope)/i.test(key)) { - delete data.headers[key]; - } - } + if (noTimeBudget || noRequests) { + this.emit('retry', req, res); + return; + } - const exposeHeaders = data.headers['access-control-expose-headers']; - if (exposeHeaders) { - const filtered = exposeHeaders - .split(', ') - .filter((header) => !/(ratelimit|scope)/i.test(header)) - .join(', '); - if (filtered) { - data.headers['access-control-expose-headers'] = filtered; - } else { - delete data.headers['access-control-expose-headers']; + req.startedAt = new Date(); + this.remaining -= 1; + + req.abortController = abortController; + const abortRequest = (): void => abortController.abort(); + const abortResponse = (): void => { + if (!res.writableEnded) abortController.abort(); + }; + req.once?.('aborted', abortRequest); + req.once?.('error', abortRequest); + req.socket?.once?.('close', abortRequest); + res.once?.('close', abortResponse); + + try { + const hasAuthorization = opts.overrideAuthorization + ? false + : !!req.headers.authorization; + + await this.proxy.proxy(req, res, { + abortController, + modifyHeaders: (headers) => { + if (!hasAuthorization) headers.authorization = `token ${token}`; + return headers; + }, + onResponse: async (data) => { + if (this.destroyed) return; + + if (opts.externalBaseUrl) { + const link = data.headers.link; + if (link !== undefined) + data.headers.link = rewriteLink(link, opts.externalBaseUrl); + const location = data.headers.location; + if (location !== undefined) { + data.headers.location = rewriteLocation(location, opts.externalBaseUrl); + } + } + + // Only update rate limits if we injected the token + if (!hasAuthorization) { + const status = data.status.toString(); + const rateLimitRemaining = headerString( + data.headers['x-ratelimit-remaining'] + ); + const rateLimitReset = headerString(data.headers['x-ratelimit-reset']); + const rateLimitLimit = headerString(data.headers['x-ratelimit-limit']); + + if (rateLimitRemaining) { + this.updateLimits({ + status, + 'x-ratelimit-remaining': rateLimitRemaining, + 'x-ratelimit-reset': rateLimitReset || '', + 'x-ratelimit-limit': rateLimitLimit || '' + }); + } + + this.timeBudget -= Date.now() - (req.startedAt?.getTime() || 1000); + this.router?.notifyWorkerAvailability(this); + + this.log(data.status, req.startedAt); + + // Remove rate limit and scope headers + for (const key of Object.keys(data.headers)) { + if (/(ratelimit|scope)/i.test(key)) { + delete data.headers[key]; + } + } + + const exposeHeaders = headerString( + data.headers['access-control-expose-headers'] + ); + if (exposeHeaders) { + const filtered = exposeHeaders + .split(', ') + .filter((header) => !/(ratelimit|scope)/i.test(header)) + .join(', '); + if (filtered) { + data.headers['access-control-expose-headers'] = filtered; + } else { + delete data.headers['access-control-expose-headers']; + } + } + } } + }); + } finally { + req.removeListener?.('aborted', abortRequest); + req.removeListener?.('error', abortRequest); + req.socket?.removeListener?.('close', abortRequest); + res.removeListener?.('close', abortResponse); + if (req.abortController === abortController) delete req.abortController; + } + } catch (error) { + activeAbortController?.abort(); + const err = error as Error & { code?: string }; + const errorCode = err.code || err.message; + this.log( + errorCode === 'ETIMEDOUT' ? 'ETIMEDOUT' : ProxyRouterResponse.PROXY_ERROR, + req.startedAt + ); + + if (!this.destroyed) { + if (error instanceof PayloadTooLargeError) { + rejectRequest(req, res, 413, { + message: 'Request body too large' + }); + } else if (context.timeoutReason === 'lifetime') { + rejectRequest(req, res, StatusCodes.GATEWAY_TIMEOUT, { + message: 'Request lifetime exceeded' + }); + } else if (!requestDisconnected(req) && !responseUnavailable(res)) { + res.status(StatusCodes.BAD_GATEWAY).send(); + } else { + terminatePartialResponse(req, res); } } + } finally { + activeAbortController = undefined; + this.scheduledRequests.delete(scheduledRequest); + if (context.state === 'active') { + this.router?.completeWork(context); + if (!this.router) cleanupRequestContext(context); + } + this.ownedContexts.delete(context); + queueMicrotask(() => this.router?.notifyWorkerAvailability(this)); + settleTask(); } + }) + .catch(() => { + this.scheduledRequests.delete(scheduledRequest); + settleTask(); }); - } catch (error) { - const err = error as Error & { code?: string }; - const errorCode = err.code || err.message; - this.log( - errorCode === 'ETIMEDOUT' ? 'ETIMEDOUT' : ProxyRouterResponse.PROXY_ERROR, - req.startedAt - ); - - if (!req.socket.destroyed && !req.socket.writableFinished) { - res.status(StatusCodes.BAD_GATEWAY).send(); - } - - req.abortController?.abort(); - res.destroy(); - } - }); + await completion; + } catch { + this.scheduledRequests.delete(scheduledRequest); + settleTask(); + } }; } - public async refreshRateLimits(): Promise { - await fetch('https://api.github.com/rate_limit', { - headers: { - authorization: `token ${this.token}`, - 'user-agent': 'GitHub API Proxy Server (@hsborges/github-proxy-server)' - } - }).then(async (response) => { - if (response.status === 401) { - this.remaining = 0; - this.reset = Number.POSITIVE_INFINITY; - this.emit('error', `Invalid token detected (${this.token}).`, this.token); - } else { - const res = (await response.json()) as { - resources: Record; - }; - this.remaining = res.resources[this.defaults.resource].remaining; - this.reset = res.resources[this.defaults.resource].reset; - this.log(undefined, new Date()); - } - }); + applyRateLimit(limit: RateLimit): void { + if (this.destroyed) return; + + this.remaining = limit.remaining; + this.reset = limit.reset; + this.log(undefined, new Date()); + this.router?.notifyWorkerAvailability(this); } private updateLimits(headers: Record): void { @@ -244,9 +713,12 @@ class ProxyWorker extends EventEmitter { this.remaining = Number.parseInt(headers['x-ratelimit-remaining'], 10) - this.queue.pending; this.reset = Number.parseInt(headers['x-ratelimit-reset'], 10); } + this.router?.notifyWorkerAvailability(this); } private log(status?: number | string, startedAt?: Date): void { + if (this.destroyed) return; + this.emit('log', { resource: this.defaults.resource, token: this.token.slice(-4), @@ -260,41 +732,95 @@ class ProxyWorker extends EventEmitter { } canAcceptWork(): boolean { + if (this.destroyed) return false; + return ( - this.queue.pending < (this.queue.concurrency ?? 1) && - this.timeBudget >= this.queue.pending * 1000 && - (this.remaining > this.opts.minRemaining || this.reset * 1000 < Date.now()) + this.queue.pending + this.reservations < (this.queue.concurrency ?? 1) && + this.timeBudget >= (this.queue.pending + this.reservations + 1) * 1000 && + (this.remaining > this.opts.minRemaining || this.reset < Math.floor(Date.now() / 1000)) ); } - setRouter(router: ProxyRouter): void { - this.router = router; - this.resourceQueue = router.getQueue(this.defaults.resource); - this.startPullLoop(); + reserveWork(): boolean { + if (!this.canAcceptWork()) return false; + this.reservations += 1; + return true; } - private startPullLoop(): void { - if (!this.router || !this.resourceQueue) return; + releaseReservation(): void { + if (!this.reservations) return; + this.reservations -= 1; + this.router?.notifyWorkerAvailability(this); + } - this.checkForWork = async () => { - if (!this.canAcceptWork() || !this.resourceQueue) return; - const work = this.resourceQueue.dequeue(); - if (work) await this.schedule(work.req, work.res); - }; + resetTimeBudget(): void { + if (this.destroyed) return; + this.timeBudget = + (this.defaults.resource === 'graphql' ? 60000 : 90000) * + (this.opts.timeBudgetMultiplier || 1); + this.router?.notifyWorkerAvailability(this); + } + + setRouter(router: ProxyRouter): void { + if (this.destroyed) return; - // Fallback polling every 100ms for missed events - this.pullInterval = setInterval(this.checkForWork, 100).unref(); + this.router = router; + router.notifyWorkerAvailability(this); } - destroy(): this { + get isDestroyed(): boolean { + return this.destroyed; + } + + destroy(): Promise { + if (this.destroyPromise) return this.destroyPromise; + + this.destroyed = true; + this.queue.pause(); this.queue.clear(); - if (this.pullInterval) { - clearInterval(this.pullInterval); - } - if (this._budgetResetInterval) { - clearInterval(this._budgetResetInterval); - } - return this; + + this.removeAllListeners(); + + this.destroyPromise = (async () => { + const errors: unknown[] = []; + this.router?.completeWorkerContexts(this); + + for (const context of [...this.ownedContexts]) { + if (context.worker && context.worker !== this) continue; + context.controller.abort(); + this.router?.completeWork(context); + if (!this.router) cleanupRequestContext(context); + disposeUnreadRequest(context.req); + terminateResponse(context.res); + this.ownedContexts.delete(context); + } + + for (const { req, res, context, settle } of this.scheduledRequests) { + try { + context.controller.abort(); + req.abortController?.abort(); + this.router?.completeWork(context); + disposeUnreadRequest(req); + terminateResponse(res); + } catch (error) { + errors.push(error); + } finally { + settle(); + } + } + this.scheduledRequests.clear(); + this.router = undefined; + + try { + await this.agent.destroy(); + } catch (error) { + errors.push(error); + } + + const error = disposalError(errors, 'Proxy worker destruction failed'); + if (error) throw error; + })(); + return this.destroyPromise; } } @@ -309,13 +835,20 @@ class QueueImpl implements RequestQueue { return this.items.shift(); } + remove(req: Request, res: Response): boolean { + const index = this.items.findIndex((item) => item.req === req && item.res === res); + if (index === -1) return false; + this.items.splice(index, 1); + return true; + } + get size(): number { return this.items.length; } } export enum ProxyRouterResponse { - PROXY_ERROR = 600 + PROXY_ERROR = StatusCodes.METHOD_NOT_ALLOWED } export default class ProxyRouter extends EventEmitter { @@ -334,8 +867,36 @@ export default class ProxyRouter extends EventEmitter { search: ProxyWorker; code_search: ProxyWorker; graphql: ProxyWorker; + refreshTimers: NodeJS.Timeout[]; + refreshPromise?: Promise; + refreshController?: AbortController; + refreshTimeout?: NodeJS.Timeout; + retryTimer?: NodeJS.Timeout; + retryWaitResolve?: () => void; + removed?: boolean; }>; + private destroyed = false; + private destroyPromise?: Promise; + private readonly removals = new Set>(); + private readonly requestContexts = new Set(); + private budgetResetTimer?: NodeJS.Timeout; + private readonly dispatchStates: Record = { + core: { cursor: 0, notificationQueued: false }, + search: { cursor: 0, notificationQueued: false }, + code_search: { cursor: 0, notificationQueued: false }, + graphql: { cursor: 0, notificationQueued: false } + }; + + private emitError(error: unknown): void { + if (!this.listenerCount('error')) return; + try { + this.emit('error', error); + } catch { + // Error listeners must not turn cleanup failures into unhandled exceptions. + } + } + // Cache worker arrays to avoid repeated map() calls private readonly workersByResource: { core: ProxyWorker[]; @@ -353,9 +914,22 @@ export default class ProxyRouter extends EventEmitter { super({}); if (!tokens.length) throw new Error('At least one token is required!'); + tokens.forEach((token) => validateGitHubToken(token)); this.clients = []; - this.options = Object.assign({ requestTimeout: 20000, minRemaining: 100 }, opts); + this.options = validateProxyRouterOptions( + Object.assign( + { + requestTimeout: 20000, + minRemaining: 100, + maxRequestBodyBytes: REQUEST_BODY_DEFAULT, + maxQueueDepthPerWorker: QUEUE_DEPTH_DEFAULT, + queueWaitTimeout: QUEUE_WAIT_DEFAULT, + requestLifetimeTimeout: REQUEST_LIFETIME_DEFAULT + }, + opts + ) + ); // Initialize per-resource queues this.queues = { @@ -366,22 +940,275 @@ export default class ProxyRouter extends EventEmitter { }; tokens.forEach((token) => this.addToken(token)); + this.armBudgetResetTimer(); } - async schedule(req: Request, res: Response): Promise { + notifyWorkerAvailability(worker: ProxyWorker): void { + if (this.destroyed) return; + this.notifyDispatch(worker.defaults.resource); + } + + private notifyDispatch(resource: APIResources): void { + if (this.destroyed) return; + const state = this.dispatchStates[resource]; + if (state.notificationQueued) return; + state.notificationQueued = true; + queueMicrotask(() => { + state.notificationQueued = false; + if (!this.destroyed) this.dispatch(resource); + }); + } + + private clearDispatchTimers(): void { + if (this.budgetResetTimer) clearTimeout(this.budgetResetTimer); + this.budgetResetTimer = undefined; + + for (const state of Object.values(this.dispatchStates)) { + if (state.resetTimer) clearTimeout(state.resetTimer); + state.resetTimer = undefined; + state.notificationQueued = false; + } + } + + private dispatch(resource: APIResources): void { + if (this.destroyed) return; + const state = this.dispatchStates[resource]; + const queue = this.queues[resource]; + if (!queue.size) { + if (state.resetTimer) clearTimeout(state.resetTimer); + state.resetTimer = undefined; + return; + } + + while (queue.size) { + const workers = this.workersByResource[resource].filter((worker) => !worker.isDestroyed); + if (!workers.length) { + if (state.resetTimer) clearTimeout(state.resetTimer); + state.resetTimer = undefined; + return; + } + + let selected: ProxyWorker | undefined; + for (let offset = 0; offset < workers.length; offset += 1) { + const index = (state.cursor + offset) % workers.length; + const worker = workers[index]; + if (worker.reserveWork()) { + selected = worker; + state.cursor = (index + 1) % workers.length; + break; + } + } + + if (!selected) { + this.armResetWake(resource, workers); + return; + } + + const work = queue.dequeue(); + if (!work) { + selected.releaseReservation(); + return; + } + void selected.schedule(work.req as ExtendedRequest, work.res); + selected.releaseReservation(); + } + + if (state.resetTimer) clearTimeout(state.resetTimer); + state.resetTimer = undefined; + } + + private armResetWake(resource: APIResources, workers: ProxyWorker[]): void { + const state = this.dispatchStates[resource]; + if (state.resetTimer) clearTimeout(state.resetTimer); + const now = Date.now(); + const earliest = workers.reduce((minimum, worker) => { + const resetAt = (worker.reset + 1) * 1000; + return worker.remaining <= (this.options.minRemaining ?? 0) && resetAt > now + ? Math.min(minimum, resetAt) + : minimum; + }, Number.POSITIVE_INFINITY); + if (!Number.isFinite(earliest)) { + state.resetTimer = undefined; + return; + } + state.resetTimer = setTimeout( + () => { + state.resetTimer = undefined; + this.notifyDispatch(resource); + }, + Math.max(1, earliest - now) + ).unref(); + } + + private armBudgetResetTimer(): void { + if (this.destroyed) return; + if (this.budgetResetTimer) clearTimeout(this.budgetResetTimer); + this.budgetResetTimer = setTimeout(() => { + this.budgetResetTimer = undefined; + if (this.destroyed) return; + for (const workers of Object.values(this.workersByResource)) { + for (const worker of workers) worker.resetTimeBudget(); + } + this.armBudgetResetTimer(); + }, 60000).unref(); + } + + private resourceFor(req: Request): APIResources { const isGraphQL = req.path.startsWith('/graphql') && req.method === 'POST'; const isCodeSearch = req.path.startsWith('/search/code'); const isSearch = req.path.startsWith('/search'); + return isGraphQL ? 'graphql' : isCodeSearch ? 'code_search' : isSearch ? 'search' : 'core'; + } + + private completeContext(context: RequestContext): void { + this.requestContexts.delete(context); + if (context.state === 'settled') return; + context.worker = undefined; + cleanupRequestContext(context); + } + + completeWork(context: RequestContext): void { + if (context.state === 'settled') return; + if (context.state === 'queued') { + const resource = this.resourceFor(context.req); + this.queues[resource].remove(context.req, context.res); + this.notifyDispatch(resource); + } + this.completeContext(context); + } + + completeWorkerContexts(worker: ProxyWorker): void { + for (const context of [...this.requestContexts]) { + if (context.worker !== worker) continue; + context.controller.abort(); + if (context.state === 'queued') { + this.queues[this.resourceFor(context.req)].remove(context.req, context.res); + } + disposeUnreadRequest(context.req); + terminateResponse(context.res); + this.completeContext(context); + } + } + + private disconnectContext(context: RequestContext): void { + if (context.state === 'queued') { + const resource = this.resourceFor(context.req); + this.queues[resource].remove(context.req, context.res); + disposeUnreadRequest(context.req); + this.completeContext(context); + this.notifyDispatch(resource); + } + } + + private rejectContext( + context: RequestContext, + status: number, + body: Record, + retryAfter?: string + ): void { + if (context.state === 'settled') return; + const resource = this.resourceFor(context.req); + if (context.state === 'queued') { + this.queues[resource].remove(context.req, context.res); + } + context.controller.abort(); + rejectRequest(context.req, context.res, status, body, retryAfter); + this.completeContext(context); + this.notifyDispatch(resource); + } + + private expireQueueContext(context: RequestContext): void { + if (this.destroyed || context.state !== 'queued') return; + context.timeoutReason = 'queue'; + this.rejectContext(context, StatusCodes.GATEWAY_TIMEOUT, { + message: 'Request expired in proxy queue' + }); + } + + private expireLifetime(context: RequestContext): void { + if (this.destroyed || context.state === 'settled') return; + context.timeoutReason = 'lifetime'; + if (context.state === 'queued') { + this.rejectContext(context, StatusCodes.GATEWAY_TIMEOUT, { + message: 'Request lifetime exceeded' + }); + return; + } + this.rejectContext(context, StatusCodes.GATEWAY_TIMEOUT, { + message: 'Request lifetime exceeded' + }); + } + + async schedule(req: Request, res: Response): Promise { + if (this.destroyed) { + terminateResponse(res); + return; + } - const queue = isGraphQL - ? this.queues['graphql'] - : isCodeSearch - ? this.queues['code_search'] - : isSearch - ? this.queues['search'] - : this.queues['core']; + const resource = this.resourceFor(req); + const queue = this.queues[resource]; + const capacity = + this.workersByResource[resource].filter((worker) => !worker.isDestroyed).length * + (this.options.maxQueueDepthPerWorker ?? QUEUE_DEPTH_DEFAULT); + const context = (req as ExtendedRequest).proxyContext; + + if (queue.size >= capacity) { + if (context) { + this.rejectContext( + context, + StatusCodes.SERVICE_UNAVAILABLE, + { + message: 'Proxy queue is full' + }, + '1' + ); + return; + } + rejectRequest( + req as ExtendedRequest, + res, + StatusCodes.SERVICE_UNAVAILABLE, + { + message: 'Proxy queue is full' + }, + '1' + ); + return; + } + const requestContext = + context ?? + createRequestContext(req as ExtendedRequest, res, (disconnectedContext) => + this.disconnectContext(disconnectedContext) + ); + if (requestContext.state === 'settled') return; + if (requestContext.lifetimeDeadline === 0) { + requestContext.lifetimeDeadline = + requestContext.acceptedAt + + (this.options.requestLifetimeTimeout ?? REQUEST_LIFETIME_DEFAULT); + requestContext.queueDeadline = + requestContext.acceptedAt + (this.options.queueWaitTimeout ?? QUEUE_WAIT_DEFAULT); + this.requestContexts.add(requestContext); + const lifetimeDelay = Math.max(0, requestContext.lifetimeDeadline - Date.now()); + requestContext.lifetimeTimer = setTimeout( + () => this.expireLifetime(requestContext), + lifetimeDelay + ).unref(); + } + requestContext.state = 'queued'; + requestContext.worker = undefined; + requestContext.enqueuedAt = Date.now(); + if (requestContext.queueDeadline <= Date.now()) { + this.expireQueueContext(requestContext); + return; + } + if (requestContext.queueTimer) clearTimeout(requestContext.queueTimer); + requestContext.queueTimer = setTimeout( + () => this.expireQueueContext(requestContext), + Math.max(0, requestContext.queueDeadline - Date.now()) + ).unref(); queue.enqueue(req, res); + this.notifyDispatch(resource); } getQueue(resource: APIResources): RequestQueue { @@ -389,6 +1216,8 @@ export default class ProxyRouter extends EventEmitter { } addToken(token: string): void { + if (this.destroyed) return; + validateGitHubToken(token); if (this.clients.map((client) => client.token).includes(token)) return; const core = new ProxyWorker(token, { ...this.options, resource: 'core' }); @@ -396,70 +1225,317 @@ export default class ProxyRouter extends EventEmitter { const codeSearch = new ProxyWorker(token, { ...this.options, resource: 'code_search' }); const graphql = new ProxyWorker(token, { ...this.options, resource: 'graphql' }); - for (const worker of [core, search, codeSearch, graphql]) { - worker.on('error', (error: unknown) => this.emit('error', error)); + const workers = [core, search, codeSearch, graphql]; + + for (const worker of workers) { + worker.on('error', (error: unknown) => this.emitError(error)); worker.on('retry', (req: ExtendedRequest, res: Response) => this.schedule(req, res)); worker.on('log', (log: WorkerLogger) => this.emit('log', log)); worker.on('warn', (message: string) => this.emit('warn', message)); - worker.refreshRateLimits().then(() => this.emit('ready')); - // Auto-refresh rate limits every 15 minutes - setInterval(() => worker.refreshRateLimits(), 15 * 60 * 1000).unref(); - // Phase 3: Set router reference to enable pull mechanism + // Workers notify the router when their scheduling state changes. worker.setRouter(this); } - this.clients.push({ token, core, search, code_search: codeSearch, graphql }); + const client = { + token, + core, + search, + code_search: codeSearch, + graphql, + refreshTimers: [] as NodeJS.Timeout[] + }; + this.clients.push(client); // Update worker caches this.workersByResource.core.push(core); this.workersByResource.search.push(search); this.workersByResource.code_search.push(codeSearch); this.workersByResource.graphql.push(graphql); + + for (const resource of ['core', 'search', 'code_search', 'graphql'] as APIResources[]) { + this.notifyDispatch(resource); + } + + this.startDetachedRefresh(client, true); + // Auto-refresh rate limits every 15 minutes, once per token. + client.refreshTimers.push( + setInterval(() => this.startDetachedRefresh(client, false), 15 * 60 * 1000).unref() + ); } - removeToken(token: string): void { - const index = this.clients.map((c) => c.token).indexOf(token); - if (index === -1) return; + private startDetachedRefresh(client: (typeof this.clients)[number], emitReady: boolean): void { + void this.refreshClient(client) + .then(() => { + if (emitReady && !this.destroyed && !client.removed) this.emit('ready'); + }) + .catch((error: unknown) => { + if (!this.destroyed && !client.removed) this.emitError(error); + }); + } - const removed = this.clients.splice(index, 1); - removed.forEach((client) => { - for (const worker of [client.core, client.search, client.code_search, client.graphql]) { - worker.destroy(); - } + private cancelRefresh(client: (typeof this.clients)[number]): void { + client.refreshController?.abort(); + if (client.refreshTimeout) clearTimeout(client.refreshTimeout); + client.refreshTimeout = undefined; - // Update worker caches - const coreIndex = this.workersByResource.core.indexOf(client.core); - if (coreIndex !== -1) this.workersByResource.core.splice(coreIndex, 1); + if (client.retryTimer) clearTimeout(client.retryTimer); + client.retryTimer = undefined; + const resolveRetry = client.retryWaitResolve; + client.retryWaitResolve = undefined; + resolveRetry?.(); + } - const searchIndex = this.workersByResource.search.indexOf(client.search); - if (searchIndex !== -1) this.workersByResource.search.splice(searchIndex, 1); + private async waitForRefreshRetry( + client: (typeof this.clients)[number], + delay: number + ): Promise { + if (this.destroyed || client.removed) return; + + await new Promise((resolve) => { + let timer: NodeJS.Timeout; + const complete = (): void => { + if (client.retryTimer === timer) client.retryTimer = undefined; + if (client.retryWaitResolve === complete) client.retryWaitResolve = undefined; + resolve(); + }; + timer = setTimeout(complete, delay).unref(); + client.retryTimer = timer; + client.retryWaitResolve = complete; + }); + } - const codeSearchIndex = this.workersByResource.code_search.indexOf(client.code_search); - if (codeSearchIndex !== -1) this.workersByResource.code_search.splice(codeSearchIndex, 1); + private async awaitRefreshAbort(operation: Promise, signal: AbortSignal): Promise { + if (signal.aborted) throw new RefreshFailure('refresh aborted'); - const graphqlIndex = this.workersByResource.graphql.indexOf(client.graphql); - if (graphqlIndex !== -1) this.workersByResource.graphql.splice(graphqlIndex, 1); + let abort: (() => void) | undefined; + const aborted = new Promise((_resolve, reject) => { + abort = () => reject(new RefreshFailure('refresh aborted')); + signal.addEventListener('abort', abort, { once: true }); }); + + try { + return await Promise.race([operation, aborted]); + } finally { + if (abort) signal.removeEventListener('abort', abort); + } + } + + private async fetchRateLimits( + client: (typeof this.clients)[number] + ): Promise { + let reason = 'network failure'; + + for (let attempt = 0; attempt < REFRESH_MAX_ATTEMPTS; attempt += 1) { + if (this.destroyed || client.removed) return undefined; + + const controller = new AbortController(); + let timedOut = false; + client.refreshController = controller; + client.refreshTimeout = setTimeout(() => { + timedOut = true; + controller.abort(); + }, this.options.requestTimeout).unref(); + + try { + const response = await this.awaitRefreshAbort( + fetch('https://api.github.com/rate_limit', { + headers: { + authorization: `token ${client.token}`, + 'user-agent': 'GitHub API Proxy Server (@hsborges/github-proxy-server)' + }, + signal: controller.signal + }), + controller.signal + ); + + if (response.status < 200 || response.status >= 300) { + throw new RefreshFailure(`HTTP response ${response.status}`); + } + + let body: unknown; + try { + body = await this.awaitRefreshAbort(response.json(), controller.signal); + } catch { + throw new RefreshFailure('malformed JSON'); + } + + if (!isRecord(body) || !('resources' in body)) { + throw new RefreshFailure('invalid resource shape'); + } + return parseRateLimitResources(body.resources); + } catch (error) { + if (this.destroyed || client.removed) return undefined; + reason = timedOut + ? `attempt timed out after ${this.options.requestTimeout}ms` + : error instanceof RefreshFailure + ? error.message + : 'network failure'; + if (attempt + 1 < REFRESH_MAX_ATTEMPTS) { + const delay = Math.min(REFRESH_BACKOFF_BASE_MS * 2 ** attempt, REFRESH_BACKOFF_MAX_MS); + await this.waitForRefreshRetry(client, delay); + } + } finally { + if (client.refreshController === controller) client.refreshController = undefined; + if (client.refreshTimeout) clearTimeout(client.refreshTimeout); + if (client.refreshController === undefined) client.refreshTimeout = undefined; + } + } + + throw new Error( + `Rate-limit refresh failed for token ending ${client.token.slice(-4)} after ${REFRESH_MAX_ATTEMPTS} attempts: ${reason}` + ); + } + + private refreshClient(client: (typeof this.clients)[number]): Promise { + if (this.destroyed || client.removed) return Promise.resolve(); + if (client.refreshPromise) return client.refreshPromise; + + const refreshPromise = (async () => { + try { + const resources = await this.fetchRateLimits(client); + if (!resources || this.destroyed || client.removed) return; + + for (const worker of [client.core, client.search, client.code_search, client.graphql]) { + worker.applyRateLimit(resources[worker.defaults.resource]); + } + } catch (error) { + if (!this.destroyed && !client.removed) throw error; + } + })(); + client.refreshPromise = refreshPromise; + void refreshPromise.then( + () => { + if (client.refreshPromise === refreshPromise) client.refreshPromise = undefined; + }, + () => { + if (client.refreshPromise === refreshPromise) client.refreshPromise = undefined; + } + ); + return refreshPromise; + } + + private async destroyClient(client: (typeof this.clients)[number]): Promise { + const workers = [client.core, client.search, client.code_search, client.graphql]; + client.removed = true; + for (const timer of client.refreshTimers) clearInterval(timer); + this.cancelRefresh(client); + + const results = await Promise.allSettled([ + client.refreshPromise ?? Promise.resolve(), + ...workers.map((worker) => worker.destroy()) + ]); + const errors = results + .filter((result): result is PromiseRejectedResult => result.status === 'rejected') + .map((result) => result.reason); + + const cacheEntries: Array<[ProxyWorker[], ProxyWorker]> = [ + [this.workersByResource.core, client.core], + [this.workersByResource.search, client.search], + [this.workersByResource.code_search, client.code_search], + [this.workersByResource.graphql, client.graphql] + ]; + for (const [workersByResource, worker] of cacheEntries) { + const workerIndex = workersByResource.indexOf(worker); + if (workerIndex !== -1) workersByResource.splice(workerIndex, 1); + worker.removeAllListeners(); + } + + for (const resource of ['core', 'search', 'code_search', 'graphql'] as APIResources[]) { + this.notifyDispatch(resource); + } + + const error = disposalError(errors, 'Proxy token destruction failed'); + if (error) throw error; + } + + removeToken(token: string): Promise { + if (this.destroyed) return this.destroyPromise ?? Promise.resolve(); + + const index = this.clients.map((c) => c.token).indexOf(token); + if (index === -1) return Promise.resolve(); + + const client = this.clients.splice(index, 1)[0]; + client.removed = true; + const removal = this.destroyClient(client); + this.removals.add(removal); + void removal.then( + () => this.removals.delete(removal), + () => this.removals.delete(removal) + ); + return removal; } async refreshRateLimits(): Promise { - await Promise.all( - this.clients.map((client) => - Promise.all( - [client.core, client.search, client.code_search, client.graphql].map((w) => - w.refreshRateLimits() - ) - ) - ) - ).then(() => this.emit('ready')); + if (this.destroyed) return; + + const clients = [...this.clients]; + try { + await Promise.all(clients.map((client) => this.refreshClient(client))); + } catch (error) { + if (!this.destroyed) this.emitError(error); + throw error; + } + if (!this.destroyed) this.emit('ready'); } get tokens(): string[] { return this.clients.map((client) => client.token); } - destroy(): this { - this.clients.forEach((client) => this.removeToken(client.token)); - return this; + destroy(): Promise { + if (this.destroyPromise) return this.destroyPromise; + + this.destroyed = true; + this.clearDispatchTimers(); + + const clients = this.clients.splice(0); + const removals = [...this.removals]; + + this.destroyPromise = (async () => { + const errors: unknown[] = []; + for (const queue of Object.values(this.queues)) { + let work = queue.dequeue(); + while (work) { + try { + const context = (work.req as ExtendedRequest).proxyContext; + context?.controller.abort(); + disposeUnreadRequest(work.req as ExtendedRequest); + if (context) this.completeContext(context); + terminateResponse(work.res); + } catch (error) { + errors.push(error); + } + work = queue.dequeue(); + } + } + + for (const context of [...this.requestContexts]) { + try { + context.controller.abort(); + disposeUnreadRequest(context.req); + terminateResponse(context.res); + } catch (error) { + errors.push(error); + } finally { + this.completeContext(context); + } + } + + const results = await Promise.allSettled([ + ...clients.map((client) => this.destroyClient(client)), + ...removals + ]); + for (const result of results) { + if (result.status === 'rejected') errors.push(result.reason); + } + + for (const workers of Object.values(this.workersByResource)) workers.length = 0; + this.removeAllListeners(); + + const error = disposalError(errors, 'Proxy router destruction failed'); + if (error) throw error; + })(); + + return this.destroyPromise; } -} +} \ No newline at end of file diff --git a/src/server.spec.ts b/src/server.spec.ts index 8bff594..9439103 100644 --- a/src/server.spec.ts +++ b/src/server.spec.ts @@ -6,11 +6,48 @@ import times from 'lodash/times.js'; import nock from 'nock'; import request from 'supertest'; import { withFile } from 'tmp-promise'; -import { beforeAll, beforeEach, describe, expect, test } from 'vitest'; +import { MockAgent, type MockPool } from 'undici'; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from 'vitest'; -import { ProxyRouterResponse } from './router.js'; import { type CliOpts, createProxyServer, parseTokens, readTokensFile } from './server.js'; +const createdApps: Array> = []; +let mockAgent: MockAgent; +let mockPool: MockPool; + +beforeAll(() => { + mockAgent = new MockAgent(); + mockAgent.disableNetConnect(); + mockPool = mockAgent.get('https://api.github.com'); + mockPool.intercept({ path: '/', method: 'GET' }).reply(StatusCodes.OK).persist(); + mockPool + .intercept({ + path: '/user', + method: 'GET', + headers: { authorization: `token ${repeat('i', 40)}` } + }) + .reply(StatusCodes.UNAUTHORIZED) + .persist(); + mockPool.intercept({ path: '/user', method: 'GET' }).reply(StatusCodes.OK).persist(); + mockPool.intercept({ path: '/graphql', method: 'POST' }).reply(StatusCodes.OK).persist(); +}); + +afterAll(async () => { + await mockAgent.close(); + nock.cleanAll(); + nock.restore(); +}); + +function createTestApp(options: CliOpts): ReturnType { + const app = createProxyServer(options); + createdApps.push(app); + return app; +} + +afterEach(async () => { + await Promise.all(createdApps.splice(0).map((app) => app.destroy())); +}); + describe('Test tokens file parser', () => { test('it should check tokens length', () => { expect(() => parseTokens(times(15, () => 'a').join(''))).toThrowError(); @@ -69,20 +106,7 @@ describe('Test create proxy server', () => { graphql: { limit: 5000, remaining: 5000, reset: Date.now() + 60 * 60 } } }) - .persist() - .get('/user') - .matchHeader('authorization', `token ${repeat('i', 40)}`) - .reply(StatusCodes.UNAUTHORIZED) - .post('/graphql') - .reply(200) - .intercept(/.*/, 'get') - .reply(200) - .intercept(/.*/, 'post') - .reply(600) - .intercept(/.*/, 'put') - .reply(600) - .intercept(/.*/, 'delete') - .reply(600); + .persist(); }); beforeEach(async () => { @@ -90,7 +114,8 @@ describe('Test create proxy server', () => { tokens: [repeat('0', 40)], minRemaining: 0, requestTimeout: 500, - silent: true + silent: true, + dispatcher: mockAgent }; }); @@ -99,22 +124,50 @@ describe('Test create proxy server', () => { expect(() => createProxyServer(params)).toThrowError(); }); - test('it should accept GET requests', async () => { - const app = createProxyServer(params); + test.each([ + ['requestTimeout', { requestTimeout: 0 }], + ['minRemaining', { minRemaining: -1 }], + ['timeBudgetMultiplier', { timeBudgetMultiplier: Infinity }] + ])('it should validate direct %s configuration', (_name, invalidOptions) => { + expect(() => createProxyServer({ ...params, ...invalidOptions })).toThrow(`Invalid ${_name}`); + }); + + test('it should expose an idempotent asynchronous app destroy method', async () => { + const app = createTestApp(params); + const destruction = app.destroy(); + + expect(app.destroy()).toBe(destruction); + await destruction; + }); + + test('it should accept GET requests and reject unsupported methods', async () => { + const app = createTestApp(params); await request(app).get('/').expect(StatusCodes.OK); - await request(app).post('/').expect(ProxyRouterResponse.PROXY_ERROR); - await request(app).patch('/').expect(ProxyRouterResponse.PROXY_ERROR); - await request(app).put('/').expect(ProxyRouterResponse.PROXY_ERROR); - await request(app).delete('/').expect(ProxyRouterResponse.PROXY_ERROR); + + for (const method of ['post', 'patch', 'put', 'delete'] as const) { + await request(app) + [method]('/') + .expect(StatusCodes.METHOD_NOT_ALLOWED) + .expect({ message: 'Endpoint not supported' }); + } }); test('it should accept POSTs only to /graphql', async () => { - const app = createProxyServer(params); + const app = createTestApp(params); await request(app).post('/graphql').expect(StatusCodes.OK); }); + test('it should expose only the public health contract when monitoring is disabled', async () => { + const app = createTestApp({ ...params, statusMonitor: false }); + + await request(app).get('/status').expect(StatusCodes.OK).expect({ status: 'ok' }); + await request(app).get('/status/').expect(StatusCodes.OK).expect({ status: 'ok' }); + await request(app).get('/status/unknown').expect(StatusCodes.NOT_FOUND); + await request(app).get('/metrics').expect(StatusCodes.NOT_FOUND); + }); + test('it should emit logs when enabled', async () => { - const app = createProxyServer({ ...params, silent: false }); + const app = createTestApp({ ...params, silent: false }); const logs: string[] = []; app.on('log', (data) => logs.push(data.toString())); @@ -128,7 +181,7 @@ describe('Test create proxy server', () => { }); test('it should not emit logs when disabled', async () => { - const app = createProxyServer({ ...params, silent: true }); + const app = createTestApp({ ...params, silent: true }); const logs: string[] = []; app.on('log', (data) => logs.push(data.toString())); @@ -138,7 +191,7 @@ describe('Test create proxy server', () => { }); test('it should emit an error when invalid token are detected', async () => { - const app = createProxyServer({ ...params, tokens: [repeat('i', 40)] }); + const app = createTestApp({ ...params, tokens: [repeat('i', 40)] }); const errors: string[] = []; app.on('error', (data) => errors.push(data.toString())); @@ -148,7 +201,7 @@ describe('Test create proxy server', () => { }); test('it should not pass authorization tokens by default', async () => { - const app = createProxyServer(params); + const app = createTestApp(params); await request(app) .get('/user') @@ -158,7 +211,7 @@ describe('Test create proxy server', () => { }); test('it should allow users to user own authorization tokens', async () => { - const app = createProxyServer({ ...params, overrideAuthorization: false }); + const app = createTestApp({ ...params, overrideAuthorization: false }); await request(app) .get('/user') @@ -183,11 +236,7 @@ describe('Test proxy authentication', () => { graphql: { limit: 5000, remaining: 5000, reset: Date.now() + 60 * 60 } } }) - .persist() - .intercept(/.*/, 'get') - .reply(200) - .post('/graphql') - .reply(200); + .persist(); }); beforeEach(() => { @@ -196,6 +245,7 @@ describe('Test proxy authentication', () => { minRemaining: 0, requestTimeout: 500, silent: true, + dispatcher: mockAgent, auth: { username: 'testuser', password: 'testpass' @@ -204,46 +254,76 @@ describe('Test proxy authentication', () => { }); test('it should require authentication when auth is configured', async () => { - const app = createProxyServer(params); + const app = createTestApp(params); await request(app).get('/').expect(StatusCodes.UNAUTHORIZED); await request(app).post('/graphql').expect(StatusCodes.UNAUTHORIZED); }); test('it should accept valid credentials', async () => { - const app = createProxyServer(params); + const app = createTestApp(params); await request(app).get('/').auth('testuser', 'testpass').expect(StatusCodes.OK); }); test('it should reject invalid username', async () => { - const app = createProxyServer(params); + const app = createTestApp(params); await request(app).get('/').auth('wronguser', 'testpass').expect(StatusCodes.UNAUTHORIZED); }); test('it should reject invalid password', async () => { - const app = createProxyServer(params); + const app = createTestApp(params); await request(app).get('/').auth('testuser', 'wrongpass').expect(StatusCodes.UNAUTHORIZED); }); + test.each(['Bearer not-basic', 'Basic not-base64'])( + 'it should reject malformed authorization header %s', + async (authorization) => { + const app = createTestApp(params); + await request(app) + .get('/') + .set('Authorization', authorization) + .expect(StatusCodes.UNAUTHORIZED); + } + ); + test('it should return WWW-Authenticate header on unauthorized', async () => { - const app = createProxyServer(params); + const app = createTestApp(params); const response = await request(app).get('/').expect(StatusCodes.UNAUTHORIZED); expect(response.headers['www-authenticate']).toBe('Basic realm="GitHub Proxy Server"'); }); test('it should allow access to /status without authentication', async () => { - const app = createProxyServer({ ...params, statusMonitor: true }); - // /status endpoint redirects to /status/ux - follow the redirect - const response = await request(app).get('/status').redirects(1); + const app = createTestApp({ ...params, statusMonitor: true }); + const response = await request(app).get('/status'); expect(response.status).toBe(StatusCodes.OK); + expect(response.body).toEqual({ status: 'ok' }); + + await request(app).get('/status/').expect(StatusCodes.OK).expect({ status: 'ok' }); + await request(app).get('/status/unknown').expect(StatusCodes.NOT_FOUND); + await request(app).get('/metrics').expect(StatusCodes.UNAUTHORIZED); + await request(app).get('/metrics/stats').expect(StatusCodes.UNAUTHORIZED); + await request(app).get('/metrics/metrics').expect(StatusCodes.UNAUTHORIZED); + await request(app).get('/metrics/stats').auth('testuser', 'testpass').expect(StatusCodes.OK); + await request(app).get('/metrics/metrics').auth('testuser', 'testpass').expect(StatusCodes.OK); + await request(app).get('/status-other').expect(StatusCodes.UNAUTHORIZED); + await request(app).get('/status-metrics').expect(StatusCodes.UNAUTHORIZED); + }); + + test('it should keep disabled metrics unavailable while protecting the health namespace', async () => { + const app = createTestApp({ ...params, statusMonitor: false }); + + await request(app).get('/status').expect(StatusCodes.OK); + await request(app).get('/status/unknown').expect(StatusCodes.NOT_FOUND); + await request(app).get('/metrics').expect(StatusCodes.UNAUTHORIZED); + await request(app).get('/metrics').auth('testuser', 'testpass').expect(StatusCodes.NOT_FOUND); }); test('it should work with POST /graphql when authenticated', async () => { - const app = createProxyServer(params); + const app = createTestApp(params); await request(app).post('/graphql').auth('testuser', 'testpass').expect(StatusCodes.OK); }); test('it should not require auth when auth option is not provided', async () => { - const app = createProxyServer({ ...params, auth: undefined }); + const app = createTestApp({ ...params, auth: undefined }); await request(app).get('/').expect(StatusCodes.OK); }); -}); +}); \ No newline at end of file diff --git a/src/server.ts b/src/server.ts index fb46b01..a7b7ec8 100644 --- a/src/server.ts +++ b/src/server.ts @@ -3,12 +3,13 @@ import { existsSync, readFileSync } from 'node:fs'; import { resolve } from 'node:path'; -import basicAuth from 'basic-auth'; +import { parse as parseBasicAuth } from 'basic-auth'; import chalk from 'chalk'; import compression from 'compression'; import dayjs from 'dayjs'; import relativeTime from 'dayjs/plugin/relativeTime.js'; import express, { type Express, type Request, type Response } from 'express'; +import { StatusCodes } from 'http-status-codes'; import compact from 'lodash/compact.js'; import uniq from 'lodash/uniq.js'; import { pino } from 'pino'; @@ -20,6 +21,8 @@ import { getBorderCharacters, table } from 'table'; import ProxyRouter, { type ProxyRouterOpts, ProxyRouterResponse, + validateGitHubToken, + validateProxyRouterOptions, type WorkerLogger } from './router.js'; @@ -79,8 +82,7 @@ export function parseTokens(text: string): string[] { // concat tokens in commander export function concatTokens(token: string, list: string[]): string[] { - if (token.length !== 40) - throw new Error('Invalid access token detected (they have 40 characters)'); + validateGitHubToken(token); return uniq([...list, token]); } @@ -101,7 +103,13 @@ export type CliOpts = ProxyRouterOpts & { }; }; -export function createProxyServer(options: CliOpts): Express { +export type ProxyServer = Express & { + destroy(): Promise; +}; + +export function createProxyServer(options: CliOpts): ProxyServer { + const validatedOptions = validateProxyRouterOptions(options); + const tokens = compact(options.tokens).reduce( (memo: string[], token: string) => concatTokens(token, memo), [] @@ -119,11 +127,22 @@ export function createProxyServer(options: CliOpts): Express { }) ); + app.get(['/status', '/status/'], (_req: Request, res: Response) => { + res.status(StatusCodes.OK).json({ status: 'ok' }); + }); + + // Keep the public health namespace separate from proxy and monitoring routes. + app.use('/status', (_req: Request, res: Response) => { + res.status(StatusCodes.NOT_FOUND).send({ message: 'Endpoint not found' }); + }); + if (options.auth) { app.use((req: Request, res: Response, next) => { - if (req.path.startsWith('/status')) return next(); + if (req.path === '/status' || req.path === '/status/') return next(); - const credentials = basicAuth(req); + const credentials = req.headers.authorization + ? parseBasicAuth(req.headers.authorization) + : undefined; if ( !credentials || @@ -152,21 +171,32 @@ export function createProxyServer(options: CliOpts): Express { } if (options.statusMonitor) { - app.use( - swaggerStats.getMiddleware({ - name: 'GitHub Proxy Server', - version: process.env.npm_package_version, - uriPath: '/status' - }) - ); + const monitoringOptions = { + name: 'GitHub Proxy Server', + version: process.env.npm_package_version, + uriPath: '/metrics', + pathUI: '/metrics/ui', + pathDist: '/metrics/dist', + pathUX: '/metrics/ux', + pathStats: '/metrics/stats', + pathMetrics: '/metrics/metrics', + pathLogout: '/metrics/logout' + }; + app.use(swaggerStats.getMiddleware(monitoringOptions)); + } else { + app.use('/metrics', (_req: Request, res: Response) => { + res.status(StatusCodes.NOT_FOUND).send({ message: 'Monitoring disabled' }); + }); } const proxy = new ProxyRouter(tokens, { overrideAuthorization: options.overrideAuthorization ?? true, - ...options + ...validatedOptions }); - proxy.on('error', (message) => app.emit('error', message)); + proxy.on('error', (message) => { + if (app.listenerCount('error')) app.emit('error', message); + }); proxy.on('warn', (message) => app.emit('warn', message)); if (!options.silent) { @@ -186,5 +216,8 @@ export function createProxyServer(options: CliOpts): Express { app.put('{/*path}', notSupported); app.post('{/*path}', notSupported); - return app; -} + const proxyApp = app as ProxyServer; + proxyApp.destroy = (): Promise => proxy.destroy(); + + return proxyApp; +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 4e802ee..606fa28 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,7 +2,7 @@ # yarn lockfile v1 -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.22.13": +"@babel/code-frame@^7.0.0": version "7.24.7" resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz" integrity sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA== @@ -10,20 +10,29 @@ "@babel/highlight" "^7.24.7" picocolors "^1.0.0" -"@babel/helper-string-parser@^7.27.1": - version "7.27.1" - resolved "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz" - integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA== +"@babel/code-frame@^7.26.2": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.29.7.tgz#f2fbbfea87c44a21590ec515b778b2c26d8866e7" + integrity sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw== + dependencies: + "@babel/helper-validator-identifier" "^7.29.7" + js-tokens "^4.0.0" + picocolors "^1.1.1" + +"@babel/helper-string-parser@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz#7f0871d99824d23137d60f86fcf6130fd5a1b51f" + integrity sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw== "@babel/helper-validator-identifier@^7.24.7": version "7.24.7" resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz" integrity sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w== -"@babel/helper-validator-identifier@^7.28.5": - version "7.28.5" - resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz" - integrity sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q== +"@babel/helper-validator-identifier@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz#bd87084ced0c796ec46bda492de6e83d29e89fc2" + integrity sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg== "@babel/highlight@^7.24.7": version "7.24.7" @@ -35,108 +44,100 @@ js-tokens "^4.0.0" picocolors "^1.0.0" -"@babel/parser@^7.28.5": - version "7.28.6" - resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz" - integrity sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ== +"@babel/parser@^7.29.7": + version "7.29.8" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.29.8.tgz#9653716a2f10c677b98fbc63d4bfb000c302cf17" + integrity sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA== dependencies: - "@babel/types" "^7.28.6" + "@babel/types" "^7.29.8" -"@babel/types@^7.28.5": - version "7.28.5" - resolved "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz" - integrity sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA== +"@babel/types@^7.29.7", "@babel/types@^7.29.8": + version "7.29.8" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.29.8.tgz#1229eef31d85156d70fa3f4cd859376d0eaf6863" + integrity sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg== dependencies: - "@babel/helper-string-parser" "^7.27.1" - "@babel/helper-validator-identifier" "^7.28.5" - -"@babel/types@^7.28.6": - version "7.28.6" - resolved "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz" - integrity sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg== - dependencies: - "@babel/helper-string-parser" "^7.27.1" - "@babel/helper-validator-identifier" "^7.28.5" + "@babel/helper-string-parser" "^7.29.7" + "@babel/helper-validator-identifier" "^7.29.7" "@bcoe/v8-coverage@^1.0.2": version "1.0.2" resolved "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz" integrity sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA== -"@biomejs/biome@^2.3.11": - version "2.3.11" - resolved "https://registry.npmjs.org/@biomejs/biome/-/biome-2.3.11.tgz" - integrity sha512-/zt+6qazBWguPG6+eWmiELqO+9jRsMZ/DBU3lfuU2ngtIQYzymocHhKiZRyrbra4aCOoyTg/BmY+6WH5mv9xmQ== +"@biomejs/biome@^2.5.11": + version "2.5.11" + resolved "https://registry.yarnpkg.com/@biomejs/biome/-/biome-2.5.11.tgz#2325a9f54a651d0420be1f1517380f56013f7ba1" + integrity sha512-Tj0dnkLPdW0ASjHfj2D/ZkkvPU2wrFmnE1jWTD2xzV1ycapV1DutbYXk4NDnR3rYTi1ZCbNFD4G2gRMEY65WaA== optionalDependencies: - "@biomejs/cli-darwin-arm64" "2.3.11" - "@biomejs/cli-darwin-x64" "2.3.11" - "@biomejs/cli-linux-arm64" "2.3.11" - "@biomejs/cli-linux-arm64-musl" "2.3.11" - "@biomejs/cli-linux-x64" "2.3.11" - "@biomejs/cli-linux-x64-musl" "2.3.11" - "@biomejs/cli-win32-arm64" "2.3.11" - "@biomejs/cli-win32-x64" "2.3.11" - -"@biomejs/cli-darwin-arm64@2.3.11": - version "2.3.11" - resolved "https://registry.yarnpkg.com/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.3.11.tgz#3aa71d119e7216d66620282121673b95257fcc35" - integrity sha512-/uXXkBcPKVQY7rc9Ys2CrlirBJYbpESEDme7RKiBD6MmqR2w3j0+ZZXRIL2xiaNPsIMMNhP1YnA+jRRxoOAFrA== - -"@biomejs/cli-darwin-x64@2.3.11": - version "2.3.11" - resolved "https://registry.yarnpkg.com/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.3.11.tgz#3003d7c24e4fbba9693fba98692c6386884fedcf" - integrity sha512-fh7nnvbweDPm2xEmFjfmq7zSUiox88plgdHF9OIW4i99WnXrAC3o2P3ag9judoUMv8FCSUnlwJCM1B64nO5Fbg== - -"@biomejs/cli-linux-arm64-musl@2.3.11": - version "2.3.11" - resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.3.11.tgz#a17fb411ba8146ba60f3592857b7c49c8f0fe936" - integrity sha512-XPSQ+XIPZMLaZ6zveQdwNjbX+QdROEd1zPgMwD47zvHV+tCGB88VH+aynyGxAHdzL+Tm/+DtKST5SECs4iwCLg== - -"@biomejs/cli-linux-arm64@2.3.11": - version "2.3.11" - resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.3.11.tgz#643830d1e53071d594e16b919e88a46fbc5c0b55" - integrity sha512-l4xkGa9E7Uc0/05qU2lMYfN1H+fzzkHgaJoy98wO+b/7Gl78srbCRRgwYSW+BTLixTBrM6Ede5NSBwt7rd/i6g== - -"@biomejs/cli-linux-x64-musl@2.3.11": - version "2.3.11" - resolved "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.3.11.tgz" - integrity sha512-vU7a8wLs5C9yJ4CB8a44r12aXYb8yYgBn+WeyzbMjaCMklzCv1oXr8x+VEyWodgJt9bDmhiaW/I0RHbn7rsNmw== - -"@biomejs/cli-linux-x64@2.3.11": - version "2.3.11" - resolved "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.3.11.tgz" - integrity sha512-/1s9V/H3cSe0r0Mv/Z8JryF5x9ywRxywomqZVLHAoa/uN0eY7F8gEngWKNS5vbbN/BsfpCG5yeBT5ENh50Frxg== - -"@biomejs/cli-win32-arm64@2.3.11": - version "2.3.11" - resolved "https://registry.yarnpkg.com/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.3.11.tgz#6769907655cd06938b00d040c6576c2fdf6c34a2" - integrity sha512-PZQ6ElCOnkYapSsysiTy0+fYX+agXPlWugh6+eQ6uPKI3vKAqNp6TnMhoM3oY2NltSB89hz59o8xIfOdyhi9Iw== - -"@biomejs/cli-win32-x64@2.3.11": - version "2.3.11" - resolved "https://registry.yarnpkg.com/@biomejs/cli-win32-x64/-/cli-win32-x64-2.3.11.tgz#71ba2fb5505b3b01dd3cf551ef329e0094636125" - integrity sha512-43VrG813EW+b5+YbDbz31uUsheX+qFKCpXeY9kfdAx+ww3naKxeVkTD9zLIWxUPfJquANMHrmW3wbe/037G0Qg== - -"@commitlint/cli@^20.3.1": - version "20.3.1" - resolved "https://registry.npmjs.org/@commitlint/cli/-/cli-20.3.1.tgz" - integrity sha512-NtInjSlyev/+SLPvx/ulz8hRE25Wf5S9dLNDcIwazq0JyB4/w1ROF/5nV0ObPTX8YpRaKYeKtXDYWqumBNHWsw== - dependencies: - "@commitlint/format" "^20.3.1" - "@commitlint/lint" "^20.3.1" - "@commitlint/load" "^20.3.1" - "@commitlint/read" "^20.3.1" - "@commitlint/types" "^20.3.1" + "@biomejs/cli-darwin-arm64" "2.5.11" + "@biomejs/cli-darwin-x64" "2.5.11" + "@biomejs/cli-linux-arm64" "2.5.11" + "@biomejs/cli-linux-arm64-musl" "2.5.11" + "@biomejs/cli-linux-x64" "2.5.11" + "@biomejs/cli-linux-x64-musl" "2.5.11" + "@biomejs/cli-win32-arm64" "2.5.11" + "@biomejs/cli-win32-x64" "2.5.11" + +"@biomejs/cli-darwin-arm64@2.5.11": + version "2.5.11" + resolved "https://registry.yarnpkg.com/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.5.11.tgz#2b8af359fc43f10c81b88621884b25ec83130054" + integrity sha512-6SGZxoKbXvUjMn1t6A98HqWISPnGNbYs0R/Rt2JarmXBSev+lva4QxUMWEBX9lX1Wo1XTJ78uk5xVDtG58SRZg== + +"@biomejs/cli-darwin-x64@2.5.11": + version "2.5.11" + resolved "https://registry.yarnpkg.com/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.5.11.tgz#4aedca314957186b0d9f9e93afcff95d1e142523" + integrity sha512-nYkXY7tLBEgnGbYapDKAyKzgt44ZEyG+AKalvTXtCWKYgepI9dw327q+cVgedxm+Udi1ZzHKUyZrIusHi/KQbw== + +"@biomejs/cli-linux-arm64-musl@2.5.11": + version "2.5.11" + resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.5.11.tgz#2938a40986262431d2d263c9ebd091ca90af6e05" + integrity sha512-qhyZUMyCbWYFV2bAwRNVvfMVZ+hv7WYl6mossGrxC+uiQQXhvsuWWU8zz6jYX0mChZd9MgQZbm4vozTmG/5iGw== + +"@biomejs/cli-linux-arm64@2.5.11": + version "2.5.11" + resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.5.11.tgz#ab7b951f313050c047294441333c7dbe2f9b622d" + integrity sha512-3PVLSTD9RR73rvVPt5G3T1gc+ycggWEGfTD7RvzzbtcDPD27NxgxBbAFfpm7DXJKW6VLHWE1lLMGvFt2Qxjcow== + +"@biomejs/cli-linux-x64-musl@2.5.11": + version "2.5.11" + resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.5.11.tgz#1c6b4eee8bba5d19fde0a9edecc3c865aa15e5c7" + integrity sha512-oRRlrchG5EfrEL/EmtT1qUjSNHk3/5LGeZhQqADBBAJF1b1ET6964xEKe7aGlGARzDfza8H/seEsFJl7S6Ql9w== + +"@biomejs/cli-linux-x64@2.5.11": + version "2.5.11" + resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-x64/-/cli-linux-x64-2.5.11.tgz#dc9e6824e38a34d9a95383e31d59a4f23ef60181" + integrity sha512-JOytptlsgM33B2MMFUg8iBrb4IKpbD5JnJrSeYiaFEeAj4vuXx0iQSQZ4qK7sqyMtfjZxxPdNdMZZVL4y/mFyA== + +"@biomejs/cli-win32-arm64@2.5.11": + version "2.5.11" + resolved "https://registry.yarnpkg.com/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.5.11.tgz#7c9fabc7d27c3794122fa499dcc944a232a96edb" + integrity sha512-e49E6K9hzH/ohJNx8Y26mY8HaV4I4ZViIeoqhKsmoXLKHhQnMeBAVqCgsGf2Wa3lXlS7RkporDXMHHWkzvZzFw== + +"@biomejs/cli-win32-x64@2.5.11": + version "2.5.11" + resolved "https://registry.yarnpkg.com/@biomejs/cli-win32-x64/-/cli-win32-x64-2.5.11.tgz#0040003958f3ba5d6a2558011ca02bdb608821f5" + integrity sha512-QSQr/KjOgXA7OzXJUWS+oguKyAZ3Q0l/lnlDGbu397eKo83atuWUjBPJrsqbKNF6CARGw8XXJLGzpHC8Ryhd4Q== + +"@commitlint/cli@^20.5.3": + version "20.5.3" + resolved "https://registry.yarnpkg.com/@commitlint/cli/-/cli-20.5.3.tgz#6b4bfcf19fc149b0a357725f551bad11a930f145" + integrity sha512-OJdL0EXWD5y9LPa0nr/geOwzaS8BsdaybKkcloB0JgsguGxNv2R+hC2FTPqrAcprg35zF33KOQerY0x8W1aesA== + dependencies: + "@commitlint/format" "^20.5.0" + "@commitlint/lint" "^20.5.3" + "@commitlint/load" "^20.5.3" + "@commitlint/read" "^20.5.0" + "@commitlint/types" "^20.5.0" tinyexec "^1.0.0" yargs "^17.0.0" -"@commitlint/config-conventional@^20.3.1": - version "20.3.1" - resolved "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-20.3.1.tgz" - integrity sha512-NCzwvxepstBZbmVXsvg49s+shCxlJDJPWxXqONVcAtJH9wWrOlkMQw/zyl+dJmt8lyVopt5mwQ3mR5M2N2rUWg== +"@commitlint/config-conventional@^20.5.3": + version "20.5.3" + resolved "https://registry.yarnpkg.com/@commitlint/config-conventional/-/config-conventional-20.5.3.tgz#ccf17ee4c695cd4b73784c6ce7fdb5b20d22be99" + integrity sha512-j34Qqeaa152chJgz2ysyk0BCpHenJn1lV0Rx0VXf8k3ccQcED+48EZrzMvo9jLmJUyBrrBwvu89I+2er4gW7QQ== dependencies: - "@commitlint/types" "^20.3.1" - conventional-changelog-conventionalcommits "^7.0.2" + "@commitlint/types" "^20.5.0" + conventional-changelog-conventionalcommits "^9.2.0" "@commitlint/config-validator@^19.0.3": version "19.0.3" @@ -146,25 +147,21 @@ "@commitlint/types" "^19.0.3" ajv "^8.11.0" -"@commitlint/config-validator@^20.3.1": - version "20.3.1" - resolved "https://registry.npmjs.org/@commitlint/config-validator/-/config-validator-20.3.1.tgz" - integrity sha512-ErVLC/IsHhcvxCyh+FXo7jy12/nkQySjWXYgCoQbZLkFp4hysov8KS6CdxBB0cWjbZWjvNOKBMNoUVqkmGmahw== +"@commitlint/config-validator@^20.5.0": + version "20.5.0" + resolved "https://registry.yarnpkg.com/@commitlint/config-validator/-/config-validator-20.5.0.tgz#b0aad5fdd520b07ac52f9ad8d41844629e5fd40e" + integrity sha512-T/Uh6iJUzyx7j35GmHWdIiGRQB+ouZDk0pwAaYq4SXgB54KZhFdJ0vYmxiW6AMYICTIWuyMxDBl1jK74oFp/Gw== dependencies: - "@commitlint/types" "^20.3.1" + "@commitlint/types" "^20.5.0" ajv "^8.11.0" -"@commitlint/ensure@^20.3.1": - version "20.3.1" - resolved "https://registry.npmjs.org/@commitlint/ensure/-/ensure-20.3.1.tgz" - integrity sha512-h664FngOEd7bHAm0j8MEKq+qm2mH+V+hwJiIE2bWcw3pzJMlO0TPKtk0ATyRAtV6jQw+xviRYiIjjSjfajiB5w== +"@commitlint/ensure@^20.5.3": + version "20.5.3" + resolved "https://registry.yarnpkg.com/@commitlint/ensure/-/ensure-20.5.3.tgz#6a63f52dad60a2db9b87ea1b11c8d483ced24aa1" + integrity sha512-4i4AgNvH62owG9MwSiWKrle7HGNpBHHdLnWFIp5fTsHUYe5kRuh15t08L/0pdbbrRk8JKXQxxN4hZQcn+szkrw== dependencies: - "@commitlint/types" "^20.3.1" - lodash.camelcase "^4.3.0" - lodash.kebabcase "^4.1.1" - lodash.snakecase "^4.1.1" - lodash.startcase "^4.4.0" - lodash.upperfirst "^4.3.1" + "@commitlint/types" "^20.5.0" + es-toolkit "^1.46.0" "@commitlint/execute-rule@^19.0.0": version "19.0.0" @@ -176,31 +173,31 @@ resolved "https://registry.npmjs.org/@commitlint/execute-rule/-/execute-rule-20.0.0.tgz" integrity sha512-xyCoOShoPuPL44gVa+5EdZsBVao/pNzpQhkzq3RdtlFdKZtjWcLlUFQHSWBuhk5utKYykeJPSz2i8ABHQA+ZZw== -"@commitlint/format@^20.3.1": - version "20.3.1" - resolved "https://registry.npmjs.org/@commitlint/format/-/format-20.3.1.tgz" - integrity sha512-jfsjGPFTd2Yti2YHwUH4SPRPbWKAJAwrfa3eNa9bXEdrXBb9mCwbIrgYX38LdEJK9zLJ3AsLBP4/FLEtxyu2AA== +"@commitlint/format@^20.5.0": + version "20.5.0" + resolved "https://registry.yarnpkg.com/@commitlint/format/-/format-20.5.0.tgz#cd73c527ec60e70fc7ae45c1ce31a4f143c49202" + integrity sha512-TI9EwFU/qZWSK7a5qyXMpKPPv3qta7FO4tKW+Wt2al7sgMbLWTsAcDpX1cU8k16TRdsiiet9aOw0zpvRXNJu7Q== dependencies: - "@commitlint/types" "^20.3.1" - chalk "^5.3.0" + "@commitlint/types" "^20.5.0" + picocolors "^1.1.1" -"@commitlint/is-ignored@^20.3.1": - version "20.3.1" - resolved "https://registry.npmjs.org/@commitlint/is-ignored/-/is-ignored-20.3.1.tgz" - integrity sha512-tWwAoh93QvAhxgp99CzCuHD86MgxE4NBtloKX+XxQxhfhSwHo7eloiar/yzx53YW9eqSLP95zgW2KDDk4/WX+A== +"@commitlint/is-ignored@^20.5.0": + version "20.5.0" + resolved "https://registry.yarnpkg.com/@commitlint/is-ignored/-/is-ignored-20.5.0.tgz#c90ac785b0673c4aa4e6bb1e3a15fcfb399727b0" + integrity sha512-JWLarAsurHJhPozbuAH6GbP4p/hdOCoqS9zJMfqwswne+/GPs5V0+rrsfOkP68Y8PSLphwtFXV0EzJ+GTXTTGg== dependencies: - "@commitlint/types" "^20.3.1" + "@commitlint/types" "^20.5.0" semver "^7.6.0" -"@commitlint/lint@^20.3.1": - version "20.3.1" - resolved "https://registry.npmjs.org/@commitlint/lint/-/lint-20.3.1.tgz" - integrity sha512-LaOtrQ24+6SfUaWg8A+a+Wc77bvLbO5RIr6iy9F7CI3/0iq1uPEWgGRCwqWTuLGHkZDAcwaq0gZ01zpwZ1jCGw== +"@commitlint/lint@^20.5.3": + version "20.5.3" + resolved "https://registry.yarnpkg.com/@commitlint/lint/-/lint-20.5.3.tgz#0848685798ea70045a1958dd3a758c01225e6cb4" + integrity sha512-M7JbWBNr2gXKaPc4i/KipsuW1gkDHpj35KPjWtKy3Z+2AQw5wu1gBi1LIO0uoaij67CqY4K8PxPZSGens4evCw== dependencies: - "@commitlint/is-ignored" "^20.3.1" - "@commitlint/parse" "^20.3.1" - "@commitlint/rules" "^20.3.1" - "@commitlint/types" "^20.3.1" + "@commitlint/is-ignored" "^20.5.0" + "@commitlint/parse" "^20.5.0" + "@commitlint/rules" "^20.5.3" + "@commitlint/types" "^20.5.0" "@commitlint/load@>6.1.1": version "19.2.0" @@ -218,44 +215,43 @@ lodash.merge "^4.6.2" lodash.uniq "^4.5.0" -"@commitlint/load@^20.3.1": - version "20.3.1" - resolved "https://registry.npmjs.org/@commitlint/load/-/load-20.3.1.tgz" - integrity sha512-YDD9XA2XhgYgbjju8itZ/weIvOOobApDqwlPYCX5NLO/cPtw2UMO5Cmn44Ks8RQULUVI5fUT6roKvyxcoLbNmw== +"@commitlint/load@^20.5.3": + version "20.5.3" + resolved "https://registry.yarnpkg.com/@commitlint/load/-/load-20.5.3.tgz#06eabcb21941b70c3e5d6ea5c7fb64f7a2b27935" + integrity sha512-1FDZWuKyu98Myb8i7Tp31jPU2rZpOwAdYRyJcy2KoGg7Xk2A+bgHN8smhMaaNSNkmE8fwt53BokywZq8Gv/5XQ== dependencies: - "@commitlint/config-validator" "^20.3.1" + "@commitlint/config-validator" "^20.5.0" "@commitlint/execute-rule" "^20.0.0" - "@commitlint/resolve-extends" "^20.3.1" - "@commitlint/types" "^20.3.1" - chalk "^5.3.0" - cosmiconfig "^9.0.0" + "@commitlint/resolve-extends" "^20.5.3" + "@commitlint/types" "^20.5.0" + cosmiconfig "^9.0.1" cosmiconfig-typescript-loader "^6.1.0" - lodash.isplainobject "^4.0.6" - lodash.merge "^4.6.2" - lodash.uniq "^4.5.0" + es-toolkit "^1.46.0" + is-plain-obj "^4.1.0" + picocolors "^1.1.1" -"@commitlint/message@^20.0.0": - version "20.0.0" - resolved "https://registry.npmjs.org/@commitlint/message/-/message-20.0.0.tgz" - integrity sha512-gLX4YmKnZqSwkmSB9OckQUrI5VyXEYiv3J5JKZRxIp8jOQsWjZgHSG/OgEfMQBK9ibdclEdAyIPYggwXoFGXjQ== - -"@commitlint/parse@^20.3.1": - version "20.3.1" - resolved "https://registry.npmjs.org/@commitlint/parse/-/parse-20.3.1.tgz" - integrity sha512-TuUTdbLpyUNLgDzLDYlI2BeTE6V/COZbf3f8WwsV0K6eq/2nSpNTMw7wHtXb+YxeY9wwxBp/Ldad4P+YIxHJoA== - dependencies: - "@commitlint/types" "^20.3.1" - conventional-changelog-angular "^7.0.0" - conventional-commits-parser "^5.0.0" - -"@commitlint/read@^20.3.1": - version "20.3.1" - resolved "https://registry.npmjs.org/@commitlint/read/-/read-20.3.1.tgz" - integrity sha512-nCmJAdIg3OdNVUpQW0Idk/eF/vfOo2W2xzmvRmNeptLrzFK7qhwwl/kIwy1Q1LZrKHUFNj7PGNpIT5INbgZWzA== - dependencies: - "@commitlint/top-level" "^20.0.0" - "@commitlint/types" "^20.3.1" - git-raw-commits "^4.0.0" +"@commitlint/message@^20.4.3": + version "20.4.3" + resolved "https://registry.yarnpkg.com/@commitlint/message/-/message-20.4.3.tgz#0c377fbbe1c72487612330a01d1e7dea991fa467" + integrity sha512-6akwCYrzcrFcTYz9GyUaWlhisY4lmQ3KvrnabmhoeAV8nRH4dXJAh4+EUQ3uArtxxKQkvxJS78hNX2EU3USgxQ== + +"@commitlint/parse@^20.5.0": + version "20.5.0" + resolved "https://registry.yarnpkg.com/@commitlint/parse/-/parse-20.5.0.tgz#196b75a7b870aa7cda311fac525f10fb6ccacd54" + integrity sha512-SeKWHBMk7YOTnnEWUhx+d1a9vHsjjuo6Uo1xRfPNfeY4bdYFasCH1dDpAv13Lyn+dDPOels+jP6D2GRZqzc5fA== + dependencies: + "@commitlint/types" "^20.5.0" + conventional-changelog-angular "^8.2.0" + conventional-commits-parser "^6.3.0" + +"@commitlint/read@^20.5.0": + version "20.5.0" + resolved "https://registry.yarnpkg.com/@commitlint/read/-/read-20.5.0.tgz#59d4b0e98429d308be6ad0d3e5144a193638ee6b" + integrity sha512-JDEIJ2+GnWpK8QqwfmW7O42h0aycJEWNqcdkJnyzLD11nf9dW2dWLTVEa8Wtlo4IZFGLPATjR5neA5QlOvIH1w== + dependencies: + "@commitlint/top-level" "^20.4.3" + "@commitlint/types" "^20.5.0" + git-raw-commits "^5.0.0" minimist "^1.2.8" tinyexec "^1.0.0" @@ -271,39 +267,39 @@ lodash.mergewith "^4.6.2" resolve-from "^5.0.0" -"@commitlint/resolve-extends@^20.3.1": - version "20.3.1" - resolved "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-20.3.1.tgz" - integrity sha512-iGTGeyaoDyHDEZNjD8rKeosjSNs8zYanmuowY4ful7kFI0dnY4b5QilVYaFQJ6IM27S57LAeH5sKSsOHy4bw5w== +"@commitlint/resolve-extends@^20.5.3": + version "20.5.3" + resolved "https://registry.yarnpkg.com/@commitlint/resolve-extends/-/resolve-extends-20.5.3.tgz#3c46fa1fa243f12f1cb0a4ffc47b38351abd9c50" + integrity sha512-+ogW9v/u9JqpvAgTrLra/YTFo0KkjU6iNblF89pPsj4NebNc+DAWctsludwezI8YnsjBmfHpApSwcXprN/f/ew== dependencies: - "@commitlint/config-validator" "^20.3.1" - "@commitlint/types" "^20.3.1" - global-directory "^4.0.1" + "@commitlint/config-validator" "^20.5.0" + "@commitlint/types" "^20.5.0" + es-toolkit "^1.46.0" + global-directory "^5.0.0" import-meta-resolve "^4.0.0" - lodash.mergewith "^4.6.2" resolve-from "^5.0.0" -"@commitlint/rules@^20.3.1": - version "20.3.1" - resolved "https://registry.npmjs.org/@commitlint/rules/-/rules-20.3.1.tgz" - integrity sha512-/uic4P+4jVNpqQxz02+Y6vvIC0A2J899DBztA1j6q3f3MOKwydlNrojSh0dQmGDxxT1bXByiRtDhgFnOFnM6Pg== +"@commitlint/rules@^20.5.3": + version "20.5.3" + resolved "https://registry.yarnpkg.com/@commitlint/rules/-/rules-20.5.3.tgz#95151b38e30b35ccf13d6d091e4cc61cd363e3ec" + integrity sha512-MPlMnb9D3wbszYMp+1hPtuhtPJndRo6I6yfkZVA4+jR8w7Kqp0u2u/Y+gzbaItx5Lltq5rw7FSZQWJMoXUC4NQ== dependencies: - "@commitlint/ensure" "^20.3.1" - "@commitlint/message" "^20.0.0" + "@commitlint/ensure" "^20.5.3" + "@commitlint/message" "^20.4.3" "@commitlint/to-lines" "^20.0.0" - "@commitlint/types" "^20.3.1" + "@commitlint/types" "^20.5.0" "@commitlint/to-lines@^20.0.0": version "20.0.0" resolved "https://registry.npmjs.org/@commitlint/to-lines/-/to-lines-20.0.0.tgz" integrity sha512-2l9gmwiCRqZNWgV+pX1X7z4yP0b3ex/86UmUFgoRt672Ez6cAM2lOQeHFRUTuE6sPpi8XBCGnd8Kh3bMoyHwJw== -"@commitlint/top-level@^20.0.0": - version "20.0.0" - resolved "https://registry.npmjs.org/@commitlint/top-level/-/top-level-20.0.0.tgz" - integrity sha512-drXaPSP2EcopukrUXvUXmsQMu3Ey/FuJDc/5oiW4heoCfoE5BdLQyuc7veGeE3aoQaTVqZnh4D5WTWe2vefYKg== +"@commitlint/top-level@^20.4.3": + version "20.4.3" + resolved "https://registry.yarnpkg.com/@commitlint/top-level/-/top-level-20.4.3.tgz#6f94e558c1aa6ba3a1d6962e9adaa34f657efe81" + integrity sha512-qD9xfP6dFg5jQ3NMrOhG0/w5y3bBUsVGyJvXxdWEwBm8hyx4WOk3kKXw28T5czBYvyeCVJgJJ6aoJZUWDpaacQ== dependencies: - find-up "^7.0.0" + escalade "^3.2.0" "@commitlint/types@^19.0.3": version "19.0.3" @@ -313,144 +309,283 @@ "@types/conventional-commits-parser" "^5.0.0" chalk "^5.3.0" -"@commitlint/types@^20.3.1": - version "20.3.1" - resolved "https://registry.npmjs.org/@commitlint/types/-/types-20.3.1.tgz" - integrity sha512-VmIFV/JkBRhDRRv7N5B7zEUkNZIx9Mp+8Pe65erz0rKycXLsi8Epcw0XJ+btSeRXgTzE7DyOyA9bkJ9mn/yqVQ== +"@commitlint/types@^20.5.0": + version "20.5.0" + resolved "https://registry.yarnpkg.com/@commitlint/types/-/types-20.5.0.tgz#65be36ca38183757563bd69451dbe465a5d001ef" + integrity sha512-ZJoS8oSq2CAZEpc/YI9SulLrdiIyXeHb/OGqGrkUP6Q7YV+0ouNAa7GjqRdXeQPncHQIDz/jbCTlHScvYvO/gA== dependencies: - "@types/conventional-commits-parser" "^5.0.0" - chalk "^5.3.0" + conventional-commits-parser "^6.3.0" + picocolors "^1.1.1" + +"@conventional-changelog/git-client@^2.6.0": + version "2.7.0" + resolved "https://registry.yarnpkg.com/@conventional-changelog/git-client/-/git-client-2.7.0.tgz#07ea8202fd822e71d32c54aaed08b2c5ae9cc7c2" + integrity sha512-j7A8/LBEQ+3rugMzPXoKYzyUPpw/0CBQCyvtTR7Lmu4olG4yRC/Tfkq79Mr3yuPs0SUitlO2HwGP3gitMJnRFw== + dependencies: + "@simple-libs/child-process-utils" "^1.0.0" + "@simple-libs/stream-utils" "^1.2.0" + semver "^7.5.2" "@esbuild/aix-ppc64@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz#521cbd968dcf362094034947f76fa1b18d2d403c" integrity sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw== +"@esbuild/aix-ppc64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz#bf6e10303bcf2e7c686975fa52f937ec2728d8bc" + integrity sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ== + "@esbuild/android-arm64@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz#61ea550962d8aa12a9b33194394e007657a6df57" integrity sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA== +"@esbuild/android-arm64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz#0c6246bc8d2c4d172aac2db3fb1190d72bd65504" + integrity sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A== + "@esbuild/android-arm@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.27.2.tgz#554887821e009dd6d853f972fde6c5143f1de142" integrity sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA== +"@esbuild/android-arm@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.28.2.tgz#2d84ece6a4e2684d92be26ee13d42757d831c381" + integrity sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg== + "@esbuild/android-x64@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.27.2.tgz#a7ce9d0721825fc578f9292a76d9e53334480ba2" integrity sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A== +"@esbuild/android-x64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.28.2.tgz#fc38d4d6358d8dc1cf53f09f7589fe436eb64801" + integrity sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q== + "@esbuild/darwin-arm64@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz#2cb7659bd5d109803c593cfc414450d5430c8256" integrity sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg== +"@esbuild/darwin-arm64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz#f83afeeac1d7dac01c7a2fd012b3e451a0591fcc" + integrity sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw== + "@esbuild/darwin-x64@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz#e741fa6b1abb0cd0364126ba34ca17fd5e7bf509" integrity sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA== +"@esbuild/darwin-x64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz#510147c055a795588dbbe14fd6b1b8ad0a2f30de" + integrity sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw== + "@esbuild/freebsd-arm64@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz#2b64e7116865ca172d4ce034114c21f3c93e397c" integrity sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g== +"@esbuild/freebsd-arm64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz#093b9200ecf0b115ba4e5e248a7485c9c5f8bd5e" + integrity sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw== + "@esbuild/freebsd-x64@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz#e5252551e66f499e4934efb611812f3820e990bb" integrity sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA== +"@esbuild/freebsd-x64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz#0be22b6df925d213e841ea87123af5df80b0faf7" + integrity sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg== + "@esbuild/linux-arm64@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz#dc4acf235531cd6984f5d6c3b13dbfb7ddb303cb" integrity sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw== +"@esbuild/linux-arm64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz#1bdbc651cda9ba9995c53ed9c71ceaa65094762d" + integrity sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug== + "@esbuild/linux-arm@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz#56a900e39240d7d5d1d273bc053daa295c92e322" integrity sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw== +"@esbuild/linux-arm@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz#beb12ad72b84f72d28488cc1b8ee9f7eb141d753" + integrity sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w== + "@esbuild/linux-ia32@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz#d4a36d473360f6870efcd19d52bbfff59a2ed1cc" integrity sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w== +"@esbuild/linux-ia32@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz#b81f9d55529b45c206a46a138214b1aa6879696b" + integrity sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ== + "@esbuild/linux-loong64@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz#fcf0ab8c3eaaf45891d0195d4961cb18b579716a" integrity sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg== +"@esbuild/linux-loong64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz#598667241a04c99b76ed6ef940ac50038c419f98" + integrity sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ== + "@esbuild/linux-mips64el@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz#598b67d34048bb7ee1901cb12e2a0a434c381c10" integrity sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw== +"@esbuild/linux-mips64el@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz#1c51eb9cea903f53d97b5af3b1841db70f5596ca" + integrity sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA== + "@esbuild/linux-ppc64@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz#3846c5df6b2016dab9bc95dde26c40f11e43b4c0" integrity sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ== +"@esbuild/linux-ppc64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz#63dd61f17ceb31a81227f413feac8a71bc2c51f2" + integrity sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ== + "@esbuild/linux-riscv64@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz#173d4475b37c8d2c3e1707e068c174bb3f53d07d" integrity sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA== +"@esbuild/linux-riscv64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz#3763b08fde5cf25ab1facb8e7752edfe45fbfc27" + integrity sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA== + "@esbuild/linux-s390x@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz#f7a4790105edcab8a5a31df26fbfac1aa3dacfab" integrity sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w== +"@esbuild/linux-s390x@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz#1a137ff293a82906eb3176385bd7e8e0e5cfb7cb" + integrity sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg== + "@esbuild/linux-x64@0.27.2": version "0.27.2" resolved "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz" integrity sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA== +"@esbuild/linux-x64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz#268b36211c146ca54f8fe12c578a8d6ef8979485" + integrity sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ== + "@esbuild/netbsd-arm64@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz#e2863c2cd1501845995cb11adf26f7fe4be527b0" integrity sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw== +"@esbuild/netbsd-arm64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz#22571ad951d62bb6accc82d8d1fad5c8c1ac0ba1" + integrity sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw== + "@esbuild/netbsd-x64@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz#93f7609e2885d1c0b5a1417885fba8d1fcc41272" integrity sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA== +"@esbuild/netbsd-x64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz#42fcc57297eb0a0ca3f5fc475291f4c1a3f7c0de" + integrity sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw== + "@esbuild/openbsd-arm64@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz#a1985604a203cdc325fd47542e106fafd698f02e" integrity sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA== +"@esbuild/openbsd-arm64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz#9eb32af104ac3dacf4edca01f596664aab0c73ef" + integrity sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ== + "@esbuild/openbsd-x64@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz#8209e46c42f1ffbe6e4ef77a32e1f47d404ad42a" integrity sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg== +"@esbuild/openbsd-x64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz#febed2402d6088225e91f20fb4ce2522ad0a4efd" + integrity sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw== + "@esbuild/openharmony-arm64@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz#8fade4441893d9cc44cbd7dcf3776f508ab6fb2f" integrity sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag== +"@esbuild/openharmony-arm64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz#85641c3d466428bfbccea5f21c26836663fef5ce" + integrity sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q== + "@esbuild/sunos-x64@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz#980d4b9703a16f0f07016632424fc6d9a789dfc2" integrity sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg== +"@esbuild/sunos-x64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz#a736f9d8962481045fc4c3e54f5479f22c870fb4" + integrity sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g== + "@esbuild/win32-arm64@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz#1c09a3633c949ead3d808ba37276883e71f6111a" integrity sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg== +"@esbuild/win32-arm64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz#ee5ab40fad186201b652a33f8a5eb149e9e42532" + integrity sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ== + "@esbuild/win32-ia32@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz#1b1e3a63ad4bef82200fef4e369e0fff7009eee5" integrity sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ== +"@esbuild/win32-ia32@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz#c40d28a6d99a127da6711f2afd74b11cb63b06a7" + integrity sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA== + "@esbuild/win32-x64@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz#9e585ab6086bef994c6e8a5b3a0481219ada862b" integrity sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ== +"@esbuild/win32-x64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz#b21affb804cc167c133d95f45b3a1dc1323b9a87" + integrity sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g== + "@fastify/ajv-compiler@^1.0.0": version "1.1.0" resolved "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-1.1.0.tgz" @@ -735,150 +870,152 @@ resolved "https://registry.npmjs.org/@hutson/parse-repository-url/-/parse-repository-url-3.0.2.tgz" integrity sha512-H9XAx3hc0BQHY6l+IFSWHDySypcXsvsuLhgYLUGywmJ5pswRVQJUHpOsobnLYp2ZUaUlKiKDrgWWhosOwAEM8Q== -"@inquirer/ansi@^1.0.2": - version "1.0.2" - resolved "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz" - integrity sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ== +"@inquirer/ansi@^2.0.7": + version "2.0.7" + resolved "https://registry.yarnpkg.com/@inquirer/ansi/-/ansi-2.0.7.tgz#86de22810cac3ed406ec10f8d66016815b8226b4" + integrity sha512-3eTuUO1vH2cZm2ZKHeQxnOqlTi9EfZDGgIe3BL3I4u+rJHocr9Fz86M4fjYABPvFnQG/gGK551HqDiIcETwU6Q== -"@inquirer/checkbox@^4.3.2": - version "4.3.2" - resolved "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.2.tgz" - integrity sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA== - dependencies: - "@inquirer/ansi" "^1.0.2" - "@inquirer/core" "^10.3.2" - "@inquirer/figures" "^1.0.15" - "@inquirer/type" "^3.0.10" - yoctocolors-cjs "^2.1.3" - -"@inquirer/confirm@^5.1.21": - version "5.1.21" - resolved "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz" - integrity sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ== - dependencies: - "@inquirer/core" "^10.3.2" - "@inquirer/type" "^3.0.10" - -"@inquirer/core@^10.3.2": - version "10.3.2" - resolved "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz" - integrity sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A== - dependencies: - "@inquirer/ansi" "^1.0.2" - "@inquirer/figures" "^1.0.15" - "@inquirer/type" "^3.0.10" +"@inquirer/checkbox@^5.2.3": + version "5.2.3" + resolved "https://registry.yarnpkg.com/@inquirer/checkbox/-/checkbox-5.2.3.tgz#3c2b2199c3e3d884a843a2c1280b253f958d5f8a" + integrity sha512-XEYX2WA8SBkLPczL6/yXPHLPCvDoptmh9v56Cy05BSV1Smk1vWy19bTC4qJBuIffw7+6l4CcaYYzGqG60RfW1g== + dependencies: + "@inquirer/ansi" "^2.0.7" + "@inquirer/core" "^12.0.1" + "@inquirer/figures" "^2.0.8" + "@inquirer/type" "^4.1.0" + +"@inquirer/confirm@^6.3.0": + version "6.3.0" + resolved "https://registry.yarnpkg.com/@inquirer/confirm/-/confirm-6.3.0.tgz#5c32d1bba3a0551dcbddc52628a22daef3f5a8cd" + integrity sha512-pZHXJImFtERmSNMBHcjwuz8Ck5vEFEYNUZnwbb8aJpjHv/TwGuFErNxF2Hp8+V+pNJs2EYPMlyWscvFEqO9jOQ== + dependencies: + "@inquirer/core" "^12.0.1" + "@inquirer/type" "^4.1.0" + +"@inquirer/core@^12.0.1": + version "12.0.1" + resolved "https://registry.yarnpkg.com/@inquirer/core/-/core-12.0.1.tgz#a59c69f48c9a67d60ac821b6f12e667cb116ffb4" + integrity sha512-JMD5Jy/ScL5TZE18m83Nw25HjqGFLoWXwnEkW7IdwwAhZpB9Bus55/WU7zn3UqR1MOCjjTQOIYpiD4vjWA3LPw== + dependencies: + "@inquirer/ansi" "^2.0.7" + "@inquirer/figures" "^2.0.8" + "@inquirer/type" "^4.1.0" cli-width "^4.1.0" - mute-stream "^2.0.0" + fast-wrap-ansi "^0.2.0" + mute-stream "^3.0.0" signal-exit "^4.1.0" - wrap-ansi "^6.2.0" - yoctocolors-cjs "^2.1.3" -"@inquirer/editor@^4.2.23": - version "4.2.23" - resolved "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.23.tgz" - integrity sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ== +"@inquirer/editor@^5.3.1": + version "5.3.1" + resolved "https://registry.yarnpkg.com/@inquirer/editor/-/editor-5.3.1.tgz#a035364e93ecd03753129813b99f4961650844a2" + integrity sha512-y43COoyVUjPWIobn2Qep/uI1drPS78aaZZZ9kVi94Tyu/GuW2N8d8Q4rifJXGAXCEAXCPTTMjD8gC1HyvM5ukA== dependencies: - "@inquirer/core" "^10.3.2" - "@inquirer/external-editor" "^1.0.3" - "@inquirer/type" "^3.0.10" + "@inquirer/core" "^12.0.1" + "@inquirer/external-editor" "^3.0.4" + "@inquirer/type" "^4.1.0" -"@inquirer/expand@^4.0.23": - version "4.0.23" - resolved "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.23.tgz" - integrity sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew== +"@inquirer/expand@^5.1.3": + version "5.1.3" + resolved "https://registry.yarnpkg.com/@inquirer/expand/-/expand-5.1.3.tgz#f587260168acb6733643665882195bbe4d2e3dc3" + integrity sha512-3NQJiXNJ/aj9wiAsr7pECdp5Qe9J0X9YUJCKsaFXS+ddOxfL6J4AIl3w3T4Gq3kK0WsQY5GMoDokK5X94m6lHw== dependencies: - "@inquirer/core" "^10.3.2" - "@inquirer/type" "^3.0.10" - yoctocolors-cjs "^2.1.3" + "@inquirer/core" "^12.0.1" + "@inquirer/type" "^4.1.0" -"@inquirer/external-editor@^1.0.3": +"@inquirer/external-editor@^1.0.0": version "1.0.3" - resolved "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz" + resolved "https://registry.yarnpkg.com/@inquirer/external-editor/-/external-editor-1.0.3.tgz#c23988291ee676290fdab3fd306e64010a6d13b8" integrity sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA== dependencies: chardet "^2.1.1" iconv-lite "^0.7.0" -"@inquirer/figures@^1.0.15": - version "1.0.15" - resolved "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz" - integrity sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g== +"@inquirer/external-editor@^3.0.4": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@inquirer/external-editor/-/external-editor-3.0.4.tgz#6f61cfbdcc578530e6d322abe30921eba3e4e73a" + integrity sha512-tZbbaK2ovq6vlrRBNQvjrypmrED/p5x2ncIHQ79cD55tei3dD96v5glMMA+6tiq7K104i/25DVYKWVPJuV6ptA== + dependencies: + chardet "^2.1.1" + iconv-lite "^0.7.2" -"@inquirer/input@^4.3.1": - version "4.3.1" - resolved "https://registry.npmjs.org/@inquirer/input/-/input-4.3.1.tgz" - integrity sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g== - dependencies: - "@inquirer/core" "^10.3.2" - "@inquirer/type" "^3.0.10" - -"@inquirer/number@^3.0.23": - version "3.0.23" - resolved "https://registry.npmjs.org/@inquirer/number/-/number-3.0.23.tgz" - integrity sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg== - dependencies: - "@inquirer/core" "^10.3.2" - "@inquirer/type" "^3.0.10" - -"@inquirer/password@^4.0.23": - version "4.0.23" - resolved "https://registry.npmjs.org/@inquirer/password/-/password-4.0.23.tgz" - integrity sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA== - dependencies: - "@inquirer/ansi" "^1.0.2" - "@inquirer/core" "^10.3.2" - "@inquirer/type" "^3.0.10" - -"@inquirer/prompts@^7.10.1": - version "7.10.1" - resolved "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.10.1.tgz" - integrity sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg== - dependencies: - "@inquirer/checkbox" "^4.3.2" - "@inquirer/confirm" "^5.1.21" - "@inquirer/editor" "^4.2.23" - "@inquirer/expand" "^4.0.23" - "@inquirer/input" "^4.3.1" - "@inquirer/number" "^3.0.23" - "@inquirer/password" "^4.0.23" - "@inquirer/rawlist" "^4.1.11" - "@inquirer/search" "^3.2.2" - "@inquirer/select" "^4.4.2" - -"@inquirer/rawlist@^4.1.11": - version "4.1.11" - resolved "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.11.tgz" - integrity sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw== +"@inquirer/figures@^2.0.8": + version "2.0.8" + resolved "https://registry.yarnpkg.com/@inquirer/figures/-/figures-2.0.8.tgz#d10c3a8a28896679797d9da5944f3093476aa0cd" + integrity sha512-tApbon79GM9ry56ja/Ud3SY2CL4TQsao9fIwDQbgTeNY55025GdMzQ2+UdegV/lx51VNGUB59M0v0nMpybYY4Q== + +"@inquirer/input@^5.1.4": + version "5.1.4" + resolved "https://registry.yarnpkg.com/@inquirer/input/-/input-5.1.4.tgz#4aadb5f4f800e6b9f7a9ef79d42f78fb0ee1d42f" + integrity sha512-3xQkQrOvgOzpSN2ciTVdRDlg1FWMCA8l+0KfB6SNlILoTCGzJTzO/gc0Rwjcb3usuGyKdaGtI6OiyMdeMeLWkg== dependencies: - "@inquirer/core" "^10.3.2" - "@inquirer/type" "^3.0.10" - yoctocolors-cjs "^2.1.3" + "@inquirer/core" "^12.0.1" + "@inquirer/type" "^4.1.0" -"@inquirer/search@^3.2.2": - version "3.2.2" - resolved "https://registry.npmjs.org/@inquirer/search/-/search-3.2.2.tgz" - integrity sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA== +"@inquirer/number@^4.2.1": + version "4.2.1" + resolved "https://registry.yarnpkg.com/@inquirer/number/-/number-4.2.1.tgz#34c302d434f78571cf6be54027d4016400d2e5bb" + integrity sha512-5KaqwZNLRpUuWcoCrYghPP9TMaXL5v2Sk4xqePM7RCVegcJStoXdWibio60YIC1bec+z1fCyb67N6XPJIkZtGA== dependencies: - "@inquirer/core" "^10.3.2" - "@inquirer/figures" "^1.0.15" - "@inquirer/type" "^3.0.10" - yoctocolors-cjs "^2.1.3" + "@inquirer/core" "^12.0.1" + "@inquirer/type" "^4.1.0" -"@inquirer/select@^4.4.2": - version "4.4.2" - resolved "https://registry.npmjs.org/@inquirer/select/-/select-4.4.2.tgz" - integrity sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w== +"@inquirer/password@^5.2.0": + version "5.2.0" + resolved "https://registry.yarnpkg.com/@inquirer/password/-/password-5.2.0.tgz#002db941c99ab3fa10b624d18decc3ee3400461c" + integrity sha512-CvVcW09emkBESEOW+4R8CjLNkP3fB3XrjeL8CDvfpjgrJN+V9oerXmJAXXM3l+4xqYPD5Yaujzy/Ph0PLOdDuA== + dependencies: + "@inquirer/ansi" "^2.0.7" + "@inquirer/core" "^12.0.1" + "@inquirer/type" "^4.1.0" + +"@inquirer/prompts@^8.7.0": + version "8.7.0" + resolved "https://registry.yarnpkg.com/@inquirer/prompts/-/prompts-8.7.0.tgz#6bdf61e32b8cb64e7e39a187842d5887cb224701" + integrity sha512-yQwBMYvpJ6jqrXtKiOwRD5XezjJoyt3VQvIyjsr5Arqb519nfIohOQymWVJ8/vEgg8xtZerrCsqlSaqt/LPC9A== + dependencies: + "@inquirer/checkbox" "^5.2.3" + "@inquirer/confirm" "^6.3.0" + "@inquirer/editor" "^5.3.1" + "@inquirer/expand" "^5.1.3" + "@inquirer/input" "^5.1.4" + "@inquirer/number" "^4.2.1" + "@inquirer/password" "^5.2.0" + "@inquirer/rawlist" "^5.3.3" + "@inquirer/search" "^4.3.1" + "@inquirer/select" "^5.2.3" + +"@inquirer/rawlist@^5.3.3": + version "5.3.3" + resolved "https://registry.yarnpkg.com/@inquirer/rawlist/-/rawlist-5.3.3.tgz#efac1bd316bbed4bd58572dee4e2826bfc3336d0" + integrity sha512-Mu7WrtmDLaXBDEyrRLS70SZgX9ZSm4Up1w0ZxiH8C1OOp9oaVCn2k8q3QGgmlnhsKYUhuaU3zFWhAP6wxkVIMA== + dependencies: + "@inquirer/core" "^12.0.1" + "@inquirer/type" "^4.1.0" + +"@inquirer/search@^4.3.1": + version "4.3.1" + resolved "https://registry.yarnpkg.com/@inquirer/search/-/search-4.3.1.tgz#e4c96e9e65ea6adcc78ed629c6a363085689968d" + integrity sha512-0VWOvsHWI0rPj6CG70MoP4oXNCB6adcyN8bVFZXnh11eLDdPIK2f2XCmva78acPPDfJisfab7qNakwBh7hdBXw== + dependencies: + "@inquirer/core" "^12.0.1" + "@inquirer/figures" "^2.0.8" + "@inquirer/type" "^4.1.0" + +"@inquirer/select@^5.2.3": + version "5.2.3" + resolved "https://registry.yarnpkg.com/@inquirer/select/-/select-5.2.3.tgz#527f4290568c2851c9e0a7a03513e90b26ab1e80" + integrity sha512-KuRTodDa6xBXX2noIpjuitpX/QT7Sfav7dIZ/OfUY54Hxg95nrGoshSzxx6Ey7qqLbImKdiGkSDt7KjPXjQgmA== dependencies: - "@inquirer/ansi" "^1.0.2" - "@inquirer/core" "^10.3.2" - "@inquirer/figures" "^1.0.15" - "@inquirer/type" "^3.0.10" - yoctocolors-cjs "^2.1.3" + "@inquirer/ansi" "^2.0.7" + "@inquirer/core" "^12.0.1" + "@inquirer/figures" "^2.0.8" + "@inquirer/type" "^4.1.0" -"@inquirer/type@^3.0.10": - version "3.0.10" - resolved "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz" - integrity sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA== +"@inquirer/type@^4.1.0": + version "4.1.0" + resolved "https://registry.yarnpkg.com/@inquirer/type/-/type-4.1.0.tgz#98f607ae62c4c5b333ae1b45b629af62a3be6472" + integrity sha512-FMiJpuHUG3Dk0ex+UIXkre7i+i4OcwHWk9YdcVtZHFwb/r2rnrU2ipTCNAB7A+QOP0ryzIcqOfy76fRyyvOEAw== "@isaacs/cliui@^8.0.2": version "8.0.2" @@ -937,10 +1074,10 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" -"@mswjs/interceptors@^0.39.5": - version "0.39.8" - resolved "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.39.8.tgz" - integrity sha512-2+BzZbjRO7Ct61k8fMNHEtoKjeWI9pIlHFTqBwZ5icHpqszIgEZbjb1MW5Z0+bITTCTl3gk4PDBxs9tA/csXvA== +"@mswjs/interceptors@^0.41.0": + version "0.41.9" + resolved "https://registry.yarnpkg.com/@mswjs/interceptors/-/interceptors-0.41.9.tgz#9d90bbd60d1ddc30dbcbb827a9bb2e470493530d" + integrity sha512-VVPPgHyQ6ShqnrmDWuxjmUIsO9gWyOZFmuOfLd9LfBGQJwZfy0gvv9pbHSJuoFNIYC7ZDX9aoFwowjcdSC4E8w== dependencies: "@open-draft/deferred-promise" "^2.2.0" "@open-draft/logger" "^0.3.0" @@ -993,6 +1130,11 @@ resolved "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz" integrity sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg== +"@oxc-project/types@=0.147.0": + version "0.147.0" + resolved "https://registry.yarnpkg.com/@oxc-project/types/-/types-0.147.0.tgz#512e43196053db4a99928e35b287549a3226268b" + integrity sha512-IJ3s6ltHLp45S0bh7phkX+gJO7A1Wuz2EaqpAhb8WjqDwbzMiWKHhyyT42tskaWjEYXtHtVCPpnBJVT9+dcRLg== + "@paralleldrive/cuid2@^2.2.2": version "2.3.1" resolved "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz" @@ -1031,6 +1173,86 @@ "@pnpm/network.ca-file" "^1.0.1" config-chain "^1.1.11" +"@rolldown/binding-android-arm-eabi@1.2.6": + version "1.2.6" + resolved "https://registry.yarnpkg.com/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.6.tgz#81bc32b79902fcd3875dffdf3be2ccba3ef8d794" + integrity sha512-b+jTcARdTiFLI6jB4a5XjTm0RWd6KcRfQj/I2356fxUZemiho9zQLxo0RtCuMDAyKcLo6cEltkgbQp6d1+sjjQ== + +"@rolldown/binding-android-arm64@1.2.6": + version "1.2.6" + resolved "https://registry.yarnpkg.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.6.tgz#9b20ae8d8a5bb979ea6aa8fa4b7e11f58c53f775" + integrity sha512-lkWU8ZJaRk9q3CIEY1Tc7vIFALp3Xw5NfGJo2hQg5oIqNgxWi1zI+IiDEK3r70BF5Dzol1tcXsnzsRc8NLhG+Q== + +"@rolldown/binding-darwin-arm64@1.2.6": + version "1.2.6" + resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.6.tgz#1031ee06b51c9134ba5a6da3a7310388875f7dd8" + integrity sha512-dgR56NYnvAszm7Ob1B2/Vn0e8bUQYZH2UjVaMMtMVOCKFSfjhfLmuA/9+O+F+ajUdG6B/bSssrKW6JJYASa8jA== + +"@rolldown/binding-darwin-x64@1.2.6": + version "1.2.6" + resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.6.tgz#11cf31040b779dba8e61d932ae3c0ee81ef94ae0" + integrity sha512-vpVxFvUCFioJqug7OTvqptkc4yb8UX0AwfDmJpaR/0sWz+BUmqSVAf7c8JkUgnN8YLspb4a/N6NhTyMAmdyQ7Q== + +"@rolldown/binding-freebsd-x64@1.2.6": + version "1.2.6" + resolved "https://registry.yarnpkg.com/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.6.tgz#72d805b459ba4a8bbfc112bca4a1c70740516ae9" + integrity sha512-h1wG6Y6K3JlRswxsI64qQJqBAy4vrLuHgRbc8CZMGSWTOFRY6ghMApM1NKzB2I0n5xV1fjkE18SuVl2QpLeNpA== + +"@rolldown/binding-linux-arm-gnueabihf@1.2.6": + version "1.2.6" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.6.tgz#43b778dd021f50182a1efdb4149cd89cff86c942" + integrity sha512-tbCiqub0q2MVWJKgF5PoAlNWCtQydiOYSLIkd8sByqK/6MMYLJRcSXSYodqYtd0O+Fw7QaVmKKlS4oL94YRZ0w== + +"@rolldown/binding-linux-arm64-gnu@1.2.6": + version "1.2.6" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.6.tgz#e9fa7a469999c1346d827b29154068d1c833c474" + integrity sha512-oxK9+baEBPhZG5HB4URY+uU04zJWeZlH6Tb9rB5DK4DF9XR1uXNLXt5Q5ZsugTKayNCNLhkcwz/ye74hRI98dg== + +"@rolldown/binding-linux-arm64-musl@1.2.6": + version "1.2.6" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.6.tgz#5e68dd6eb85797745f494dfe8c490c2bce4218a6" + integrity sha512-muWCk27FVBEZtv0MsK8gnfSmgczA8KQ0uRVJbTABKhkRfQc38aUrcb7fhi3BNiyseFmgcRsoMfQsSNJ+DbZdSw== + +"@rolldown/binding-linux-ppc64-gnu@1.2.6": + version "1.2.6" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.6.tgz#2a0efc15437a55dd2caf58f0318e129d11d98ee6" + integrity sha512-eWDoSfU7Co2qj3vgB3Dt4lj1mG6CoWbcJQkRMP3XJplyCMtuaq3LHvPFjS9QIPvMGWVadJC04Xiy0IdcVPtnwQ== + +"@rolldown/binding-linux-s390x-gnu@1.2.6": + version "1.2.6" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.6.tgz#bc2b0b6325d8aae0f1a1c06fb10d665c82258541" + integrity sha512-2bWNjRSIayvupRKxXUY2tWG9fYdoUlTqWywHRvE8Eq3GvuQ+f2HeIkve697fIt+IQs/PV8yFsdWuhp1aJ1PdnA== + +"@rolldown/binding-linux-x64-gnu@1.2.6": + version "1.2.6" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.6.tgz#e6e5022f6138fdc0d14cefacc67dc7bc5dff6430" + integrity sha512-KekI0gS0wLxe1UBSQSjenBVwou/JkcQPDzBPICGZjxUv9k3RteHDPBQaiOicZUFKRIH2wKEimGwVpnJsbPzu7w== + +"@rolldown/binding-linux-x64-musl@1.2.6": + version "1.2.6" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.6.tgz#c8b462609809237bbf6265491b1e78d5c260ed46" + integrity sha512-TvtPnfVr+HtyGiDmPK4VWmlNm7QhNNAcK5Q9A7aOXsI8545yCyaoMaicXrFZ72JzeYjaUVk7yT243zT0jzjFKQ== + +"@rolldown/binding-openharmony-arm64@1.2.6": + version "1.2.6" + resolved "https://registry.yarnpkg.com/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.6.tgz#aa208791647b3b0bd23b372d5ee6bd5ed5988118" + integrity sha512-iOo0VEay2XFhaCcH0sps5XIimkSuOnNaZrf6+ZkoSOQBJPKNU48RkmJv0/lSpipexu5P+ouFgafe5IGr/DiQfg== + +"@rolldown/binding-win32-arm64-msvc@1.2.6": + version "1.2.6" + resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.6.tgz#a2a5f1a84555d00afe0ac0ff387878bcf4ada52a" + integrity sha512-y5NTmmasMS455JlOCO4ZM9krIchv3Mvm1crL1iUPGOPgEzSkves9n0SdC5Sjz6+qWDFhd8/JpfWMH8NSWNHe+A== + +"@rolldown/binding-win32-x64-msvc@1.2.6": + version "1.2.6" + resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.6.tgz#1162903e264ed10db4be8af40ba8cf13cd4a0b4a" + integrity sha512-np8iZSLfXlAD4kWhiyq/u0Yt8oZDtRQ8lGhQaCXo2rl37KNjeU0GjJuwr4P3oeZ++ROfofsKNBqR5LTO8aXyWQ== + +"@rolldown/pluginutils@^1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz#e3fcee093fbb5ce765e1ad088ff4de2889f6f9be" + integrity sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw== + "@rollup/rollup-android-arm-eabi@4.55.1": version "4.55.1" resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz#76e0fef6533b3ce313f969879e61e8f21f0eeb28" @@ -1163,6 +1385,11 @@ dependencies: any-observable "^0.3.0" +"@sec-ant/readable-stream@^0.4.1": + version "0.4.1" + resolved "https://registry.yarnpkg.com/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz#60de891bb126abfdc5410fdc6166aca065f10a0c" + integrity sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg== + "@sideway/address@^4.1.5": version "4.1.5" resolved "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz" @@ -1180,20 +1407,37 @@ resolved "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz" integrity sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ== +"@simple-libs/child-process-utils@^1.0.0": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@simple-libs/child-process-utils/-/child-process-utils-1.0.2.tgz#cb182d310c9bed3ace200b26258e090d898a1736" + integrity sha512-/4R8QKnd/8agJynkNdJmNw2MBxuFTRcNFnE5Sg/G+jkSsV8/UBgULMzhizWWW42p8L5H7flImV2ATi79Ove2Tw== + dependencies: + "@simple-libs/stream-utils" "^1.2.0" + +"@simple-libs/stream-utils@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@simple-libs/stream-utils/-/stream-utils-1.2.0.tgz#5af724b826f1ab4d7f2826d31d3efccec124102b" + integrity sha512-KxXvfapcixpz6rVEB6HPjOUZT22yN6v0vI0urQSk1L8MlEWPDFCZkhw2xmkyoTGYeFw7tWTZd7e3lVzRZRN/EA== + "@sindresorhus/merge-streams@^2.1.0": version "2.3.0" resolved "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz" integrity sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg== -"@standard-schema/spec@^1.0.0": +"@sindresorhus/merge-streams@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz#abb11d99aeb6d27f1b563c38147a72d50058e339" + integrity sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ== + +"@standard-schema/spec@^1.1.0": version "1.1.0" - resolved "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz" + resolved "https://registry.yarnpkg.com/@standard-schema/spec/-/spec-1.1.0.tgz#a79b55dbaf8604812f52d140b2c9ab41bc150bb8" integrity sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w== -"@tsconfig/node20@^20.1.8": - version "20.1.8" - resolved "https://registry.npmjs.org/@tsconfig/node20/-/node20-20.1.8.tgz" - integrity sha512-Em+IdPfByIzWRRpqWL4Z7ArLHZGxmc36BxE3jCz9nBFSm+5aLaPMZyjwu4yetvyKXeogWcxik4L1jB5JTWfw7A== +"@tsconfig/node20@^20.1.10": + version "20.1.10" + resolved "https://registry.yarnpkg.com/@tsconfig/node20/-/node20-20.1.10.tgz#e41fb85a1f224b4f0704f7b33df71da8a55891b6" + integrity sha512-5OZgdnxbFuvwm05iVwPXMU5/7iPuVDDZzAnxPDLj+kjN7fwQZSSVgHQhKTNkBPOO6Qef2D5MxPAoPLCGqE2VuQ== "@types/async@^3.2.25": version "3.2.25" @@ -1292,10 +1536,10 @@ dependencies: "@types/node" "*" -"@types/lodash@^4.17.23": - version "4.17.23" - resolved "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.23.tgz" - integrity sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA== +"@types/lodash@^4.17.25": + version "4.17.25" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.25.tgz#69765ac7bcddb0eb072961cf292524a8f5b3c2c0" + integrity sha512-+K1NIO8I+F9/wNulfVvu23QYd0Pe9/OCqRrim4NoYIf1VoEDL90Ve4ClzpyqBLc7NpGGWRvYNCKZ1BE/Jpf8dQ== "@types/methods@^1.1.4": version "1.1.4" @@ -1312,16 +1556,23 @@ resolved "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz" integrity sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag== -"@types/node@*", "@types/node@^25.0.9": +"@types/node@*": version "25.0.9" resolved "https://registry.npmjs.org/@types/node/-/node-25.0.9.tgz" integrity sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw== dependencies: undici-types "~7.16.0" -"@types/normalize-package-data@^2.4.0", "@types/normalize-package-data@^2.4.3": +"@types/node@^26.4.0": + version "26.4.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-26.4.0.tgz#4c4ca071c42241fe602741d02999f16b4e64c468" + integrity sha512-faiGnoIrLH/V8cibOMEAZ8pMw6oXqSukl29ra4mN8GdaB2ZewzeaLj+INpV5N+Z1eKWzY+IzaIZH2EIR6YZRNQ== + dependencies: + undici-types "~8.3.0" + +"@types/normalize-package-data@^2.4.0", "@types/normalize-package-data@^2.4.4": version "2.4.4" - resolved "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz" + resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz#56e2cc26c397c038fab0e3a917a12d5c5909e901" integrity sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA== "@types/qs@*": @@ -1360,10 +1611,10 @@ "@types/methods" "^1.1.4" "@types/node" "*" -"@types/supertest@^6.0.3": - version "6.0.3" - resolved "https://registry.npmjs.org/@types/supertest/-/supertest-6.0.3.tgz" - integrity sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w== +"@types/supertest@^7.2.1": + version "7.2.1" + resolved "https://registry.yarnpkg.com/@types/supertest/-/supertest-7.2.1.tgz#165e99f10fd652027cf1eaa74b55081b6daa0965" + integrity sha512-4CbBvoYVLHL7+yhbYrZET0vsvuyXTC05aRe7dNQkwMzm56auceoy6Yu3K50uZmwfHna1os3CMSgM/3QVkUtPTw== dependencies: "@types/methods" "^1.1.4" "@types/superagent" "^8.1.0" @@ -1380,81 +1631,183 @@ joi "^17.7.0" prom-client ">=11.5.3" -"@vitest/coverage-v8@^4.0.17": - version "4.0.17" - resolved "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.17.tgz" - integrity sha512-/6zU2FLGg0jsd+ePZcwHRy3+WpNTBBhDY56P4JTRqUN/Dp6CvOEa9HrikcQ4KfV2b2kAHUFB4dl1SuocWXSFEw== +"@typescript/typescript-aix-ppc64@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz#cdc7ce81d60f1e09034960ddfb1fb880d7a776b6" + integrity sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ== + +"@typescript/typescript-darwin-arm64@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz#a55fdfcfa58df58d27db2237cde6a5c1e35a7235" + integrity sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA== + +"@typescript/typescript-darwin-x64@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz#38d1c9172800a91d707bec64d2a370a016634db4" + integrity sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA== + +"@typescript/typescript-freebsd-arm64@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz#f1ff8810030b35d2b5be0db6a2dc650460ea94fa" + integrity sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ== + +"@typescript/typescript-freebsd-x64@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz#3d86b03f353c5b1ba95162eb6ce35533bfc294bd" + integrity sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw== + +"@typescript/typescript-linux-arm64@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz#d9334d96d6dac6ff85da9c865588948de939e91f" + integrity sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ== + +"@typescript/typescript-linux-arm@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz#ad94b41e1aee2a4dcc6a298c7b67c43345fde32e" + integrity sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ== + +"@typescript/typescript-linux-loong64@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz#2965aee4fc873360139d893daafe6397a29138ad" + integrity sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ== + +"@typescript/typescript-linux-mips64el@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz#1a887a311bed3a833f80bfd4a9ed37c271936cf0" + integrity sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA== + +"@typescript/typescript-linux-ppc64@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz#8b63c9b2f445b393eb4e43ec21da225dade3577d" + integrity sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA== + +"@typescript/typescript-linux-riscv64@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz#b6e8a35c289b3ea97a92a41d461aaeed0d3b36e1" + integrity sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ== + +"@typescript/typescript-linux-s390x@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz#2ef96693be4861f6d17965427e5b009cbbed1a3e" + integrity sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw== + +"@typescript/typescript-linux-x64@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz#73269cb0baba50aea0ca060445a6b88e583f1ce2" + integrity sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A== + +"@typescript/typescript-netbsd-arm64@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz#3a3649f97fafa210b4e6e3798c15e06605c8a901" + integrity sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA== + +"@typescript/typescript-netbsd-x64@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz#47ec59491a40c470d2807dc4d2b825528fd979ab" + integrity sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA== + +"@typescript/typescript-openbsd-arm64@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz#796be8da0bd989d8a3fb96f2801e38a8365b4baf" + integrity sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ== + +"@typescript/typescript-openbsd-x64@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz#d37fe2a729eb942c076c454ee7f1815faf7d560f" + integrity sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg== + +"@typescript/typescript-sunos-x64@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz#aba8d3464c3565a7044789baba96916bd4ab2c88" + integrity sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g== + +"@typescript/typescript-win32-arm64@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz#b9de50a17196383f62620b5f9d0a2f34ad3b60d7" + integrity sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ== + +"@typescript/typescript-win32-x64@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz#cf3b7b0d6ce5635daca4c8e01c189cdcde47ec3c" + integrity sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g== + +"@vitest/coverage-v8@^4.1.11": + version "4.1.11" + resolved "https://registry.yarnpkg.com/@vitest/coverage-v8/-/coverage-v8-4.1.11.tgz#6f0636abe7e23e86dd35127244554be2c60bc2a5" + integrity sha512-8MVGEFnJIcdGjcbfKmeq8z0pZHH0JlVtoVZH9Q/qwUp6wyFnEJUBMrw9DCaj+ra3vShGmhavjalMIhPNxZAUcw== dependencies: "@bcoe/v8-coverage" "^1.0.2" - "@vitest/utils" "4.0.17" - ast-v8-to-istanbul "^0.3.10" + "@vitest/utils" "4.1.11" + ast-v8-to-istanbul "^1.0.0" istanbul-lib-coverage "^3.2.2" istanbul-lib-report "^3.0.1" istanbul-reports "^3.2.0" - magicast "^0.5.1" + magicast "^0.5.2" obug "^2.1.1" - std-env "^3.10.0" - tinyrainbow "^3.0.3" + std-env "^4.0.0-rc.1" + tinyrainbow "^3.1.0" -"@vitest/expect@4.0.17": - version "4.0.17" - resolved "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.17.tgz" - integrity sha512-mEoqP3RqhKlbmUmntNDDCJeTDavDR+fVYkSOw8qRwJFaW/0/5zA9zFeTrHqNtcmwh6j26yMmwx2PqUDPzt5ZAQ== +"@vitest/expect@4.1.11": + version "4.1.11" + resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-4.1.11.tgz#5f580d1f9cdbba314dbf23b2d911f8eb23878f5f" + integrity sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw== dependencies: - "@standard-schema/spec" "^1.0.0" + "@standard-schema/spec" "^1.1.0" "@types/chai" "^5.2.2" - "@vitest/spy" "4.0.17" - "@vitest/utils" "4.0.17" - chai "^6.2.1" - tinyrainbow "^3.0.3" + "@vitest/spy" "4.1.11" + "@vitest/utils" "4.1.11" + chai "^6.2.2" + tinyrainbow "^3.1.0" -"@vitest/mocker@4.0.17": - version "4.0.17" - resolved "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.17.tgz" - integrity sha512-+ZtQhLA3lDh1tI2wxe3yMsGzbp7uuJSWBM1iTIKCbppWTSBN09PUC+L+fyNlQApQoR+Ps8twt2pbSSXg2fQVEQ== +"@vitest/mocker@4.1.11": + version "4.1.11" + resolved "https://registry.yarnpkg.com/@vitest/mocker/-/mocker-4.1.11.tgz#8e2906361bc5dfa271757a858ae80643118fcbb4" + integrity sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ== dependencies: - "@vitest/spy" "4.0.17" + "@vitest/spy" "4.1.11" estree-walker "^3.0.3" magic-string "^0.30.21" -"@vitest/pretty-format@4.0.17": - version "4.0.17" - resolved "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.17.tgz" - integrity sha512-Ah3VAYmjcEdHg6+MwFE17qyLqBHZ+ni2ScKCiW2XrlSBV4H3Z7vYfPfz7CWQ33gyu76oc0Ai36+kgLU3rfF4nw== +"@vitest/pretty-format@4.1.11": + version "4.1.11" + resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-4.1.11.tgz#8b28eb8240771d6ea970e33beaeb41384b51868e" + integrity sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw== dependencies: - tinyrainbow "^3.0.3" + tinyrainbow "^3.1.0" -"@vitest/runner@4.0.17": - version "4.0.17" - resolved "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.17.tgz" - integrity sha512-JmuQyf8aMWoo/LmNFppdpkfRVHJcsgzkbCA+/Bk7VfNH7RE6Ut2qxegeyx2j3ojtJtKIbIGy3h+KxGfYfk28YQ== +"@vitest/runner@4.1.11": + version "4.1.11" + resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-4.1.11.tgz#bfbad98c8d6c3f1fb4df12056ad569821ff77f21" + integrity sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw== dependencies: - "@vitest/utils" "4.0.17" + "@vitest/utils" "4.1.11" pathe "^2.0.3" -"@vitest/snapshot@4.0.17": - version "4.0.17" - resolved "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.17.tgz" - integrity sha512-npPelD7oyL+YQM2gbIYvlavlMVWUfNNGZPcu0aEUQXt7FXTuqhmgiYupPnAanhKvyP6Srs2pIbWo30K0RbDtRQ== +"@vitest/snapshot@4.1.11": + version "4.1.11" + resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-4.1.11.tgz#df461eb165924a3155986dde68e13360f53f3d4c" + integrity sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog== dependencies: - "@vitest/pretty-format" "4.0.17" + "@vitest/pretty-format" "4.1.11" + "@vitest/utils" "4.1.11" magic-string "^0.30.21" pathe "^2.0.3" -"@vitest/spy@4.0.17": - version "4.0.17" - resolved "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.17.tgz" - integrity sha512-I1bQo8QaP6tZlTomQNWKJE6ym4SHf3oLS7ceNjozxxgzavRAgZDc06T7kD8gb9bXKEgcLNt00Z+kZO6KaJ62Ew== +"@vitest/spy@4.1.11": + version "4.1.11" + resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-4.1.11.tgz#0add45cae953afed9c88f98e2f6fc9164558c32a" + integrity sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA== -"@vitest/utils@4.0.17": - version "4.0.17" - resolved "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.17.tgz" - integrity sha512-RG6iy+IzQpa9SB8HAFHJ9Y+pTzI+h8553MrciN9eC6TFBErqrQaTas4vG+MVj8S4uKk8uTT2p0vgZPnTdxd96w== +"@vitest/utils@4.1.11": + version "4.1.11" + resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-4.1.11.tgz#9b27a4293b827942b223539bfab1bd9f7eada31b" + integrity sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ== dependencies: - "@vitest/pretty-format" "4.0.17" - tinyrainbow "^3.0.3" + "@vitest/pretty-format" "4.1.11" + convert-source-map "^2.0.0" + tinyrainbow "^3.1.0" -JSONStream@^1.0.4, JSONStream@^1.3.5: +JSONStream@^1.0.4: version "1.3.5" resolved "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz" integrity sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ== @@ -1485,11 +1838,6 @@ add-stream@^1.0.0: resolved "https://registry.npmjs.org/add-stream/-/add-stream-1.0.0.tgz" integrity sha512-qQLMr+8o0WC4FZGQTcJiKBVC59JylcPSrTtk6usvmIDFUOCKegapy1VHQwRbFMOFyb/inzUVqHs+eMYKDM1YeQ== -agent-base@^7.1.2: - version "7.1.4" - resolved "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz" - integrity sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ== - ajv@^6.11.0, ajv@^6.12.6: version "6.12.6" resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz" @@ -1529,12 +1877,12 @@ ansi-escapes@^4.2.1: dependencies: type-fest "^0.21.3" -ansi-escapes@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-5.0.0.tgz" - integrity sha512-5GFMVX8HqE/TB+FuBJGuO5XG0WrsA6ptUqoODaT/n9mmUaZFkqnBueB4leqGBCmrUHnCnC4PCZTCd0E7QQ83bA== +ansi-escapes@^7.0.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-7.3.0.tgz#5395bb74b2150a4a1d6e3c2565f4aeca78d28627" + integrity sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg== dependencies: - type-fest "^1.0.2" + environment "^1.0.0" ansi-regex@^2.0.0: version "2.1.1" @@ -1630,14 +1978,14 @@ assertion-error@^2.0.1: resolved "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz" integrity sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA== -ast-v8-to-istanbul@^0.3.10: - version "0.3.10" - resolved "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.10.tgz" - integrity sha512-p4K7vMz2ZSk3wN8l5o3y2bJAoZXT3VuJI5OLTATY/01CYWumWvwkUw0SqDBnNq6IiTO3qDa1eSQDibAV8g7XOQ== +ast-v8-to-istanbul@^1.0.0: + version "1.0.5" + resolved "https://registry.yarnpkg.com/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.5.tgz#708baeb6f5c879226d112a341ffa821c43881d2d" + integrity sha512-UPAgKJFSEGMWSDr3LX4tqnAb4f7KGT8O40Tyx8wbYmmZ/yn58lNCm8h3svs3eXgiGd5AXxz8NDOvXWvicq+rJA== dependencies: "@jridgewell/trace-mapping" "^0.3.31" estree-walker "^3.0.3" - js-tokens "^9.0.1" + js-tokens "^10.0.0" astral-regex@^2.0.0: version "2.0.0" @@ -1691,6 +2039,11 @@ balanced-match@^1.0.0: resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== +balanced-match@^4.0.2: + version "4.0.4" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-4.0.4.tgz#bfb10662feed8196a2c62e7c68e17720c274179a" + integrity sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA== + base64-js@^1.3.1: version "1.5.1" resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz" @@ -1703,6 +2056,11 @@ basic-auth@^2.0.1: dependencies: safe-buffer "5.1.2" +basic-auth@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-3.0.0.tgz#18665a504dc0cdf22e9f83d51b14dc1d226d4e21" + integrity sha512-B63UKsSJ2atgiSuCJvzAjpfAdFY6mYT3C0vZFlLV/81Rw2/VCs6Tp29UzkpKETB3qA2cHwuU1LjBnhUQ0WkPKw== + bintrees@1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz" @@ -1761,6 +2119,13 @@ brace-expansion@^2.0.1: dependencies: balanced-match "^1.0.0" +brace-expansion@^5.0.8: + version "5.0.9" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-5.0.9.tgz#7c72438809b5fa5babf54199a1f1c281a6984fcf" + integrity sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg== + dependencies: + balanced-match "^4.0.2" + braces@^3.0.3: version "3.0.3" resolved "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz" @@ -1810,6 +2175,11 @@ cachedir@2.3.0: resolved "https://registry.npmjs.org/cachedir/-/cachedir-2.3.0.tgz" integrity sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw== +cachedir@2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/cachedir/-/cachedir-2.4.0.tgz#7fef9cf7367233d7c88068fe6e34ed0d355a610d" + integrity sha512-9EtFOZR8g22CL7BWjJ9BUx1+A/djkofnyW3aOXZORNW2kxoUpx2h+uN2cOqwPmFhnpVmxg+KW2OjOSgChTEvsQ== + call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz" @@ -1861,22 +2231,22 @@ camelcase@^8.0.0: resolved "https://registry.npmjs.org/camelcase/-/camelcase-8.0.0.tgz" integrity sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA== -chai@^6.2.1: +chai@^6.2.2: version "6.2.2" - resolved "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz" + resolved "https://registry.yarnpkg.com/chai/-/chai-6.2.2.tgz#ae41b52c9aca87734505362717f3255facda360e" integrity sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg== -chalk-template@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/chalk-template/-/chalk-template-1.1.0.tgz" - integrity sha512-T2VJbcDuZQ0Tb2EWwSotMPJjgpy1/tGee1BTpUNsGZ/qgNjV2t7Mvu+d4600U564nbLesN1x2dPL+xii174Ekg== +chalk-template@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/chalk-template/-/chalk-template-1.1.2.tgz#88ff13e75a333d232304e13abc48c5b5be15f1ce" + integrity sha512-2bxTP2yUH7AJj/VAXfcA+4IcWGdQ87HwBANLt5XxGTeomo8yG0y95N1um9i5StvhT/Bl0/2cARA5v1PpPXUxUA== dependencies: chalk "^5.2.0" -chalk@5.6.2, chalk@^5.4.1: - version "5.6.2" - resolved "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz" - integrity sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA== +chalk@6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-6.0.0.tgz#a3bae843bc8454f41ef66ef3fd584dcf34dd805d" + integrity sha512-2uNTXIuTTxk7ciZgAU1BQcgnchcG0xXnrs6jzkQfj9SsRa9M2s5zE8WT96hS6KmG4MzWHSrvH43DF1m4XRkrFg== chalk@^1.0.0, chalk@^1.1.3: version "1.1.3" @@ -1911,6 +2281,11 @@ chalk@^5.2.0, chalk@^5.3.0: resolved "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz" integrity sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w== +chalk@^5.6.2: + version "5.6.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.6.2.tgz#b1238b6e23ea337af71c7f8a295db5af0c158aea" + integrity sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA== + chardet@^0.7.0: version "0.7.0" resolved "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz" @@ -1975,6 +2350,25 @@ cli-width@^4.1.0: resolved "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz" integrity sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ== +clipboard-image@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/clipboard-image/-/clipboard-image-0.1.0.tgz#e067250ddfebaff3e4379f9d9290c98786375bb1" + integrity sha512-SWk7FgaXLNFld19peQ/rTe0n97lwR1WbkqxV6JKCAOh7U52AKV/PeMFCyt/8IhBdqyDA8rdyewQMKZqvWT5Akg== + dependencies: + run-jxa "^3.0.0" + +clipboardy@^5.3.1: + version "5.3.2" + resolved "https://registry.yarnpkg.com/clipboardy/-/clipboardy-5.3.2.tgz#40fcdc0c41a4bd37cfd109e438a46352813bf5bb" + integrity sha512-R35PENCHFCw6lsd5SjYPuAVV3Zawr74mKc7ogFNzoDPoQsmWDoJgUNNnWCk/czeqdZGZs8Y0M8zkOlVoySfHEQ== + dependencies: + clipboard-image "^0.1.0" + execa "^9.6.1" + is-wayland "^0.1.0" + is-wsl "^3.1.0" + is64bit "^2.0.0" + powershell-utils "^0.2.0" + cliui@^7.0.2: version "7.0.4" resolved "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz" @@ -2039,10 +2433,10 @@ combined-stream@^1.0.8: dependencies: delayed-stream "~1.0.0" -commander@^14.0.2: - version "14.0.2" - resolved "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz" - integrity sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ== +commander@^15.0.0: + version "15.0.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-15.0.0.tgz#96f3961f12adac1799ef3fbd8bc61d40572d1b11" + integrity sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg== commander@^4.0.0: version "4.1.1" @@ -2069,12 +2463,12 @@ commitizen@^4.0.3: strip-bom "4.0.0" strip-json-comments "3.1.1" -commitizen@^4.3.1: - version "4.3.1" - resolved "https://registry.npmjs.org/commitizen/-/commitizen-4.3.1.tgz" - integrity sha512-gwAPAVTy/j5YcOOebcCRIijn+mSjWJC+IYKivTu6aG8Ei/scoXgfsMRnuAk6b0GRste2J4NGxVdMN3ZpfNaVaw== +commitizen@^4.3.2: + version "4.3.2" + resolved "https://registry.yarnpkg.com/commitizen/-/commitizen-4.3.2.tgz#c2288d5518673ba4deae54e0b6843f3e582406c8" + integrity sha512-1Zs37z9JPvAcuTSSricZZwBhOPVNNxJouuY4yDEt+eD70EoxT2TU9kViG8CuB/PmVg2G4XsAGQiK4YCst97aDQ== dependencies: - cachedir "2.3.0" + cachedir "2.4.0" cz-conventional-changelog "3.3.0" dedent "0.7.0" detect-indent "6.1.0" @@ -2082,10 +2476,10 @@ commitizen@^4.3.1: find-root "1.1.0" fs-extra "9.1.0" glob "7.2.3" - inquirer "8.2.5" + inquirer "8.2.7" is-utf8 "^0.2.1" - lodash "4.17.21" - minimist "1.2.7" + lodash "4.18.1" + minimist "1.2.8" strip-bom "4.0.0" strip-json-comments "3.1.1" @@ -2183,10 +2577,10 @@ conventional-changelog-angular@^5.0.12: compare-func "^2.0.0" q "^1.5.1" -conventional-changelog-angular@^7.0.0: - version "7.0.0" - resolved "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-7.0.0.tgz" - integrity sha512-ROjNchA9LgfNMTTFSIWPzebCwOGFdgkEq45EnvvrmSLvCtAw0HSmrCs7/ty+wAeYUZyNay0YMUNYFTRL72PkBQ== +conventional-changelog-angular@^8.2.0: + version "8.3.1" + resolved "https://registry.yarnpkg.com/conventional-changelog-angular/-/conventional-changelog-angular-8.3.1.tgz#0b015e25ca7f2766a8c5352ab6488dcb76a9d881" + integrity sha512-6gfI3otXK5Ph5DfCOI1dblr+kN3FAm5a97hYoQkqNZxOaYa5WKfXH+AnpsmS+iUH2mgVC2Cg2Qw9m5OKcmNrIg== dependencies: compare-func "^2.0.0" @@ -2218,10 +2612,10 @@ conventional-changelog-conventionalcommits@4.6.3, conventional-changelog-convent lodash "^4.17.15" q "^1.5.1" -conventional-changelog-conventionalcommits@^7.0.2: - version "7.0.2" - resolved "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-7.0.2.tgz" - integrity sha512-NKXYmMR/Hr1DevQegFB4MwfM5Vv0m4UIxKZTTYuD98lpTknaZlSRrDOG4X7wIXpGkfsYxZTghUN+Qq+T0YQI7w== +conventional-changelog-conventionalcommits@^9.2.0: + version "9.3.1" + resolved "https://registry.yarnpkg.com/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-9.3.1.tgz#14f2dd65ccc5de09322a7eb0159f3e0259d7399c" + integrity sha512-dTYtpIacRpcZgrvBYvBfArMmK2xvIpv2TaxM0/ZI5CBtNUzvF2x0t15HsbRABWprS6UPmvj+PzHVjSx4qAVKyw== dependencies: compare-func "^2.0.0" @@ -2343,15 +2737,13 @@ conventional-commits-parser@^3.2.0: split2 "^3.0.0" through2 "^4.0.0" -conventional-commits-parser@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-5.0.0.tgz" - integrity sha512-ZPMl0ZJbw74iS9LuX9YIAiW8pfM5p3yh2o/NbXHbkFuZzY5jvdi5jFycEOkmBW5H5I7nA+D6f3UcsCLP2vvSEA== +conventional-commits-parser@^6.3.0: + version "6.4.0" + resolved "https://registry.yarnpkg.com/conventional-commits-parser/-/conventional-commits-parser-6.4.0.tgz#8ac1c12ec467354ed4d73ec940efe380e1e83686" + integrity sha512-tvRg7FIBNlyPzjdG8wWRlPHQJJHI7DylhtRGeU9Lq+JuoPh5BKpPRX83ZdLrvXuOSu5Eo/e7SzOQhU4Hd2Miuw== dependencies: - JSONStream "^1.3.5" - is-text-path "^2.0.0" - meow "^12.0.1" - split2 "^4.0.0" + "@simple-libs/stream-utils" "^1.2.0" + meow "^13.0.0" conventional-recommended-bump@6.1.0: version "6.1.0" @@ -2367,6 +2759,11 @@ conventional-recommended-bump@6.1.0: meow "^8.0.0" q "^1.5.1" +convert-source-map@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" + integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== + cookie-signature@^1.2.1, cookie-signature@^1.2.2: version "1.2.2" resolved "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz" @@ -2414,20 +2811,20 @@ cosmiconfig-typescript-loader@^6.1.0: dependencies: jiti "^2.6.1" -cosmiconfig@^8.3.6: - version "8.3.6" - resolved "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz" - integrity sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA== +cosmiconfig@^9.0.0: + version "9.0.0" + resolved "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz" + integrity sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg== dependencies: + env-paths "^2.2.1" import-fresh "^3.3.0" js-yaml "^4.1.0" parse-json "^5.2.0" - path-type "^4.0.0" -cosmiconfig@^9.0.0: - version "9.0.0" - resolved "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz" - integrity sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg== +cosmiconfig@^9.0.1: + version "9.0.2" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-9.0.2.tgz#9e5615163becf6a82211fb33d2f68947c25d0c5e" + integrity sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg== dependencies: env-paths "^2.2.1" import-fresh "^3.3.0" @@ -2454,6 +2851,22 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" +cross-spawn@^7.0.6: + version "7.0.6" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" + integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +crypto-random-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-4.0.0.tgz#5a3cc53d7dd86183df5da0312816ceeeb5bb1fc2" + integrity sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA== + dependencies: + type-fest "^1.0.1" + cz-conventional-changelog@3.3.0: version "3.3.0" resolved "https://registry.npmjs.org/cz-conventional-changelog/-/cz-conventional-changelog-3.3.0.tgz" @@ -2473,11 +2886,6 @@ dargs@^7.0.0: resolved "https://registry.npmjs.org/dargs/-/dargs-7.0.0.tgz" integrity sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg== -dargs@^8.0.0: - version "8.1.0" - resolved "https://registry.npmjs.org/dargs/-/dargs-8.1.0.tgz" - integrity sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw== - date-fns@^1.27.2: version "1.30.1" resolved "https://registry.npmjs.org/date-fns/-/date-fns-1.30.1.tgz" @@ -2493,10 +2901,10 @@ dateformat@^4.6.3: resolved "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz" integrity sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA== -dayjs@^1.11.19: - version "1.11.19" - resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz" - integrity sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw== +dayjs@^1.11.23: + version "1.11.23" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.23.tgz#b0a363506dde5f36cf5075e42ebe8115165a8c79" + integrity sha512-QDTCU0M0MxR3hQfnlDJfwekQiaanm1ubOD231u73WBckQ/fsamwRLiE2GBz6D3a/xF1NgfiDLJjXBa1hYOYTtQ== debug@2.6.9: version "2.6.9" @@ -2505,13 +2913,6 @@ debug@2.6.9: dependencies: ms "2.0.0" -debug@4: - version "4.3.6" - resolved "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz" - integrity sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg== - dependencies: - ms "2.1.2" - debug@^4.0.0, debug@^4.3.4: version "4.3.5" resolved "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz" @@ -2559,10 +2960,10 @@ default-browser-id@^5.0.0: resolved "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz" integrity sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA== -default-browser@^5.2.1: - version "5.2.1" - resolved "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz" - integrity sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg== +default-browser@^5.5.1: + version "5.5.1" + resolved "https://registry.yarnpkg.com/default-browser/-/default-browser-5.5.1.tgz#1790affc52680fbb11e17cab2752d69aa2a37d2c" + integrity sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw== dependencies: bundle-name "^4.1.0" default-browser-id "^5.0.0" @@ -2588,9 +2989,9 @@ define-lazy-prop@^3.0.0: resolved "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz" integrity sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg== -del@^8.0.0: +del@^8.0.1: version "8.0.1" - resolved "https://registry.npmjs.org/del/-/del-8.0.1.tgz" + resolved "https://registry.yarnpkg.com/del/-/del-8.0.1.tgz#86772fe3a8c9f91bad0d7c6f79e04c6f310e50ac" integrity sha512-gPqh0mKTPvaUZGAuHbrBUYKZWBNAeHG7TU3QH5EhVwPMyKvmfJaNXhcD2jTcXsJRRcffuho4vaYweu80dRrMGA== dependencies: globby "^14.0.2" @@ -2626,6 +3027,11 @@ detect-indent@6.1.0, detect-indent@^6.0.0: resolved "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz" integrity sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA== +detect-libc@^2.0.3: + version "2.1.2" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.1.2.tgz#689c5dcdc1900ef5583a4cb9f6d7b473742074ad" + integrity sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ== + detect-newline@^3.1.0: version "3.1.0" resolved "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz" @@ -2653,11 +3059,6 @@ dot-prop@^9.0.0: dependencies: type-fest "^4.18.2" -dotenv-override-true@^6.2.2: - version "6.2.2" - resolved "https://registry.npmjs.org/dotenv-override-true/-/dotenv-override-true-6.2.2.tgz" - integrity sha512-a8jtuLPLBl6rCWRfGJe+9iNdh0qqU/4b7IZBvzPYlOEx9vCBSF2Mak4xz5J9aX6GzCkIlCHUlTZW4Im+MIJx1g== - dotgitignore@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/dotgitignore/-/dotgitignore-2.1.0.tgz" @@ -2727,6 +3128,11 @@ env-paths@^2.2.1: resolved "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz" integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A== +environment@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/environment/-/environment-1.1.0.tgz#8e86c66b180f363c7ab311787e0259665f45a9f1" + integrity sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q== + error-ex@^1.3.1: version "1.3.2" resolved "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz" @@ -2751,10 +3157,10 @@ es-errors@^1.3.0: resolved "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz" integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== -es-module-lexer@^1.7.0: - version "1.7.0" - resolved "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz" - integrity sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA== +es-module-lexer@^2.0.0: + version "2.3.2" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-2.3.2.tgz#311fa4f40168c1975c505477c51b23234d41ad55" + integrity sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw== es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: version "1.1.1" @@ -2773,7 +3179,12 @@ es-set-tostringtag@^2.1.0: has-tostringtag "^1.0.2" hasown "^2.0.2" -esbuild@^0.27.0, esbuild@~0.27.0: +es-toolkit@^1.46.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/es-toolkit/-/es-toolkit-1.52.0.tgz#71eaf1a8b18834ef77637eccbb885ba4c03cd6dd" + integrity sha512-XTNEJQh1tY1ZJVcf6ayP/2n4ZPyaHlW2FWs7xvw5ddPuhUVjLD3olQVQS7kf58JbAB48iL0uL/jerTrjtV3lDA== + +esbuild@^0.27.0: version "0.27.2" resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz" integrity sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw== @@ -2805,11 +3216,48 @@ esbuild@^0.27.0, esbuild@~0.27.0: "@esbuild/win32-ia32" "0.27.2" "@esbuild/win32-x64" "0.27.2" +esbuild@~0.28.0: + version "0.28.2" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.28.2.tgz#0f43bd1bad955b72d24e2261e3abe5957ccf0816" + integrity sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA== + optionalDependencies: + "@esbuild/aix-ppc64" "0.28.2" + "@esbuild/android-arm" "0.28.2" + "@esbuild/android-arm64" "0.28.2" + "@esbuild/android-x64" "0.28.2" + "@esbuild/darwin-arm64" "0.28.2" + "@esbuild/darwin-x64" "0.28.2" + "@esbuild/freebsd-arm64" "0.28.2" + "@esbuild/freebsd-x64" "0.28.2" + "@esbuild/linux-arm" "0.28.2" + "@esbuild/linux-arm64" "0.28.2" + "@esbuild/linux-ia32" "0.28.2" + "@esbuild/linux-loong64" "0.28.2" + "@esbuild/linux-mips64el" "0.28.2" + "@esbuild/linux-ppc64" "0.28.2" + "@esbuild/linux-riscv64" "0.28.2" + "@esbuild/linux-s390x" "0.28.2" + "@esbuild/linux-x64" "0.28.2" + "@esbuild/netbsd-arm64" "0.28.2" + "@esbuild/netbsd-x64" "0.28.2" + "@esbuild/openbsd-arm64" "0.28.2" + "@esbuild/openbsd-x64" "0.28.2" + "@esbuild/openharmony-arm64" "0.28.2" + "@esbuild/sunos-x64" "0.28.2" + "@esbuild/win32-arm64" "0.28.2" + "@esbuild/win32-ia32" "0.28.2" + "@esbuild/win32-x64" "0.28.2" + escalade@^3.1.1: version "3.1.2" resolved "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz" integrity sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA== +escalade@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" + integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== + escape-goat@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/escape-goat/-/escape-goat-4.0.0.tgz" @@ -2842,10 +3290,10 @@ etag@^1.8.1, etag@~1.8.1: resolved "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz" integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== -eventemitter3@^5.0.1: - version "5.0.1" - resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz" - integrity sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA== +eventemitter3@^5.0.4: + version "5.0.4" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-5.0.4.tgz#a86d66170433712dde814707ac52b5271ceb1feb" + integrity sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw== execa@^1.0.0: version "1.0.0" @@ -2860,25 +3308,43 @@ execa@^1.0.0: signal-exit "^3.0.0" strip-eof "^1.0.0" -execa@^8.0.1: - version "8.0.1" - resolved "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz" - integrity sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg== +execa@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" + integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== dependencies: cross-spawn "^7.0.3" - get-stream "^8.0.1" - human-signals "^5.0.0" - is-stream "^3.0.0" + get-stream "^6.0.0" + human-signals "^2.1.0" + is-stream "^2.0.0" merge-stream "^2.0.0" - npm-run-path "^5.1.0" - onetime "^6.0.0" + npm-run-path "^4.0.1" + onetime "^5.1.2" + signal-exit "^3.0.3" + strip-final-newline "^2.0.0" + +execa@^9.6.1: + version "9.6.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-9.6.1.tgz#5b90acedc6bdc0fa9b9a6ddf8f9cbb0c75a7c471" + integrity sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA== + dependencies: + "@sindresorhus/merge-streams" "^4.0.0" + cross-spawn "^7.0.6" + figures "^6.1.0" + get-stream "^9.0.0" + human-signals "^8.0.1" + is-plain-obj "^4.1.0" + is-stream "^4.0.1" + npm-run-path "^6.0.0" + pretty-ms "^9.2.0" signal-exit "^4.1.0" - strip-final-newline "^3.0.0" + strip-final-newline "^4.0.0" + yoctocolors "^2.1.1" -exit-hook@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/exit-hook/-/exit-hook-4.0.0.tgz" - integrity sha512-Fqs7ChZm72y40wKjOFXBKg7nJZvQJmewP5/7LtePDdnah/+FH9Hp5sgMujSCMPXlxOAW2//1jrW9pnsY7o20vQ== +exit-hook@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-5.1.0.tgz#f59338d192e150c6997d0af02d72d572d3d635d7" + integrity sha512-INjr2xyxHo7bhAqf5ong++GZPPnpcuBcaXUKt03yf7Fie9yWD7FapL4teOU0+awQazGs5ucBh7xWs/AD+6nhog== expand-tilde@^2.0.0, expand-tilde@^2.0.2: version "2.0.2" @@ -2887,10 +3353,10 @@ expand-tilde@^2.0.0, expand-tilde@^2.0.2: dependencies: homedir-polyfill "^1.0.1" -expect-type@^1.2.2: - version "1.3.0" - resolved "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz" - integrity sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA== +expect-type@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/expect-type/-/expect-type-1.4.0.tgz#24edf7f0cc69a44d008567ba4594ab96f3c3a3d6" + integrity sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA== express@^5.2.1: version "5.2.1" @@ -2991,6 +3457,25 @@ fast-safe-stringify@^2.0.8, fast-safe-stringify@^2.1.1: resolved "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz" integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== +fast-string-truncated-width@^3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz#23afe0da67d752ca0727538f1e6967759728ce49" + integrity sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g== + +fast-string-width@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/fast-string-width/-/fast-string-width-3.0.2.tgz#16dbabb491ce5585b5ecb675b65c165d71688eeb" + integrity sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg== + dependencies: + fast-string-truncated-width "^3.0.2" + +fast-wrap-ansi@^0.2.0: + version "0.2.2" + resolved "https://registry.yarnpkg.com/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz#95e952a0145bce3f59ad56e179f84c48d4072935" + integrity sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q== + dependencies: + fast-string-width "^3.0.2" + fastify@^3.0.0: version "3.29.5" resolved "https://registry.npmjs.org/fastify/-/fastify-3.29.5.tgz" @@ -3047,6 +3532,13 @@ figures@^3.0.0, figures@^3.1.0: dependencies: escape-string-regexp "^1.0.5" +figures@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-6.1.0.tgz#935479f51865fa7479f6fa94fc6fc7ac14e62c4a" + integrity sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg== + dependencies: + is-unicode-supported "^2.0.0" + fill-range@^7.1.1: version "7.1.1" resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz" @@ -3094,6 +3586,11 @@ find-up-simple@^1.0.0: resolved "https://registry.npmjs.org/find-up-simple/-/find-up-simple-1.0.0.tgz" integrity sha512-q7Us7kcjj2VMePAa02hDAF6d+MzsdsAWEwYyOpwUtlerRBkOEPBCRZrAV4XfcSN8fHAgaD0hP7miwoay6DCprw== +find-up-simple@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/find-up-simple/-/find-up-simple-1.0.1.tgz#18fb90ad49e45252c4d7fca56baade04fa3fca1e" + integrity sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ== + find-up@^2.0.0: version "2.1.0" resolved "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz" @@ -3124,15 +3621,6 @@ find-up@^5.0.0: locate-path "^6.0.0" path-exists "^4.0.0" -find-up@^7.0.0: - version "7.0.0" - resolved "https://registry.npmjs.org/find-up/-/find-up-7.0.0.tgz" - integrity sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g== - dependencies: - locate-path "^7.2.0" - path-exists "^5.0.0" - unicorn-magic "^0.1.0" - findup-sync@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/findup-sync/-/findup-sync-4.0.0.tgz" @@ -3301,17 +3789,18 @@ get-stream@^4.0.0: dependencies: pump "^3.0.0" -get-stream@^8.0.1: - version "8.0.1" - resolved "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz" - integrity sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA== +get-stream@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" + integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== -get-tsconfig@^4.7.5: - version "4.8.0" - resolved "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.8.0.tgz" - integrity sha512-Pgba6TExTZ0FJAn1qkJAjIeKoDJ3CsI2ChuLohJnZl/tTU8MVrq3b+2t5UOPfRa4RMsorClBjJALkJUMjG1PAw== +get-stream@^9.0.0: + version "9.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-9.0.1.tgz#95157d21df8eb90d1647102b63039b1df60ebd27" + integrity sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA== dependencies: - resolve-pkg-maps "^1.0.0" + "@sec-ant/readable-stream" "^0.4.1" + is-stream "^4.0.1" git-raw-commits@^2.0.8: version "2.0.11" @@ -3324,14 +3813,13 @@ git-raw-commits@^2.0.8: split2 "^3.0.0" through2 "^4.0.0" -git-raw-commits@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-4.0.0.tgz" - integrity sha512-ICsMM1Wk8xSGMowkOmPrzo2Fgmfo4bMHLNX6ytHjajRJUqvHOw/TFapQ+QG75c3X/tTDDhOSRPGC52dDbNM8FQ== +git-raw-commits@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/git-raw-commits/-/git-raw-commits-5.0.1.tgz#e91d8fd4e3a264142166956fe1a23d08c069657e" + integrity sha512-Y+csSm2GD/PCSh6Isd/WiMjNAydu0VBiG9J7EdQsNA5P9uXvLayqjmTsNlK5Gs9IhblFZqOU0yid5Il5JPoLiQ== dependencies: - dargs "^8.0.0" - meow "^12.0.1" - split2 "^4.0.0" + "@conventional-changelog/git-client" "^2.6.0" + meow "^13.0.0" git-remote-origin-url@^2.0.0: version "2.0.0" @@ -3399,6 +3887,13 @@ global-directory@^4.0.1: dependencies: ini "4.1.1" +global-directory@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/global-directory/-/global-directory-5.0.0.tgz#0f66a94212acd0f81ee838d0a991e88d1c2836cf" + integrity sha512-1pgFdhK3J2LeM+dVf2Pd424yHx2ou338lC0ErNP2hPx4j8eW1Sp0XqSjNxtk6Tc4Kr5wlWtSvz8cn2yb7/SG/w== + dependencies: + ini "6.0.0" + global-modules@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz" @@ -3487,6 +3982,11 @@ has-flag@^4.0.0: resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== +has-flag@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-5.0.1.tgz#5483db2ae02a472d1d0691462fc587d1843cd940" + integrity sha512-CsNUt5x9LUdx6hnk/E2SZLsDyvfqANZSUq4+D3D8RzDJ2M+HDTIkF60ibS1vHaK55vzgiZw1bEPFG9yH7l33wA== + has-property-descriptors@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz" @@ -3547,19 +4047,12 @@ hosted-git-info@^4.0.0, hosted-git-info@^4.0.1: dependencies: lru-cache "^6.0.0" -hosted-git-info@^7.0.0: - version "7.0.2" - resolved "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz" - integrity sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w== - dependencies: - lru-cache "^10.0.1" - -hosted-git-info@^8.0.2: - version "8.1.0" - resolved "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-8.1.0.tgz" - integrity sha512-Rw/B2DNQaPBICNXEm8balFz9a6WpZrkCGpcWFpy7nCj+NyhSdqXipmfvtmWt9xGfp0wZnBxB+iVpLmQMYt47Tw== +hosted-git-info@^9.0.0, hosted-git-info@^9.0.2: + version "9.0.3" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-9.0.3.tgz#637b511ce62a28e4261a92b8da0a4d6be3522cd4" + integrity sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg== dependencies: - lru-cache "^10.0.1" + lru-cache "^11.1.0" html-escaper@^2.0.0: version "2.0.2" @@ -3593,18 +4086,15 @@ http-status-codes@^2.3.0: resolved "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.3.0.tgz" integrity sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA== -https-proxy-agent@^7.0.6: - version "7.0.6" - resolved "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz" - integrity sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw== - dependencies: - agent-base "^7.1.2" - debug "4" +human-signals@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" + integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== -human-signals@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz" - integrity sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ== +human-signals@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-8.0.1.tgz#f08bb593b6d1db353933d06156cedec90abe51fb" + integrity sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ== husky@^9.1.7: version "9.1.7" @@ -3625,17 +4115,24 @@ iconv-lite@^0.7.0, iconv-lite@~0.7.0: dependencies: safer-buffer ">= 2.1.2 < 3.0.0" +iconv-lite@^0.7.2: + version "0.7.3" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.7.3.tgz#84ee12f963e7de50bc01a13e160a078b3b0f415f" + integrity sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + ieee754@^1.1.13: version "1.2.1" resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== -ignore-walk@^7.0.0: - version "7.0.0" - resolved "https://registry.npmjs.org/ignore-walk/-/ignore-walk-7.0.0.tgz" - integrity sha512-T4gbf83A4NH95zvhVYZc+qWocBBGlpzUXLPGurJggw/WIOwicfXJChLDP/iBZnN5WqROSu5Bm3hhle4z8a8YGQ== +ignore-walk@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-8.0.0.tgz#380c173badc3a18c57ff33440753f0052f572b14" + integrity sha512-FCeMZT4NiRQGh+YkeKMtWrOmBgWjHjMJ26WQWrRQyoyzqevdaGSakUaJW5xQYmjLlUVk2qUnCjYVBax9EKKg8A== dependencies: - minimatch "^9.0.0" + minimatch "^10.0.3" ignore@^7.0.3: version "7.0.5" @@ -3673,10 +4170,10 @@ indent-string@^4.0.0: resolved "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz" integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== -index-to-position@^0.1.2: - version "0.1.2" - resolved "https://registry.npmjs.org/index-to-position/-/index-to-position-0.1.2.tgz" - integrity sha512-MWDKS3AS1bGCHLBA2VLImJz42f7bJh8wQsTGCzI3j519/CASStoDONUBVz2I/VID0MpiX3SGSnbOD2xUalbE5g== +index-to-position@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/index-to-position/-/index-to-position-1.2.0.tgz#c800eb34dacf4dbf96b9b06c7eb78d5f704138b4" + integrity sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw== inflight@^1.0.4: version "1.0.6" @@ -3696,6 +4193,11 @@ ini@4.1.1: resolved "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz" integrity sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g== +ini@6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/ini/-/ini-6.0.0.tgz#efc7642b276f6a37d22fdf56ef50889d7146bf30" + integrity sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ== + ini@^1.3.2, ini@^1.3.4, ini@~1.3.0: version "1.3.8" resolved "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz" @@ -3731,18 +4233,38 @@ inquirer@8.2.5: through "^2.3.6" wrap-ansi "^7.0.0" -inquirer@^12.3.2: - version "12.11.1" - resolved "https://registry.npmjs.org/inquirer/-/inquirer-12.11.1.tgz" - integrity sha512-9VF7mrY+3OmsAfjH3yKz/pLbJ5z22E23hENKw3/LNSaA/sAt3v49bDRY+Ygct1xwuKT+U+cBfTzjCPySna69Qw== +inquirer@8.2.7: + version "8.2.7" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-8.2.7.tgz#62f6b931a9b7f8735dc42db927316d8fb6f71de8" + integrity sha512-UjOaSel/iddGZJ5xP/Eixh6dY1XghiBw4XK13rCCIJcJfyhhoul/7KhLLUGtebEj6GDYM6Vnx/mVsjx2L/mFIA== dependencies: - "@inquirer/ansi" "^1.0.2" - "@inquirer/core" "^10.3.2" - "@inquirer/prompts" "^7.10.1" - "@inquirer/type" "^3.0.10" - mute-stream "^2.0.0" + "@inquirer/external-editor" "^1.0.0" + ansi-escapes "^4.2.1" + chalk "^4.1.1" + cli-cursor "^3.1.0" + cli-width "^3.0.0" + figures "^3.0.0" + lodash "^4.17.21" + mute-stream "0.0.8" + ora "^5.4.1" + run-async "^2.4.0" + rxjs "^7.5.5" + string-width "^4.1.0" + strip-ansi "^6.0.0" + through "^2.3.6" + wrap-ansi "^6.0.1" + +inquirer@^14.0.2: + version "14.2.0" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-14.2.0.tgz#3c471cecda0e327d469316105b1f0330af59a020" + integrity sha512-OUSQNF7um7AvgPyBiK6ENfjqEG434oeVkRG97U8foLftOfcJfKcTi5pMtagJe5ytANPH5TlbfVgEokAl6RSj6g== + dependencies: + "@inquirer/ansi" "^2.0.7" + "@inquirer/core" "^12.0.1" + "@inquirer/prompts" "^8.7.0" + "@inquirer/type" "^4.1.0" + mute-stream "^3.0.0" run-async "^4.0.6" - rxjs "^7.8.2" inquirer@^6.2.1: version "6.5.2" @@ -3848,6 +4370,11 @@ is-in-ci@^1.0.0: resolved "https://registry.npmjs.org/is-in-ci/-/is-in-ci-1.0.0.tgz" integrity sha512-eUuAjybVTHMYWm/U+vBO1sY/JOCgoPCXRxzdju0K+K0BiGW0SChEL1MLC0PoCIR1OlPo5YAp8HuQoUlsWEICwg== +is-in-ssh@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-in-ssh/-/is-in-ssh-1.0.0.tgz#8eb73c1cabba77748d389588eeea132a63057622" + integrity sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw== + is-inside-container@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz" @@ -3915,6 +4442,11 @@ is-plain-obj@^1.1.0: resolved "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz" integrity sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg== +is-plain-obj@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz#d65025edec3657ce032fd7db63c97883eaed71f0" + integrity sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg== + is-promise@^2.1.0: version "2.2.2" resolved "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz" @@ -3937,10 +4469,15 @@ is-stream@^1.1.0: resolved "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz" integrity sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ== -is-stream@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz" - integrity sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA== +is-stream@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" + integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== + +is-stream@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-4.0.1.tgz#375cf891e16d2e4baec250b85926cffc14720d9b" + integrity sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A== is-text-path@^1.0.1: version "1.0.1" @@ -3949,13 +4486,6 @@ is-text-path@^1.0.1: dependencies: text-extensions "^1.0.0" -is-text-path@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/is-text-path/-/is-text-path-2.0.0.tgz" - integrity sha512-+oDTluR6WEjdXEJMnC2z6A4FRwFoYuvShVVEGsS7ewc0UTi2QtAKMDJuL4BDEVt+5T7MjFo12RP8ghOM75oKJw== - dependencies: - text-extensions "^2.0.0" - is-unicode-supported@^0.1.0: version "0.1.0" resolved "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz" @@ -3976,6 +4506,11 @@ is-utf8@^0.2.1: resolved "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz" integrity sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q== +is-wayland@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-wayland/-/is-wayland-0.1.0.tgz#ed966c54a608af5ba3c407922589859a0d424fe5" + integrity sha512-QkbMsWkIfkrzOPxenwye0h56iAXirZYHG9eHVPb22fO9y+wPbaX/CHacOWBa/I++4ohTcByimhM1/nyCsH8KNA== + is-windows@^1.0.1: version "1.0.2" resolved "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz" @@ -3988,6 +4523,13 @@ is-wsl@^3.1.0: dependencies: is-inside-container "^1.0.0" +is64bit@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is64bit/-/is64bit-2.0.0.tgz#198c627cbcb198bbec402251f88e5e1a51236c07" + integrity sha512-jv+8jaWCl0g2lSBkNSVXdzfBA0npK1HGC2KtWM9FumFRoGS94g3NbCCLVnCYHLjp4GrW2KZeeSTMo5ddtznmGw== + dependencies: + system-architecture "^0.1.0" + isarray@~1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz" @@ -4060,16 +4602,16 @@ joycon@^3.1.1: resolved "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz" integrity sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw== +js-tokens@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-10.0.0.tgz#dffe7599b4a8bb7fe30aff8d0235234dffb79831" + integrity sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q== + js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== -js-tokens@^9.0.1: - version "9.0.1" - resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz" - integrity sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ== - js-yaml@^4.1.0: version "4.1.0" resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz" @@ -4150,6 +4692,80 @@ light-my-request@^4.2.0: process-warning "^1.0.0" set-cookie-parser "^2.4.1" +lightningcss-android-arm64@1.33.0: + version "1.33.0" + resolved "https://registry.yarnpkg.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz#9a6841f88ae50fc83502903892b41af41bc2b907" + integrity sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg== + +lightningcss-darwin-arm64@1.33.0: + version "1.33.0" + resolved "https://registry.yarnpkg.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz#c0f2c31c0bfd19fa4dd3f18e957a1f1a152097d6" + integrity sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg== + +lightningcss-darwin-x64@1.33.0: + version "1.33.0" + resolved "https://registry.yarnpkg.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz#cb0705965acb538c6683949ce6925fb3cdf7c361" + integrity sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ== + +lightningcss-freebsd-x64@1.33.0: + version "1.33.0" + resolved "https://registry.yarnpkg.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz#763538828b26bab2680dadafcc84ee78b0eb502b" + integrity sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg== + +lightningcss-linux-arm-gnueabihf@1.33.0: + version "1.33.0" + resolved "https://registry.yarnpkg.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz#6862e3176a331aedbdec1ed352b4d7d0dd0784de" + integrity sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ== + +lightningcss-linux-arm64-gnu@1.33.0: + version "1.33.0" + resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz#c6a3a2ed15141daf6bdc2628930f8e39bdf473aa" + integrity sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg== + +lightningcss-linux-arm64-musl@1.33.0: + version "1.33.0" + resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz#7fa1334971fc82845f9827df6ef8a0b20914bac6" + integrity sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ== + +lightningcss-linux-x64-gnu@1.33.0: + version "1.33.0" + resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz#8b927862ea8c2bbc6831a46509244b50d9936e55" + integrity sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg== + +lightningcss-linux-x64-musl@1.33.0: + version "1.33.0" + resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz#0c525bb077dfd94404c059cfe42dad797e96aeaf" + integrity sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw== + +lightningcss-win32-arm64-msvc@1.33.0: + version "1.33.0" + resolved "https://registry.yarnpkg.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz#850ee1103dac989cfab50e3ac22d1a69e394e63d" + integrity sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA== + +lightningcss-win32-x64-msvc@1.33.0: + version "1.33.0" + resolved "https://registry.yarnpkg.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz#e343ae152eed3609dc6e11949d1a3bf39a1c946f" + integrity sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA== + +lightningcss@^1.33.0: + version "1.33.0" + resolved "https://registry.yarnpkg.com/lightningcss/-/lightningcss-1.33.0.tgz#c08867d71a79385c6e190214fd72fef3e5f95f0b" + integrity sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA== + dependencies: + detect-libc "^2.0.3" + optionalDependencies: + lightningcss-android-arm64 "1.33.0" + lightningcss-darwin-arm64 "1.33.0" + lightningcss-darwin-x64 "1.33.0" + lightningcss-freebsd-x64 "1.33.0" + lightningcss-linux-arm-gnueabihf "1.33.0" + lightningcss-linux-arm64-gnu "1.33.0" + lightningcss-linux-arm64-musl "1.33.0" + lightningcss-linux-x64-gnu "1.33.0" + lightningcss-linux-x64-musl "1.33.0" + lightningcss-win32-arm64-msvc "1.33.0" + lightningcss-win32-x64-msvc "1.33.0" + lilconfig@^3.1.1: version "3.1.2" resolved "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz" @@ -4259,18 +4875,6 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" -locate-path@^7.2.0: - version "7.2.0" - resolved "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz" - integrity sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA== - dependencies: - p-locate "^6.0.0" - -lodash.camelcase@^4.3.0: - version "4.3.0" - resolved "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz" - integrity sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA== - lodash.ismatch@^4.4.0: version "4.4.0" resolved "https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz" @@ -4281,11 +4885,6 @@ lodash.isplainobject@^4.0.6: resolved "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz" integrity sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA== -lodash.kebabcase@^4.1.1: - version "4.1.1" - resolved "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz" - integrity sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g== - lodash.map@^4.5.1: version "4.6.0" resolved "https://registry.npmjs.org/lodash.map/-/lodash.map-4.6.0.tgz" @@ -4301,16 +4900,6 @@ lodash.mergewith@^4.6.2: resolved "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz" integrity sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ== -lodash.snakecase@^4.1.1: - version "4.1.1" - resolved "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz" - integrity sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw== - -lodash.startcase@^4.4.0: - version "4.4.0" - resolved "https://registry.npmjs.org/lodash.startcase/-/lodash.startcase-4.4.0.tgz" - integrity sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg== - lodash.truncate@^4.4.2: version "4.4.2" resolved "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz" @@ -4321,11 +4910,6 @@ lodash.uniq@^4.5.0: resolved "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz" integrity sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ== -lodash.upperfirst@^4.3.1: - version "4.3.1" - resolved "https://registry.npmjs.org/lodash.upperfirst/-/lodash.upperfirst-4.3.1.tgz" - integrity sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg== - lodash.zip@^4.2.0: version "4.2.0" resolved "https://registry.npmjs.org/lodash.zip/-/lodash.zip-4.2.0.tgz" @@ -4336,6 +4920,11 @@ lodash@4.17.21, lodash@^4.17.12, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17. resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== +lodash@4.18.1, lodash@^4.18.1: + version "4.18.1" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.18.1.tgz#ff2b66c1f6326d59513de2407bf881439812771c" + integrity sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q== + log-symbols@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/log-symbols/-/log-symbols-1.0.2.tgz" @@ -4351,9 +4940,9 @@ log-symbols@^4.1.0: chalk "^4.1.0" is-unicode-supported "^0.1.0" -log-symbols@^7.0.0: +log-symbols@^7.0.1: version "7.0.1" - resolved "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-7.0.1.tgz#f52e68037d96f589fc572ff2193dc424d48c195b" integrity sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg== dependencies: is-unicode-supported "^2.0.0" @@ -4373,16 +4962,16 @@ longest@^2.0.1: resolved "https://registry.npmjs.org/longest/-/longest-2.0.1.tgz" integrity sha512-Ajzxb8CM6WAnFjgiloPsI3bF+WCxcvhdIG3KNA2KN962+tdBsHcuQ4k4qX/EcS/2CRkcc0iAkR956Nib6aXU/Q== -lru-cache@^10.0.1: - version "10.3.1" - resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-10.3.1.tgz" - integrity sha512-9/8QXrtbGeMB6LxwQd4x1tIMnsmUxMvIH/qWGsccz6bt9Uln3S+sgAaqfQNhbGA8ufzs2fHuP/yqapGgP9Hh2g== - lru-cache@^10.2.0: version "10.4.3" resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz" integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== +lru-cache@^11.1.0: + version "11.5.2" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-11.5.2.tgz#00e16665c90c620fba14a3c368732a976493f760" + integrity sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g== + lru-cache@^6.0.0: version "6.0.0" resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz" @@ -4390,6 +4979,13 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +macos-version@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/macos-version/-/macos-version-6.0.0.tgz#86d901610895eb1252f132d846e92970b5d7d77c" + integrity sha512-O2S8voA+pMfCHhBn/TIYDXzJ1qNHpPDU32oFxglKnVdJABiYYITt45oLkV9yhwA3E2FDwn3tQqUFrTsr1p3sBQ== + dependencies: + semver "^7.3.5" + magic-string@^0.30.17, magic-string@^0.30.21: version "0.30.21" resolved "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz" @@ -4397,13 +4993,13 @@ magic-string@^0.30.17, magic-string@^0.30.21: dependencies: "@jridgewell/sourcemap-codec" "^1.5.5" -magicast@^0.5.1: - version "0.5.1" - resolved "https://registry.npmjs.org/magicast/-/magicast-0.5.1.tgz" - integrity sha512-xrHS24IxaLrvuo613F719wvOIv9xPHFWQHuvGUBmPnCA/3MQxKI3b+r7n1jAoDHmsbC5bRhTZYR77invLAxVnw== +magicast@^0.5.2: + version "0.5.4" + resolved "https://registry.yarnpkg.com/magicast/-/magicast-0.5.4.tgz#bbe38dfd6037670057f33abf22b8aa79d5e99d0a" + integrity sha512-llBEhWm1SacoRwgHUoQJYtwp4PBLF4faQi5TCpIGyGs9n4y5+juI0tDgyKIfpqxckRHaHzouUEph3THklWh03w== dependencies: - "@babel/parser" "^7.28.5" - "@babel/types" "^7.28.5" + "@babel/parser" "^7.29.7" + "@babel/types" "^7.29.7" source-map-js "^1.2.1" make-dir@^4.0.0: @@ -4433,16 +5029,16 @@ media-typer@^1.1.0: resolved "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz" integrity sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw== -meow@^12.0.1: - version "12.1.1" - resolved "https://registry.npmjs.org/meow/-/meow-12.1.1.tgz" - integrity sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw== - -meow@^13.2.0: +meow@^13.0.0: version "13.2.0" - resolved "https://registry.npmjs.org/meow/-/meow-13.2.0.tgz" + resolved "https://registry.yarnpkg.com/meow/-/meow-13.2.0.tgz#6b7d63f913f984063b3cc261b6e8800c4cd3474f" integrity sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA== +meow@^14.1.0: + version "14.1.0" + resolved "https://registry.yarnpkg.com/meow/-/meow-14.1.0.tgz#3cd2d16ad534829ab12fcb5010fc2fdb89facd31" + integrity sha512-EDYo6VlmtnumlcBCbh1gLJ//9jvM/ndXHfVXIFrZVr6fGcwTUyCTFNTLCKuY3ffbK8L/+3Mzqnd58RojiZqHVw== + meow@^8.0.0: version "8.1.2" resolved "https://registry.npmjs.org/meow/-/meow-8.1.2.tgz" @@ -4545,14 +5141,9 @@ mimic-fn@^2.1.0: resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== -mimic-fn@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz" - integrity sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw== - -mimic-function@^5.0.0: +mimic-function@^5.0.1: version "5.0.1" - resolved "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz" + resolved "https://registry.yarnpkg.com/mimic-function/-/mimic-function-5.0.1.tgz#acbe2b3349f99b9deaca7fb70e48b83e94e67076" integrity sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA== min-indent@^1.0.0: @@ -4560,6 +5151,13 @@ min-indent@^1.0.0: resolved "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz" integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== +minimatch@^10.0.3: + version "10.2.6" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.2.6.tgz#fd956bbe0b77241e9f15ac5dccb1c638060968ef" + integrity sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A== + dependencies: + brace-expansion "^5.0.8" + minimatch@^3.0.4, minimatch@^3.1.1: version "3.1.2" resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" @@ -4567,7 +5165,7 @@ minimatch@^3.0.4, minimatch@^3.1.1: dependencies: brace-expansion "^1.1.7" -minimatch@^9.0.0, minimatch@^9.0.4: +minimatch@^9.0.4: version "9.0.5" resolved "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz" integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== @@ -4588,7 +5186,7 @@ minimist@1.2.7: resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz" integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g== -minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6, minimist@^1.2.8: +minimist@1.2.8, minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6, minimist@^1.2.8: version "1.2.8" resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== @@ -4643,10 +5241,10 @@ mute-stream@0.0.8: resolved "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz" integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== -mute-stream@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz" - integrity sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA== +mute-stream@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-3.0.0.tgz#cd8014dd2acb72e1e91bb67c74f0019e620ba2d1" + integrity sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw== mz@^2.7.0: version "2.7.0" @@ -4657,10 +5255,10 @@ mz@^2.7.0: object-assign "^4.0.1" thenify-all "^1.0.0" -nanoid@^3.3.11: - version "3.3.11" - resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz" - integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w== +nanoid@^3.3.17: + version "3.3.18" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.18.tgz#f66a2de1199ffde0fcf21c8a5f13106b1c081913" + integrity sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w== negotiator@^1.0.0: version "1.0.0" @@ -4689,12 +5287,12 @@ nice-try@^1.0.4: resolved "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz" integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== -nock@^14.0.10: - version "14.0.10" - resolved "https://registry.npmjs.org/nock/-/nock-14.0.10.tgz" - integrity sha512-Q7HjkpyPeLa0ZVZC5qpxBt5EyLczFJ91MEewQiIi9taWuA0KB/MDJlUWtON+7dGouVdADTQsf9RA7TZk6D8VMw== +nock@^14.0.17: + version "14.0.17" + resolved "https://registry.yarnpkg.com/nock/-/nock-14.0.17.tgz#356d0ba8cc8ff48194abf93c7f569f10f07b05e7" + integrity sha512-EjRr1weMa4ALQX35AgZTEnP+weJJjlW1KGDiNM2IQC2069YDHas4f4B4UUYR+TTLyKWxJvOz2wObDKQs/LNreA== dependencies: - "@mswjs/interceptors" "^0.39.5" + "@mswjs/interceptors" "^0.41.0" json-stringify-safe "^5.0.1" propagate "^2.0.0" @@ -4718,67 +5316,68 @@ normalize-package-data@^3.0.0: semver "^7.3.4" validate-npm-package-license "^3.0.1" -normalize-package-data@^6.0.0: - version "6.0.2" - resolved "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz" - integrity sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g== +normalize-package-data@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-8.0.0.tgz#bdce7ff2d6ba891b853e179e45a5337766e304a7" + integrity sha512-RWk+PI433eESQ7ounYxIp67CYuVsS1uYSonX3kA6ps/3LWfjVQa/ptEg6Y3T6uAMq1mWpX9PQ+qx+QaHpsc7gQ== dependencies: - hosted-git-info "^7.0.0" + hosted-git-info "^9.0.0" semver "^7.3.5" validate-npm-package-license "^3.0.4" -np@^10.2.0: - version "10.2.0" - resolved "https://registry.npmjs.org/np/-/np-10.2.0.tgz" - integrity sha512-7Pwk8qcsks2c9ETS35aeJSON6uJAbOsx7TwTFzZNUGgH4djT+Yt/p9S7PZuqH5pkcpNUhasne3cDRBzaUtvetg== +np@^12.0.1: + version "12.0.1" + resolved "https://registry.yarnpkg.com/np/-/np-12.0.1.tgz#7de1c73cfe49044e8f95889630ad3206a97b1997" + integrity sha512-BRT4/T8hOfY0+0ZlXC2JO+gPjPYoo9mQVw4D51+L6Aq3PX3aHwgD5lfNpbAUBHa3fZUbkhAUR3RlS00cLubRTw== dependencies: - chalk "^5.4.1" - chalk-template "^1.1.0" - cosmiconfig "^8.3.6" - del "^8.0.0" + chalk "^5.6.2" + chalk-template "^1.1.2" + clipboardy "^5.3.1" + cosmiconfig "^9.0.1" + del "^8.0.1" escape-goat "^4.0.0" escape-string-regexp "^5.0.0" - execa "^8.0.1" - exit-hook "^4.0.0" + execa "^9.6.1" + exit-hook "^5.1.0" github-url-from-git "^1.5.0" - hosted-git-info "^8.0.2" - ignore-walk "^7.0.0" + hosted-git-info "^9.0.2" + ignore-walk "^8.0.0" import-local "^3.2.0" - inquirer "^12.3.2" + inquirer "^14.0.2" is-installed-globally "^1.0.0" is-interactive "^2.0.0" is-scoped "^3.0.0" issue-regex "^4.3.0" listr "^0.14.3" listr-input "^0.2.1" - log-symbols "^7.0.0" - meow "^13.2.0" + log-symbols "^7.0.1" + meow "^14.1.0" new-github-release-url "^2.0.0" - npm-name "^8.0.0" - onetime "^7.0.0" - open "^10.0.4" - p-memoize "^7.1.1" - p-timeout "^6.1.4" + npm-name "^8.1.0" + onetime "^8.0.0" + open "^11.0.0" + p-memoize "^8.0.0" + package-directory "^8.2.0" path-exists "^5.0.0" - pkg-dir "^8.0.0" - read-package-up "^11.0.0" - read-pkg "^9.0.1" - rxjs "^7.8.1" - semver "^7.6.0" + read-package-up "^12.0.0" + read-pkg "^10.1.0" + rxjs "^7.8.2" + semver "^7.7.4" symbol-observable "^4.0.0" - terminal-link "^3.0.0" + terminal-link "^5.0.0" update-notifier "^7.3.1" -npm-name@^8.0.0: - version "8.0.0" - resolved "https://registry.npmjs.org/npm-name/-/npm-name-8.0.0.tgz" - integrity sha512-DIuCGcKYYhASAZW6Xh/tiaGMko8IHOHe0n3zOA7SzTi0Yvy00x8L7sa5yNiZ75Ny58O/KeRtNouy8Ut6gPbKiw== +npm-name@^8.1.0: + version "8.1.1" + resolved "https://registry.yarnpkg.com/npm-name/-/npm-name-8.1.1.tgz#ce7b11ecedf912d5ddbfc82bc3e457e30f9fe352" + integrity sha512-dPKFWBATFnnAuLYCM1h0OmSnYuOWx6R3gEZp1Aj+cnBu3dvGbMKGkLcSCBZHD2Df03bBY/44U0b5AbRVWtkt5g== dependencies: is-scoped "^3.0.0" is-url-superb "^6.1.0" ky "^1.2.0" lodash.zip "^4.2.0" org-regex "^1.0.0" + p-limit "^6.2.0" p-map "^7.0.1" registry-auth-token "^5.0.2" registry-url "^6.0.1" @@ -4791,12 +5390,20 @@ npm-run-path@^2.0.0: dependencies: path-key "^2.0.0" -npm-run-path@^5.1.0: - version "5.3.0" - resolved "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz" - integrity sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ== +npm-run-path@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" + integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== + dependencies: + path-key "^3.0.0" + +npm-run-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-6.0.0.tgz#25cfdc4eae04976f3349c0b1afc089052c362537" + integrity sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA== dependencies: path-key "^4.0.0" + unicorn-magic "^0.3.0" number-is-nan@^1.0.0: version "1.0.1" @@ -4854,36 +5461,31 @@ onetime@^2.0.0: dependencies: mimic-fn "^1.0.0" -onetime@^5.1.0: +onetime@^5.1.0, onetime@^5.1.2: version "5.1.2" resolved "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz" integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== dependencies: mimic-fn "^2.1.0" -onetime@^6.0.0: - version "6.0.0" - resolved "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz" - integrity sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ== - dependencies: - mimic-fn "^4.0.0" - -onetime@^7.0.0: - version "7.0.0" - resolved "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz" - integrity sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ== +onetime@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-8.0.0.tgz#ce03ca837d5089fa7e637c22d9f4188ffff2ada6" + integrity sha512-ODz2CnF+9043eG3exS38Efor4ekggmKvIydHaXWr1n9mXA5Afn8BdSsvVpa5AhBWricxhdBMfyeG3Yo3SB+VWA== dependencies: - mimic-function "^5.0.0" + mimic-function "^5.0.1" -open@^10.0.4: - version "10.1.0" - resolved "https://registry.npmjs.org/open/-/open-10.1.0.tgz" - integrity sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw== +open@^11.0.0: + version "11.0.2" + resolved "https://registry.yarnpkg.com/open/-/open-11.0.2.tgz#146069263c50839eebbac6e5810f7da9ac80ab2a" + integrity sha512-RWqF+pBSkqecEvCKOn8QYhaNdRMJDZRIrlS/7rTDdLHaPcfXGCZ/h8zb413NfvdeAV0MR7T1yJcA34/q+CSm1Q== dependencies: - default-browser "^5.2.1" + default-browser "^5.5.1" define-lazy-prop "^3.0.0" + is-in-ssh "^1.0.0" is-inside-container "^1.0.0" - is-wsl "^3.1.0" + powershell-utils "^0.2.1" + wsl-utils "^1.0.0" ora@^5.4.1: version "5.4.1" @@ -4941,17 +5543,17 @@ p-limit@^3.0.2: dependencies: yocto-queue "^0.1.0" -p-limit@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz" - integrity sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ== +p-limit@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-6.2.0.tgz#c254d22ba6aeef441a3564c5e6c2f2da59268a0f" + integrity sha512-kuUqqHNUqoIWp/c467RI4X6mmyuojY5jGutNU0wVTmEOOfcuwLqyMVoAi9MKi2Ak+5i9+nhmrK4ufZE8069kHA== dependencies: - yocto-queue "^1.0.0" + yocto-queue "^1.1.1" -p-limit@^7.2.0: - version "7.2.0" - resolved "https://registry.npmjs.org/p-limit/-/p-limit-7.2.0.tgz" - integrity sha512-ATHLtwoTNDloHRFFxFJdHnG6n2WUeFjaR8XQMFdKIv0xkXjrER8/iG9iu265jOM95zXHAfv9oTkqhrfbIzosrQ== +p-limit@^7.3.1: + version "7.3.1" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-7.3.1.tgz#ded48cbfa10b161a9928120261fa82c9a282eb3f" + integrity sha512-0trZaiG7Y7kN/Egy9a8j47t9osC0Tch4PaIWd9yGF6bvmlk7muExRvGNYb8sXBwEKMoNKsbNN9P8EefuQekE4Q== dependencies: yocto-queue "^1.2.1" @@ -4983,13 +5585,6 @@ p-locate@^5.0.0: dependencies: p-limit "^3.0.2" -p-locate@^6.0.0: - version "6.0.0" - resolved "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz" - integrity sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw== - dependencies: - p-limit "^4.0.0" - p-map@^2.0.0: version "2.1.0" resolved "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz" @@ -5005,26 +5600,26 @@ p-map@^7.0.2: resolved "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz" integrity sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ== -p-memoize@^7.1.1: - version "7.1.1" - resolved "https://registry.npmjs.org/p-memoize/-/p-memoize-7.1.1.tgz" - integrity sha512-DZ/bONJILHkQ721hSr/E9wMz5Am/OTJ9P6LhLFo2Tu+jL8044tgc9LwHO8g4PiaYePnlVVRAJcKmgy8J9MVFrA== +p-memoize@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/p-memoize/-/p-memoize-8.0.0.tgz#1312d5a1ff5e902ef2d77b6ccadf00c67f127b6f" + integrity sha512-jdZ10MCxavHoIHwJ5oweOtYy6ElPixEHaMkz0AuaEMovR1MRpVvYFzIEHRxgMEpXYzNpRVByFAniAzwmd1/uug== dependencies: - mimic-fn "^4.0.0" - type-fest "^3.0.0" + mimic-function "^5.0.1" + type-fest "^4.41.0" -p-queue@^8.0.1: - version "8.1.1" - resolved "https://registry.npmjs.org/p-queue/-/p-queue-8.1.1.tgz" - integrity sha512-aNZ+VfjobsWryoiPnEApGGmf5WmNsCo9xu8dfaYamG5qaLP7ClhLN6NgsFe6SwJ2UbLEBK5dv9x8Mn5+RVhMWQ== +p-queue@^9.3.3: + version "9.3.3" + resolved "https://registry.yarnpkg.com/p-queue/-/p-queue-9.3.3.tgz#c5e528c7eb361b7ba8eb5a9406902f7f2e75fc8f" + integrity sha512-NXAOdnEe5FsZJfT4oK84lE1Y5cFFdWlRuOo5tww8DyNMxyRXwn39fIkUtNLKppcPC+UYU/bXujNCUGDv01y7CA== dependencies: - eventemitter3 "^5.0.1" - p-timeout "^6.1.2" + eventemitter3 "^5.0.4" + p-timeout "^7.0.0" -p-timeout@^6.1.2, p-timeout@^6.1.4: - version "6.1.4" - resolved "https://registry.npmjs.org/p-timeout/-/p-timeout-6.1.4.tgz" - integrity sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg== +p-timeout@^7.0.0: + version "7.0.1" + resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-7.0.1.tgz#95680a6aa693c530f14ac337b8bd32d4ec6ae4f0" + integrity sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg== p-try@^1.0.0: version "1.0.0" @@ -5036,6 +5631,13 @@ p-try@^2.0.0: resolved "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== +package-directory@^8.2.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/package-directory/-/package-directory-8.2.0.tgz#b4f9df2e56782beb1d805945e2c530d29a3806f9" + integrity sha512-qJSu5Mo6tHmRxCy2KCYYKYgcfBdUpy9dwReaZD/xwf608AUk/MoRtIOWzgDtUeGeC7n/55yC3MI1Q+MbSoektw== + dependencies: + find-up-simple "^1.0.0" + package-json-from-dist@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz" @@ -5076,14 +5678,19 @@ parse-json@^5.0.0, parse-json@^5.2.0: json-parse-even-better-errors "^2.3.0" lines-and-columns "^1.1.6" -parse-json@^8.0.0: - version "8.1.0" - resolved "https://registry.npmjs.org/parse-json/-/parse-json-8.1.0.tgz" - integrity sha512-rum1bPifK5SSar35Z6EKZuYPJx85pkNaFrxBK3mwdfSJ1/WKbYrjoW/zTPSjRRamfmVX1ACBIdFAO0VRErW/EA== +parse-json@^8.3.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-8.3.0.tgz#88a195a2157025139a2317a4f2f9252b61304ed5" + integrity sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ== dependencies: - "@babel/code-frame" "^7.22.13" - index-to-position "^0.1.2" - type-fest "^4.7.1" + "@babel/code-frame" "^7.26.2" + index-to-position "^1.1.0" + type-fest "^4.39.1" + +parse-ms@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/parse-ms/-/parse-ms-4.0.0.tgz#c0c058edd47c2a590151a718990533fd62803df4" + integrity sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw== parse-passwd@^1.0.0: version "1.0.0" @@ -5120,7 +5727,7 @@ path-key@^2.0.0, path-key@^2.0.1: resolved "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz" integrity sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw== -path-key@^3.1.0: +path-key@^3.0.0, path-key@^3.1.0: version "3.1.1" resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== @@ -5160,11 +5767,6 @@ path-type@^3.0.0: dependencies: pify "^3.0.0" -path-type@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz" - integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== - path-type@^6.0.0: version "6.0.0" resolved "https://registry.npmjs.org/path-type/-/path-type-6.0.0.tgz" @@ -5195,6 +5797,11 @@ picomatch@^4.0.3: resolved "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz" integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q== +picomatch@^4.0.4, picomatch@^4.0.5: + version "4.0.7" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.7.tgz#6313360034ccb36b3dc61ecbdff78121f90fe21f" + integrity sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA== + pify@^2.3.0: version "2.3.0" resolved "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz" @@ -5251,7 +5858,7 @@ pino-std-serializers@^7.0.0: resolved "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz" integrity sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA== -pino@^10.0.0, pino@^10.2.0: +pino@^10.0.0: version "10.2.0" resolved "https://registry.npmjs.org/pino/-/pino-10.2.0.tgz" integrity sha512-NFnZqUliT+OHkRXVSf8vdOr13N1wv31hRryVjqbreVh/SDCNaI6mnRDDq89HVRCbem1SAl7yj04OANeqP0nT6A== @@ -5268,6 +5875,23 @@ pino@^10.0.0, pino@^10.2.0: sonic-boom "^4.0.1" thread-stream "^4.0.0" +pino@^10.3.1: + version "10.3.1" + resolved "https://registry.yarnpkg.com/pino/-/pino-10.3.1.tgz#6552c8f8d8481844c9e452e7bf0be90bff1939ce" + integrity sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg== + dependencies: + "@pinojs/redact" "^0.4.0" + atomic-sleep "^1.0.0" + on-exit-leak-free "^2.1.0" + pino-abstract-transport "^3.0.0" + pino-std-serializers "^7.0.0" + process-warning "^5.0.0" + quick-format-unescaped "^4.0.3" + real-require "^0.2.0" + safe-stable-stringify "^2.3.1" + sonic-boom "^4.0.1" + thread-stream "^4.0.0" + pino@^6.13.0: version "6.14.0" resolved "https://registry.npmjs.org/pino/-/pino-6.14.0.tgz" @@ -5293,13 +5917,6 @@ pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" -pkg-dir@^8.0.0: - version "8.0.0" - resolved "https://registry.npmjs.org/pkg-dir/-/pkg-dir-8.0.0.tgz" - integrity sha512-4peoBq4Wks0riS0z8741NVv+/8IiTvqnZAr8QGgtdifrtpdXbNw/FxRS1l6NFqm4EMzuS0EDqNNx4XGaz8cuyQ== - dependencies: - find-up-simple "^1.0.0" - pkg-types@^1.3.1: version "1.3.1" resolved "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz" @@ -5316,20 +5933,37 @@ postcss-load-config@^6.0.1: dependencies: lilconfig "^3.1.1" -postcss@^8.5.6: - version "8.5.6" - resolved "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz" - integrity sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg== +postcss@^8.5.26: + version "8.5.26" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.26.tgz#6e75135780c7e10df3433bf2266c552d35c8c620" + integrity sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ== dependencies: - nanoid "^3.3.11" + nanoid "^3.3.17" picocolors "^1.1.1" source-map-js "^1.2.1" +powershell-utils@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/powershell-utils/-/powershell-utils-0.1.0.tgz#5a42c9a824fb4f2f251ccb41aaae73314f5d6ac2" + integrity sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A== + +powershell-utils@^0.2.0, powershell-utils@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/powershell-utils/-/powershell-utils-0.2.1.tgz#0853016f5b3c12d52b9571c8fe8059677d9ddbe2" + integrity sha512-C+y9x90UElAddDZmV4qOx9W53B61PO7cIqWz2dQsWlwswuq4mr8NEwytdGKboYbQlGZ3awrkTeNvcZiZNHnQ8A== + presentable-error@^0.0.1: version "0.0.1" resolved "https://registry.npmjs.org/presentable-error/-/presentable-error-0.0.1.tgz" integrity sha512-E6rsNU1QNJgB3sjj7OANinGncFKuK+164sLXw1/CqBjj/EkXSoSdHCtWQGBNlREIGLnL7IEUEGa08YFVUbrhVg== +pretty-ms@^9.2.0: + version "9.3.1" + resolved "https://registry.yarnpkg.com/pretty-ms/-/pretty-ms-9.3.1.tgz#7b58d8fbf376f01602ff05230358e8fa5e0f072b" + integrity sha512-HzMy3Geq23nVALD/M2LliU+F+M+gVNsvkQWWqeBZ8HDiCgzo6YPJ/Omrmtq24EFrIsk0a3EkQGEd7bDOo+IhGA== + dependencies: + parse-ms "^4.0.0" + process-nextick-args@~2.0.0: version "2.0.1" resolved "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz" @@ -5345,9 +5979,9 @@ process-warning@^5.0.0: resolved "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz" integrity sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA== -prom-client@>=11.5.3: +prom-client@>=11.5.3, prom-client@^14.2.0: version "14.2.0" - resolved "https://registry.npmjs.org/prom-client/-/prom-client-14.2.0.tgz" + resolved "https://registry.yarnpkg.com/prom-client/-/prom-client-14.2.0.tgz#ca94504e64156f6506574c25fb1c34df7812cf11" integrity sha512-sF308EhTenb/pDRPakm+WgiN+VdM/T1RaHj1x+MvAuT8UiQP8JmOEbxVqtkbfR4LrvOg5n7ic01kRBDGXjYikA== dependencies: tdigest "^0.1.1" @@ -5454,14 +6088,14 @@ rc@1.2.8: minimist "^1.2.0" strip-json-comments "~2.0.1" -read-package-up@^11.0.0: - version "11.0.0" - resolved "https://registry.npmjs.org/read-package-up/-/read-package-up-11.0.0.tgz" - integrity sha512-MbgfoNPANMdb4oRBNg5eqLbB2t2r+o5Ua1pNt8BqGp4I0FJZhuVSOj3PaBPni4azWuSzEdNn2evevzVmEk1ohQ== +read-package-up@^12.0.0: + version "12.0.0" + resolved "https://registry.yarnpkg.com/read-package-up/-/read-package-up-12.0.0.tgz#7ae889586f397b7a291ca59ce08caf7e9f68a61c" + integrity sha512-Q5hMVBYur/eQNWDdbF4/Wqqr9Bjvtrw2kjGxxBbKLbx8bVCL8gcArjTy8zDUuLGQicftpMuU0riQNcAsbtOVsw== dependencies: - find-up-simple "^1.0.0" - read-pkg "^9.0.0" - type-fest "^4.6.0" + find-up-simple "^1.0.1" + read-pkg "^10.0.0" + type-fest "^5.2.0" read-pkg-up@^3.0.0: version "3.0.0" @@ -5480,6 +6114,17 @@ read-pkg-up@^7.0.1: read-pkg "^5.2.0" type-fest "^0.8.1" +read-pkg@^10.0.0, read-pkg@^10.1.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-10.1.0.tgz#eff31c7e505a4995a85c5af017b3dc413745431c" + integrity sha512-I8g2lArQiP78ll51UeMZojewtYgIRCKCWqZEgOO8c/uefTI+XDXvCSXu3+YNUaTNvZzobrL5+SqHjBrByRRTdg== + dependencies: + "@types/normalize-package-data" "^2.4.4" + normalize-package-data "^8.0.0" + parse-json "^8.3.0" + type-fest "^5.4.4" + unicorn-magic "^0.4.0" + read-pkg@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz" @@ -5499,17 +6144,6 @@ read-pkg@^5.2.0: parse-json "^5.0.0" type-fest "^0.6.0" -read-pkg@^9.0.0, read-pkg@^9.0.1: - version "9.0.1" - resolved "https://registry.npmjs.org/read-pkg/-/read-pkg-9.0.1.tgz" - integrity sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA== - dependencies: - "@types/normalize-package-data" "^2.4.3" - normalize-package-data "^6.0.0" - parse-json "^8.0.0" - type-fest "^4.6.0" - unicorn-magic "^0.1.0" - readable-stream@3, readable-stream@^3.0.0, readable-stream@^3.0.2, readable-stream@^3.4.0: version "3.6.2" resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz" @@ -5606,11 +6240,6 @@ resolve-from@^5.0.0: resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz" integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== -resolve-pkg-maps@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz" - integrity sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw== - resolve@^1.1.6, resolve@^1.10.0: version "1.22.8" resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz" @@ -5651,7 +6280,31 @@ rfdc@^1.1.4, rfdc@^1.2.0: resolved "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz" integrity sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA== -rollup@^4.34.8, rollup@^4.43.0: +rolldown@~1.2.4: + version "1.2.6" + resolved "https://registry.yarnpkg.com/rolldown/-/rolldown-1.2.6.tgz#025f5c11975cc70129ea9c76c54f35dd35881786" + integrity sha512-vMM4q3aixf46GiF1Kok8jDPFsEpXgFWGjUHXNkNHNm+Y2adXAG2dbX91jkti3i0ZRsOlcmbuzAz1poObSHCmUA== + dependencies: + "@oxc-project/types" "=0.147.0" + "@rolldown/pluginutils" "^1.0.0" + optionalDependencies: + "@rolldown/binding-android-arm-eabi" "1.2.6" + "@rolldown/binding-android-arm64" "1.2.6" + "@rolldown/binding-darwin-arm64" "1.2.6" + "@rolldown/binding-darwin-x64" "1.2.6" + "@rolldown/binding-freebsd-x64" "1.2.6" + "@rolldown/binding-linux-arm-gnueabihf" "1.2.6" + "@rolldown/binding-linux-arm64-gnu" "1.2.6" + "@rolldown/binding-linux-arm64-musl" "1.2.6" + "@rolldown/binding-linux-ppc64-gnu" "1.2.6" + "@rolldown/binding-linux-s390x-gnu" "1.2.6" + "@rolldown/binding-linux-x64-gnu" "1.2.6" + "@rolldown/binding-linux-x64-musl" "1.2.6" + "@rolldown/binding-openharmony-arm64" "1.2.6" + "@rolldown/binding-win32-arm64-msvc" "1.2.6" + "@rolldown/binding-win32-x64-msvc" "1.2.6" + +rollup@^4.34.8: version "4.55.1" resolved "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz" integrity sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A== @@ -5711,6 +6364,16 @@ run-async@^4.0.6: resolved "https://registry.npmjs.org/run-async/-/run-async-4.0.6.tgz" integrity sha512-IoDlSLTs3Yq593mb3ZoKWKXMNu3UpObxhgA/Xuid5p4bbfi2jdY1Hj0m1K+0/tEuQTxIGMhQDqGjKb7RuxGpAQ== +run-jxa@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/run-jxa/-/run-jxa-3.0.0.tgz#7d7a0fb183cef4b7132237bcfeb58cb705aecbfc" + integrity sha512-4f2CrY7H+sXkKXJn/cE6qRA3z+NMVO7zvlZ/nUV0e62yWftpiLAfw5eV9ZdomzWd2TXWwEIiGjAT57+lWIzzvA== + dependencies: + execa "^5.1.1" + macos-version "^6.0.0" + subsume "^4.0.0" + type-fest "^2.0.0" + run-parallel@^1.1.9: version "1.2.0" resolved "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz" @@ -5725,7 +6388,7 @@ rxjs@^6.3.3, rxjs@^6.4.0, rxjs@^6.5.3, rxjs@^6.6.0: dependencies: tslib "^1.9.0" -rxjs@^7.5.5, rxjs@^7.8.1: +rxjs@^7.5.5: version "7.8.1" resolved "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz" integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg== @@ -5801,6 +6464,11 @@ semver@^7.1.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.5.3, semve resolved "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz" integrity sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w== +semver@^7.5.2, semver@^7.7.4: + version "7.8.5" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.8.5.tgz#39b646037dd50c14fb451e7e4cac58ed8b863f69" + integrity sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA== + semver@^7.6.3: version "7.7.3" resolved "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz" @@ -5971,7 +6639,7 @@ siginfo@^2.0.0: resolved "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz" integrity sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g== -signal-exit@^3.0.0, signal-exit@^3.0.2: +signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3: version "3.0.7" resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== @@ -6110,10 +6778,10 @@ statuses@^2.0.1, statuses@^2.0.2, statuses@~2.0.2: resolved "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz" integrity sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw== -std-env@^3.10.0: - version "3.10.0" - resolved "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz" - integrity sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg== +std-env@^4.0.0-rc.1: + version "4.2.0" + resolved "https://registry.yarnpkg.com/std-env/-/std-env-4.2.0.tgz#8ebe0ec60485668ab47227b312f4254cdf80c9d3" + integrity sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw== strict-event-emitter@^0.5.1: version "0.5.1" @@ -6261,10 +6929,15 @@ strip-eof@^1.0.0: resolved "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz" integrity sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q== -strip-final-newline@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz" - integrity sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw== +strip-final-newline@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" + integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== + +strip-final-newline@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-4.0.0.tgz#35a369ec2ac43df356e3edd5dcebb6429aa1fa5c" + integrity sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw== strip-indent@^3.0.0: version "3.0.0" @@ -6300,6 +6973,14 @@ stubborn-utils@^1.0.1: resolved "https://registry.npmjs.org/stubborn-utils/-/stubborn-utils-1.0.2.tgz" integrity sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg== +subsume@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/subsume/-/subsume-4.0.0.tgz#506c72bfb76596ebc6f96cbdccceadda41ab0849" + integrity sha512-BWnYJElmHbYZ/zKevy+TG+SsyoFCmRPDHJbR1MzLxkPOv1Jp/4hGhVUtP98s+wZBsBsHwCXvPTP0x287/WMjGg== + dependencies: + escape-string-regexp "^5.0.0" + unique-string "^3.0.0" + sucrase@^3.35.0: version "3.35.0" resolved "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz" @@ -6337,6 +7018,11 @@ supertest@^7.2.2: methods "^1.1.2" superagent "^10.3.0" +supports-color@^10.2.2: + version "10.2.2" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-10.2.2.tgz#466c2978cc5cd0052d542a0b576461c2b802ebb4" + integrity sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g== + supports-color@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz" @@ -6349,20 +7035,20 @@ supports-color@^5.3.0: dependencies: has-flag "^3.0.0" -supports-color@^7.0.0, supports-color@^7.1.0: +supports-color@^7.1.0: version "7.2.0" resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz" integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== dependencies: has-flag "^4.0.0" -supports-hyperlinks@^2.2.0: - version "2.3.0" - resolved "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz" - integrity sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA== +supports-hyperlinks@^4.1.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-4.5.0.tgz#5e5cd74542a31ae6fdfbae75e1362b7b009c74cd" + integrity sha512-ZW2OvfeCXrNTbLakPUzjQG922EeGCOteFSVoek5DKStTh898wf7zgtuFlzQN8HfZCxC3Eh02yJVrRW51hADf+w== dependencies: - has-flag "^4.0.0" - supports-color "^7.0.0" + has-flag "^5.0.1" + supports-color "^10.2.2" supports-preserve-symlinks-flag@^1.0.0: version "1.0.0" @@ -6394,6 +7080,11 @@ symbol-observable@^4.0.0: resolved "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz" integrity sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ== +system-architecture@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/system-architecture/-/system-architecture-0.1.0.tgz#71012b3ac141427d97c67c56bc7921af6bff122d" + integrity sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA== + table@^6.9.0: version "6.9.0" resolved "https://registry.npmjs.org/table/-/table-6.9.0.tgz" @@ -6405,6 +7096,11 @@ table@^6.9.0: string-width "^4.2.3" strip-ansi "^6.0.1" +tagged-tag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/tagged-tag/-/tagged-tag-1.0.0.tgz#a0b5917c2864cba54841495abfa3f6b13edcf4d6" + integrity sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng== + tdigest@^0.1.1: version "0.1.2" resolved "https://registry.npmjs.org/tdigest/-/tdigest-0.1.2.tgz" @@ -6412,24 +7108,19 @@ tdigest@^0.1.1: dependencies: bintrees "1.0.2" -terminal-link@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/terminal-link/-/terminal-link-3.0.0.tgz" - integrity sha512-flFL3m4wuixmf6IfhFJd1YPiLiMuxEc8uHRM1buzIeZPm22Au2pDqBJQgdo7n1WfPU1ONFGv7YDwpFBmHGF6lg== +terminal-link@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/terminal-link/-/terminal-link-5.0.0.tgz#f0447c8940418ab49b9b9bbc47a4ad2fa8ba81e7" + integrity sha512-qFAy10MTMwjzjU8U16YS4YoZD+NQLHzLssFMNqgravjbvIPNiqkGFR4yjhJfmY9R5OFU7+yHxc6y+uGHkKwLRA== dependencies: - ansi-escapes "^5.0.0" - supports-hyperlinks "^2.2.0" + ansi-escapes "^7.0.0" + supports-hyperlinks "^4.1.0" text-extensions@^1.0.0: version "1.9.0" resolved "https://registry.npmjs.org/text-extensions/-/text-extensions-1.9.0.tgz" integrity sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ== -text-extensions@^2.0.0: - version "2.4.0" - resolved "https://registry.npmjs.org/text-extensions/-/text-extensions-2.4.0.tgz" - integrity sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g== - thenify-all@^1.0.0: version "1.6.0" resolved "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz" @@ -6499,10 +7190,18 @@ tinyglobby@^0.2.11, tinyglobby@^0.2.15: fdir "^6.5.0" picomatch "^4.0.3" -tinyrainbow@^3.0.3: - version "3.0.3" - resolved "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz" - integrity sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q== +tinyglobby@^0.2.17: + version "0.2.17" + resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.17.tgz#562a9a6c9eb2b3b123d39719f9af5bb44fcd7631" + integrity sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g== + dependencies: + fdir "^6.5.0" + picomatch "^4.0.4" + +tinyrainbow@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/tinyrainbow/-/tinyrainbow-3.1.1.tgz#c0168387d3d8d70b6b3c2c0936de5fee738cea20" + integrity sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw== tmp-promise@^3.0.3: version "3.0.3" @@ -6588,13 +7287,12 @@ tsup@^8.5.1: tinyglobby "^0.2.11" tree-kill "^1.2.2" -tsx@^4.21.0: - version "4.21.0" - resolved "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz" - integrity sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw== +tsx@^4.23.12: + version "4.23.12" + resolved "https://registry.yarnpkg.com/tsx/-/tsx-4.23.12.tgz#3a4919591cd9b9e00011b75e596c8ab8db23c09c" + integrity sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q== dependencies: - esbuild "~0.27.0" - get-tsconfig "^4.7.5" + esbuild "~0.28.0" optionalDependencies: fsevents "~2.3.3" @@ -6618,30 +7316,27 @@ type-fest@^0.8.1: resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== -type-fest@^1.0.2: +type-fest@^1.0.1: version "1.4.0" - resolved "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-1.4.0.tgz#e9fb813fe3bf1744ec359d55d1affefa76f14be1" integrity sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA== -type-fest@^2.5.1: +type-fest@^2.0.0, type-fest@^2.5.1: version "2.19.0" resolved "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz" integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA== -type-fest@^3.0.0: - version "3.13.1" - resolved "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz" - integrity sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g== - -type-fest@^4.18.2, type-fest@^4.21.0: +type-fest@^4.18.2, type-fest@^4.21.0, type-fest@^4.39.1, type-fest@^4.41.0: version "4.41.0" resolved "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz" integrity sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA== -type-fest@^4.6.0, type-fest@^4.7.1: - version "4.21.0" - resolved "https://registry.npmjs.org/type-fest/-/type-fest-4.21.0.tgz" - integrity sha512-ADn2w7hVPcK6w1I0uWnM//y1rLXZhzB9mr0a3OirzclKF1Wp6VzevUmzz/NRAWunOT6E8HrnpGY7xOfc6K57fA== +type-fest@^5.2.0, type-fest@^5.4.4: + version "5.8.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-5.8.0.tgz#6d517998257c33159db4d4da6f18efa33bd47df3" + integrity sha512-YGYEVz3Fm5iy/AybuA0oyNFq7H4CgQNfRp/qfe8nurE1kuCeNm3/vfm9X4Mtl+qLyaKJUh5xrFZwogr41SMjYA== + dependencies: + tagged-tag "^1.0.0" type-is@^2.0.1: version "2.0.1" @@ -6657,10 +7352,31 @@ typedarray@^0.0.6: resolved "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz" integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== -typescript@^5.9.3: - version "5.9.3" - resolved "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz" - integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw== +typescript@7.0.2: + version "7.0.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-7.0.2.tgz#9ec773d7954a8c182c17cc5bbd575aa28bc51582" + integrity sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA== + optionalDependencies: + "@typescript/typescript-aix-ppc64" "7.0.2" + "@typescript/typescript-darwin-arm64" "7.0.2" + "@typescript/typescript-darwin-x64" "7.0.2" + "@typescript/typescript-freebsd-arm64" "7.0.2" + "@typescript/typescript-freebsd-x64" "7.0.2" + "@typescript/typescript-linux-arm" "7.0.2" + "@typescript/typescript-linux-arm64" "7.0.2" + "@typescript/typescript-linux-loong64" "7.0.2" + "@typescript/typescript-linux-mips64el" "7.0.2" + "@typescript/typescript-linux-ppc64" "7.0.2" + "@typescript/typescript-linux-riscv64" "7.0.2" + "@typescript/typescript-linux-s390x" "7.0.2" + "@typescript/typescript-linux-x64" "7.0.2" + "@typescript/typescript-netbsd-arm64" "7.0.2" + "@typescript/typescript-netbsd-x64" "7.0.2" + "@typescript/typescript-openbsd-arm64" "7.0.2" + "@typescript/typescript-openbsd-x64" "7.0.2" + "@typescript/typescript-sunos-x64" "7.0.2" + "@typescript/typescript-win32-arm64" "7.0.2" + "@typescript/typescript-win32-x64" "7.0.2" ufo@^1.6.1: version "1.6.3" @@ -6677,21 +7393,33 @@ undici-types@~7.16.0: resolved "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz" integrity sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw== -undici@^7.18.2: - version "7.18.2" - resolved "https://registry.npmjs.org/undici/-/undici-7.18.2.tgz" - integrity sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw== +undici-types@~8.3.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-8.3.0.tgz#44e9fc9f3244648cdea35e4f9bb2d681e9410809" + integrity sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ== -unicorn-magic@^0.1.0: - version "0.1.0" - resolved "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz" - integrity sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ== +undici@^8.10.0: + version "8.10.0" + resolved "https://registry.yarnpkg.com/undici/-/undici-8.10.0.tgz#67ed7c4087f0f40fba7bef3a46f2be80572f2473" + integrity sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ== unicorn-magic@^0.3.0: version "0.3.0" resolved "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz" integrity sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA== +unicorn-magic@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/unicorn-magic/-/unicorn-magic-0.4.0.tgz#78c6a090fd6d07abd2468b83b385603e00dfdb24" + integrity sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw== + +unique-string@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-3.0.0.tgz#84a1c377aff5fd7a8bc6b55d8244b2bd90d75b9a" + integrity sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ== + dependencies: + crypto-random-string "^4.0.0" + universalify@^2.0.0: version "2.0.1" resolved "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz" @@ -6753,44 +7481,43 @@ vary@^1.1.2, vary@~1.1.2: resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz" integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== -"vite@^6.0.0 || ^7.0.0": - version "7.3.1" - resolved "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz" - integrity sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA== +"vite@^6.0.0 || ^7.0.0 || ^8.0.0": + version "8.2.2" + resolved "https://registry.yarnpkg.com/vite/-/vite-8.2.2.tgz#399aefad3656145145be110d137a07ea5bb55014" + integrity sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q== dependencies: - esbuild "^0.27.0" - fdir "^6.5.0" - picomatch "^4.0.3" - postcss "^8.5.6" - rollup "^4.43.0" - tinyglobby "^0.2.15" + lightningcss "^1.33.0" + picomatch "^4.0.5" + postcss "^8.5.26" + rolldown "~1.2.4" + tinyglobby "^0.2.17" optionalDependencies: fsevents "~2.3.3" -vitest@^4.0.17: - version "4.0.17" - resolved "https://registry.npmjs.org/vitest/-/vitest-4.0.17.tgz" - integrity sha512-FQMeF0DJdWY0iOnbv466n/0BudNdKj1l5jYgl5JVTwjSsZSlqyXFt/9+1sEyhR6CLowbZpV7O1sCHrzBhucKKg== - dependencies: - "@vitest/expect" "4.0.17" - "@vitest/mocker" "4.0.17" - "@vitest/pretty-format" "4.0.17" - "@vitest/runner" "4.0.17" - "@vitest/snapshot" "4.0.17" - "@vitest/spy" "4.0.17" - "@vitest/utils" "4.0.17" - es-module-lexer "^1.7.0" - expect-type "^1.2.2" +vitest@^4.1.11: + version "4.1.11" + resolved "https://registry.yarnpkg.com/vitest/-/vitest-4.1.11.tgz#1653c1521ae917f960d9b21877797c47dfd8bf21" + integrity sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw== + dependencies: + "@vitest/expect" "4.1.11" + "@vitest/mocker" "4.1.11" + "@vitest/pretty-format" "4.1.11" + "@vitest/runner" "4.1.11" + "@vitest/snapshot" "4.1.11" + "@vitest/spy" "4.1.11" + "@vitest/utils" "4.1.11" + es-module-lexer "^2.0.0" + expect-type "^1.3.0" magic-string "^0.30.21" obug "^2.1.1" pathe "^2.0.3" picomatch "^4.0.3" - std-env "^3.10.0" + std-env "^4.0.0-rc.1" tinybench "^2.9.0" tinyexec "^1.0.2" tinyglobby "^0.2.15" - tinyrainbow "^3.0.3" - vite "^6.0.0 || ^7.0.0" + tinyrainbow "^3.1.0" + vite "^6.0.0 || ^7.0.0 || ^8.0.0" why-is-node-running "^2.3.0" wcwidth@^1.0.1: @@ -6861,9 +7588,9 @@ wrap-ansi@^3.0.1: string-width "^2.1.1" strip-ansi "^4.0.0" -wrap-ansi@^6.2.0: +wrap-ansi@^6.0.1: version "6.2.0" - resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== dependencies: ansi-styles "^4.0.0" @@ -6902,6 +7629,14 @@ wrappy@1: resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== +wsl-utils@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/wsl-utils/-/wsl-utils-1.0.0.tgz#e113d9964e766657c53f5d570da1bbbdfee7f837" + integrity sha512-Hl0ZOAs672vg+06kfujwRhoS6/jehvULrlFkuF2dRu6pHgA8U06h3xqNIqNNU1LTXPcedxByAR4GS6pwQK0mgA== + dependencies: + is-wsl "^3.1.0" + powershell-utils "^0.1.0" + xdg-basedir@^5.1.0: version "5.1.0" resolved "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-5.1.0.tgz" @@ -6963,21 +7698,11 @@ yocto-queue@^0.1.0: resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== -yocto-queue@^1.0.0: - version "1.1.1" - resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.1.1.tgz" - integrity sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g== - -yocto-queue@^1.2.1: +yocto-queue@^1.1.1, yocto-queue@^1.2.1: version "1.2.2" resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz" integrity sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ== -yoctocolors-cjs@^2.1.3: - version "2.1.3" - resolved "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz" - integrity sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw== - yoctocolors@^2.1.1: version "2.1.2" resolved "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz"