diff --git a/.env.example b/.env.example index 8e98547..a9af54b 100644 --- a/.env.example +++ b/.env.example @@ -6,9 +6,28 @@ SEATABLE_MOCK= # Server mode: selfhosted (default) or managed (multi-tenant HTTP with per-client auth) SEATABLE_MODE=selfhosted +# Required in managed mode (min. 32 chars). Seals the OAuth access/refresh tokens and +# client registrations the server issues, so the raw SeaTable API token is never handed +# to a client. Must stay stable across restarts: generate with `openssl rand -hex 32`. +# SEATABLE_TOKEN_SECRET= + # Multi-base mode (selfhosted only): serve multiple bases from one process. # JSON array with base_name and api_token. Use instead of SEATABLE_API_TOKEN. # SEATABLE_BASES='[{"base_name":"CRM","api_token":"token_abc"},{"base_name":"Projects","api_token":"token_def"}]' # CORS: comma-separated list of allowed origins (HTTP mode only). If empty, CORS is disabled. # CORS_ALLOWED_ORIGINS=https://cloud.seatable.io,https://seatable-demo.de,http://localhost:3000 + +# OAuth (managed mode only): comma-separated hosts whose https callbacks are shown +# without the "unknown destination" confirmation step. Loopback addresses and +# private-use app schemes (cursor://, vscode://, ...) never need an entry — the +# code stays on the user's machine. Unknown https hosts still work; the user just +# has to confirm the destination first. Unset = the built-in list of hosted MCP +# clients. A single '*' disables the confirmation entirely (not recommended). +# SEATABLE_OAUTH_TRUSTED_REDIRECT_HOSTS=claude.ai,claude.com,chatgpt.com + +# Lifetime of an issued OAuth access token, in seconds (default 3600, range 30..2592000). +# Lower it to narrow the window after a SeaTable token is revoked; raise it if real +# clients renew badly and would otherwise re-prompt their users. Useful for testing: +# set it to 120 and you see within minutes whether a client refreshes silently. +# SEATABLE_ACCESS_TOKEN_TTL=3600 diff --git a/CLAUDE.md b/CLAUDE.md index ac50b99..c8dc767 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -33,6 +33,8 @@ Required: `SEATABLE_SERVER_URL` Auth (one required in selfhosted): `SEATABLE_API_TOKEN` (single-base) or `SEATABLE_BASES` (multi-base, JSON array `'[{"base_name":"CRM","api_token":"..."}]'`) +Required in managed mode: `SEATABLE_TOKEN_SECRET` (min. 32 chars, stable across restarts) — seals issued OAuth tokens and client registrations. + Optional: `SEATABLE_MODE` (`selfhosted`|`managed`, default `selfhosted`), `SEATABLE_MOCK=true` (offline mock), `SEATABLE_ENABLE_DEBUG_TOOLS=1` (enables `echo_args` tool) Copy `.env.example` to `.env` for local development. @@ -46,7 +48,15 @@ Copy `.env.example` to `.env` for local development. ### Modes - **Selfhosted** (default): Single API token from env, one client per process. Supports multi-base via `SEATABLE_BASES`. -- **Managed** (`SEATABLE_MODE=managed`): HTTP-only, each client authenticates with their own Bearer token. Token validated against SeaTable (`src/auth/tokenValidator.ts`) with positive (5 min) / negative (1 min) cache. Rate limiting via `src/ratelimit/` (per-token, per-IP, global, concurrent connections). +- **Managed** (`SEATABLE_MODE=managed`): HTTP-only, each client authenticates with their own Bearer token **on every request** — the `mcp-session-id` header is a routing value, never a credential, and a request must resolve to the identity that created the session. Token validated against SeaTable (`src/auth/tokenValidator.ts`) with positive (1 min) / negative (1 min) cache. Rate limiting via `src/ratelimit/` (per-token, per-IP, global, concurrent connections). + +### OAuth (managed mode) + +`src/auth/oauthProvider.ts` bridges SeaTable API tokens into an OAuth 2.0 authorization code flow. Client registrations and issued tokens are **stateless sealed envelopes** (`src/auth/tokenCipher.ts`, AES-256-GCM keyed from `SEATABLE_TOKEN_SECRET`), so no server-side store is needed and they survive restarts. The `client_id` carries the client's registered `redirect_uris`; `/authorize` rejects anything it cannot open. PKCE `S256` is mandatory and every code is bound to client + exact callback + challenge. The raw SeaTable API token is never returned — `resolveAccessToken()` unseals it server-side. + +Callback policy is in `isPermittedRedirectUri()` (may it be used at all) and `isTrustedRedirectUri()` (may it skip the confirmation step). Unknown remote https destinations are allowed but meet an acknowledgement page; the acknowledgement is read from the form body only and requires `Sec-Fetch-Site: same-origin`, so neither the entry link nor a foreign auto-submit can skip it. `SEATABLE_OAUTH_TRUSTED_REDIRECT_HOSTS` only removes that friction. + +Adversarial coverage lives in `tests/oauthProvider.security.spec.ts`, `tests/oauthRedirectPolicy.spec.ts`, `tests/oauthRateLimit.spec.ts` and `tests/managedSessionAuth.spec.ts`; they encode attacker behaviour, not honest-client mistakes. `tests/oauthObservability.spec.ts` pins the audit fields (`flow`, `ip`, `callback`, `clientName`) that the July–August 2026 log analysis found missing. ### Tool Registration Pattern diff --git a/README.md b/README.md index 25d4af4..f4f030d 100644 --- a/README.md +++ b/README.md @@ -128,12 +128,33 @@ For hosting an MCP endpoint where each client authenticates with their own SeaTa ```bash SEATABLE_MODE=managed \ SEATABLE_SERVER_URL=https://your-seatable-server.com \ +SEATABLE_TOKEN_SECRET=$(openssl rand -hex 32) \ PORT=3000 npx -y @seatable/mcp-seatable --sse ``` -Clients pass their API token via `Authorization: Bearer ` on session initialization. The server validates the token against SeaTable and applies rate limits (60 req/min per token, 120/min per IP, 20 concurrent connections per token). +`SEATABLE_TOKEN_SECRET` is **required** in managed mode. It seals the OAuth tokens the server issues, so the underlying SeaTable API token never has to be handed to a client. Keep it stable across restarts — changing it invalidates every issued access and refresh token and forces all clients to re-authorize. -**OAuth support:** Managed mode also exposes OAuth 2.0 endpoints (`/authorize` and `/token`), enabling OAuth-compatible clients like ChatGPT to connect. During the OAuth flow, the user enters their SeaTable API token, which is then used as the access token — no external OAuth provider required. +Clients pass their credential via `Authorization: Bearer ` — on session initialization **and on every subsequent request**, including `GET` and `DELETE`. The `mcp-session-id` header is a routing value only; it is never accepted on its own. Each request is re-validated and must resolve to the same identity that created the session, otherwise the server answers `401` (missing/invalid credential) or `403` (valid credential, wrong session). Rate limits apply as before (60 req/min per token, 120/min per IP, 20 concurrent connections per token). + +**OAuth support:** Managed mode also exposes OAuth 2.0 endpoints (`/authorize` and `/token`), enabling OAuth-compatible clients like ChatGPT to connect — no external OAuth provider required. During the flow the user enters their SeaTable API token; the server seals it into its own short-lived access token (1 h) and a rotating refresh token (14 d). The raw SeaTable API token is never returned to a client. + +Clients must register at `/register` first: the returned `client_id` carries the client's name and its `redirect_uris`, and the server accepts a callback only if it is one the client registered (loopback callbacks may vary the port, per RFC 8252). PKCE with `S256` is mandatory, and every authorization code is bound to the client, the exact callback and the challenge. + +**Where a code may be delivered.** With open dynamic registration, "registered client" is not a trust statement — anyone can register. What matters is whether the code leaves the user's machine: + +| Callback | Behaviour | +|---|---| +| Loopback (`http://127.0.0.1:…`, `localhost`, `[::1]`) | allowed, no extra step — the code stays on the user's machine | +| Private-use scheme (`cursor://`, `vscode://`, `com.example.app:/…`) | allowed, no extra step — handed to a local application | +| `https` on a host in `SEATABLE_OAUTH_TRUSTED_REDIRECT_HOSTS` | allowed, no extra step | +| `https` on any other host | allowed **after** the user confirms the destination on a separate page | +| Remote plaintext `http`, `javascript:`, `data:`, `file:`, `blob:` | rejected | + +The confirmation cannot be skipped from the entry link: it is read from the form body only, and a POST auto-submitted by a foreign page is refused via `Sec-Fetch-Site`. The trusted-host list therefore removes friction — it is not a gate, and leaving it unset breaks nothing. + +The consent screen leads with the destination the authorization will be sent to. The application's name is shown as **self-reported**, because with open registration it is chosen by whoever registered the client and cannot be verified. + +The OAuth endpoints are rate limited per IP (30/min overall, 10/min for token submissions), so `/authorize` cannot be used as an unthrottled oracle for testing SeaTable API tokens. OAuth endpoints follow the MCP specification (RFC 8414 metadata discovery, PKCE, dynamic client registration): @@ -207,6 +228,8 @@ Authentication (one of these is required in selfhosted mode): Optional: - `SEATABLE_MODE` — `selfhosted` (default) or `managed` (multi-tenant HTTP with per-client auth) +- `SEATABLE_TOKEN_SECRET` — **required in managed mode**, min. 32 chars. Seals issued OAuth tokens and client registrations; must be stable across restarts (`openssl rand -hex 32`) +- `SEATABLE_ACCESS_TOKEN_TTL` — lifetime of an issued access token in seconds (default `3600`, range `30`–`2592000`). Lower narrows the window after a SeaTable token is revoked; higher spares users a re-prompt if their client renews badly. The refresh token is never issued shorter-lived than the access token. - `SEATABLE_MOCK=true` — Enable mock mode for offline testing - `CORS_ALLOWED_ORIGINS` — Comma-separated list of allowed origins for CORS (HTTP mode only, disabled if unset) - `METRICS_PORT` — Prometheus metrics port (default: `9090`, HTTP mode only) diff --git a/package-lock.json b/package-lock.json index 47d7e32..67896ee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@seatable/mcp-seatable", - "version": "1.5.2", + "version": "1.6.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@seatable/mcp-seatable", - "version": "1.5.2", + "version": "1.6.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.17.4", @@ -43,13 +43,14 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz", - "integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "aix" @@ -59,13 +60,14 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz", - "integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -75,13 +77,14 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz", - "integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -91,13 +94,14 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz", - "integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -107,13 +111,14 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz", - "integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -123,13 +128,14 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz", - "integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -139,13 +145,14 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz", - "integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" @@ -155,13 +162,14 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz", - "integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" @@ -171,13 +179,14 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz", - "integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -187,13 +196,14 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz", - "integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -203,13 +213,14 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz", - "integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -219,13 +230,14 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz", - "integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", "cpu": [ "loong64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -235,13 +247,14 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz", - "integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", "cpu": [ "mips64el" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -251,13 +264,14 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz", - "integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -267,13 +281,14 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz", - "integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", "cpu": [ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -283,13 +298,14 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz", - "integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", "cpu": [ "s390x" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -299,13 +315,14 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz", - "integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -315,13 +332,14 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz", - "integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "netbsd" @@ -331,13 +349,14 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz", - "integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "netbsd" @@ -347,13 +366,14 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz", - "integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "openbsd" @@ -363,13 +383,14 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz", - "integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "openbsd" @@ -379,13 +400,14 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz", - "integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "openharmony" @@ -395,13 +417,14 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz", - "integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "sunos" @@ -411,13 +434,14 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz", - "integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -427,13 +451,14 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz", - "integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -443,13 +468,14 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz", - "integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -459,10 +485,11 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", - "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", "dev": true, + "license": "MIT", "dependencies": { "eslint-visitor-keys": "^3.4.3" }, @@ -477,43 +504,54 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, + "license": "MIT", "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, "node_modules/@eslint/config-array": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", - "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.6", + "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", - "minimatch": "^3.1.2" + "minimatch": "^3.1.5" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@eslint/config-array/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "node_modules/@eslint/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -522,19 +560,24 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", - "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/core": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", - "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", "dev": true, + "license": "Apache-2.0", "dependencies": { "@types/json-schema": "^7.0.15" }, @@ -543,19 +586,20 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.6.tgz", + "integrity": "sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA==", "dev": true, + "license": "MIT", "dependencies": { - "ajv": "^6.12.4", + "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", + "js-yaml": "^4.3.0", + "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" }, "engines": { @@ -565,11 +609,36 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@eslint/eslintrc/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -580,15 +649,24 @@ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, + "license": "MIT", "engines": { "node": ">= 4" } }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -597,10 +675,11 @@ } }, "node_modules/@eslint/js": { - "version": "9.34.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.34.0.tgz", - "integrity": "sha512-EoyvqQnBNsV1CWaEJ559rxXL4c8V92gxirbawSmVUOWXlsRxxQXl6LmCpdUblgxgSkDIqKnhzba2SjRTI/A5Rw==", + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz", + "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==", "dev": true, + "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -609,21 +688,23 @@ } }, "node_modules/@eslint/object-schema": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", - "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", "dev": true, + "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/plugin-kit": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", - "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.15.2", + "@eslint/core": "^0.17.0", "levn": "^0.4.1" }, "engines": { @@ -631,50 +712,53 @@ } }, "node_modules/@hono/node-server": { - "version": "1.19.10", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.10.tgz", - "integrity": "sha512-hZ7nOssGqRgyV3FVVQdfi+U4q02uB23bpnYpdvNXkYTRRyWx84b7yf1ans+dnJ/7h41sGL3CeQTfO+ZGxuO+Iw==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.1.1.tgz", + "integrity": "sha512-ELuehkj5VCBdgEw9zs+ivkKwyzzUCSQuE96YmiPvn1ECBoZCczbFXJLeEGMTYjphP6gydh4pHMqEYPVMYUVgQg==", "license": "MIT", "engines": { - "node": ">=18.14.1" + "node": ">=20" }, "peerDependencies": { "hono": "^4" } }, "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, "engines": { "node": ">=18.18.0" } }, "node_modules/@humanfs/node": { - "version": "0.16.6", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", - "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.3.0" + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" }, "engines": { "node": ">=18.18.0" } }, - "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", - "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", "dev": true, + "license": "Apache-2.0", "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "node": ">=18.18.0" } }, "node_modules/@humanwhocodes/module-importer": { @@ -682,6 +766,7 @@ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=12.22" }, @@ -695,6 +780,7 @@ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=18.18" }, @@ -707,15 +793,16 @@ "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.27.1", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.27.1.tgz", - "integrity": "sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA==", + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.30.0.tgz", + "integrity": "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==", "license": "MIT", "dependencies": { - "@hono/node-server": "^1.19.9", + "@hono/node-server": "^1.19.9 || ^2.0.5", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", @@ -749,339 +836,425 @@ } } }, - "node_modules/@modelcontextprotocol/sdk/node_modules/ajv": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/eventsource": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", - "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", - "license": "MIT", - "dependencies": { - "eventsource-parser": "^3.0.1" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 8" + "node": "^22.20 || ^24.12 || >=25" } }, "node_modules/@opentelemetry/api": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", - "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", + "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", "license": "Apache-2.0", "engines": { "node": ">=8.0.0" } }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "license": "MIT" + }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.49.0.tgz", - "integrity": "sha512-rlKIeL854Ed0e09QGYFlmDNbka6I3EQFw7iZuugQjMb11KMpJCLPFL4ZPbMfaEhLADEL1yx0oujGkBQ7+qW3eA==", + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.0.tgz", + "integrity": "sha512-70TeIFezKKy65LgAVyQh+w94/gjWhvPWaLaGGeMEgVrPkQhuj/M5bAYYZzIFUj9Y69oHyTm5Um/R6gcLh4A8JA==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.49.0.tgz", - "integrity": "sha512-cqPpZdKUSQYRtLLr6R4X3sD4jCBO1zUmeo3qrWBCqYIeH8Q3KRL4F3V7XJ2Rm8/RJOQBZuqzQGWPjjvFUcYa/w==", + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.0.tgz", + "integrity": "sha512-YC86tYIHK6M1IV+wbzO+Bxk8RCBr6ZyWYgWxUCzaZD8mc8rrFoIJDNzDrkHBYRc/wKdrsIXmm6/F7NzrAO+OrA==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.49.0.tgz", - "integrity": "sha512-99kMMSMQT7got6iYX3yyIiJfFndpojBmkHfTc1rIje8VbjhmqBXE+nb7ZZP3A5skLyujvT0eIUCUsxAe6NjWbw==", + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.0.tgz", + "integrity": "sha512-oI+ECtUcli0y0fi4xpW82GdPIXdTkI8G8DSjG2LRuw09fPAGykaWYH/hXxiKuTxiAjiPSTIIuYUqof5Z2hShWw==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.49.0.tgz", - "integrity": "sha512-y8cXoD3wdWUDpjOLMKLx6l+NFz3NlkWKcBCBfttUn+VGSfgsQ5o/yDUGtzE9HvsodkP0+16N0P4Ty1VuhtRUGg==", + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.0.tgz", + "integrity": "sha512-NwV+1s7TiKrMe4owHyKB/dTLD7ZJD0YEBEhIz+hvav1Cu1GReJjF+rsdNwjzENQeIAbE/CoNiaAc5Vz2h5DPAA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.49.0.tgz", - "integrity": "sha512-3mY5Pr7qv4GS4ZvWoSP8zha8YoiqrU+e0ViPvB549jvliBbdNLrg2ywPGkgLC3cmvN8ya3za+Q2xVyT6z+vZqA==", + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.0.tgz", + "integrity": "sha512-tWtHBTu5gOPK4u4Urtk4qAHW3zZ9rQAmbssO8gp7ELvGTGI3aCiq6NqyTQ0PCIg7KbHJF2UkGDDs77YZGxfjCA==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.49.0.tgz", - "integrity": "sha512-C9KzzOAQU5gU4kG8DTk+tjdKjpWhVWd5uVkinCwwFub2m7cDYLOdtXoMrExfeBmeRy9kBQMkiyJ+HULyF1yj9w==", + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.0.tgz", + "integrity": "sha512-2qPoJiwTvtHQ27NnYvTnsgk8laXWYuVmNESG8WFZBcEPKLfZ3I27qBJarjVRQtwGeYyRfq5ZowHXih9lm2BItw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.49.0.tgz", - "integrity": "sha512-OVSQgEZDVLnTbMq5NBs6xkmz3AADByCWI4RdKSFNlDsYXdFtlxS59J+w+LippJe8KcmeSSM3ba+GlsM9+WwC1w==", + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.0.tgz", + "integrity": "sha512-FQwsTRvLNuHoTdICABJQfbPUSEueISGmnpT06tXTMpfprf5NiKLSXKA0A+w45wJnCmZAnzgqBwbt6ARFuyOi5w==", "cpu": [ "arm" ], "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.49.0.tgz", - "integrity": "sha512-ZnfSFA7fDUHNa4P3VwAcfaBLakCbYaxCk0jUnS3dTou9P95kwoOLAMlT3WmEJDBCSrOEFFV0Y1HXiwfLYJuLlA==", + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.0.tgz", + "integrity": "sha512-BBVTXziw8mY1a4ZbWME9tZyfzqXCDPqaC7Z3heQ29p5dkvXzwL0NwelO8zLa8c3RBKvl3YTuSnBgsBhYBtwjIw==", "cpu": [ "arm" ], "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.49.0.tgz", - "integrity": "sha512-Z81u+gfrobVK2iV7GqZCBfEB1y6+I61AH466lNK+xy1jfqFLiQ9Qv716WUM5fxFrYxwC7ziVdZRU9qvGHkYIJg==", + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.0.tgz", + "integrity": "sha512-w2Iyy9+RqKwx3d9qWMKsJg0FfRBsY0/pXNv0mCQ3ueRvJI6+QAScfD4nrMlzFLs2HNVW6Ew+mtZfDl9b7Ew5/Q==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.49.0.tgz", - "integrity": "sha512-zoAwS0KCXSnTp9NH/h9aamBAIve0DXeYpll85shf9NJ0URjSTzzS+Z9evmolN+ICfD3v8skKUPyk2PO0uGdFqg==", + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.0.tgz", + "integrity": "sha512-YK++KtrFRHYE0P6/RtYEAy9t8F37znP+K03RrIuLPYOL6SVlObRumf/0OE4V/h63xL9DwkWbNssZfmA9hawuDA==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", "optional": true, "os": [ "linux" ] }, - "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.49.0.tgz", - "integrity": "sha512-2QyUyQQ1ZtwZGiq0nvODL+vLJBtciItC3/5cYN8ncDQcv5avrt2MbKt1XU/vFAJlLta5KujqyHdYtdag4YEjYQ==", + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.0.tgz", + "integrity": "sha512-aBfOG6fP7YkkPmTqPwufRJeFyz7WPpECv9XNbnsk9+vg7rxdih0lbtEel7jcRng4LZrrmU3FfitCFyEj4BWDWg==", "cpu": [ "loong64" ], "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.0.tgz", + "integrity": "sha512-LGaHEOeHNAag9VuS1Crs5DFg4RrU9MPi2nVnNJk9DTePx/B6RRYKVmrIXt2h7YOJlwjaFJ6lwtFDliZxScTLrQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.49.0.tgz", - "integrity": "sha512-k9aEmOWt+mrMuD3skjVJSSxHckJp+SiFzFG+v8JLXbc/xi9hv2icSkR3U7uQzqy+/QbbYY7iNB9eDTwrELo14g==", + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.0.tgz", + "integrity": "sha512-jClvk+J0FC3b7Udvegiw5/4hErbHtmsNsQgENnKXDWtNCJXsJYZH5WURvu7imDOO38xYml24eeh5x3A04ppwCw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.0.tgz", + "integrity": "sha512-0OJlaGK+8+B777Ql5okIpD7ua5Ro9+VB9Ve0OKa28OQJZ1RbuUBVNHK/e3pr4BROqsyPl1JrPO1ZxJseCNffcA==", "cpu": [ "ppc64" ], "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.49.0.tgz", - "integrity": "sha512-rDKRFFIWJ/zJn6uk2IdYLc09Z7zkE5IFIOWqpuU0o6ZpHcdniAyWkwSUWE/Z25N/wNDmFHHMzin84qW7Wzkjsw==", + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.0.tgz", + "integrity": "sha512-Ygsx+HoNH7afwi1bTIXbnTvVnsO+zurPLSYxybV1hHFVU72OWOCl6v05ql/z0hkpAPx+DK7Kn9Bi7MayCcjLTA==", "cpu": [ "riscv64" ], "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.49.0.tgz", - "integrity": "sha512-FkkhIY/hYFVnOzz1WeV3S9Bd1h0hda/gRqvZCMpHWDHdiIHn6pqsY3b5eSbvGccWHMQ1uUzgZTKS4oGpykf8Tw==", + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.0.tgz", + "integrity": "sha512-pDQxtMGb+OvG3fLwR2OkZlSd47hW+kWg4BYMG/++sR6RqorQccwPTDsxda5hPwiIeIErAnCF9ma3SAU06bdQtQ==", "cpu": [ "riscv64" ], "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.49.0.tgz", - "integrity": "sha512-gRf5c+A7QiOG3UwLyOOtyJMD31JJhMjBvpfhAitPAoqZFcOeK3Kc1Veg1z/trmt+2P6F/biT02fU19GGTS529A==", + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.0.tgz", + "integrity": "sha512-0BnUG9mS8I4SSHr3XsxVhuCMEiu+rX61xxZF5vujso4LaiAGFZFxvDjg6Xn6tLPNTUAfuCvQYas4LMQMVsKRSQ==", "cpu": [ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.49.0.tgz", - "integrity": "sha512-BR7+blScdLW1h/2hB/2oXM+dhTmpW3rQt1DeSiCP9mc2NMMkqVgjIN3DDsNpKmezffGC9R8XKVOLmBkRUcK/sA==", + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.0.tgz", + "integrity": "sha512-Adu/VttB1dpPNW+FEacrZ+xVm9tFty84+RrFzsqlFaPxoJB+9XXyDGtp5dCOoBwGBIEVH0To7lExFXEx0BIF4A==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.49.0.tgz", - "integrity": "sha512-hDMOAe+6nX3V5ei1I7Au3wcr9h3ktKzDvF2ne5ovX8RZiAHEtX1A5SNNk4zt1Qt77CmnbqT+upb/umzoPMWiPg==", + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.0.tgz", + "integrity": "sha512-NQ3bDvjUbFKmP23671xUlXtKmqVsUBd6M4PQCvbmNtOy06hnQIdKHy8oG/6S3R/S6He1JgPk6A5VT+prAJMYEw==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", "optional": true, "os": [ "linux" ] }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.0.tgz", + "integrity": "sha512-u2eDAl4+0aFvA13GxlGBtTI3SS3sdgwgtV0HyjZ0QaQVCgNE+jqNGey+GtxWiq+wxr/UycAx/OnfJzApCFamvA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.0.tgz", + "integrity": "sha512-XvRb5vfW3wAZQ+ZUG21AnHHDKtNcw99eigzEhjr//NZ3u7SoBaPP0seSc7FgP7p1epAEdAoZckMW9WY/+4w70w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.49.0.tgz", - "integrity": "sha512-wkNRzfiIGaElC9kXUT+HLx17z7D0jl+9tGYRKwd8r7cUqTL7GYAvgUY++U2hK6Ar7z5Z6IRRoWC8kQxpmM7TDA==", + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.0.tgz", + "integrity": "sha512-iZPmniy4kNBf5yo2RezbkYNNK5HPbXE9+g+twnbqSng7dtLEJy1SKoxiE/ni4FDacjyuZpEeb9U054N4EoKHYw==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.49.0.tgz", - "integrity": "sha512-gq5aW/SyNpjp71AAzroH37DtINDcX1Qw2iv9Chyz49ZgdOP3NV8QCyKZUrGsYX9Yyggj5soFiRCgsL3HwD8TdA==", + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.0.tgz", + "integrity": "sha512-mFBBd+LF37fnE8JnYUOH+imj0aPFPK30vpar4ehJkgnLj9sZn8ZxiRENmLtgIwxK7TC8klF6N57fxdNBwQoqOA==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.0.tgz", + "integrity": "sha512-ujeqEY3B+zbGn3Z4Q03cUBG/LGWnBJncVT36WER31LcOsQk9+1dmINKKtvmmfChUvRbK1G0R8OhMWFgHgaZtAw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.49.0.tgz", - "integrity": "sha512-gEtqFbzmZLFk2xKh7g0Rlo8xzho8KrEFEkzvHbfUGkrgXOpZ4XagQ6n+wIZFNh1nTb8UD16J4nFSFKXYgnbdBg==", + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.0.tgz", + "integrity": "sha512-hncn90N4sOky0L2LKE5oESKLbxCPeVo4eLA2LSMoDzM+879ml4WSr+Rr4DWknNIVVvS1Hirkc9hx02W6YxS8rQ==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -1091,50 +1264,55 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/node": { - "version": "22.18.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.0.tgz", - "integrity": "sha512-m5ObIqwsUp6BZzyiy4RdZpzWGub9bqLJMvZDD0QMXhxjqMHMENlj+SqF5QxoUwaQNFe+8kz8XM8ZQhqkQPTgMQ==", + "version": "22.20.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", "dev": true, + "license": "MIT", "dependencies": { "undici-types": "~6.21.0" } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.41.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.41.0.tgz", - "integrity": "sha512-8fz6oa6wEKZrhXWro/S3n2eRJqlRcIa6SlDh59FXJ5Wp5XRZ8B9ixpJDcjadHq47hMx0u+HW6SNa6LjJQ6NLtw==", - "dev": true, - "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.41.0", - "@typescript-eslint/type-utils": "8.41.0", - "@typescript-eslint/utils": "8.41.0", - "@typescript-eslint/visitor-keys": "8.41.0", - "graphemer": "^1.4.0", - "ignore": "^7.0.0", + "version": "8.68.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.68.0.tgz", + "integrity": "sha512-WASHDpCm6qO5jj9g1a+8NiW5+GCkAyLReR56/4VruYmNgfUmqpxOfZ2Yfb8xGfJPWv5Qi6LSD8sXdces3vbp/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.68.0", + "@typescript-eslint/type-utils": "8.68.0", + "@typescript-eslint/utils": "8.68.0", + "@typescript-eslint/visitor-keys": "8.68.0", + "ignore": "^7.0.5", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.1.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1144,22 +1322,23 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.41.0", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "@typescript-eslint/parser": "^8.68.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.41.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.41.0.tgz", - "integrity": "sha512-gTtSdWX9xiMPA/7MV9STjJOOYtWwIJIYxkQxnSV1U3xcE+mnJSH3f6zI0RYP+ew66WSlZ5ed+h0VCxsvdC1jJg==", + "version": "8.68.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.68.0.tgz", + "integrity": "sha512-fHq2VC1kpyYfvEcbiMjOpySY4WS7voEp89yAThrHRX5sm9j2lzYppCb2umFMEed4fWcyeLjHxrz0mpjNBaBxMQ==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.41.0", - "@typescript-eslint/types": "8.41.0", - "@typescript-eslint/typescript-estree": "8.41.0", - "@typescript-eslint/visitor-keys": "8.41.0", - "debug": "^4.3.4" + "@typescript-eslint/scope-manager": "8.68.0", + "@typescript-eslint/types": "8.68.0", + "@typescript-eslint/typescript-estree": "8.68.0", + "@typescript-eslint/visitor-keys": "8.68.0", + "debug": "^4.4.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1169,19 +1348,20 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.41.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.41.0.tgz", - "integrity": "sha512-b8V9SdGBQzQdjJ/IO3eDifGpDBJfvrNTp2QD9P2BeqWTGrRibgfgIlBSw6z3b6R7dPzg752tOs4u/7yCLxksSQ==", + "version": "8.68.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.68.0.tgz", + "integrity": "sha512-5GQtWZCXFcFYux955pvoS02WLc49pXNlvIxocKjS0clvwo3in1RdlzVKyiqQH9vE5AKWFLTaUgeQkOrTS+0Qxw==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.41.0", - "@typescript-eslint/types": "^8.41.0", - "debug": "^4.3.4" + "@typescript-eslint/tsconfig-utils": "^8.68.0", + "@typescript-eslint/types": "^8.68.0", + "debug": "^4.4.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1191,17 +1371,18 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.41.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.41.0.tgz", - "integrity": "sha512-n6m05bXn/Cd6DZDGyrpXrELCPVaTnLdPToyhBoFkLIMznRUQUEQdSp96s/pcWSQdqOhrgR1mzJ+yItK7T+WPMQ==", + "version": "8.68.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.68.0.tgz", + "integrity": "sha512-T5eXpcaJNg8bhjHJ8Rjp68Vq/QBteYtTKY8TZqVNPaUbuz0f6jI9t6aDkylwvalpAB9XTTFeFOjrjXAZ3YvmVA==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.41.0", - "@typescript-eslint/visitor-keys": "8.41.0" + "@typescript-eslint/types": "8.68.0", + "@typescript-eslint/visitor-keys": "8.68.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1212,10 +1393,11 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.41.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.41.0.tgz", - "integrity": "sha512-TDhxYFPUYRFxFhuU5hTIJk+auzM/wKvWgoNYOPcOf6i4ReYlOoYN8q1dV5kOTjNQNJgzWN3TUUQMtlLOcUgdUw==", + "version": "8.68.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.68.0.tgz", + "integrity": "sha512-F7zrGQfiJHojPwi8vhxZQC1tWtJzvL74cK/nqri2lk8YUXvYaYwl263xOJ69jDWPUk1hmcdoayFwk9lX09npVw==", "dev": true, + "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -1224,20 +1406,21 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.41.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.41.0.tgz", - "integrity": "sha512-63qt1h91vg3KsjVVonFJWjgSK7pZHSQFKH6uwqxAH9bBrsyRhO6ONoKyXxyVBzG1lJnFAJcKAcxLS54N1ee1OQ==", + "version": "8.68.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.68.0.tgz", + "integrity": "sha512-X77zqoY1EjeWGs/0JNxeaMfp5C5lIz4Tw8y66F1Ne8Faq6g424sBNYM6xBAqElfGZPLpWS+CZAp0DXyKDzWiHg==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.41.0", - "@typescript-eslint/typescript-estree": "8.41.0", - "@typescript-eslint/utils": "8.41.0", - "debug": "^4.3.4", - "ts-api-utils": "^2.1.0" + "@typescript-eslint/types": "8.68.0", + "@typescript-eslint/typescript-estree": "8.68.0", + "@typescript-eslint/utils": "8.68.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1247,15 +1430,16 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.41.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.41.0.tgz", - "integrity": "sha512-9EwxsWdVqh42afLbHP90n2VdHaWU/oWgbH2P0CfcNfdKL7CuKpwMQGjwev56vWu9cSKU7FWSu6r9zck6CVfnag==", + "version": "8.68.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.68.0.tgz", + "integrity": "sha512-9RnpsGJjrAllCMefGVVsImJM24YurhC0Q1h4UbvivtvOqXmR/vEJge2OoE++z9m6hyg8T1Q8t5SNT6tHSbrxcg==", "dev": true, + "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -1265,21 +1449,21 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.41.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.41.0.tgz", - "integrity": "sha512-D43UwUYJmGhuwHfY7MtNKRZMmfd8+p/eNSfFe6tH5mbVDto+VQCayeAt35rOx3Cs6wxD16DQtIKw/YXxt5E0UQ==", + "version": "8.68.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.68.0.tgz", + "integrity": "sha512-OKKsD0tYmoNiU5PW2zehO1yO56jYOm1ShYlxon/Z0SJNidAkdVg86eg9ruRuoXf8xfnuWZGbwDsStkoXbZtIIA==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.41.0", - "@typescript-eslint/tsconfig-utils": "8.41.0", - "@typescript-eslint/types": "8.41.0", - "@typescript-eslint/visitor-keys": "8.41.0", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.1.0" + "@typescript-eslint/project-service": "8.68.0", + "@typescript-eslint/tsconfig-utils": "8.68.0", + "@typescript-eslint/types": "8.68.0", + "@typescript-eslint/visitor-keys": "8.68.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1289,19 +1473,20 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/utils": { - "version": "8.41.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.41.0.tgz", - "integrity": "sha512-udbCVstxZ5jiPIXrdH+BZWnPatjlYwJuJkDA4Tbo3WyYLh8NvB+h/bKeSZHDOFKfphsZYJQqaFtLeXEqurQn1A==", + "version": "8.68.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.68.0.tgz", + "integrity": "sha512-PB5gJMMOg0Q5P1tsgWtEAqQacJXq0qEqRHDX/YJ4FaTMLfZPpHB3gjl2EJuiZyPABxmj4ZQYiY9m1bdAJ5y7tQ==", "dev": true, + "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.41.0", - "@typescript-eslint/types": "8.41.0", - "@typescript-eslint/typescript-estree": "8.41.0" + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.68.0", + "@typescript-eslint/types": "8.68.0", + "@typescript-eslint/typescript-estree": "8.68.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1311,18 +1496,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.41.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.41.0.tgz", - "integrity": "sha512-+GeGMebMCy0elMNg67LRNoVnUFPIm37iu5CmHESVx56/9Jsfdpsvbv605DQ81Pi/x11IdKUsS5nzgTYbCQU9fg==", + "version": "8.68.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.68.0.tgz", + "integrity": "sha512-YR65gGdGvTUAWLldC3xLOvOzamdGzB4A5/N8rehEaHs3Zvoe39BhgY+u0SPch1OvrVTfLcc55wsSgK2NcnTS/A==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.41.0", - "eslint-visitor-keys": "^4.2.1" + "@typescript-eslint/types": "8.68.0", + "eslint-visitor-keys": "^5.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1333,12 +1519,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "dev": true, + "license": "Apache-2.0", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" @@ -1349,6 +1536,7 @@ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", "dev": true, + "license": "MIT", "dependencies": { "@vitest/spy": "2.1.9", "@vitest/utils": "2.1.9", @@ -1364,6 +1552,7 @@ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", "dev": true, + "license": "MIT", "dependencies": { "@vitest/spy": "2.1.9", "estree-walker": "^3.0.3", @@ -1390,6 +1579,7 @@ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", "dev": true, + "license": "MIT", "dependencies": { "tinyrainbow": "^1.2.0" }, @@ -1402,6 +1592,7 @@ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", "dev": true, + "license": "MIT", "dependencies": { "@vitest/utils": "2.1.9", "pathe": "^1.1.2" @@ -1415,6 +1606,7 @@ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", "dev": true, + "license": "MIT", "dependencies": { "@vitest/pretty-format": "2.1.9", "magic-string": "^0.30.12", @@ -1429,6 +1621,7 @@ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", "dev": true, + "license": "MIT", "dependencies": { "tinyspy": "^3.0.2" }, @@ -1441,6 +1634,7 @@ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", "dev": true, + "license": "MIT", "dependencies": { "@vitest/pretty-format": "2.1.9", "loupe": "^3.1.2", @@ -1464,10 +1658,11 @@ } }, "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", "dev": true, + "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -1480,20 +1675,33 @@ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, + "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" }, "funding": { "type": "github", @@ -1517,33 +1725,12 @@ } } }, - "node_modules/ajv-formats/node_modules/ajv": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" - }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, + "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -1558,13 +1745,15 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "dev": true, + "license": "Python-2.0" }, "node_modules/array-buffer-byte-length": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" @@ -1581,6 +1770,7 @@ "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", @@ -1603,6 +1793,7 @@ "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", @@ -1624,6 +1815,7 @@ "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", @@ -1642,6 +1834,7 @@ "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", @@ -1660,6 +1853,7 @@ "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", "dev": true, + "license": "MIT", "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", @@ -1681,6 +1875,7 @@ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" } @@ -1690,6 +1885,7 @@ "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -1704,6 +1900,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", "engines": { "node": ">=8.0.0" } @@ -1713,6 +1910,7 @@ "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", "dev": true, + "license": "MIT", "dependencies": { "possible-typed-array-names": "^1.0.0" }, @@ -1724,20 +1922,22 @@ } }, "node_modules/axios": { - "version": "1.13.6", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", - "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.19.0.tgz", + "integrity": "sha512-ht/iuYZXEjFxLH/Hkezgd7m6JKlHHXEUSneaDz8uZe1Gj5QZtCnpyDsckvAiEnT89OEbCLmnte4R4sn7P0EKFw==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.11", - "form-data": "^4.0.5", - "proxy-from-env": "^1.1.0" + "follow-redirects": "^1.16.0", + "form-data": "^4.0.6", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" } }, "node_modules/axios-retry": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/axios-retry/-/axios-retry-4.5.0.tgz", "integrity": "sha512-aR99oXhpEDGo0UuAlYcn2iGRds30k366Zfa05XWScR9QaQD4JYiP3/1Qt1u7YlefUOK+cn0CcwoL1oefavQUlQ==", + "license": "Apache-2.0", "dependencies": { "is-retry-allowed": "^2.2.0" }, @@ -1746,10 +1946,14 @@ } }, "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } }, "node_modules/bintrees": { "version": "1.0.2", @@ -1758,20 +1962,20 @@ "license": "MIT" }, "node_modules/body-parser": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", - "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", "license": "MIT", "dependencies": { "bytes": "^3.1.2", - "content-type": "^1.0.5", + "content-type": "^2.0.0", "debug": "^4.4.3", - "http-errors": "^2.0.0", - "iconv-lite": "^0.7.0", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", "on-finished": "^2.4.1", - "qs": "^6.14.1", - "raw-body": "^3.0.1", - "type-is": "^2.0.1" + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" }, "engines": { "node": ">=18" @@ -1781,30 +1985,36 @@ "url": "https://opencollective.com/express" } }, + "node_modules/body-parser/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/bottleneck": { "version": "2.19.5", "resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz", - "integrity": "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==" + "integrity": "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==", + "license": "MIT" }, "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "dev": true, + "license": "MIT", "dependencies": { - "fill-range": "^7.1.1" + "balanced-match": "^4.0.2" }, "engines": { - "node": ">=8" + "node": "20 || >=22" } }, "node_modules/bytes": { @@ -1821,19 +2031,21 @@ "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/call-bind": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.0", - "es-define-property": "^1.0.0", - "get-intrinsic": "^1.2.4", + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", "set-function-length": "^1.2.2" }, "engines": { @@ -1847,6 +2059,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" @@ -1859,6 +2072,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" @@ -1875,6 +2089,7 @@ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -1884,6 +2099,7 @@ "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", "dev": true, + "license": "MIT", "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", @@ -1900,6 +2116,7 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -1912,10 +2129,11 @@ } }, "node_modules/check-error": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", - "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 16" } @@ -1925,6 +2143,7 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -1936,7 +2155,8 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/combined-stream": { "version": "1.0.8", @@ -1954,12 +2174,13 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/content-disposition": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", - "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", "license": "MIT", "engines": { "node": ">=18" @@ -1997,21 +2218,27 @@ } }, "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", "dependencies": { "object-assign": "^4", "vary": "^1" }, "engines": { "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -2026,6 +2253,7 @@ "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", @@ -2043,6 +2271,7 @@ "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", @@ -2060,6 +2289,7 @@ "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", @@ -2094,6 +2324,7 @@ "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -2102,13 +2333,15 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "dev": true, + "license": "MIT", "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -2126,6 +2359,7 @@ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", "dev": true, + "license": "MIT", "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", @@ -2161,6 +2395,7 @@ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", "dev": true, + "license": "Apache-2.0", "dependencies": { "esutils": "^2.0.2" }, @@ -2172,6 +2407,7 @@ "version": "16.6.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", "engines": { "node": ">=12" }, @@ -2183,6 +2419,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", @@ -2208,10 +2445,11 @@ } }, "node_modules/es-abstract": { - "version": "1.24.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", - "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", + "version": "1.24.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", + "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==", "dev": true, + "license": "MIT", "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", @@ -2275,10 +2513,30 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-abstract-get": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-abstract-get/-/es-abstract-get-1.0.0.tgz", + "integrity": "sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.2", + "is-callable": "^1.2.7", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -2287,6 +2545,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -2295,12 +2554,14 @@ "version": "1.7.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", "dependencies": { "es-errors": "^1.3.0" }, @@ -2312,6 +2573,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", @@ -2327,6 +2589,7 @@ "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", "dev": true, + "license": "MIT", "dependencies": { "hasown": "^2.0.2" }, @@ -2335,14 +2598,18 @@ } }, "node_modules/es-to-primitive": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", - "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.4.tgz", + "integrity": "sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw==", "dev": true, + "license": "MIT", "dependencies": { + "es-abstract-get": "^1.0.0", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", "is-callable": "^1.2.7", - "is-date-object": "^1.0.5", - "is-symbol": "^1.0.4" + "is-date-object": "^1.1.0", + "is-symbol": "^1.1.1" }, "engines": { "node": ">= 0.4" @@ -2352,11 +2619,12 @@ } }, "node_modules/esbuild": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz", - "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", "dev": true, "hasInstallScript": true, + "license": "MIT", "bin": { "esbuild": "bin/esbuild" }, @@ -2364,32 +2632,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.9", - "@esbuild/android-arm": "0.25.9", - "@esbuild/android-arm64": "0.25.9", - "@esbuild/android-x64": "0.25.9", - "@esbuild/darwin-arm64": "0.25.9", - "@esbuild/darwin-x64": "0.25.9", - "@esbuild/freebsd-arm64": "0.25.9", - "@esbuild/freebsd-x64": "0.25.9", - "@esbuild/linux-arm": "0.25.9", - "@esbuild/linux-arm64": "0.25.9", - "@esbuild/linux-ia32": "0.25.9", - "@esbuild/linux-loong64": "0.25.9", - "@esbuild/linux-mips64el": "0.25.9", - "@esbuild/linux-ppc64": "0.25.9", - "@esbuild/linux-riscv64": "0.25.9", - "@esbuild/linux-s390x": "0.25.9", - "@esbuild/linux-x64": "0.25.9", - "@esbuild/netbsd-arm64": "0.25.9", - "@esbuild/netbsd-x64": "0.25.9", - "@esbuild/openbsd-arm64": "0.25.9", - "@esbuild/openbsd-x64": "0.25.9", - "@esbuild/openharmony-arm64": "0.25.9", - "@esbuild/sunos-x64": "0.25.9", - "@esbuild/win32-arm64": "0.25.9", - "@esbuild/win32-ia32": "0.25.9", - "@esbuild/win32-x64": "0.25.9" + "@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" } }, "node_modules/escape-html": { @@ -2403,6 +2671,7 @@ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -2411,25 +2680,26 @@ } }, "node_modules/eslint": { - "version": "9.34.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.34.0.tgz", - "integrity": "sha512-RNCHRX5EwdrESy3Jc9o8ie8Bog+PeYvvSR8sDGoZxNFTvZ4dlxUB3WzQ3bQMztFrSRODGrLLj8g6OFuGY/aiQg==", + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.5.tgz", + "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, + "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.0", - "@eslint/config-helpers": "^0.3.1", - "@eslint/core": "^0.15.2", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.34.0", - "@eslint/plugin-kit": "^0.3.5", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.6", + "@eslint/js": "9.39.5", + "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", - "@types/json-schema": "^7.0.15", - "ajv": "^6.12.4", + "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", @@ -2448,7 +2718,7 @@ "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", + "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, @@ -2475,6 +2745,7 @@ "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.2.tgz", "integrity": "sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==", "dev": true, + "license": "MIT", "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -2483,14 +2754,15 @@ } }, "node_modules/eslint-import-resolver-node": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", - "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.10.tgz", + "integrity": "sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ==", "dev": true, + "license": "MIT", "dependencies": { "debug": "^3.2.7", - "is-core-module": "^2.13.0", - "resolve": "^1.22.4" + "is-core-module": "^2.16.1", + "resolve": "^2.0.0-next.6" } }, "node_modules/eslint-import-resolver-node/node_modules/debug": { @@ -2498,15 +2770,17 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, + "license": "MIT", "dependencies": { "ms": "^2.1.1" } }, "node_modules/eslint-module-utils": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", - "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.14.0.tgz", + "integrity": "sha512-W2WCRZ9Dqntd+2u8jJcVMV2PKulc6RdLgUUoh/yQr3uB6lo/ZOeGx11sv60/8S4QFFKNslAlWhr9u0Ef7ZW6Ig==", "dev": true, + "license": "MIT", "dependencies": { "debug": "^3.2.7" }, @@ -2524,6 +2798,7 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, + "license": "MIT", "dependencies": { "ms": "^2.1.1" } @@ -2533,6 +2808,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, + "license": "MIT", "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -2561,11 +2837,19 @@ "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" } }, + "node_modules/eslint-plugin-import/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/eslint-plugin-import/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2576,15 +2860,17 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, + "license": "MIT", "dependencies": { "ms": "^2.1.1" } }, "node_modules/eslint-plugin-import/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -2597,6 +2883,7 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" } @@ -2606,6 +2893,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-simple-import-sort/-/eslint-plugin-simple-import-sort-12.1.1.tgz", "integrity": "sha512-6nuzu4xwQtE3332Uz0to+TxDQYRLTKRESSc2hefVT48Zc8JthmN23Gx9lnYhu0FtkRSL1oxny3kJ2aveVhmOVA==", "dev": true, + "license": "MIT", "peerDependencies": { "eslint": ">=5.0.0" } @@ -2615,6 +2903,7 @@ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" @@ -2631,6 +2920,7 @@ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, + "license": "Apache-2.0", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -2638,11 +2928,36 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2653,6 +2968,7 @@ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, + "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -2665,15 +2981,24 @@ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, + "license": "MIT", "engines": { "node": ">= 4" } }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -2686,6 +3011,7 @@ "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", @@ -2703,6 +3029,7 @@ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, + "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -2711,10 +3038,11 @@ } }, "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "estraverse": "^5.1.0" }, @@ -2727,6 +3055,7 @@ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" }, @@ -2739,6 +3068,7 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=4.0" } @@ -2748,6 +3078,7 @@ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", "dev": true, + "license": "MIT", "dependencies": { "@types/estree": "^1.0.0" } @@ -2757,6 +3088,7 @@ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" } @@ -2770,19 +3102,33 @@ "node": ">= 0.6" } }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/eventsource-parser": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.5.tgz", - "integrity": "sha512-bSRG85ZrMdmWtm7qkF9He9TNRzc/Bm99gEJMaQoHJ9E6Kv9QBbsldh2oMj7iXmYNEAVvNgvv5vPorG6W+XtBhQ==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.1.tgz", + "integrity": "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==", + "license": "MIT", "engines": { - "node": ">=20.0.0" + "node": ">=18.0.0" } }, "node_modules/expect-type": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", - "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=12.0.0" } @@ -2831,12 +3177,13 @@ } }, "node_modules/express-rate-limit": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.0.tgz", - "integrity": "sha512-KJzBawY6fB9FiZGdE/0aftepZ91YlaGIrV8vgblRM3J8X+dHx/aiowJWwkx6LIGyuqGiANsjSwwrbb8mifOJ4Q==", + "version": "8.6.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.6.2.tgz", + "integrity": "sha512-YH4ru+eOJxQABscKFfRCy9R7x9QFGdezclVMwwgFFndzS2Xnm0uo6B0ABZsLhcpeptGv2qvuJVWlQr9gQZoC3A==", "license": "MIT", "dependencies": { - "ip-address": "10.1.0" + "debug": "^4.4.3", + "ip-address": "^10.2.0" }, "engines": { "node": ">= 16" @@ -2851,60 +3198,27 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true - }, - "node_modules/fast-redact": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz", - "integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==", - "engines": { - "node": ">=6" - } + "dev": true, + "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.6.tgz", + "integrity": "sha512-7Ical1vFEMr0onbVzEDIreM22I4khW+fzyQPwvAFWBp1iwdshSZRsL4jjRvPG9JP1uiqMHRto+YU6R2/CzDz5Q==", "funding": [ { "type": "github", @@ -2914,16 +3228,25 @@ "type": "opencollective", "url": "https://opencollective.com/fastify" } - ], - "license": "BSD-3-Clause" - }, - "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", - "dev": true, - "dependencies": { - "reusify": "^1.0.4" + ], + "license": "BSD-3-Clause" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } } }, "node_modules/file-entry-cache": { @@ -2931,6 +3254,7 @@ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, + "license": "MIT", "dependencies": { "flat-cache": "^4.0.0" }, @@ -2938,18 +3262,6 @@ "node": ">=16.0.0" } }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/finalhandler": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", @@ -2976,6 +3288,7 @@ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, + "license": "MIT", "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" @@ -2992,6 +3305,7 @@ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, + "license": "MIT", "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" @@ -3001,21 +3315,23 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", - "dev": true + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" }, "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", "funding": [ { "type": "individual", "url": "https://github.com/sponsors/RubenVerborgh" } ], + "license": "MIT", "engines": { "node": ">=4.0" }, @@ -3030,6 +3346,7 @@ "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", "dev": true, + "license": "MIT", "dependencies": { "is-callable": "^1.2.7" }, @@ -3041,16 +3358,16 @@ } }, "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" + "hasown": "^2.0.4", + "mime-types": "^2.1.35" }, "engines": { "node": ">= 6" @@ -3101,6 +3418,7 @@ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, "hasInstallScript": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -3113,22 +3431,27 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/function.prototype.name": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", - "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.2.0.tgz", + "integrity": "sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", - "hasown": "^2.0.2", - "is-callable": "^1.2.7" + "has-property-descriptors": "^1.0.2", + "hasown": "^2.0.4", + "is-callable": "^1.2.7", + "is-document.all": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -3142,14 +3465,26 @@ "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", @@ -3173,6 +3508,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" @@ -3186,6 +3522,7 @@ "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", @@ -3198,23 +3535,12 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-tsconfig": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", - "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==", - "dev": true, - "dependencies": { - "resolve-pkg-maps": "^1.0.0" - }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" - } - }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, + "license": "ISC", "dependencies": { "is-glob": "^4.0.3" }, @@ -3227,6 +3553,7 @@ "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, @@ -3239,6 +3566,7 @@ "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", "dev": true, + "license": "MIT", "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" @@ -3254,6 +3582,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -3261,17 +3590,12 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true - }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -3284,6 +3608,7 @@ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -3293,6 +3618,7 @@ "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dev": true, + "license": "MIT", "dependencies": { "es-define-property": "^1.0.0" }, @@ -3305,6 +3631,7 @@ "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", "dev": true, + "license": "MIT", "dependencies": { "dunder-proto": "^1.0.0" }, @@ -3319,6 +3646,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -3330,6 +3658,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" }, @@ -3341,9 +3670,10 @@ } }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", "dependencies": { "function-bind": "^1.1.2" }, @@ -3352,9 +3682,9 @@ } }, "node_modules/hono": { - "version": "4.12.4", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.4.tgz", - "integrity": "sha512-ooiZW1Xy8rQ4oELQ++otI2T9DsKpV0M6c6cO6JGx4RTfav9poFFLlet9UMXHZnoM1yG0HWGlQLswBGX3RZmHtg==", + "version": "4.13.4", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.4.tgz", + "integrity": "sha512-AGEwKIyRMHRv1t8Wjwa3LHxQ61X5CqrdFT+4BRNTpqS5aJNnpl5WLjADb7vFlJzI/8uK7T5QLVApCMQKNa3LgQ==", "license": "MIT", "engines": { "node": ">=16.9.0" @@ -3380,10 +3710,23 @@ "url": "https://opencollective.com/express" } }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/iconv-lite": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -3397,10 +3740,11 @@ } }, "node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", + "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 4" } @@ -3410,6 +3754,7 @@ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dev": true, + "license": "MIT", "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -3426,6 +3771,7 @@ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.8.19" } @@ -3441,6 +3787,7 @@ "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", "dev": true, + "license": "MIT", "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", @@ -3451,9 +3798,9 @@ } }, "node_modules/ip-address": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", - "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.5.0.tgz", + "integrity": "sha512-R5SnVLJmgYYvf2F2ZgwSBnelz5G4q5AxIC277GDfUaNbrZKNANcBC7RHqYYePlszf4kBolVkJauG0ZjHHFh55g==", "license": "MIT", "engines": { "node": ">= 12" @@ -3473,6 +3820,7 @@ "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", @@ -3490,6 +3838,7 @@ "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", "dev": true, + "license": "MIT", "dependencies": { "async-function": "^1.0.0", "call-bound": "^1.0.3", @@ -3509,6 +3858,7 @@ "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", "dev": true, + "license": "MIT", "dependencies": { "has-bigints": "^1.0.2" }, @@ -3524,6 +3874,7 @@ "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" @@ -3540,6 +3891,7 @@ "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -3548,12 +3900,13 @@ } }, "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", "dev": true, + "license": "MIT", "dependencies": { - "hasown": "^2.0.2" + "hasown": "^2.0.3" }, "engines": { "node": ">= 0.4" @@ -3567,6 +3920,7 @@ "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", @@ -3584,6 +3938,7 @@ "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" @@ -3595,11 +3950,28 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-document.all": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-document.all/-/is-document.all-1.0.0.tgz", + "integrity": "sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -3609,6 +3981,7 @@ "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.3" }, @@ -3620,13 +3993,15 @@ } }, "node_modules/is-generator-function": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", - "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", "dev": true, + "license": "MIT", "dependencies": { - "call-bound": "^1.0.3", - "get-proto": "^1.0.0", + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" }, @@ -3642,6 +4017,7 @@ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, + "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" }, @@ -3654,6 +4030,7 @@ "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -3666,6 +4043,7 @@ "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -3673,20 +4051,12 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, "node_modules/is-number-object": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" @@ -3709,6 +4079,7 @@ "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", @@ -3726,6 +4097,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-2.2.0.tgz", "integrity": "sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg==", + "license": "MIT", "engines": { "node": ">=10" }, @@ -3738,6 +4110,7 @@ "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -3750,6 +4123,7 @@ "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.3" }, @@ -3765,6 +4139,7 @@ "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" @@ -3781,6 +4156,7 @@ "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "has-symbols": "^1.1.0", @@ -3798,6 +4174,7 @@ "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", "dev": true, + "license": "MIT", "dependencies": { "which-typed-array": "^1.1.16" }, @@ -3813,6 +4190,7 @@ "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -3825,6 +4203,7 @@ "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.3" }, @@ -3840,6 +4219,7 @@ "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" @@ -3855,27 +4235,40 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" }, "node_modules/jose": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", - "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==", + "version": "6.2.10", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.10.tgz", + "integrity": "sha512-iiW7J9qRFlGxvCOIBDBDxFePQSn7ZMAnrYGhrrOo6siO/MIqwfyilLR27pkfDgUk+raLuzADS8A3S/KLBisc0g==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/panva" } }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -3887,13 +4280,14 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" }, "node_modules/json-schema-typed": { "version": "8.0.2", @@ -3905,13 +4299,15 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/json5": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", "dev": true, + "license": "MIT", "dependencies": { "minimist": "^1.2.0" }, @@ -3924,6 +4320,7 @@ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, + "license": "MIT", "dependencies": { "json-buffer": "3.0.1" } @@ -3933,6 +4330,7 @@ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, + "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" @@ -3946,6 +4344,7 @@ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, + "license": "MIT", "dependencies": { "p-locate": "^5.0.0" }, @@ -3960,19 +4359,22 @@ "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/loupe": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/magic-string": { - "version": "0.30.18", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.18.tgz", - "integrity": "sha512-yi8swmWbO17qHhwIBNeeZxTceJMeBvWJaId6dyvTSOwTipqeHhMhOrz6513r1sOKnpvQ7zkhlG8tPrpilwTxHQ==", + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } @@ -3981,17 +4383,22 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", "engines": { "node": ">= 0.4" } }, "node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz", + "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==", "license": "MIT", "engines": { "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/merge-descriptors": { @@ -4006,28 +4413,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, "node_modules/mime-db": { "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", @@ -4054,15 +4439,16 @@ } }, "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^5.0.8" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -4073,6 +4459,7 @@ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -4080,12 +4467,13 @@ "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", "dev": true, "funding": [ { @@ -4093,6 +4481,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -4104,21 +4493,72 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.1.0.tgz", + "integrity": "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==", "license": "MIT", + "dependencies": { + "content-type": "^2.1.0" + }, "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/negotiator/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/node-exports-info": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.2.tgz", + "integrity": "sha512-kXs9Go0cah0qHVV2v389IXQLdLCeE1xfFtjOAF+iobu0OIoG1pje8At2vMHyaPMiPMnG/LWP50twML21eMcAag==", + "dev": true, + "license": "MIT", + "dependencies": { + "array.prototype.flatmap": "^1.3.3", + "es-errors": "^1.3.0", + "object.entries": "^1.1.9", + "semver": "^6.3.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/node-exports-info/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" } }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -4127,6 +4567,7 @@ "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -4139,6 +4580,7 @@ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -4148,6 +4590,7 @@ "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", @@ -4163,11 +4606,28 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/object.fromentries": { "version": "2.0.8", "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -4186,6 +4646,7 @@ "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -4200,6 +4661,7 @@ "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", @@ -4217,6 +4679,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", "engines": { "node": ">=14.0.0" } @@ -4247,6 +4710,7 @@ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, + "license": "MIT", "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", @@ -4260,12 +4724,14 @@ } }, "node_modules/own-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", - "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.2.tgz", + "integrity": "sha512-19YVAg7T+WTrxggPukVq7DjTv6+PJ867TmhCvBsYwmbFCsZd344rq2Ld1p0wo8f8Qrrhgp82c6FJRqdXWtSEhg==", "dev": true, + "license": "MIT", "dependencies": { - "get-intrinsic": "^1.2.6", + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" }, @@ -4281,6 +4747,7 @@ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, + "license": "MIT", "dependencies": { "yocto-queue": "^0.1.0" }, @@ -4296,6 +4763,7 @@ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, + "license": "MIT", "dependencies": { "p-limit": "^3.0.2" }, @@ -4311,6 +4779,7 @@ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, + "license": "MIT", "dependencies": { "callsites": "^3.0.0" }, @@ -4332,6 +4801,7 @@ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -4340,6 +4810,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", "engines": { "node": ">=8" } @@ -4348,12 +4819,13 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/path-to-regexp": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", - "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", "license": "MIT", "funding": { "type": "opencollective", @@ -4364,13 +4836,15 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/pathval": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", "dev": true, + "license": "MIT", "engines": { "node": ">= 14.16" } @@ -4379,27 +4853,30 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", "dev": true, + "license": "MIT", "engines": { - "node": ">=8.6" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/pino": { - "version": "9.9.0", - "resolved": "https://registry.npmjs.org/pino/-/pino-9.9.0.tgz", - "integrity": "sha512-zxsRIQG9HzG+jEljmvmZupOMDUQ0Jpj0yAgE28jQvvrdYTlEaiGwelJpdndMl/MBuRr70heIj83QyqJUWaU8mQ==", + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-9.14.0.tgz", + "integrity": "sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==", + "license": "MIT", "dependencies": { + "@pinojs/redact": "^0.4.0", "atomic-sleep": "^1.0.0", - "fast-redact": "^3.1.1", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^2.0.0", "pino-std-serializers": "^7.0.0", @@ -4418,19 +4895,22 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "license": "MIT", "dependencies": { "split2": "^4.0.0" } }, "node_modules/pino-std-serializers": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz", - "integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==" + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "license": "MIT" }, "node_modules/pkce-challenge": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", - "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", "engines": { "node": ">=16.20.0" } @@ -4440,14 +4920,15 @@ "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", "dev": true, "funding": [ { @@ -4463,8 +4944,9 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -4477,15 +4959,17 @@ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8.0" } }, "node_modules/prettier": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", - "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "version": "3.9.6", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz", + "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==", "dev": true, + "license": "MIT", "bin": { "prettier": "bin/prettier.cjs" }, @@ -4497,9 +4981,9 @@ } }, "node_modules/process-warning": { - "version": "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==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.1.0.tgz", + "integrity": "sha512-jQSaVHsPgtyw60e1rQ/A+/ArPEj/S8pS/vFnyGa/gYFXrKk/6RuDkoqVDQ5NI5MmS01698ltlAk0NoDBNLujRw==", "funding": [ { "type": "github", @@ -4509,12 +4993,14 @@ "type": "opencollective", "url": "https://opencollective.com/fastify" } - ] + ], + "license": "MIT" }, "node_modules/prom-client": { "version": "15.1.3", "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-15.1.3.tgz", "integrity": "sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g==", + "deprecated": "prom-client has been replaced by @prometheus-io/client", "license": "Apache-2.0", "dependencies": { "@opentelemetry/api": "^1.4.0", @@ -4538,26 +5024,32 @@ } }, "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/qs": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", - "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.1.0" + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" }, "engines": { "node": ">=0.6" @@ -4566,38 +5058,23 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, "node_modules/quick-format-unescaped": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", - "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==" + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" }, "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", "license": "MIT", "engines": { "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/raw-body": { @@ -4619,6 +5096,7 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", "engines": { "node": ">= 12.13.0" } @@ -4628,6 +5106,7 @@ "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", @@ -4650,6 +5129,7 @@ "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", @@ -4675,12 +5155,16 @@ } }, "node_modules/resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "version": "2.0.0-next.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz", + "integrity": "sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==", "dev": true, + "license": "MIT", "dependencies": { - "is-core-module": "^2.16.0", + "es-errors": "^1.3.0", + "is-core-module": "^2.16.2", + "node-exports-info": "^1.6.0", + "object-keys": "^1.1.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, @@ -4699,36 +5183,19 @@ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "dev": true, - "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" - } - }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, "node_modules/rollup": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.49.0.tgz", - "integrity": "sha512-3IVq0cGJ6H7fKXXEdVt+RcYvRCt8beYY9K1760wGQwSAHZcS9eot1zDG5axUbcp/kWRi5zKIIDX8MoKv/TzvZA==", + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.0.tgz", + "integrity": "sha512-T5vnZ2y4QqC3/4P+w2+JO+Q/OVdnPsv4XcSYJYMEn0R9/jjl5AgLwO9LAZMzP2lN71O6pypn91rB7lDstUkfrQ==", "dev": true, + "license": "MIT", "dependencies": { - "@types/estree": "1.0.8" + "@types/estree": "1.0.9" }, "bin": { "rollup": "dist/bin/rollup" @@ -4738,26 +5205,32 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.49.0", - "@rollup/rollup-android-arm64": "4.49.0", - "@rollup/rollup-darwin-arm64": "4.49.0", - "@rollup/rollup-darwin-x64": "4.49.0", - "@rollup/rollup-freebsd-arm64": "4.49.0", - "@rollup/rollup-freebsd-x64": "4.49.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.49.0", - "@rollup/rollup-linux-arm-musleabihf": "4.49.0", - "@rollup/rollup-linux-arm64-gnu": "4.49.0", - "@rollup/rollup-linux-arm64-musl": "4.49.0", - "@rollup/rollup-linux-loongarch64-gnu": "4.49.0", - "@rollup/rollup-linux-ppc64-gnu": "4.49.0", - "@rollup/rollup-linux-riscv64-gnu": "4.49.0", - "@rollup/rollup-linux-riscv64-musl": "4.49.0", - "@rollup/rollup-linux-s390x-gnu": "4.49.0", - "@rollup/rollup-linux-x64-gnu": "4.49.0", - "@rollup/rollup-linux-x64-musl": "4.49.0", - "@rollup/rollup-win32-arm64-msvc": "4.49.0", - "@rollup/rollup-win32-ia32-msvc": "4.49.0", - "@rollup/rollup-win32-x64-msvc": "4.49.0", + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.63.0", + "@rollup/rollup-android-arm64": "4.63.0", + "@rollup/rollup-darwin-arm64": "4.63.0", + "@rollup/rollup-darwin-x64": "4.63.0", + "@rollup/rollup-freebsd-arm64": "4.63.0", + "@rollup/rollup-freebsd-x64": "4.63.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.63.0", + "@rollup/rollup-linux-arm-musleabihf": "4.63.0", + "@rollup/rollup-linux-arm64-gnu": "4.63.0", + "@rollup/rollup-linux-arm64-musl": "4.63.0", + "@rollup/rollup-linux-loong64-gnu": "4.63.0", + "@rollup/rollup-linux-loong64-musl": "4.63.0", + "@rollup/rollup-linux-ppc64-gnu": "4.63.0", + "@rollup/rollup-linux-ppc64-musl": "4.63.0", + "@rollup/rollup-linux-riscv64-gnu": "4.63.0", + "@rollup/rollup-linux-riscv64-musl": "4.63.0", + "@rollup/rollup-linux-s390x-gnu": "4.63.0", + "@rollup/rollup-linux-x64-gnu": "4.63.0", + "@rollup/rollup-linux-x64-musl": "4.63.0", + "@rollup/rollup-openbsd-x64": "4.63.0", + "@rollup/rollup-openharmony-arm64": "4.63.0", + "@rollup/rollup-win32-arm64-msvc": "4.63.0", + "@rollup/rollup-win32-ia32-msvc": "4.63.0", + "@rollup/rollup-win32-x64-gnu": "4.63.0", + "@rollup/rollup-win32-x64-msvc": "4.63.0", "fsevents": "~2.3.2" } }, @@ -4777,38 +5250,16 @@ "node": ">= 18" } }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, "node_modules/safe-array-concat": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", - "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz", + "integrity": "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", "has-symbols": "^1.1.0", "isarray": "^2.0.5" }, @@ -4824,6 +5275,7 @@ "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", "dev": true, + "license": "MIT", "dependencies": { "es-errors": "^1.3.0", "isarray": "^2.0.5" @@ -4840,6 +5292,7 @@ "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", @@ -4856,6 +5309,7 @@ "version": "2.5.0", "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", "engines": { "node": ">=10" } @@ -4867,10 +5321,11 @@ "license": "MIT" }, "node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -4928,6 +5383,7 @@ "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "dev": true, + "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", @@ -4945,6 +5401,7 @@ "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", "dev": true, + "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", @@ -4960,6 +5417,7 @@ "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", "dev": true, + "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", @@ -4979,6 +5437,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" }, @@ -4990,18 +5449,20 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" }, @@ -5013,12 +5474,13 @@ } }, "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" + "object-inspect": "^1.13.4" }, "engines": { "node": ">= 0.4" @@ -5031,6 +5493,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", @@ -5048,6 +5511,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", @@ -5066,12 +5530,14 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/sonic-boom": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz", - "integrity": "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "license": "MIT", "dependencies": { "atomic-sleep": "^1.0.0" } @@ -5081,6 +5547,7 @@ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -5089,6 +5556,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", "engines": { "node": ">= 10.x" } @@ -5097,7 +5565,8 @@ "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/statuses": { "version": "2.0.2", @@ -5109,16 +5578,18 @@ } }, "node_modules/std-env": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", - "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==", - "dev": true + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", "dev": true, + "license": "MIT", "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" @@ -5128,18 +5599,20 @@ } }, "node_modules/string.prototype.trim": { - "version": "1.2.10", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", - "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.11.tgz", + "integrity": "sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", "define-data-property": "^1.1.4", "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-object-atoms": "^1.0.0", - "has-property-descriptors": "^1.0.2" + "es-abstract": "^1.24.2", + "es-object-atoms": "^1.1.2", + "has-property-descriptors": "^1.0.2", + "safe-regex-test": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -5149,15 +5622,16 @@ } }, "node_modules/string.prototype.trimend": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", - "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.10.tgz", + "integrity": "sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" + "es-object-atoms": "^1.1.2" }, "engines": { "node": ">= 0.4" @@ -5171,6 +5645,7 @@ "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -5188,6 +5663,7 @@ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -5197,6 +5673,7 @@ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" }, @@ -5209,6 +5686,7 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -5221,6 +5699,7 @@ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -5238,9 +5717,10 @@ } }, "node_modules/thread-stream": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", - "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.2.0.tgz", + "integrity": "sha512-zLBvqpwr4Esa0kRjcrzGU6zL25lePWaCLMx0RQFrmteozIfeNdaMLpG5U7PeHzvlFkAWaRKA9/KVW4F60iB+qw==", + "license": "MIT", "dependencies": { "real-require": "^0.2.0" } @@ -5249,19 +5729,39 @@ "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/tinyexec": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", - "dev": true + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } }, "node_modules/tinypool": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", "dev": true, + "license": "MIT", "engines": { "node": "^18.0.0 || >=20.0.0" } @@ -5271,6 +5771,7 @@ "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=14.0.0" } @@ -5280,22 +5781,11 @@ "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=14.0.0" } }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -5306,10 +5796,11 @@ } }, "node_modules/ts-api-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", - "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", "dev": true, + "license": "MIT", "engines": { "node": ">=18.12" }, @@ -5322,6 +5813,7 @@ "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", "dev": true, + "license": "MIT", "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", @@ -5330,13 +5822,13 @@ } }, "node_modules/tsx": { - "version": "4.20.5", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.5.tgz", - "integrity": "sha512-+wKjMNU9w/EaQayHXb7WA7ZaHY6hN8WgfvHNQ3t1PnU91/7O8TcTnIhCDYTZwnt8JsO9IBqZ30Ln1r7pPF52Aw==", + "version": "4.23.12", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.12.tgz", + "integrity": "sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q==", "dev": true, + "license": "MIT", "dependencies": { - "esbuild": "~0.25.0", - "get-tsconfig": "^4.7.5" + "esbuild": "~0.28.0" }, "bin": { "tsx": "dist/cli.mjs" @@ -5353,6 +5845,7 @@ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, + "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1" }, @@ -5361,17 +5854,34 @@ } }, "node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", "license": "MIT", "dependencies": { - "content-type": "^1.0.5", + "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" }, "engines": { - "node": ">= 0.6" + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/typed-array-buffer": { @@ -5379,6 +5889,7 @@ "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", @@ -5393,6 +5904,7 @@ "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "for-each": "^0.3.3", @@ -5412,6 +5924,7 @@ "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", "dev": true, + "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", @@ -5429,17 +5942,18 @@ } }, "node_modules/typed-array-length": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", - "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.8.tgz", + "integrity": "sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "is-typed-array": "^1.1.13", - "possible-typed-array-names": "^1.0.0", - "reflect.getprototypeof": "^1.0.6" + "call-bind": "^1.0.9", + "for-each": "^0.3.5", + "gopd": "^1.2.0", + "is-typed-array": "^1.1.15", + "possible-typed-array-names": "^1.1.0", + "reflect.getprototypeof": "^1.0.10" }, "engines": { "node": ">= 0.4" @@ -5449,10 +5963,11 @@ } }, "node_modules/typescript": { - "version": "5.9.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", - "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -5462,15 +5977,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.41.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.41.0.tgz", - "integrity": "sha512-n66rzs5OBXW3SFSnZHr2T685q1i4ODm2nulFJhMZBotaTavsS8TrI3d7bDlRSs9yWo7HmyWrN9qDu14Qv7Y0Dw==", + "version": "8.68.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.68.0.tgz", + "integrity": "sha512-MHy0Y0ynqeEbx/S45+i/bBssdy3X6KNBfmJAP35GrgtNxu2TQ5K5xsFDhAnmsq1jvpdoZOPG1LGtJo0HWqYCrQ==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.41.0", - "@typescript-eslint/parser": "8.41.0", - "@typescript-eslint/typescript-estree": "8.41.0", - "@typescript-eslint/utils": "8.41.0" + "@typescript-eslint/eslint-plugin": "8.68.0", + "@typescript-eslint/parser": "8.68.0", + "@typescript-eslint/typescript-estree": "8.68.0", + "@typescript-eslint/utils": "8.68.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5480,8 +5996,8 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/unbox-primitive": { @@ -5489,6 +6005,7 @@ "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", @@ -5506,15 +6023,19 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/unpdf": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/unpdf/-/unpdf-1.4.0.tgz", - "integrity": "sha512-TahIk0xdH/4jh/MxfclzU79g40OyxtP00VnEUZdEkJoYtXAHWLiir6t3FC6z3vDqQTzc2ZHcla6uEiVTNjejuA==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/unpdf/-/unpdf-1.8.1.tgz", + "integrity": "sha512-xkURhy2SoGpOIH0a1gLHNkASPIQYonadDJs2AQwPEfUakafeD9EA1WTWWsaR++gfTCXJpV27W7tU1nXuk82UKQ==", "license": "MIT", + "engines": { + "node": ">=22" + }, "peerDependencies": { - "@napi-rs/canvas": "^0.1.69" + "@napi-rs/canvas": "^0.1.69 || ^1.0.0" }, "peerDependenciesMeta": { "@napi-rs/canvas": { @@ -5536,6 +6057,7 @@ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" } @@ -5544,15 +6066,17 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/vite": { - "version": "5.4.19", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz", - "integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==", + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, + "license": "MIT", "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -5612,6 +6136,7 @@ "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", "dev": true, + "license": "MIT", "dependencies": { "cac": "^6.7.14", "debug": "^4.3.7", @@ -5637,6 +6162,7 @@ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "aix" @@ -5653,6 +6179,7 @@ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -5669,6 +6196,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -5685,6 +6213,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -5701,6 +6230,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -5717,6 +6247,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -5733,6 +6264,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" @@ -5749,6 +6281,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" @@ -5765,6 +6298,7 @@ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -5781,6 +6315,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -5797,6 +6332,7 @@ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -5813,6 +6349,7 @@ "loong64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -5829,6 +6366,7 @@ "mips64el" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -5845,6 +6383,7 @@ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -5861,6 +6400,7 @@ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -5877,6 +6417,7 @@ "s390x" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -5893,6 +6434,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -5909,6 +6451,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "netbsd" @@ -5925,6 +6468,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "openbsd" @@ -5941,6 +6485,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "sunos" @@ -5957,6 +6502,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -5973,6 +6519,7 @@ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -5989,6 +6536,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -6003,6 +6551,7 @@ "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", "dev": true, "hasInstallScript": true, + "license": "MIT", "bin": { "esbuild": "bin/esbuild" }, @@ -6040,6 +6589,7 @@ "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", "dev": true, + "license": "MIT", "dependencies": { "@vitest/expect": "2.1.9", "@vitest/mocker": "2.1.9", @@ -6104,6 +6654,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", "dependencies": { "isexe": "^2.0.0" }, @@ -6119,6 +6670,7 @@ "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", "dev": true, + "license": "MIT", "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", @@ -6138,6 +6690,7 @@ "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "function.prototype.name": "^1.1.6", @@ -6165,6 +6718,7 @@ "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", "dev": true, + "license": "MIT", "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", @@ -6179,13 +6733,14 @@ } }, "node_modules/which-typed-array": { - "version": "1.1.19", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", - "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "version": "1.1.22", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.22.tgz", + "integrity": "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==", "dev": true, + "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", + "call-bind": "^1.0.9", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", @@ -6204,6 +6759,7 @@ "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", "dev": true, + "license": "MIT", "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" @@ -6220,6 +6776,7 @@ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -6235,6 +6792,7 @@ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -6246,17 +6804,18 @@ "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } }, "node_modules/zod-to-json-schema": { - "version": "3.25.1", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz", - "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", "license": "ISC", "peerDependencies": { - "zod": "^3.25 || ^4" + "zod": "^3.25.28 || ^4" } } } diff --git a/package.json b/package.json index 2bc16c4..457fb96 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@seatable/mcp-seatable", - "version": "1.5.2", + "version": "1.6.0", "type": "module", "license": "MIT", "mcpName": "io.github.seatable/seatable", diff --git a/src/auth/oauthProvider.ts b/src/auth/oauthProvider.ts index 04df0d7..154d87f 100644 --- a/src/auth/oauthProvider.ts +++ b/src/auth/oauthProvider.ts @@ -1,23 +1,114 @@ -import { createHash, randomBytes } from 'node:crypto' +import { createHash, randomBytes, timingSafeEqual } from 'node:crypto' import type { IncomingMessage, ServerResponse } from 'node:http' import { logger } from '../logger.js' +import { TokenCipher } from './tokenCipher.js' interface AuthorizationCode { apiToken: string + /** The client the code was issued to. The same client must present it at /token. */ + clientId: string redirectUri: string - codeChallenge?: string - codeChallengeMethod?: string + codeChallenge: string expiresAt: number } +interface AuthorizePageOptions { + client: ClientRegistration + clientId: string + redirectUri: string + callbackOrigin: string + state: string + responseType: string + codeChallenge: string + codeChallengeMethod: string +} + +interface ClientRegistration extends Record { + /** client_name */ + n: string + /** registered redirect_uris */ + r: string[] +} + const CODE_TTL_MS = 5 * 60 * 1000 // 5 minutes +const DEFAULT_ACCESS_TOKEN_TTL_MS = 60 * 60 * 1000 // 1 hour +const MIN_REFRESH_TOKEN_TTL_MS = 14 * 24 * 60 * 60 * 1000 // 14 days +const CLIENT_TTL_MS = 365 * 24 * 60 * 60 * 1000 // 1 year const CLEANUP_INTERVAL_MS = 60 * 1000 export interface OAuthProviderOptions { hostname?: string /** Optional callback to validate an API token before issuing an authorization code. */ validateToken?: (token: string) => Promise + /** + * Hostnames whose https callbacks may receive an authorization code. + * Exact, case-insensitive match — no subdomain wildcards. Loopback and + * private-use schemes are always allowed and need no entry here. + * A single '*' disables curation entirely (dangerous; see README). + * Defaults to DEFAULT_TRUSTED_REDIRECT_HOSTS. + */ + trustedRedirectHosts?: string[] + /** + * Lifetime of an issued access token, in ms. Defaults to one hour. + * Shorter narrows the window after a SeaTable token is revoked; longer + * spares users a re-prompt if their client renews badly. + */ + accessTokenTtlMs?: number + /** Resolves the client IP for audit logging. Falls back to 'unknown'. */ + getClientIp?: (req: IncomingMessage) => string + /** + * Distinguishes the two SeaTable token types. Called only after validation + * failed, to turn "Invalid API token" into a message that names the mistake. + */ + looksLikeAccountToken?: (token: string) => Promise + /** + * Secret used to seal access tokens, refresh tokens and client registrations. + * Must be stable across restarts, otherwise every client is forced to re-authorize. + */ + secret?: string +} + +function parseRedirectUri(raw: string): URL | undefined { + try { + const url = new URL(raw) + return url.hash ? undefined : url + } catch { + return undefined + } +} + +const LOOPBACK_HOSTS = new Set(['127.0.0.1', '::1', '[::1]', 'localhost']) + +/** + * Hosted MCP clients whose callbacks live on their own servers. These are the + * only destinations that receive an authorization code over the network, so + * they are curated rather than open. Operators extend or replace this via + * SEATABLE_OAUTH_TRUSTED_REDIRECT_HOSTS. + */ +export const DEFAULT_TRUSTED_REDIRECT_HOSTS = ['claude.ai', 'claude.com', 'chatgpt.com'] + +/** Schemes that can execute or read local content and must never be a callback. */ +const FORBIDDEN_SCHEMES = new Set(['javascript', 'data', 'file', 'blob', 'vbscript', 'about', 'view-source']) + +function isLoopback(url: URL): boolean { + return LOOPBACK_HOSTS.has(url.hostname) +} + +/** + * Exact string match, with the RFC 8252 §7.3 carve-out: a loopback client may + * use a different port than it registered, because the port is picked at runtime. + */ +function redirectUriMatches(registered: string, candidate: string): boolean { + if (registered === candidate) return true + try { + const a = new URL(registered) + const b = new URL(candidate) + if (!isLoopback(a) || !isLoopback(b)) return false + return a.protocol === b.protocol && a.hostname === b.hostname && a.pathname === b.pathname + } catch { + return false + } } export class OAuthProvider { @@ -25,16 +116,97 @@ export class OAuthProvider { private readonly cleanupInterval: ReturnType private readonly configuredHostname?: string private readonly validateToken?: (token: string) => Promise + private readonly cipher: TokenCipher + private readonly trustedRedirectHosts: Set + private readonly getClientIp?: (req: IncomingMessage) => string + private readonly accessTokenTtlMs: number + private readonly refreshTokenTtlMs: number + private readonly looksLikeAccountToken?: (token: string) => Promise constructor(options?: OAuthProviderOptions) { this.configuredHostname = options?.hostname this.validateToken = options?.validateToken + this.getClientIp = options?.getClientIp + this.accessTokenTtlMs = options?.accessTokenTtlMs ?? DEFAULT_ACCESS_TOKEN_TTL_MS + // A refresh token that outlives its access token is the whole point, so + // never let a long access lifetime silently invert the relationship. + this.refreshTokenTtlMs = Math.max(MIN_REFRESH_TOKEN_TTL_MS, this.accessTokenTtlMs) + this.looksLikeAccountToken = options?.looksLikeAccountToken + this.trustedRedirectHosts = new Set( + (options?.trustedRedirectHosts ?? DEFAULT_TRUSTED_REDIRECT_HOSTS) + .map((host) => host.trim().toLowerCase()) + .filter(Boolean), + ) + // Without a configured secret, fall back to an ephemeral one. Tokens then + // stop working after a restart, which is acceptable for dev/test but not + // for production — startHttpServer refuses to boot managed mode without it. + this.cipher = new TokenCipher(options?.secret || randomBytes(32).toString('hex')) this.cleanupInterval = setInterval(() => this.cleanup(), CLEANUP_INTERVAL_MS) if (this.cleanupInterval.unref) { this.cleanupInterval.unref() } } + private clientIp(req: IncomingMessage): string { + return this.getClientIp?.(req) ?? 'unknown' + } + + /** + * May this callback be used at all? + * + * Rejects only destinations that can never be safe: schemes that execute or + * read local content, and plaintext http to a remote host. An unknown https + * host is permitted — it meets the acknowledgement instead (see below). + */ + isPermittedRedirectUri(raw: string): boolean { + const url = parseRedirectUri(raw) + if (!url) return false + + const scheme = url.protocol.replace(/:$/, '').toLowerCase() + if (FORBIDDEN_SCHEMES.has(scheme)) return false + if (scheme === 'http') return isLoopback(url) + if (scheme === 'https') return true + + // Private-use / app scheme (RFC 8252 §7.1): the code is handed to a + // locally installed application, never transmitted to a remote host. + return /^[a-z][a-z0-9+.-]*$/.test(scheme) + } + + /** + * May this callback receive an authorization code without further friction? + * + * The question is not whether we recognise the client — with open dynamic + * registration an attacker registers honestly — but whether the code would + * leave the user's machine: + * + * loopback stays on the user's own machine + * private-use scheme goes to a locally installed app + * https + curated host a known hosted client (ChatGPT, Claude, ...) + * + * Everything else is still allowed, but only after the user has explicitly + * acknowledged where their token is about to be sent. Curation therefore + * removes friction; it is not a gate, and an empty list breaks nothing. + */ + isTrustedRedirectUri(raw: string): boolean { + if (!this.isPermittedRedirectUri(raw)) return false + const url = parseRedirectUri(raw)! + + const scheme = url.protocol.replace(/:$/, '').toLowerCase() + if (scheme !== 'https') return true + if (isLoopback(url)) return true + if (this.trustedRedirectHosts.has('*')) return true + return this.trustedRedirectHosts.has(url.hostname.toLowerCase()) + } + + /** + * Resolves one of our own access tokens back to the SeaTable API token it seals. + * Returns undefined for anything we did not issue, or that has expired. + */ + resolveAccessToken(accessToken: string): string | undefined { + const payload = this.cipher.open<{ t: string }>('access', accessToken) + return typeof payload?.t === 'string' ? payload.t : undefined + } + /** * Derive the base URL from SEATABLE_MCP_HOSTNAME or the incoming Host header. */ @@ -60,14 +232,19 @@ export class OAuthProvider { response_types_supported: ['code'], grant_types_supported: ['authorization_code', 'refresh_token'], token_endpoint_auth_methods_supported: ['none'], - code_challenge_methods_supported: ['S256', 'plain'], + // S256 only — 'plain' offers no protection against a stolen code. + code_challenge_methods_supported: ['S256'], } res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(metadata)) } /** * POST /register — Dynamic Client Registration (RFC 7591) - * Returns a generated client_id. We don't validate client credentials. + * + * The returned client_id is a sealed envelope containing the client's name and + * its redirect_uris. That makes the registration tamper-proof and verifiable at + * /authorize without any server-side storage: a client_id we did not issue + * cannot be opened, and the callback list inside it cannot be edited. */ async handleRegister(req: IncomingMessage, res: ServerResponse): Promise { if (req.method !== 'POST') { @@ -76,103 +253,203 @@ export class OAuthProvider { } const body = await this.parseBody(req) - const clientName = body.get('client_name') ?? 'mcp-client' - const redirectUris = body.get('redirect_uris') ?? '' + const clientName = (body.get('client_name') ?? 'mcp-client').slice(0, 200) + const redirectUris = this.parseRedirectUris(body.get('redirect_uris')) - const clientId = randomBytes(16).toString('hex') + if (redirectUris.length === 0) { + this.registrationError(res, 'invalid_redirect_uri', 'At least one redirect_uri is required') + return + } - logger.info({ clientName }, 'OAuth dynamic client registration') + const rejected = redirectUris.filter((uri) => !this.isPermittedRedirectUri(uri)) + if (rejected.length > 0) { + logger.warn({ clientName, count: rejected.length }, 'OAuth registration rejected: callback not permitted') + this.registrationError( + res, + 'invalid_redirect_uri', + 'A callback must use https, a private-use application scheme, or http on a loopback address.', + ) + return + } - const response: Record = { + const registration: ClientRegistration = { n: clientName, r: redirectUris } + const clientId = this.cipher.seal('client', registration, CLIENT_TTL_MS) + + // The registered callbacks are what /authorize later checks against, so a + // rejected authorization is only explainable if they were recorded here. + // Both fields come from an unauthenticated caller: cap count and length. + logger.info( + { + clientName, + callbacks: redirectUris.slice(0, 5).map((uri) => uri.slice(0, 120)), + ip: this.clientIp(req), + }, + 'OAuth dynamic client registration', + ) + + res.writeHead(201, { 'content-type': 'application/json' }).end(JSON.stringify({ client_id: clientId, client_name: clientName, + redirect_uris: redirectUris, token_endpoint_auth_method: 'none', - } - - if (redirectUris) { - response.redirect_uris = typeof redirectUris === 'string' && redirectUris.startsWith('[') - ? JSON.parse(redirectUris) - : [redirectUris] - } - - res.writeHead(201, { 'content-type': 'application/json' }).end(JSON.stringify(response)) + grant_types: ['authorization_code', 'refresh_token'], + response_types: ['code'], + client_id_issued_at: Math.floor(Date.now() / 1000), + })) } /** * GET /authorize — renders the authorization form * POST /authorize — processes the form submission + * + * Both paths validate the request the same way, and neither shows the token + * prompt until the client, the callback and the PKCE challenge all check out. */ async handleAuthorize(req: IncomingMessage, res: ServerResponse, url: URL): Promise { - const clientId = url.searchParams.get('client_id') ?? '' - const redirectUri = url.searchParams.get('redirect_uri') ?? '' - const state = url.searchParams.get('state') ?? '' - const responseType = url.searchParams.get('response_type') ?? '' - const codeChallenge = url.searchParams.get('code_challenge') ?? '' - const codeChallengeMethod = url.searchParams.get('code_challenge_method') ?? '' + if (req.method !== 'GET' && req.method !== 'POST') { + res.writeHead(405, { 'content-type': 'text/plain' }).end('Method not allowed') + return + } - if (req.method === 'GET') { - res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' }) - res.end(this.renderAuthorizePage(clientId, redirectUri, state, responseType, codeChallenge, codeChallengeMethod)) + const form = req.method === 'POST' ? await this.parseFormBody(req) : new Map() + const pick = (key: string) => form.get(key) ?? url.searchParams.get(key) ?? '' + + const clientId = pick('client_id') + const redirectUri = pick('redirect_uri') + const state = pick('state') + const responseType = pick('response_type') + const codeChallenge = pick('code_challenge') + const codeChallengeMethod = pick('code_challenge_method') + + const client = clientId ? this.cipher.open('client', clientId) : undefined + if (!client) { + logger.warn('OAuth authorize rejected: unknown or expired client registration') + this.authorizeError(res, 'Unknown client', 'This application is not registered with SeaTable MCP, or its registration has expired. Nothing has been sent to it.') return } - if (req.method === 'POST') { - const body = await this.parseFormBody(req) - const apiToken = body.get('api_token') ?? '' - const formRedirectUri = body.get('redirect_uri') ?? redirectUri - const formState = body.get('state') ?? state - const formCodeChallenge = body.get('code_challenge') ?? codeChallenge - const formCodeChallengeMethod = body.get('code_challenge_method') ?? codeChallengeMethod + if (responseType !== 'code') { + this.authorizeError(res, 'Unsupported request', 'Only the authorization code flow is supported.') + return + } - if (!apiToken) { - res.writeHead(400, { 'content-type': 'text/html; charset=utf-8' }) - res.end(this.renderAuthorizePage(clientId, formRedirectUri, formState, responseType, formCodeChallenge, formCodeChallengeMethod, 'Please enter your API token.')) - return - } + if (redirectUri && !this.isPermittedRedirectUri(redirectUri)) { + logger.warn({ clientName: client.n, callback: safeCallback(redirectUri), ip: this.clientIp(req) }, 'OAuth authorize rejected: callback not permitted') + this.authorizeError(res, 'Callback not permitted', 'SeaTable does not deliver authorizations to this kind of address. Nothing has been sent.') + return + } - // Validate token against SeaTable before issuing a code - if (this.validateToken) { - const valid = await this.validateToken(apiToken) - if (!valid) { - logger.warn('OAuth authorization rejected: invalid API token') - res.writeHead(400, { 'content-type': 'text/html; charset=utf-8' }) - res.end(this.renderAuthorizePage(clientId, formRedirectUri, formState, responseType, formCodeChallenge, formCodeChallengeMethod, 'Invalid API token. Please check your token and try again.')) - return - } - } + if (!redirectUri || !client.r.some((registered) => redirectUriMatches(registered, redirectUri))) { + logger.warn( + { clientName: client.n, callback: safeCallback(redirectUri), ip: this.clientIp(req) }, + 'OAuth authorize rejected: redirect_uri not registered', + ) + this.authorizeError(res, 'Unregistered callback address', 'This application asked SeaTable to send your authorization to an address it never registered. This is what a phishing attempt looks like — nothing has been sent.') + return + } - if (!formRedirectUri) { - res.writeHead(400, { 'content-type': 'text/plain' }).end('Missing redirect_uri') - return + if (!codeChallenge || codeChallengeMethod !== 'S256') { + logger.warn({ clientName: client.n, ip: this.clientIp(req) }, 'OAuth authorize rejected: missing or downgraded PKCE') + this.authorizeError(res, 'Insecure request', 'This application did not provide a valid PKCE challenge (S256 is required).') + return + } + + const callbackOrigin = new URL(redirectUri).origin + const pageOpts = { client, clientId, redirectUri, callbackOrigin, state, responseType, codeChallenge, codeChallengeMethod } + + /* + * An unfamiliar remote destination must be acknowledged before the token + * field appears. The attacker writes the entry link, so the acknowledgement + * is read from our own form body only — never from the query string — and + * a POST auto-submitted by a foreign page is refused via Sec-Fetch-Site. + */ + const needsAcknowledgement = !this.isTrustedRedirectUri(redirectUri) + const acknowledged = needsAcknowledgement + ? form.get('acknowledged') === 'yes' && req.headers['sec-fetch-site'] === 'same-origin' + : true + + if (!acknowledged) { + if (req.method === 'POST') { + logger.warn({ clientName: client.n, callback: safeCallback(redirectUri), ip: this.clientIp(req) }, 'OAuth authorize: unacknowledged destination') } + // 200 on first view, 400 when a submission tried to skip the step. + res.writeHead(req.method === 'POST' ? 400 : 200, { 'content-type': 'text/html; charset=utf-8' }) + res.end(this.renderAcknowledgementPage(pageOpts)) + return + } - const code = randomBytes(32).toString('hex') - this.codes.set(code, { - apiToken, - redirectUri: formRedirectUri, - codeChallenge: formCodeChallenge || undefined, - codeChallengeMethod: formCodeChallengeMethod || undefined, - expiresAt: Date.now() + CODE_TTL_MS, - }) + if (req.method === 'GET') { + res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' }) + res.end(this.renderAuthorizePage({ ...pageOpts, acknowledged: needsAcknowledgement })) + return + } - logger.info('OAuth authorization code issued') + // POST that passed the acknowledgement: the token itself is the payload. + const apiToken = (form.get('api_token') ?? '').trim() + const renderWithError = (message: string) => { + res.writeHead(400, { 'content-type': 'text/html; charset=utf-8' }) + res.end(this.renderAuthorizePage({ ...pageOpts, acknowledged: needsAcknowledgement, error: message })) + } - const redirect = new URL(formRedirectUri) - redirect.searchParams.set('code', code) - if (formState) { - redirect.searchParams.set('state', formState) + if (!apiToken) { + // Coming straight from the acknowledgement page there is nothing to + // complain about yet — this is the first sight of the token field. + if (!form.has('api_token')) { + res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' }) + res.end(this.renderAuthorizePage({ ...pageOpts, acknowledged: needsAcknowledgement })) + return } + renderWithError('Please enter your API token.') + return + } - res.writeHead(302, { location: redirect.toString() }) - res.end() + if (this.validateToken && !(await this.validateToken(apiToken))) { + // Half of all real rejections are the wrong *kind* of token, not a + // wrong token. Say which, instead of leaving the user guessing. + const isAccountToken = this.looksLikeAccountToken + ? await this.looksLikeAccountToken(apiToken).catch(() => false) + : false + logger.warn( + { ip: this.clientIp(req), clientName: client.n, kind: isAccountToken ? 'account_token' : 'unknown' }, + 'OAuth authorization rejected: invalid API token', + ) + renderWithError( + isAccountToken + ? 'That is an account API token. This connection needs a base API token: open the base, then Advanced \u2192 API Tokens \u2192 Add API Token.' + : 'Invalid API token. Please check your token and try again.', + ) return } - res.writeHead(405, { 'content-type': 'text/plain' }).end('Method not allowed') + const code = randomBytes(32).toString('hex') + this.codes.set(code, { + apiToken, + clientId, + redirectUri, + codeChallenge, + expiresAt: Date.now() + CODE_TTL_MS, + }) + + logger.info( + { flow: flowId(code), clientName: client.n, callback: safeCallback(redirectUri), ip: this.clientIp(req) }, + 'OAuth authorization code issued', + ) + + const redirect = new URL(redirectUri) + redirect.searchParams.set('code', code) + if (state) { + redirect.searchParams.set('state', state) + } + + res.writeHead(302, { location: redirect.toString() }) + res.end() } /** - * POST /token — exchanges authorization code for access token + * POST /token — exchanges an authorization code (or refresh token) for an access token. + * + * The issued access token is a sealed envelope around the SeaTable API token, + * never the API token itself. */ async handleToken(req: IncomingMessage, res: ServerResponse): Promise { if (req.method !== 'POST') { @@ -182,97 +459,144 @@ export class OAuthProvider { const body = await this.parseFormBody(req) const grantType = body.get('grant_type') - const code = body.get('code') - const redirectUri = body.get('redirect_uri') - const codeVerifier = body.get('code_verifier') - // Refresh token grant — the refresh token IS the API token if (grantType === 'refresh_token') { - const refreshToken = body.get('refresh_token') ?? '' - if (!refreshToken) { - res.writeHead(400, { 'content-type': 'application/json' }) - .end(JSON.stringify({ error: 'invalid_request', error_description: 'Missing refresh_token' })) - return - } - res.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store' }) - res.end(JSON.stringify({ - access_token: refreshToken, - token_type: 'Bearer', - refresh_token: refreshToken, - })) + this.handleRefreshGrant(body, res) return } if (grantType !== 'authorization_code') { - res.writeHead(400, { 'content-type': 'application/json' }) - .end(JSON.stringify({ error: 'unsupported_grant_type' })) + logger.warn({ grantType: String(grantType ?? '(none)').slice(0, 40) }, 'OAuth token exchange rejected: unsupported grant_type') + this.tokenError(res, 'unsupported_grant_type') return } + const code = body.get('code') + const clientId = body.get('client_id') + const redirectUri = body.get('redirect_uri') + const codeVerifier = body.get('code_verifier') + if (!code) { - res.writeHead(400, { 'content-type': 'application/json' }) - .end(JSON.stringify({ error: 'invalid_request', error_description: 'Missing code' })) + this.tokenError(res, 'invalid_request', 'Missing code') return } const stored = this.codes.get(code) + // Single-use: consume the code before any further check, so a failed + // attempt cannot be retried with different parameters. + this.codes.delete(code) + if (!stored) { - logger.warn('OAuth token exchange with invalid/expired code') - res.writeHead(400, { 'content-type': 'application/json' }) - .end(JSON.stringify({ error: 'invalid_grant', error_description: 'Invalid or expired authorization code' })) + logger.warn({ flow: flowId(code) }, 'OAuth token exchange with invalid/expired code') + this.tokenError(res, 'invalid_grant', 'Invalid or expired authorization code') return } - // Single-use: delete immediately - this.codes.delete(code) - if (Date.now() > stored.expiresAt) { - res.writeHead(400, { 'content-type': 'application/json' }) - .end(JSON.stringify({ error: 'invalid_grant', error_description: 'Authorization code expired' })) + logger.warn({ flow: flowId(code) }, 'OAuth token exchange rejected: authorization code expired') + this.tokenError(res, 'invalid_grant', 'Authorization code expired') return } - // Validate redirect_uri - if (redirectUri && redirectUri !== stored.redirectUri) { - res.writeHead(400, { 'content-type': 'application/json' }) - .end(JSON.stringify({ error: 'invalid_grant', error_description: 'redirect_uri mismatch' })) + // The code belongs to one client only. + if (!clientId || !constantTimeEquals(clientId, stored.clientId)) { + logger.warn({ flow: flowId(code) }, 'OAuth token exchange rejected: client_id does not match the code') + this.tokenError(res, 'invalid_grant', 'client_id does not match the authorization code') return } - // PKCE verification - if (stored.codeChallenge) { - if (!codeVerifier) { - res.writeHead(400, { 'content-type': 'application/json' }) - .end(JSON.stringify({ error: 'invalid_request', error_description: 'Missing code_verifier' })) - return - } + // redirect_uri is required here, not optional — omitting it must not skip the check. + if (!redirectUri || redirectUri !== stored.redirectUri) { + logger.warn({ flow: flowId(code) }, 'OAuth token exchange rejected: redirect_uri missing or mismatched') + this.tokenError(res, 'invalid_grant', 'redirect_uri mismatch') + return + } - const expected = stored.codeChallengeMethod === 'plain' - ? codeVerifier - : base64UrlEncode(createHash('sha256').update(codeVerifier).digest()) + if (!codeVerifier) { + logger.warn({ flow: flowId(code) }, 'OAuth token exchange rejected: missing code_verifier') + this.tokenError(res, 'invalid_grant', 'Missing code_verifier') + return + } - if (expected !== stored.codeChallenge) { - logger.warn('OAuth PKCE verification failed') - res.writeHead(400, { 'content-type': 'application/json' }) - .end(JSON.stringify({ error: 'invalid_grant', error_description: 'PKCE verification failed' })) - return - } + const expected = base64UrlEncode(createHash('sha256').update(codeVerifier).digest()) + if (!constantTimeEquals(expected, stored.codeChallenge)) { + logger.warn({ flow: flowId(code) }, 'OAuth token exchange rejected: PKCE verification failed') + this.tokenError(res, 'invalid_grant', 'PKCE verification failed') + return + } + + logger.info({ flow: flowId(code) }, 'OAuth token exchange successful') + this.issueTokens(res, stored.apiToken, stored.clientId) + } + + destroy(): void { + clearInterval(this.cleanupInterval) + this.codes.clear() + } + + private handleRefreshGrant(body: Map, res: ServerResponse): void { + const refreshToken = body.get('refresh_token') ?? '' + if (!refreshToken) { + this.tokenError(res, 'invalid_request', 'Missing refresh_token') + return + } + + const payload = this.cipher.open<{ t: string; c: string }>('refresh', refreshToken) + if (!payload || typeof payload.t !== 'string') { + logger.warn('OAuth refresh rejected: unknown or expired refresh token') + this.tokenError(res, 'invalid_grant', 'Invalid or expired refresh token') + return + } + + const clientId = body.get('client_id') + if (clientId && !constantTimeEquals(clientId, payload.c)) { + logger.warn('OAuth refresh rejected: client_id does not match the refresh token') + this.tokenError(res, 'invalid_grant', 'client_id does not match the refresh token') + return } - logger.info('OAuth token exchange successful') + // A silent refresh is the normal, healthy case -- and therefore the one + // worth being able to see. Without this line there is no way to tell a + // client that renews cleanly from one that re-prompts its user hourly. + logger.info({ clientId: fingerprint(payload.c) }, 'OAuth access token refreshed') + this.issueTokens(res, payload.t, payload.c) + } + + private issueTokens(res: ServerResponse, apiToken: string, clientId: string): void { + const accessToken = this.cipher.seal('access', { t: apiToken }, this.accessTokenTtlMs) + const refreshToken = this.cipher.seal('refresh', { t: apiToken, c: clientId }, this.refreshTokenTtlMs) - // Return the API token as the OAuth access token res.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store' }) res.end(JSON.stringify({ - access_token: stored.apiToken, + access_token: accessToken, token_type: 'Bearer', - refresh_token: stored.apiToken, + expires_in: Math.floor(this.accessTokenTtlMs / 1000), + refresh_token: refreshToken, })) } - destroy(): void { - clearInterval(this.cleanupInterval) - this.codes.clear() + private tokenError(res: ServerResponse, error: string, description?: string): void { + res.writeHead(400, { 'content-type': 'application/json', 'cache-control': 'no-store' }) + .end(JSON.stringify(description ? { error, error_description: description } : { error })) + } + + private registrationError(res: ServerResponse, error: string, description: string): void { + res.writeHead(400, { 'content-type': 'application/json' }) + .end(JSON.stringify({ error, error_description: description })) + } + + private parseRedirectUris(raw: string | undefined): string[] { + if (!raw) return [] + let value: unknown = raw + if (raw.startsWith('[')) { + try { + value = JSON.parse(raw) + } catch { + return [] + } + } + const list = Array.isArray(value) ? value : [value] + return list.filter((entry): entry is string => typeof entry === 'string' && entry.length > 0).slice(0, 20) } private cleanup(): void { @@ -326,85 +650,138 @@ export class OAuthProvider { private readBody(req: IncomingMessage): Promise { return new Promise((resolve, reject) => { const chunks: Buffer[] = [] - req.on('data', (chunk) => chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk)) + let size = 0 + req.on('data', (chunk) => { + const buf = typeof chunk === 'string' ? Buffer.from(chunk) : chunk + size += buf.length + if (size > 64 * 1024) { + req.destroy() + reject(new Error('Request body too large')) + return + } + chunks.push(buf) + }) req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8'))) req.on('error', reject) }) } - private renderAuthorizePage(clientId: string, redirectUri: string, state: string, responseType: string, codeChallenge: string, codeChallengeMethod: string, error?: string): string { - const errorHtml = error ? `
${this.escapeHtml(error)}
` : '' + /** An error page that deliberately does not render the API token prompt. */ + private authorizeError(res: ServerResponse, heading: string, message: string): void { + res.writeHead(400, { 'content-type': 'text/html; charset=utf-8' }) + res.end(` + + + + + SeaTable MCP — Authorization refused + + + +
+

${this.escapeHtml(heading)}

+

${this.escapeHtml(message)}

+
+ +`) + } + + private renderAcknowledgementPage(opts: AuthorizePageOptions): string { + return this.page(` +

Where is this going?

+

Before you enter anything, check the destination.

+
+ Your API token will be sent to + ${this.escapeHtml(opts.callbackOrigin)} +
+
+ SeaTable does not recognise this destination. That does not necessarily mean it is + malicious — but it does mean we cannot vouch for it. Only continue if + you started this connection yourself, from software you trust. + If you arrived here from a link or a message, close this page. +
+

The application calls itself + “${this.escapeHtml(opts.client.n)}” — self-reported, not verified by SeaTable.

+
+ ${this.hiddenFlowFields(opts)} + + +
+

If you did not, simply close this page. Nothing has been sent yet.

`) + } + + private renderAuthorizePage(opts: AuthorizePageOptions & { acknowledged?: boolean; error?: string }): string { + const errorHtml = opts.error ? `
${this.escapeHtml(opts.error)}
` : '' + + return this.page(` +

SeaTable MCP

+

An application is asking for access to your SeaTable base.

+
+ Your authorization will be sent to + ${this.escapeHtml(opts.callbackOrigin)} +
+

The application calls itself + “${this.escapeHtml(opts.client.n)}” — self-reported, not verified by SeaTable.

+ ${errorHtml} +
+ ${this.hiddenFlowFields(opts)} + ${opts.acknowledged ? '' : ''} + + + +

Use a base API token, not your account token. A read-only token keeps the permissions minimal.

+
`) + } + + private hiddenFlowFields(opts: AuthorizePageOptions): string { + const field = (name: string, value: string) => + `` + return [ + field('redirect_uri', opts.redirectUri), + field('state', opts.state), + field('client_id', opts.clientId), + field('response_type', opts.responseType), + field('code_challenge', opts.codeChallenge), + field('code_challenge_method', opts.codeChallengeMethod), + ].join('\n ') + } + /** Shared chrome for every page this provider renders. */ + private page(body: string): string { return ` - SeaTable MCP — Authorize + SeaTable MCP — Authorize -
-

SeaTable MCP

-

Enter your SeaTable API token to authorize access to your base.

- ${errorHtml} -
- - - - - - - - - -

Your API token will be used as your access credential. Use a read-only token for minimal permissions.

-
+
${body}
` @@ -415,9 +792,39 @@ export class OAuthProvider { } } +/** + * Correlation id for one authorization, derived from the code so that the + * /authorize and /token log lines can be paired without ever writing the code + * itself (or a prefix of it) to disk. + */ +/** + * A callback as written to the log: the full destination, because for an + * incident the question is where the code actually went, not just which host. + * The value comes from an unauthenticated caller, so it is capped. + */ +function safeCallback(raw: string): string { + return raw.slice(0, 200) +} + +/** Short, non-reversible handle for a sealed value that is too long to log. */ +function fingerprint(value: string): string { + return createHash('sha256').update(value).digest('hex').slice(0, 12) +} + +function flowId(code: string): string { + return createHash('sha256').update(`flow:${code}`).digest('hex').slice(0, 12) +} + function base64UrlEncode(buffer: Buffer): string { return buffer.toString('base64') .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=+$/, '') } + +function constantTimeEquals(a: string, b: string): boolean { + const bufA = Buffer.from(a, 'utf-8') + const bufB = Buffer.from(b, 'utf-8') + if (bufA.length !== bufB.length) return false + return timingSafeEqual(bufA, bufB) +} diff --git a/src/auth/tokenCipher.ts b/src/auth/tokenCipher.ts new file mode 100644 index 0000000..b656f26 --- /dev/null +++ b/src/auth/tokenCipher.ts @@ -0,0 +1,78 @@ +import { createCipheriv, createDecipheriv, hkdfSync, randomBytes } from 'node:crypto' + +/** + * Authenticated, stateless envelope for values the server hands out but must be + * able to read back — OAuth access/refresh tokens and client registrations. + * + * The SeaTable API token is sealed inside; it never leaves the process in the + * clear. Because the envelope carries its own expiry and is authenticated with + * AES-256-GCM, no server-side storage is required and the values survive both a + * restart and a second instance sharing the same secret. + */ + +/** Domain separator — a value sealed as one kind can never be opened as another. */ +export type TokenKind = 'access' | 'refresh' | 'client' + +const PREFIX: Record = { + access: 'mcpa1.', + refresh: 'mcpr1.', + client: 'mcpc1.', +} + +const NONCE_BYTES = 12 +const TAG_BYTES = 16 +const HKDF_SALT = 'seatable-mcp/token-cipher/v1' + +export interface SealedPayload { + /** Absolute expiry, epoch ms. Written by seal(), checked by open(). */ + exp: number + [key: string]: unknown +} + +export class TokenCipher { + private readonly key: Buffer + + constructor(secret: string) { + if (!secret || secret.length < 16) { + throw new Error('TokenCipher secret must be at least 16 characters') + } + this.key = Buffer.from(hkdfSync('sha256', Buffer.from(secret, 'utf-8'), Buffer.from(HKDF_SALT), Buffer.from('key'), 32)) + } + + seal(kind: TokenKind, payload: Record, ttlMs: number): string { + const body: SealedPayload = { ...payload, exp: Date.now() + ttlMs } + const nonce = randomBytes(NONCE_BYTES) + const cipher = createCipheriv('aes-256-gcm', this.key, nonce) + cipher.setAAD(Buffer.from(kind)) + const ciphertext = Buffer.concat([cipher.update(JSON.stringify(body), 'utf-8'), cipher.final()]) + const tag = cipher.getAuthTag() + return PREFIX[kind] + Buffer.concat([nonce, ciphertext, tag]).toString('base64url') + } + + /** Returns the payload, or undefined if the value is forged, tampered with, of the wrong kind, or expired. */ + open = Record>(kind: TokenKind, value: string): (T & SealedPayload) | undefined { + const prefix = PREFIX[kind] + if (typeof value !== 'string' || !value.startsWith(prefix)) return undefined + + try { + const raw = Buffer.from(value.slice(prefix.length), 'base64url') + if (raw.length <= NONCE_BYTES + TAG_BYTES) return undefined + + const nonce = raw.subarray(0, NONCE_BYTES) + const ciphertext = raw.subarray(NONCE_BYTES, raw.length - TAG_BYTES) + const tag = raw.subarray(raw.length - TAG_BYTES) + + const decipher = createDecipheriv('aes-256-gcm', this.key, nonce) + decipher.setAAD(Buffer.from(kind)) + decipher.setAuthTag(tag) + const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString('utf-8') + + const payload = JSON.parse(plaintext) as T & SealedPayload + if (typeof payload.exp !== 'number' || Date.now() >= payload.exp) return undefined + return payload + } catch { + // Any failure — bad base64, failed auth tag, malformed JSON — is a rejection. + return undefined + } + } +} diff --git a/src/auth/tokenValidator.ts b/src/auth/tokenValidator.ts index e81526c..b4ad120 100644 --- a/src/auth/tokenValidator.ts +++ b/src/auth/tokenValidator.ts @@ -8,7 +8,9 @@ interface CacheEntry { expiresAt: number } -const POSITIVE_TTL_MS = 5 * 60 * 1000 // 5 minutes +// Kept deliberately short: this is the window in which a revoked SeaTable token +// still passes validation, for established sessions as well as new ones. +const POSITIVE_TTL_MS = 60 * 1000 // 1 minute const NEGATIVE_TTL_MS = 1 * 60 * 1000 // 1 minute export class TokenValidator { @@ -52,6 +54,24 @@ export class TokenValidator { } } + /** + * Does this token authenticate as a SeaTable *account* token? + * + * Only asked after validate() failed, so the extra request is bounded by the + * failure rate. Used to tell the user they pasted the wrong kind of token. + */ + async looksLikeAccountToken(apiToken: string): Promise { + try { + const res = await axios.get(`${this.serverUrl}/api2/account/info/`, { + headers: { Authorization: `Token ${apiToken}` }, + timeout: 10_000, + }) + return res.status === 200 + } catch { + return false + } + } + cleanup(): void { const now = Date.now() for (const [key, entry] of this.cache) { diff --git a/src/config/env.ts b/src/config/env.ts index 974b939..a59829d 100644 --- a/src/config/env.ts +++ b/src/config/env.ts @@ -20,6 +20,17 @@ const EnvSchema = z .object({ SEATABLE_SERVER_URL: z.string().url(), SEATABLE_MODE: ServerModeSchema, + // Secret used to seal OAuth access/refresh tokens and client registrations. + // Required in managed mode; must be stable across restarts. + SEATABLE_TOKEN_SECRET: z.string().min(32).optional(), + // Lifetime of an issued OAuth access token, in seconds (default 3600). + // Between 30 s and 30 days. Lower it to narrow the window after a token + // is revoked, raise it if clients renew badly and re-prompt their users. + SEATABLE_ACCESS_TOKEN_TTL: z + .string() + .optional() + .transform((v) => (v === undefined || v === '' ? undefined : Number(v))) + .pipe(z.number({ message: 'must be a number of seconds' }).int().min(30).max(30 * 24 * 60 * 60).optional()), SEATABLE_API_TOKEN: z.string().min(1).optional(), // Multi-base: JSON array, e.g. '[{"base_name":"CRM","api_token":"..."}]' SEATABLE_BASES: z.string().optional(), @@ -56,6 +67,16 @@ const EnvSchema = z .default('true'), }) .superRefine((data, ctx) => { + if (data.SEATABLE_MODE === 'managed' && !data.SEATABLE_TOKEN_SECRET) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: + 'SEATABLE_TOKEN_SECRET (min. 32 chars) is required in managed mode. ' + + 'It seals the OAuth tokens; without it the raw SeaTable API token would have to be handed to clients. ' + + 'Generate one with: openssl rand -hex 32', + path: ['SEATABLE_TOKEN_SECRET'], + }) + } if (data.SEATABLE_MODE === 'selfhosted' && !data.SEATABLE_API_TOKEN && !data.SEATABLE_BASES) { ctx.addIssue({ code: z.ZodIssueCode.custom, diff --git a/src/http/httpServer.ts b/src/http/httpServer.ts index 57b48d2..0f6d356 100644 --- a/src/http/httpServer.ts +++ b/src/http/httpServer.ts @@ -1,4 +1,4 @@ -import { randomUUID } from 'node:crypto' +import { createHash, randomUUID, timingSafeEqual } from 'node:crypto' import { createServer, type IncomingMessage, type ServerResponse } from 'node:http' import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js' @@ -24,10 +24,21 @@ export interface StartHttpServerOptions { type ActiveSession = { transport: StreamableHTTPServerTransport apiToken?: string + /** Digest of the SeaTable API token that created this session; every later request must resolve to the same one. */ + apiTokenDigest?: Buffer lastActivity: number close: () => Promise } +function digest(value: string): Buffer { + return createHash('sha256').update(value).digest() +} + +/** Session IDs are credentials-adjacent routing values — log a fingerprint, never the value. */ +function sessionFingerprint(sessionId: string): string { + return createHash('sha256').update(sessionId).digest('hex').slice(0, 12) +} + const MAX_BODY_SIZE = 10 * 1024 * 1024 // 10 MB async function parseJsonBody(req: IncomingMessage): Promise { @@ -60,6 +71,16 @@ async function parseJsonBody(req: IncomingMessage): Promise { }) } +/** + * Hosts allowed as remote https callbacks in the OAuth flow. Unset means the + * built-in list of hosted MCP clients; '*' disables curation (dangerous). + */ +function parseTrustedRedirectHosts(): string[] | undefined { + const raw = process.env.SEATABLE_OAUTH_TRUSTED_REDIRECT_HOSTS + if (raw === undefined) return undefined + return raw.split(',').map((h) => h.trim().toLowerCase()).filter(Boolean) +} + function parseCorsOrigins(): string[] { const raw = process.env.CORS_ALLOWED_ORIGINS if (!raw) return [] @@ -89,7 +110,12 @@ export async function startHttpServer(options: StartHttpServerOptions = {}) { const oauthProvider = mode === 'managed' ? new OAuthProvider({ hostname: process.env.SEATABLE_MCP_HOSTNAME, + secret: env.SEATABLE_TOKEN_SECRET, + trustedRedirectHosts: parseTrustedRedirectHosts(), + accessTokenTtlMs: env.SEATABLE_ACCESS_TOKEN_TTL ? env.SEATABLE_ACCESS_TOKEN_TTL * 1000 : undefined, validateToken: tokenValidator ? (token) => tokenValidator.validate(token) : undefined, + looksLikeAccountToken: tokenValidator ? (token) => tokenValidator.looksLikeAccountToken(token) : undefined, + getClientIp: (req) => getClientIp(req), }) : undefined const toolDefinitions = getStaticToolDefinitions() @@ -101,6 +127,20 @@ export async function startHttpServer(options: StartHttpServerOptions = {}) { return auth.startsWith('Bearer ') ? auth.slice(7) : auth } + /** + * Turns a presented Bearer credential into the SeaTable API token behind it. + * + * Accepts both an OAuth access token we issued (sealed, resolved locally) and a + * raw SeaTable API token (for clients that configure one directly). Either way + * the underlying token is validated against SeaTable, so a revoked token stops + * working within the validator's cache window. + */ + async function resolveApiToken(bearer: string): Promise { + const candidate = oauthProvider?.resolveAccessToken(bearer) ?? bearer + if (!(await tokenValidator!.validate(candidate))) return undefined + return candidate + } + const trustProxy = env.TRUST_PROXY ?? true function getClientIp(req: IncomingMessage): string { @@ -111,6 +151,29 @@ export async function startHttpServer(options: StartHttpServerOptions = {}) { return req.socket.remoteAddress ?? 'unknown' } + /** + * Throttle an OAuth endpoint by client IP. Returns true when the request was + * rejected and a response has already been sent. + */ + function oauthThrottled(req: IncomingMessage, res: ServerResponse, isTokenSubmission = false): boolean { + if (!rateLimiter) return false + const ip = getClientIp(req) + const checks = isTokenSubmission + ? [rateLimiter.tokenSubmission.check(ip), rateLimiter.oauth.check(ip)] + : [rateLimiter.oauth.check(ip)] + for (const result of checks) { + if (!result.allowed) { + logger.warn({ ip, endpoint: req.url }, 'OAuth rate limit exceeded') + res.writeHead(429, { + 'content-type': 'text/plain', + 'retry-after': String(Math.max(1, Math.ceil(result.retryAfterMs / 1000))), + }).end('Too many requests') + return true + } + } + return false + } + async function handleMcpRequest(req: IncomingMessage, res: ServerResponse): Promise { // Rate limiting (managed mode only) if (rateLimiter) { @@ -154,14 +217,14 @@ export async function startHttpServer(options: StartHttpServerOptions = {}) { // In managed mode: require and validate Bearer token let apiToken: string | undefined if (mode === 'managed') { - apiToken = extractBearerToken(req) - if (!apiToken) { + const bearer = extractBearerToken(req) + if (!bearer) { logger.warn({ ip: getClientIp(req) }, 'Missing Authorization header') res.writeHead(401, { 'content-type': 'text/plain' }).end('Missing Authorization header') return } - const valid = await tokenValidator!.validate(apiToken) - if (!valid) { + apiToken = await resolveApiToken(bearer) + if (!apiToken) { logger.warn({ ip: getClientIp(req) }, 'Invalid API token') res.writeHead(401, { 'content-type': 'text/plain' }).end('Invalid API token') return @@ -183,8 +246,14 @@ export async function startHttpServer(options: StartHttpServerOptions = {}) { sessionIdGenerator: () => randomUUID(), onsessioninitialized: (id) => { mcpServer.setSessionId(id) - logger.info({ sessionId: id }, 'Session initialized') - sessions.set(id, { transport, apiToken, lastActivity: Date.now(), close: cleanup }) + logger.info({ session: sessionFingerprint(id) }, 'Session initialized') + sessions.set(id, { + transport, + apiToken, + apiTokenDigest: apiToken ? digest(apiToken) : undefined, + lastActivity: Date.now(), + close: cleanup, + }) activeSessions.inc() }, }) @@ -222,14 +291,42 @@ export async function startHttpServer(options: StartHttpServerOptions = {}) { return } - // For requests with an existing session ID: look up the session + // For requests with an existing session ID: authenticate first, then look up the session. + // The session ID is a routing value, never an authorization credential. if (sessionId) { + let presentedDigest: Buffer | undefined + if (mode === 'managed') { + const bearer = extractBearerToken(req) + if (!bearer) { + logger.warn({ ip: getClientIp(req), session: sessionFingerprint(sessionId) }, 'Session request without Authorization header') + res.writeHead(401, { 'content-type': 'text/plain' }).end('Missing Authorization header') + return + } + const apiToken = await resolveApiToken(bearer) + if (!apiToken) { + logger.warn({ ip: getClientIp(req), session: sessionFingerprint(sessionId) }, 'Session request with invalid credential') + res.writeHead(401, { 'content-type': 'text/plain' }).end('Invalid API token') + return + } + presentedDigest = digest(apiToken) + } + const session = sessions.get(sessionId) if (!session) { - logger.debug({ sessionId }, 'Session not found') + logger.debug({ session: sessionFingerprint(sessionId) }, 'Session not found') res.writeHead(404, { 'content-type': 'text/plain' }).end('Session expired. Please reconnect to start a new session.') return } + + if (presentedDigest) { + const owner = session.apiTokenDigest + if (!owner || owner.length !== presentedDigest.length || !timingSafeEqual(owner, presentedDigest)) { + logger.warn({ ip: getClientIp(req), session: sessionFingerprint(sessionId) }, 'Session request from a different identity') + res.writeHead(403, { 'content-type': 'text/plain' }).end('Credential does not match this session') + return + } + } + session.lastActivity = Date.now() await session.transport.handleRequest(req, res, body) return @@ -311,6 +408,7 @@ export async function startHttpServer(options: StartHttpServerOptions = {}) { } if (oauthProvider && (url.pathname === '/authorize' || url.pathname === '/oauth/authorize') && (req.method === 'GET' || req.method === 'POST')) { + if (oauthThrottled(req, res, req.method === 'POST')) return try { await oauthProvider.handleAuthorize(req, res, url) } catch (error) { @@ -323,6 +421,7 @@ export async function startHttpServer(options: StartHttpServerOptions = {}) { } if (oauthProvider && (url.pathname === '/token' || url.pathname === '/oauth/token') && req.method === 'POST') { + if (oauthThrottled(req, res)) return try { await oauthProvider.handleToken(req, res) } catch (error) { @@ -335,6 +434,7 @@ export async function startHttpServer(options: StartHttpServerOptions = {}) { } if (oauthProvider && url.pathname === '/register' && req.method === 'POST') { + if (oauthThrottled(req, res)) return try { await oauthProvider.handleRegister(req, res) } catch (error) { @@ -364,7 +464,7 @@ export async function startHttpServer(options: StartHttpServerOptions = {}) { const now = Date.now() for (const [sessionId, session] of sessions.entries()) { if (now - session.lastActivity > sessionIdleTimeoutMs) { - logger.info({ sessionId }, 'Closing idle session') + logger.info({ session: sessionFingerprint(sessionId) }, 'Closing idle session') void session.close() } } @@ -380,7 +480,7 @@ export async function startHttpServer(options: StartHttpServerOptions = {}) { oauthProvider?.destroy() const sessionCount = sessions.size for (const [sessionId, session] of sessions.entries()) { - logger.debug({ sessionId }, 'Closing session during shutdown') + logger.debug({ session: sessionFingerprint(sessionId) }, 'Closing session during shutdown') await session.close() } await new Promise((resolve) => server.close(() => resolve())) diff --git a/src/ratelimit/index.ts b/src/ratelimit/index.ts index 44da280..d5cba9c 100644 --- a/src/ratelimit/index.ts +++ b/src/ratelimit/index.ts @@ -24,6 +24,14 @@ export class RateLimitManager { readonly global = new SlidingWindowLimiter(5000, ONE_MINUTE) /** Pre-auth limiter: caps new session creation per IP to prevent token-validation flooding */ readonly preAuth = new SlidingWindowLimiter(30, ONE_MINUTE) + /** All OAuth endpoints, per IP. They carry no session and no credential of their own. */ + readonly oauth = new SlidingWindowLimiter(30, ONE_MINUTE) + /** + * POST /authorize specifically. Each submission forwards a candidate token to + * SeaTable, so an unthrottled endpoint is an oracle for testing stolen tokens + * and an amplifier against our own backend. + */ + readonly tokenSubmission = new SlidingWindowLimiter(10, ONE_MINUTE) readonly connections = new ConnectionCounter(20) private cleanupInterval?: ReturnType @@ -70,6 +78,8 @@ export class RateLimitManager { this.perIp.cleanup() this.global.cleanup() this.preAuth.cleanup() + this.oauth.cleanup() + this.tokenSubmission.cleanup() } destroy(): void { diff --git a/tests/accessTokenTtl.spec.ts b/tests/accessTokenTtl.spec.ts new file mode 100644 index 0000000..7f2604e --- /dev/null +++ b/tests/accessTokenTtl.spec.ts @@ -0,0 +1,151 @@ +import { createHash, randomBytes } from 'node:crypto' +import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http' +import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest' + +import { OAuthProvider } from '../src/auth/oauthProvider.js' +import { clearEnvOverrides, getEnv, setEnvOverrides } from '../src/config/env.js' + +/** + * How long an issued access token stays valid. + * + * One hour is the default compromise between "a stolen token expires soon" and + * "the user is not asked for their API token again". Operators need to move it: + * shorter to narrow the window after a revocation, longer if real clients turn + * out to renew badly and would otherwise re-prompt their users hourly. + */ + +const SECRET = 'access-token-ttl-spec-secret-long-enough' +const CB = 'http://127.0.0.1:5150/cb' +const API_TOKEN = 'a-base-token' + +let server: Server +let port: number +let provider: OAuthProvider +let ttlMs: number | undefined + +const base = (p: string) => `http://localhost:${port}${p}` + +function b64url(b: Buffer) { + return b.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') +} + +/** Registers, authorizes and exchanges; returns the token endpoint's response. */ +async function issue(): Promise { + const clientId = await (await fetch(base('/register'), { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ client_name: 'ttl', redirect_uris: [CB] }), + })).json().then((r: any) => r.client_id) + + const verifier = b64url(randomBytes(32)) + const challenge = b64url(createHash('sha256').update(verifier).digest()) + const authorized = await fetch(base('/authorize'), { + method: 'POST', + headers: { 'content-type': 'application/x-www-form-urlencoded', 'sec-fetch-site': 'same-origin' }, + body: new URLSearchParams({ + api_token: API_TOKEN, client_id: clientId, redirect_uri: CB, + response_type: 'code', code_challenge: challenge, code_challenge_method: 'S256', + }).toString(), + redirect: 'manual', + }) + const code = new URL(authorized.headers.get('location')!).searchParams.get('code')! + return await (await fetch(base('/token'), { + method: 'POST', + headers: { 'content-type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams({ + grant_type: 'authorization_code', code, client_id: clientId, + redirect_uri: CB, code_verifier: verifier, + }).toString(), + })).json() +} + +beforeAll(async () => { + server = createServer(async (req: IncomingMessage, res: ServerResponse) => { + const url = new URL(req.url!, 'http://localhost') + if (url.pathname === '/authorize') await provider.handleAuthorize(req, res, url) + else if (url.pathname === '/token') await provider.handleToken(req, res) + else if (url.pathname === '/register') await provider.handleRegister(req, res) + else res.writeHead(404).end() + }) + await new Promise((resolve) => { + server.listen(0, () => { port = (server.address() as any).port; resolve() }) + }) +}) + +afterAll(async () => { + provider?.destroy() + await new Promise((resolve) => server.close(() => resolve())) +}) + +function makeProvider(ms?: number) { + provider?.destroy() + ttlMs = ms + provider = new OAuthProvider({ + secret: SECRET, + accessTokenTtlMs: ms, + validateToken: async (t) => t === API_TOKEN, + }) +} + +describe('access token lifetime', () => { + it('defaults to one hour', async () => { + makeProvider(undefined) + const res = await issue() + expect(res.expires_in).toBe(3600) + }) + + it('honours a configured lifetime and reports it as expires_in', async () => { + makeProvider(120_000) + const res = await issue() + expect(res.expires_in).toBe(120) + expect(provider.resolveAccessToken(res.access_token)).toBe(API_TOKEN) + }) + + it('stops resolving the token once its lifetime has passed', async () => { + makeProvider(40) + const res = await issue() + await new Promise((r) => setTimeout(r, 70)) + expect(provider.resolveAccessToken(res.access_token)).toBeUndefined() + }) + + it('never issues a refresh token shorter-lived than the access token', async () => { + // A 30-day access token must not come with a 14-day refresh token. + makeProvider(30 * 24 * 60 * 60 * 1000) + const res = await issue() + const refreshed = await fetch(base('/token'), { + method: 'POST', + headers: { 'content-type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams({ grant_type: 'refresh_token', refresh_token: res.refresh_token }).toString(), + }) + expect(refreshed.status).toBe(200) + expect(provider.resolveAccessToken((await refreshed.json()).access_token)).toBe(API_TOKEN) + }) +}) + +describe('SEATABLE_ACCESS_TOKEN_TTL', () => { + afterEach(() => clearEnvOverrides()) + + const withEnv = (value?: string) => { + setEnvOverrides({ + SEATABLE_SERVER_URL: 'https://example.com', + SEATABLE_MODE: 'managed', + SEATABLE_TOKEN_SECRET: 'x'.repeat(32), + ...(value === undefined ? {} : { SEATABLE_ACCESS_TOKEN_TTL: value }), + } as any) + } + + it('is optional and unset by default', () => { + withEnv(undefined) + expect(getEnv().SEATABLE_ACCESS_TOKEN_TTL).toBeUndefined() + }) + + it('parses a value in seconds', () => { + withEnv('120') + expect(getEnv().SEATABLE_ACCESS_TOKEN_TTL).toBe(120) + }) + + it.each(['0', '-1', '29', 'abc', '3000000'])('rejects the unusable value %s', (value) => { + withEnv(value) + expect(() => getEnv()).toThrow(/SEATABLE_ACCESS_TOKEN_TTL/) + }) +}) diff --git a/tests/managedSessionAuth.spec.ts b/tests/managedSessionAuth.spec.ts new file mode 100644 index 0000000..3a66f9a --- /dev/null +++ b/tests/managedSessionAuth.spec.ts @@ -0,0 +1,154 @@ +import type { AddressInfo } from 'node:net' + +import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest' + +vi.mock('../src/metrics/metricsServer', () => ({ + startMetricsServer: vi.fn().mockResolvedValue(undefined), +})) + +/** Tokens the fake SeaTable backend considers valid. */ +const VALID_TOKENS = new Set(['token-account-a', 'token-account-b']) + +vi.mock('../src/auth/tokenValidator', () => ({ + TokenValidator: class { + async validate(token: string): Promise { + return VALID_TOKENS.has(token) + } + cleanup(): void {} + destroy(): void {} + }, +})) + +const logCalls: unknown[][] = [] +vi.mock('../src/logger', () => { + const record = (...args: unknown[]) => { logCalls.push(args) } + return { + logger: { fatal: record, error: record, warn: record, info: record, debug: record, trace: record }, + } +}) + +import { startHttpServer } from '../src/http/httpServer' + +let server: ReturnType +let baseUrl: string + +const JSON_HEADERS = { + 'content-type': 'application/json', + accept: 'application/json, text/event-stream', +} + +async function initSession(token: string): Promise { + const res = await fetch(`${baseUrl}/mcp`, { + method: 'POST', + headers: { ...JSON_HEADERS, authorization: `Bearer ${token}` }, + body: JSON.stringify({ + jsonrpc: '2.0', + id: 1, + method: 'initialize', + params: { protocolVersion: '2025-03-26', capabilities: {}, clientInfo: { name: 'test', version: '1.0' } }, + }), + }) + expect(res.status).toBe(200) + const sessionId = res.headers.get('mcp-session-id') + expect(sessionId).toBeTruthy() + return sessionId! +} + +function callTools(sessionId: string, token?: string) { + const headers: Record = { ...JSON_HEADERS, 'mcp-session-id': sessionId } + if (token !== undefined) headers.authorization = `Bearer ${token}` + return fetch(`${baseUrl}/mcp`, { + method: 'POST', + headers, + body: JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'tools/list', params: {} }), + }) +} + +beforeAll(async () => { + process.env.SEATABLE_SERVER_URL = 'http://localhost' + process.env.SEATABLE_MODE = 'managed' + process.env.SEATABLE_MOCK = 'true' + process.env.SEATABLE_TOKEN_SECRET = 'managed-session-auth-spec-secret-long-enough' + delete process.env.SEATABLE_API_TOKEN + + server = await startHttpServer({ port: 0 }) + baseUrl = `http://127.0.0.1:${(server.address() as AddressInfo).port}` +}) + +afterAll(async () => { + delete process.env.SEATABLE_MODE + delete process.env.SEATABLE_TOKEN_SECRET + if (server) await new Promise((resolve) => server.close(() => resolve())) +}) + +/** + * Adversarial tests for established-session authorization (external report, ST-01). + * The session ID is a routing value. It must never be an authorization credential. + */ +describe('ST-01 / every request must carry a valid credential', () => { + it('rejects a POST that presents only the session ID', async () => { + const sessionId = await initSession('token-account-a') + const res = await callTools(sessionId) + expect(res.status).toBe(401) + }) + + it('rejects a POST with an explicitly invalid bearer token', async () => { + const sessionId = await initSession('token-account-a') + const res = await callTools(sessionId, 'invalid-controlled-value') + expect(res.status).toBe(401) + }) + + it('rejects a GET that presents only the session ID', async () => { + const sessionId = await initSession('token-account-a') + const res = await fetch(`${baseUrl}/mcp`, { + method: 'GET', + headers: { accept: 'text/event-stream', 'mcp-session-id': sessionId }, + }) + expect(res.status).toBe(401) + }) + + it('rejects a DELETE that presents only the session ID', async () => { + const sessionId = await initSession('token-account-a') + const res = await fetch(`${baseUrl}/mcp`, { + method: 'DELETE', + headers: { 'mcp-session-id': sessionId }, + }) + expect(res.status).toBe(401) + }) +}) + +describe('ST-01 / sessions are bound to the identity that created them', () => { + it("rejects account B's valid token against account A's session", async () => { + const sessionA = await initSession('token-account-a') + const res = await callTools(sessionA, 'token-account-b') + expect(res.status).toBe(403) + }) + + it("rejects account A's valid token against account B's session", async () => { + const sessionB = await initSession('token-account-b') + const res = await callTools(sessionB, 'token-account-a') + expect(res.status).toBe(403) + }) + + it('accepts the owning token on its own session', async () => { + const sessionId = await initSession('token-account-a') + const res = await callTools(sessionId, 'token-account-a') + expect(res.status).toBe(200) + }) + + it('still returns 404 for an unknown session even with a valid token', async () => { + const res = await callTools('11111111-2222-3333-4444-555555555555', 'token-account-a') + expect(res.status).toBe(404) + }) +}) + +describe('ST-01 / session IDs are not written to the log in cleartext', () => { + it('never logs the raw session ID', async () => { + logCalls.length = 0 + const sessionId = await initSession('token-account-a') + await callTools(sessionId, 'token-account-a') + + const serialized = JSON.stringify(logCalls) + expect(serialized).not.toContain(sessionId) + }) +}) diff --git a/tests/oauthObservability.spec.ts b/tests/oauthObservability.spec.ts new file mode 100644 index 0000000..a7bd2b9 --- /dev/null +++ b/tests/oauthObservability.spec.ts @@ -0,0 +1,283 @@ +import { createHash, randomBytes } from 'node:crypto' +import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http' +import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' + +const logCalls: { fields: Record; msg: string }[] = [] +vi.mock('../src/logger', () => { + const record = (a: unknown, b?: unknown) => { + if (typeof a === 'object' && a !== null) logCalls.push({ fields: a as Record, msg: String(b ?? '') }) + else logCalls.push({ fields: {}, msg: String(a) }) + } + return { logger: { fatal: record, error: record, warn: record, info: record, debug: record, trace: record } } +}) + +import { OAuthProvider } from '../src/auth/oauthProvider.js' + +/** + * After the external report, the forensic questions could not be answered from + * the logs: no client IP, no callback destination, no link between an issued + * code and its exchange, and several rejection paths logged nothing at all. + */ + +const SECRET = 'oauth-observability-spec-secret-long-enough' +const CB = 'http://127.0.0.1:6611/cb' +const API_TOKEN = 'the-users-actual-base-token' +const CHALLENGE = 'Zm9vYmFyLWNoYWxsZW5nZS12YWx1ZS1oZXJlLXh4eHh4' + +let server: Server +let port: number +let provider: OAuthProvider + +const base = (p: string) => `http://localhost:${port}${p}` +const find = (needle: string) => logCalls.filter((c) => c.msg.includes(needle)) + +async function registerClient(name = 'Acme Client'): Promise { + const res = await fetch(base('/register'), { + method: 'POST', + headers: { 'content-type': 'application/json', 'x-forwarded-for': '198.51.100.7' }, + body: JSON.stringify({ client_name: name, redirect_uris: [CB] }), + }) + return (await res.json()).client_id as string +} + +async function issueCode(clientId: string): Promise { + const res = await fetch(base('/authorize'), { + method: 'POST', + headers: { + 'content-type': 'application/x-www-form-urlencoded', + 'x-forwarded-for': '198.51.100.7', + 'sec-fetch-site': 'same-origin', + }, + body: new URLSearchParams({ + api_token: API_TOKEN, client_id: clientId, redirect_uri: CB, + response_type: 'code', code_challenge: CHALLENGE, code_challenge_method: 'S256', + }).toString(), + redirect: 'manual', + }) + return new URL(res.headers.get('location')!).searchParams.get('code')! +} + +function b64url(buf: Buffer): string { + return buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') +} + +/** A complete, valid authorization -- the fixed CHALLENGE above has no verifier. */ +async function fullFlow(clientId: string): Promise<{ refresh_token: string }> { + const verifier = b64url(randomBytes(32)) + const challenge = b64url(createHash('sha256').update(verifier).digest()) + const authorized = await fetch(base('/authorize'), { + method: 'POST', + headers: { + 'content-type': 'application/x-www-form-urlencoded', + 'x-forwarded-for': '198.51.100.7', + 'sec-fetch-site': 'same-origin', + }, + body: new URLSearchParams({ + api_token: API_TOKEN, client_id: clientId, redirect_uri: CB, + response_type: 'code', code_challenge: challenge, code_challenge_method: 'S256', + }).toString(), + redirect: 'manual', + }) + const code = new URL(authorized.headers.get('location')!).searchParams.get('code')! + const res = await exchange({ + grant_type: 'authorization_code', code, client_id: clientId, + redirect_uri: CB, code_verifier: verifier, + }) + return await res.json() +} + +function exchange(params: Record) { + return fetch(base('/token'), { + method: 'POST', + headers: { 'content-type': 'application/x-www-form-urlencoded', 'x-forwarded-for': '198.51.100.7' }, + body: new URLSearchParams(params).toString(), + }) +} + +beforeAll(async () => { + provider = new OAuthProvider({ + secret: SECRET, + validateToken: async (t) => t === API_TOKEN, + getClientIp: (req) => (req.headers['x-forwarded-for'] as string) ?? 'unknown', + }) + server = createServer(async (req: IncomingMessage, res: ServerResponse) => { + const url = new URL(req.url!, 'http://localhost') + if (url.pathname === '/authorize') await provider.handleAuthorize(req, res, url) + else if (url.pathname === '/token') await provider.handleToken(req, res) + else if (url.pathname === '/register') await provider.handleRegister(req, res) + else res.writeHead(404).end() + }) + await new Promise((resolve) => { + server.listen(0, () => { + port = (server.address() as any).port + resolve() + }) + }) +}) + +afterAll(async () => { + provider.destroy() + await new Promise((resolve) => server.close(() => resolve())) +}) + +beforeEach(() => { logCalls.length = 0 }) + +describe('an issued code is attributable', () => { + it('records client, callback and IP when a code is issued', async () => { + const clientId = await registerClient('Acme Client') + await issueCode(clientId) + + const issued = find('authorization code issued') + expect(issued).toHaveLength(1) + expect(issued[0].fields.clientName).toBe('Acme Client') + // The full destination, not just its origin: for an incident the question + // is where the code actually went, and registration logs full URIs too. + expect(issued[0].fields.callback).toBe(CB) + expect(issued[0].fields.ip).toBe('198.51.100.7') + }) + + it('links the exchange back to the authorization with a flow id', async () => { + const clientId = await registerClient() + const code = await issueCode(clientId) + await exchange({ + grant_type: 'authorization_code', code, client_id: clientId, + redirect_uri: CB, code_verifier: 'irrelevant-verifier', + }) + + const flow = find('authorization code issued')[0].fields.flow + expect(flow).toBeTruthy() + expect(logCalls.filter((c) => c.fields.flow === flow && c.msg.includes('exchange')).length).toBeGreaterThan(0) + }) + + it('never writes the code or the API token to the log', async () => { + const clientId = await registerClient() + const code = await issueCode(clientId) + await exchange({ grant_type: 'authorization_code', code, client_id: clientId, redirect_uri: CB }) + + const serialized = JSON.stringify(logCalls) + expect(serialized).not.toContain(code) + expect(serialized).not.toContain(API_TOKEN) + // the flow id must be derived, not the code truncated + expect(serialized).not.toContain(code.slice(0, 12)) + }) + + it('records the IP on a rejected token submission', async () => { + const clientId = await registerClient() + await fetch(base('/authorize'), { + method: 'POST', + headers: { + 'content-type': 'application/x-www-form-urlencoded', + 'x-forwarded-for': '198.51.100.99', + 'sec-fetch-site': 'same-origin', + }, + body: new URLSearchParams({ + api_token: 'wrong', client_id: clientId, redirect_uri: CB, + response_type: 'code', code_challenge: CHALLENGE, code_challenge_method: 'S256', + }).toString(), + redirect: 'manual', + }) + const rejected = find('invalid API token') + expect(rejected).toHaveLength(1) + expect(rejected[0].fields.ip).toBe('198.51.100.99') + }) +}) + +describe('an attacker cannot flood the log through the callback', () => { + it('truncates an over-long callback', async () => { + const long = 'https://attacker.example/' + 'x'.repeat(500) + const clientId = await registerClient() + await fetch(base( + `/authorize?response_type=code&client_id=${encodeURIComponent(clientId)}` + + `&redirect_uri=${encodeURIComponent(long)}` + + `&code_challenge=${CHALLENGE}&code_challenge_method=S256`, + ), { headers: { 'x-forwarded-for': '198.51.100.66' } }) + const rejected = find('redirect_uri not registered') + expect(rejected).toHaveLength(1) + expect((rejected[0].fields.callback as string).length).toBeLessThanOrEqual(200) + }) +}) + +describe('token renewal is visible', () => { + it('logs a successful refresh, not only a failed one', async () => { + const clientId = await registerClient() + const first = await fullFlow(clientId) + expect(first.refresh_token).toBeTruthy() + + logCalls.length = 0 + await exchange({ grant_type: 'refresh_token', refresh_token: first.refresh_token, client_id: clientId }) + + const refreshed = find('refresh') + expect(refreshed.length).toBeGreaterThan(0) + expect(refreshed.some((c) => c.msg.toLowerCase().includes('reject'))).toBe(false) + }) + + it('never writes the refresh token itself to the log', async () => { + const clientId = await registerClient() + const first = await fullFlow(clientId) + expect(first.refresh_token).toBeTruthy() + + logCalls.length = 0 + await exchange({ grant_type: 'refresh_token', refresh_token: first.refresh_token, client_id: clientId }) + const serialized = JSON.stringify(logCalls) + expect(serialized).not.toContain(first.refresh_token) + expect(serialized).not.toContain(API_TOKEN) + }) +}) + +describe('a registration is reconstructable', () => { + it('records the callbacks a client registered, not just its name', async () => { + await registerClient('Acme Client') + const reg = find('dynamic client registration') + expect(reg).toHaveLength(1) + expect(reg[0].fields.clientName).toBe('Acme Client') + expect(reg[0].fields.callbacks).toEqual([CB]) + expect(reg[0].fields.ip).toBe('198.51.100.7') + }) + + it('caps what an unknown caller can write into the log', async () => { + const many = Array.from({ length: 12 }, (_, i) => `https://c${i}.example/${'x'.repeat(400)}`) + await fetch(base('/register'), { + method: 'POST', + headers: { 'content-type': 'application/json', 'x-forwarded-for': '198.51.100.7' }, + body: JSON.stringify({ client_name: 'Noisy', redirect_uris: many }), + }) + const reg = find('dynamic client registration') + expect(reg).toHaveLength(1) + const logged = reg[0].fields.callbacks as string[] + expect(logged.length).toBeLessThanOrEqual(5) + for (const entry of logged) expect(entry.length).toBeLessThanOrEqual(120) + }) +}) + +describe('no rejection path is silent', () => { + it('logs a missing code_verifier', async () => { + const clientId = await registerClient() + const code = await issueCode(clientId) + await exchange({ grant_type: 'authorization_code', code, client_id: clientId, redirect_uri: CB }) + expect(find('code_verifier').length).toBeGreaterThan(0) + }) + + it('logs an unsupported grant type', async () => { + await exchange({ grant_type: 'client_credentials' }) + expect(find('grant_type').length).toBeGreaterThan(0) + }) + + it('logs an expired authorization code distinctly', async () => { + await exchange({ grant_type: 'authorization_code', code: 'no-such-code', client_id: 'x', redirect_uri: CB }) + expect(find('invalid/expired code').length).toBeGreaterThan(0) + }) + + it('logs a rejected callback with the destination that was attempted', async () => { + const clientId = await registerClient() + await fetch(base( + `/authorize?response_type=code&client_id=${encodeURIComponent(clientId)}` + + `&redirect_uri=${encodeURIComponent('https://attacker.example/cb')}` + + `&code_challenge=${CHALLENGE}&code_challenge_method=S256`, + ), { headers: { 'x-forwarded-for': '198.51.100.66' } }) + + const rejected = find('redirect_uri not registered') + expect(rejected).toHaveLength(1) + expect(rejected[0].fields.callback).toBe('https://attacker.example/cb') + expect(rejected[0].fields.ip).toBe('198.51.100.66') + }) +}) diff --git a/tests/oauthProvider.security.spec.ts b/tests/oauthProvider.security.spec.ts new file mode 100644 index 0000000..0662b50 --- /dev/null +++ b/tests/oauthProvider.security.spec.ts @@ -0,0 +1,403 @@ +import { createHash, randomBytes } from 'node:crypto' +import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http' +import { afterAll, beforeAll, describe, expect, it } from 'vitest' + +import { OAuthProvider } from '../src/auth/oauthProvider.js' + +/** + * Adversarial tests for the OAuth bridge. + * + * Every test here models what a *hostile* client does deliberately, + * not what an honest client does by mistake. Derived from the + * external security report against v1.5.2 (ST-02). + */ + +const SECRET = 'oauth-security-spec-secret-value-long-enough' +const HONEST_CB = 'https://honest-client.example/callback' +const ATTACKER_CB = 'https://attacker.example.invalid/callback' + +let server: Server +let port: number +let provider: OAuthProvider + +function base(path: string) { + return `http://localhost:${port}${path}` +} + +function b64url(buf: Buffer): string { + return buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') +} + +function pkcePair() { + const verifier = b64url(randomBytes(32)) + const challenge = b64url(createHash('sha256').update(verifier).digest()) + return { verifier, challenge } +} + +async function registerClient(redirectUris: string[], clientName = 'Honest Client'): Promise { + const res = await fetch(base('/register'), { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ client_name: clientName, redirect_uris: redirectUris }), + }) + expect(res.status).toBe(201) + const body = await res.json() + expect(body.client_id).toBeTruthy() + return body.client_id as string +} + +/** Drives a complete, well-behaved authorization and returns the code. */ +async function authorize(opts: { + clientId: string + redirectUri: string + challenge: string + apiToken?: string +}): Promise<{ status: number; location: string | null; code: string | null }> { + const body = new URLSearchParams({ + api_token: opts.apiToken ?? 'victim-seatable-api-token', + client_id: opts.clientId, + redirect_uri: opts.redirectUri, + response_type: 'code', + code_challenge: opts.challenge, + code_challenge_method: 'S256', + state: 'st', + }) + const res = await fetch(base('/authorize'), { + method: 'POST', + headers: { 'content-type': 'application/x-www-form-urlencoded' }, + body: body.toString(), + redirect: 'manual', + }) + const location = res.headers.get('location') + const code = location ? new URL(location).searchParams.get('code') : null + return { status: res.status, location, code } +} + +function exchange(params: Record) { + return fetch(base('/token'), { + method: 'POST', + headers: { 'content-type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams(params).toString(), + }) +} + +beforeAll(async () => { + // Both hosts are trusted here on purpose: this spec proves the *bindings* + // hold even when the attacker's callback is itself registerable. + provider = new OAuthProvider({ + secret: SECRET, + trustedRedirectHosts: ['honest-client.example', 'attacker.example.invalid'], + }) + server = createServer(async (req: IncomingMessage, res: ServerResponse) => { + const url = new URL(req.url!, 'http://localhost') + if (url.pathname === '/authorize') await provider.handleAuthorize(req, res, url) + else if (url.pathname === '/token') await provider.handleToken(req, res) + else if (url.pathname === '/register') await provider.handleRegister(req, res) + else res.writeHead(404).end() + }) + await new Promise((resolve) => { + server.listen(0, () => { + port = (server.address() as any).port + resolve() + }) + }) +}) + +afterAll(async () => { + provider.destroy() + await new Promise((resolve) => server.close(() => resolve())) +}) + +describe('ST-02 / client and callback binding', () => { + it('rejects an unregistered client_id before rendering the form', async () => { + const res = await fetch( + base(`/authorize?response_type=code&client_id=unknown-client-ZZ&redirect_uri=${encodeURIComponent(ATTACKER_CB)}`), + ) + expect(res.status).toBe(400) + const html = await res.text() + // The token prompt must not be shown to the victim at all + expect(html).not.toContain('name="api_token"') + }) + + it('rejects a registered client using a callback it never registered', async () => { + const clientId = await registerClient([HONEST_CB]) + const { challenge } = pkcePair() + const res = await fetch( + base(`/authorize?response_type=code&client_id=${encodeURIComponent(clientId)}&redirect_uri=${encodeURIComponent(ATTACKER_CB)}&code_challenge=${challenge}&code_challenge_method=S256`), + ) + expect(res.status).toBe(400) + const html = await res.text() + expect(html).not.toContain('name="api_token"') + }) + + it('refuses to redirect to a foreign callback even if POSTed directly', async () => { + const clientId = await registerClient([HONEST_CB]) + const { challenge } = pkcePair() + const result = await authorize({ clientId, redirectUri: ATTACKER_CB, challenge }) + expect(result.status).toBe(400) + expect(result.location).toBeNull() + }) + + it('shows the client name and callback origin on the consent screen', async () => { + const clientId = await registerClient([HONEST_CB], 'Acme MCP Client') + const { challenge } = pkcePair() + const res = await fetch( + base(`/authorize?response_type=code&client_id=${encodeURIComponent(clientId)}&redirect_uri=${encodeURIComponent(HONEST_CB)}&code_challenge=${challenge}&code_challenge_method=S256`), + ) + expect(res.status).toBe(200) + const html = await res.text() + expect(html).toContain('Acme MCP Client') + expect(html).toContain('honest-client.example') + }) + + it('allows loopback redirects on a different port (RFC 8252)', async () => { + const clientId = await registerClient(['http://127.0.0.1:1234/cb'], 'Desktop Client') + const { challenge } = pkcePair() + const res = await fetch( + base(`/authorize?response_type=code&client_id=${encodeURIComponent(clientId)}&redirect_uri=${encodeURIComponent('http://127.0.0.1:59876/cb')}&code_challenge=${challenge}&code_challenge_method=S256`), + ) + expect(res.status).toBe(200) + }) +}) + +describe('ST-02 / registration rejects unusable callbacks', () => { + it('rejects non-http(s) redirect schemes', async () => { + for (const uri of ['javascript:alert(1)', 'data:text/html,x', 'file:///etc/passwd']) { + const res = await fetch(base('/register'), { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ client_name: 'evil', redirect_uris: [uri] }), + }) + expect(res.status).toBe(400) + } + }) + + it('accepts an untrusted https host at registration — it meets the acknowledgement later', async () => { + const res = await fetch(base('/register'), { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ client_name: 'stranger', redirect_uris: ['https://not-trusted.example/cb'] }), + }) + expect(res.status).toBe(201) + // ...but the token field is withheld until the destination is acknowledged. + const clientId = (await res.json()).client_id + const { challenge } = pkcePair() + const form = await fetch(base( + `/authorize?response_type=code&client_id=${encodeURIComponent(clientId)}` + + `&redirect_uri=${encodeURIComponent('https://not-trusted.example/cb')}` + + `&code_challenge=${challenge}&code_challenge_method=S256`, + )) + const html = await form.text() + expect(html).not.toContain('name="api_token"') + expect(html).toContain('name="acknowledged"') + }) + + it('rejects plaintext http callbacks that are not loopback', async () => { + const res = await fetch(base('/register'), { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ client_name: 'insecure', redirect_uris: ['http://example.com/cb'] }), + }) + expect(res.status).toBe(400) + }) + + it('rejects a registration without any redirect_uris', async () => { + const res = await fetch(base('/register'), { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ client_name: 'no-callback' }), + }) + expect(res.status).toBe(400) + }) +}) + +describe('ST-02 / authorization code bindings at the token endpoint', () => { + it('rejects an exchange that omits redirect_uri', async () => { + const clientId = await registerClient([HONEST_CB]) + const { challenge, verifier } = pkcePair() + const { code } = await authorize({ clientId, redirectUri: HONEST_CB, challenge }) + expect(code).toBeTruthy() + + const res = await exchange({ + grant_type: 'authorization_code', + code: code!, + client_id: clientId, + code_verifier: verifier, + }) + expect(res.status).toBe(400) + expect((await res.json()).error).toBe('invalid_grant') + }) + + it('rejects an exchange by a different client than the one the code was issued to', async () => { + const victimClient = await registerClient([HONEST_CB]) + const attackerClient = await registerClient([ATTACKER_CB], 'Attacker Client') + const { challenge, verifier } = pkcePair() + const { code } = await authorize({ clientId: victimClient, redirectUri: HONEST_CB, challenge }) + + const res = await exchange({ + grant_type: 'authorization_code', + code: code!, + client_id: attackerClient, + redirect_uri: HONEST_CB, + code_verifier: verifier, + }) + expect(res.status).toBe(400) + expect((await res.json()).error).toBe('invalid_grant') + }) + + it('rejects an exchange that omits client_id entirely', async () => { + const clientId = await registerClient([HONEST_CB]) + const { challenge, verifier } = pkcePair() + const { code } = await authorize({ clientId, redirectUri: HONEST_CB, challenge }) + + const res = await exchange({ + grant_type: 'authorization_code', + code: code!, + redirect_uri: HONEST_CB, + code_verifier: verifier, + }) + expect(res.status).toBe(400) + }) +}) + +describe('ST-02 / PKCE is mandatory, not optional', () => { + it('rejects an authorization request without a code_challenge', async () => { + const clientId = await registerClient([HONEST_CB]) + const res = await fetch( + base(`/authorize?response_type=code&client_id=${encodeURIComponent(clientId)}&redirect_uri=${encodeURIComponent(HONEST_CB)}`), + ) + expect(res.status).toBe(400) + const html = await res.text() + expect(html).not.toContain('name="api_token"') + }) + + it('rejects the downgrade to code_challenge_method=plain', async () => { + const clientId = await registerClient([HONEST_CB]) + const res = await fetch( + base(`/authorize?response_type=code&client_id=${encodeURIComponent(clientId)}&redirect_uri=${encodeURIComponent(HONEST_CB)}&code_challenge=plain-value&code_challenge_method=plain`), + ) + expect(res.status).toBe(400) + }) + + it('rejects an authorization request that omits response_type=code', async () => { + const clientId = await registerClient([HONEST_CB]) + const { challenge } = pkcePair() + const res = await fetch( + base(`/authorize?response_type=token&client_id=${encodeURIComponent(clientId)}&redirect_uri=${encodeURIComponent(HONEST_CB)}&code_challenge=${challenge}&code_challenge_method=S256`), + ) + expect(res.status).toBe(400) + }) +}) + +describe('ST-02 / the raw SeaTable token must never leave the server', () => { + it('does not return the API token as access_token or refresh_token', async () => { + const clientId = await registerClient([HONEST_CB]) + const { challenge, verifier } = pkcePair() + const { code } = await authorize({ + clientId, + redirectUri: HONEST_CB, + challenge, + apiToken: 'VICTIM-RAW-TOKEN-XYZ', + }) + + const res = await exchange({ + grant_type: 'authorization_code', + code: code!, + client_id: clientId, + redirect_uri: HONEST_CB, + code_verifier: verifier, + }) + expect(res.status).toBe(200) + const data = await res.json() + + expect(data.access_token).not.toBe('VICTIM-RAW-TOKEN-XYZ') + expect(data.refresh_token).not.toBe('VICTIM-RAW-TOKEN-XYZ') + expect(JSON.stringify(data)).not.toContain('VICTIM-RAW-TOKEN-XYZ') + expect(data.access_token).not.toBe(data.refresh_token) + expect(data.expires_in).toBeGreaterThan(0) + }) + + it('resolves its own access token back to the API token server-side', async () => { + const clientId = await registerClient([HONEST_CB]) + const { challenge, verifier } = pkcePair() + const { code } = await authorize({ + clientId, + redirectUri: HONEST_CB, + challenge, + apiToken: 'RESOLVE-ME-123', + }) + const data = await (await exchange({ + grant_type: 'authorization_code', + code: code!, + client_id: clientId, + redirect_uri: HONEST_CB, + code_verifier: verifier, + })).json() + + expect(provider.resolveAccessToken(data.access_token)).toBe('RESOLVE-ME-123') + // A refresh token must not be accepted as an access token + expect(provider.resolveAccessToken(data.refresh_token)).toBeUndefined() + }) + + it('rejects an arbitrary attacker-chosen refresh_token instead of echoing it', async () => { + const res = await exchange({ + grant_type: 'refresh_token', + refresh_token: 'attacker-supplied-value', + }) + expect(res.status).toBe(400) + const data = await res.json() + expect(data.error).toBe('invalid_grant') + }) + + it('rotates the refresh token on use', async () => { + const clientId = await registerClient([HONEST_CB]) + const { challenge, verifier } = pkcePair() + const { code } = await authorize({ clientId, redirectUri: HONEST_CB, challenge, apiToken: 'ROT-1' }) + const first = await (await exchange({ + grant_type: 'authorization_code', + code: code!, + client_id: clientId, + redirect_uri: HONEST_CB, + code_verifier: verifier, + })).json() + + const refreshed = await exchange({ + grant_type: 'refresh_token', + refresh_token: first.refresh_token, + client_id: clientId, + }) + expect(refreshed.status).toBe(200) + const second = await refreshed.json() + expect(second.access_token).toBeTruthy() + expect(second.refresh_token).not.toBe(first.refresh_token) + expect(provider.resolveAccessToken(second.access_token)).toBe('ROT-1') + }) +}) + +describe('ST-02 / the honest client still works end to end', () => { + it('completes register -> authorize -> exchange', async () => { + const clientId = await registerClient([HONEST_CB], 'Well Behaved Client') + const { challenge, verifier } = pkcePair() + + const form = await fetch( + base(`/authorize?response_type=code&client_id=${encodeURIComponent(clientId)}&redirect_uri=${encodeURIComponent(HONEST_CB)}&code_challenge=${challenge}&code_challenge_method=S256&state=abc`), + ) + expect(form.status).toBe(200) + expect(await form.text()).toContain('name="api_token"') + + const { status, location, code } = await authorize({ clientId, redirectUri: HONEST_CB, challenge }) + expect(status).toBe(302) + expect(location!.startsWith(HONEST_CB)).toBe(true) + expect(new URL(location!).searchParams.get('state')).toBe('st') + + const res = await exchange({ + grant_type: 'authorization_code', + code: code!, + client_id: clientId, + redirect_uri: HONEST_CB, + code_verifier: verifier, + }) + expect(res.status).toBe(200) + expect((await res.json()).token_type).toBe('Bearer') + }) +}) diff --git a/tests/oauthProvider.spec.ts b/tests/oauthProvider.spec.ts index 3d25f2e..ab82540 100644 --- a/tests/oauthProvider.spec.ts +++ b/tests/oauthProvider.spec.ts @@ -1,4 +1,4 @@ -import { createHash } from 'node:crypto' +import { createHash, randomBytes } from 'node:crypto' import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http' import { describe, it, expect, beforeAll, afterAll } from 'vitest' @@ -8,8 +8,12 @@ let server: Server let port: number let provider: OAuthProvider +const SECRET = 'oauth-provider-spec-secret-value-long-enough' +const CB = 'https://client.example/cb' + function startTestServer(): Promise { - provider = new OAuthProvider() + // This spec covers the flow and its bindings; callback curation has its own spec. + provider = new OAuthProvider({ secret: SECRET, trustedRedirectHosts: ['client.example'] }) server = createServer(async (req: IncomingMessage, res: ServerResponse) => { const url = new URL(req.url!, `http://localhost`) if (url.pathname === '/.well-known/oauth-authorization-server') { @@ -43,6 +47,48 @@ function base64UrlEncode(buffer: Buffer): string { .replace(/=+$/, '') } +function pkcePair() { + const verifier = base64UrlEncode(randomBytes(32)) + return { verifier, challenge: base64UrlEncode(createHash('sha256').update(verifier).digest()) } +} + +async function registerClient(redirectUris: string[] = [CB], clientName = 'Test Client'): Promise { + const res = await fetch(base('/register'), { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ client_name: clientName, redirect_uris: redirectUris }), + }) + expect(res.status).toBe(201) + return (await res.json()).client_id as string +} + +/** Runs a valid authorization and returns the issued code. */ +async function getCode(opts: { clientId: string; challenge: string; apiToken: string; redirectUri?: string; state?: string }) { + const res = await fetch(base('/authorize'), { + method: 'POST', + headers: { 'content-type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams({ + api_token: opts.apiToken, + client_id: opts.clientId, + redirect_uri: opts.redirectUri ?? CB, + response_type: 'code', + code_challenge: opts.challenge, + code_challenge_method: 'S256', + ...(opts.state ? { state: opts.state } : {}), + }).toString(), + redirect: 'manual', + }) + return res +} + +function exchange(params: Record) { + return fetch(base('/token'), { + method: 'POST', + headers: { 'content-type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams(params).toString(), + }) +} + describe('OAuthProvider', () => { beforeAll(async () => { await startTestServer() @@ -58,167 +104,174 @@ describe('OAuthProvider', () => { const res = await fetch(base('/.well-known/oauth-authorization-server')) expect(res.status).toBe(200) const data = await res.json() + expect(data.issuer).toBeTruthy() expect(data.authorization_endpoint).toContain('/authorize') expect(data.token_endpoint).toContain('/token') expect(data.registration_endpoint).toContain('/register') - expect(data.response_types_supported).toContain('code') + expect(data.response_types_supported).toEqual(['code']) expect(data.grant_types_supported).toContain('authorization_code') - expect(data.grant_types_supported).toContain('refresh_token') - expect(data.code_challenge_methods_supported).toContain('S256') + }) + + it('advertises S256 only — plain PKCE is not offered', async () => { + const data = await (await fetch(base('/.well-known/oauth-authorization-server'))).json() + expect(data.code_challenge_methods_supported).toEqual(['S256']) }) }) describe('dynamic client registration', () => { - it('POST /register returns a client_id', async () => { + it('POST /register returns a client_id and echoes the registration', async () => { const res = await fetch(base('/register'), { method: 'POST', headers: { 'content-type': 'application/json' }, - body: JSON.stringify({ client_name: 'test-client', redirect_uris: ['http://example.com/cb'] }), + body: JSON.stringify({ client_name: 'my-client', redirect_uris: [CB] }), }) expect(res.status).toBe(201) const data = await res.json() expect(data.client_id).toBeTruthy() - expect(data.client_name).toBe('test-client') + expect(data.client_name).toBe('my-client') + expect(data.redirect_uris).toEqual([CB]) + expect(data.token_endpoint_auth_method).toBe('none') }) }) describe('authorize', () => { - it('GET /authorize renders HTML form', async () => { - const res = await fetch(base('/authorize?client_id=test&redirect_uri=http://example.com/cb&state=abc123')) + it('GET /authorize renders HTML form for a registered client', async () => { + const clientId = await registerClient() + const { challenge } = pkcePair() + const res = await fetch(base( + `/authorize?response_type=code&client_id=${encodeURIComponent(clientId)}&redirect_uri=${encodeURIComponent(CB)}&code_challenge=${challenge}&code_challenge_method=S256`, + )) expect(res.status).toBe(200) expect(res.headers.get('content-type')).toContain('text/html') const html = await res.text() + expect(html).toContain('name="api_token"') expect(html).toContain('SeaTable MCP') - expect(html).toContain('api_token') - expect(html).toContain('abc123') }) it('POST /authorize without token returns error', async () => { - const res = await fetch(base('/authorize?redirect_uri=http://example.com/cb&state=xyz'), { + const clientId = await registerClient() + const { challenge } = pkcePair() + const res = await fetch(base('/authorize'), { method: 'POST', headers: { 'content-type': 'application/x-www-form-urlencoded' }, - body: 'api_token=', + body: new URLSearchParams({ + api_token: '', + client_id: clientId, + redirect_uri: CB, + response_type: 'code', + code_challenge: challenge, + code_challenge_method: 'S256', + }).toString(), redirect: 'manual', }) expect(res.status).toBe(400) - const html = await res.text() - expect(html).toContain('Please enter your API token') + expect(await res.text()).toContain('Please enter your API token') }) - it('POST /authorize redirects with code', async () => { - const res = await fetch(base('/authorize'), { - method: 'POST', - headers: { 'content-type': 'application/x-www-form-urlencoded' }, - body: 'api_token=my-secret-token&redirect_uri=http://example.com/cb&state=xyz', - redirect: 'manual', - }) + it('POST /authorize redirects to the registered callback with a code', async () => { + const clientId = await registerClient() + const { challenge } = pkcePair() + const res = await getCode({ clientId, challenge, apiToken: 'my-secret-token', state: 'xyz' }) expect(res.status).toBe(302) const location = res.headers.get('location')! - expect(location).toContain('http://example.com/cb') + expect(location).toContain(CB) expect(location).toContain('code=') expect(location).toContain('state=xyz') }) }) describe('token exchange', () => { - it('full OAuth flow: authorize -> token exchange', async () => { - const authorizeRes = await fetch(base('/authorize'), { - method: 'POST', - headers: { 'content-type': 'application/x-www-form-urlencoded' }, - body: 'api_token=test-api-token-123&redirect_uri=http://example.com/cb&state=s1', - redirect: 'manual', - }) - const location = new URL(authorizeRes.headers.get('location')!) - const code = location.searchParams.get('code')! + it('full OAuth flow: register -> authorize -> token exchange', async () => { + const clientId = await registerClient() + const { challenge, verifier } = pkcePair() + const authorizeRes = await getCode({ clientId, challenge, apiToken: 'test-api-token-123', state: 's1' }) + const code = new URL(authorizeRes.headers.get('location')!).searchParams.get('code')! expect(code).toBeTruthy() - const tokenRes = await fetch(base('/token'), { - method: 'POST', - headers: { 'content-type': 'application/x-www-form-urlencoded' }, - body: `grant_type=authorization_code&code=${code}&redirect_uri=http://example.com/cb`, + const tokenRes = await exchange({ + grant_type: 'authorization_code', + code, + client_id: clientId, + redirect_uri: CB, + code_verifier: verifier, }) expect(tokenRes.status).toBe(200) const tokenData = await tokenRes.json() - expect(tokenData.access_token).toBe('test-api-token-123') expect(tokenData.token_type).toBe('Bearer') - expect(tokenData.refresh_token).toBe('test-api-token-123') + expect(tokenData.expires_in).toBeGreaterThan(0) + // The API token is sealed inside the access token, never handed out as-is. + expect(tokenData.access_token).not.toBe('test-api-token-123') + expect(provider.resolveAccessToken(tokenData.access_token)).toBe('test-api-token-123') }) it('code is single-use', async () => { - const authorizeRes = await fetch(base('/authorize'), { - method: 'POST', - headers: { 'content-type': 'application/x-www-form-urlencoded' }, - body: 'api_token=single-use-token&redirect_uri=http://example.com/cb', - redirect: 'manual', - }) - const location = new URL(authorizeRes.headers.get('location')!) - const code = location.searchParams.get('code')! + const clientId = await registerClient() + const { challenge, verifier } = pkcePair() + const authorizeRes = await getCode({ clientId, challenge, apiToken: 'single-use-token' }) + const code = new URL(authorizeRes.headers.get('location')!).searchParams.get('code')! - const res1 = await fetch(base('/token'), { - method: 'POST', - headers: { 'content-type': 'application/x-www-form-urlencoded' }, - body: `grant_type=authorization_code&code=${code}`, - }) - expect(res1.status).toBe(200) + const params = { + grant_type: 'authorization_code', + code, + client_id: clientId, + redirect_uri: CB, + code_verifier: verifier, + } + expect((await exchange(params)).status).toBe(200) - const res2 = await fetch(base('/token'), { - method: 'POST', - headers: { 'content-type': 'application/x-www-form-urlencoded' }, - body: `grant_type=authorization_code&code=${code}`, - }) + const res2 = await exchange(params) expect(res2.status).toBe(400) - const err = await res2.json() - expect(err.error).toBe('invalid_grant') + expect((await res2.json()).error).toBe('invalid_grant') }) it('invalid code returns error', async () => { - const res = await fetch(base('/token'), { - method: 'POST', - headers: { 'content-type': 'application/x-www-form-urlencoded' }, - body: 'grant_type=authorization_code&code=bogus-code', - }) + const res = await exchange({ grant_type: 'authorization_code', code: 'bogus-code' }) expect(res.status).toBe(400) - const data = await res.json() - expect(data.error).toBe('invalid_grant') + expect((await res.json()).error).toBe('invalid_grant') }) it('unsupported grant_type returns error', async () => { - const res = await fetch(base('/token'), { - method: 'POST', - headers: { 'content-type': 'application/x-www-form-urlencoded' }, - body: 'grant_type=client_credentials', - }) + const res = await exchange({ grant_type: 'client_credentials' }) expect(res.status).toBe(400) - const data = await res.json() - expect(data.error).toBe('unsupported_grant_type') + expect((await res.json()).error).toBe('unsupported_grant_type') }) - it('refresh_token grant returns same token', async () => { - const res = await fetch(base('/token'), { - method: 'POST', - headers: { 'content-type': 'application/x-www-form-urlencoded' }, - body: 'grant_type=refresh_token&refresh_token=my-api-token', + it('refresh_token grant issues a fresh access token for the same base', async () => { + const clientId = await registerClient() + const { challenge, verifier } = pkcePair() + const authorizeRes = await getCode({ clientId, challenge, apiToken: 'refreshable-token' }) + const code = new URL(authorizeRes.headers.get('location')!).searchParams.get('code')! + const first = await (await exchange({ + grant_type: 'authorization_code', + code, + client_id: clientId, + redirect_uri: CB, + code_verifier: verifier, + })).json() + + const res = await exchange({ + grant_type: 'refresh_token', + refresh_token: first.refresh_token, + client_id: clientId, }) expect(res.status).toBe(200) const data = await res.json() - expect(data.access_token).toBe('my-api-token') expect(data.token_type).toBe('Bearer') + expect(provider.resolveAccessToken(data.access_token)).toBe('refreshable-token') }) it('redirect_uri mismatch returns error', async () => { - const authorizeRes = await fetch(base('/authorize'), { - method: 'POST', - headers: { 'content-type': 'application/x-www-form-urlencoded' }, - body: 'api_token=mismatch-token&redirect_uri=http://example.com/cb', - redirect: 'manual', - }) - const location = new URL(authorizeRes.headers.get('location')!) - const code = location.searchParams.get('code')! + const clientId = await registerClient() + const { challenge, verifier } = pkcePair() + const authorizeRes = await getCode({ clientId, challenge, apiToken: 'mismatch-token' }) + const code = new URL(authorizeRes.headers.get('location')!).searchParams.get('code')! - const res = await fetch(base('/token'), { - method: 'POST', - headers: { 'content-type': 'application/x-www-form-urlencoded' }, - body: `grant_type=authorization_code&code=${code}&redirect_uri=http://other.com/cb`, + const res = await exchange({ + grant_type: 'authorization_code', + code, + client_id: clientId, + redirect_uri: 'https://other.example/cb', + code_verifier: verifier, }) expect(res.status).toBe(400) const data = await res.json() @@ -229,48 +282,34 @@ describe('OAuthProvider', () => { describe('PKCE', () => { it('S256 PKCE flow succeeds with correct verifier', async () => { - const codeVerifier = 'dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk' - const codeChallenge = base64UrlEncode( - createHash('sha256').update(codeVerifier).digest() - ) + const clientId = await registerClient() + const { challenge, verifier } = pkcePair() + const authorizeRes = await getCode({ clientId, challenge, apiToken: 'pkce-token' }) + const code = new URL(authorizeRes.headers.get('location')!).searchParams.get('code')! - const authorizeRes = await fetch(base('/authorize'), { - method: 'POST', - headers: { 'content-type': 'application/x-www-form-urlencoded' }, - body: `api_token=pkce-token&redirect_uri=http://example.com/cb&code_challenge=${codeChallenge}&code_challenge_method=S256`, - redirect: 'manual', - }) - const location = new URL(authorizeRes.headers.get('location')!) - const code = location.searchParams.get('code')! - - const tokenRes = await fetch(base('/token'), { - method: 'POST', - headers: { 'content-type': 'application/x-www-form-urlencoded' }, - body: `grant_type=authorization_code&code=${code}&code_verifier=${codeVerifier}`, + const tokenRes = await exchange({ + grant_type: 'authorization_code', + code, + client_id: clientId, + redirect_uri: CB, + code_verifier: verifier, }) expect(tokenRes.status).toBe(200) - const data = await tokenRes.json() - expect(data.access_token).toBe('pkce-token') + expect(provider.resolveAccessToken((await tokenRes.json()).access_token)).toBe('pkce-token') }) it('S256 PKCE flow fails with wrong verifier', async () => { - const codeChallenge = base64UrlEncode( - createHash('sha256').update('correct-verifier').digest() - ) + const clientId = await registerClient() + const { challenge } = pkcePair() + const authorizeRes = await getCode({ clientId, challenge, apiToken: 'pkce-token' }) + const code = new URL(authorizeRes.headers.get('location')!).searchParams.get('code')! - const authorizeRes = await fetch(base('/authorize'), { - method: 'POST', - headers: { 'content-type': 'application/x-www-form-urlencoded' }, - body: `api_token=pkce-token&redirect_uri=http://example.com/cb&code_challenge=${codeChallenge}&code_challenge_method=S256`, - redirect: 'manual', - }) - const location = new URL(authorizeRes.headers.get('location')!) - const code = location.searchParams.get('code')! - - const tokenRes = await fetch(base('/token'), { - method: 'POST', - headers: { 'content-type': 'application/x-www-form-urlencoded' }, - body: `grant_type=authorization_code&code=${code}&code_verifier=wrong-verifier`, + const tokenRes = await exchange({ + grant_type: 'authorization_code', + code, + client_id: clientId, + redirect_uri: CB, + code_verifier: 'wrong-verifier', }) expect(tokenRes.status).toBe(400) const data = await tokenRes.json() @@ -278,29 +317,20 @@ describe('OAuthProvider', () => { expect(data.error_description).toContain('PKCE') }) - it('PKCE fails when verifier is missing but challenge was sent', async () => { - const codeChallenge = base64UrlEncode( - createHash('sha256').update('some-verifier').digest() - ) + it('PKCE fails when the verifier is missing', async () => { + const clientId = await registerClient() + const { challenge } = pkcePair() + const authorizeRes = await getCode({ clientId, challenge, apiToken: 'pkce-token' }) + const code = new URL(authorizeRes.headers.get('location')!).searchParams.get('code')! - const authorizeRes = await fetch(base('/authorize'), { - method: 'POST', - headers: { 'content-type': 'application/x-www-form-urlencoded' }, - body: `api_token=pkce-token&redirect_uri=http://example.com/cb&code_challenge=${codeChallenge}&code_challenge_method=S256`, - redirect: 'manual', - }) - const location = new URL(authorizeRes.headers.get('location')!) - const code = location.searchParams.get('code')! - - const tokenRes = await fetch(base('/token'), { - method: 'POST', - headers: { 'content-type': 'application/x-www-form-urlencoded' }, - body: `grant_type=authorization_code&code=${code}`, + const tokenRes = await exchange({ + grant_type: 'authorization_code', + code, + client_id: clientId, + redirect_uri: CB, }) expect(tokenRes.status).toBe(400) - const data = await tokenRes.json() - expect(data.error).toBe('invalid_request') - expect(data.error_description).toContain('code_verifier') + expect((await tokenRes.json()).error_description).toContain('code_verifier') }) }) }) diff --git a/tests/oauthRateLimit.spec.ts b/tests/oauthRateLimit.spec.ts new file mode 100644 index 0000000..c1ca355 --- /dev/null +++ b/tests/oauthRateLimit.spec.ts @@ -0,0 +1,141 @@ +import type { AddressInfo } from 'node:net' + +import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest' + +vi.mock('../src/metrics/metricsServer', () => ({ + startMetricsServer: vi.fn().mockResolvedValue(undefined), +})) + +vi.mock('../src/auth/tokenValidator', () => ({ + TokenValidator: class { + async validate(token: string): Promise { + return token === 'good-token' + } + async looksLikeAccountToken(): Promise { + return false + } + cleanup(): void {} + destroy(): void {} + }, +})) + +import { startHttpServer } from '../src/http/httpServer' + +let server: ReturnType +let baseUrl: string + +/** + * The OAuth endpoints bypassed the rate limiter entirely — it only ran inside + * handleMcpRequest. That left POST /authorize usable as an unauthenticated, + * unthrottled oracle for testing arbitrary SeaTable API tokens, with every + * attempt forwarded to the SeaTable backend. + */ +beforeAll(async () => { + process.env.SEATABLE_SERVER_URL = 'http://localhost' + process.env.SEATABLE_MODE = 'managed' + process.env.SEATABLE_MOCK = 'true' + process.env.SEATABLE_TOKEN_SECRET = 'oauth-rate-limit-spec-secret-long-enough' + delete process.env.SEATABLE_API_TOKEN + + server = await startHttpServer({ port: 0 }) + baseUrl = `http://127.0.0.1:${(server.address() as AddressInfo).port}` +}) + +afterAll(async () => { + delete process.env.SEATABLE_MODE + delete process.env.SEATABLE_TOKEN_SECRET + if (server) await new Promise((resolve) => server.close(() => resolve())) +}) + +/** Each test uses its own X-Forwarded-For so the per-IP buckets stay independent. */ +function asIp(ip: string) { + return { 'x-forwarded-for': ip } +} + +async function postAuthorize(ip: string, token = 'wrong-token') { + return fetch(`${baseUrl}/authorize`, { + method: 'POST', + headers: { ...asIp(ip), 'content-type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams({ api_token: token, client_id: 'x', redirect_uri: 'http://127.0.0.1:7777/cb', response_type: 'code' }).toString(), + redirect: 'manual', + }) +} + +describe('OAuth endpoints are rate limited', () => { + it('throttles repeated token submissions from one IP', async () => { + const ip = '203.0.113.10' + let throttled = 0 + for (let i = 0; i < 25; i++) { + const res = await postAuthorize(ip) + if (res.status === 429) throttled++ + } + expect(throttled).toBeGreaterThan(0) + }) + + it('answers a throttled request with retry-after', async () => { + const ip = '203.0.113.11' + let last: Response | undefined + for (let i = 0; i < 25; i++) { + last = await postAuthorize(ip) + if (last.status === 429) break + } + expect(last!.status).toBe(429) + expect(Number(last!.headers.get('retry-after'))).toBeGreaterThan(0) + }) + + it('throttles the registration endpoint', async () => { + const ip = '203.0.113.12' + let throttled = 0 + for (let i = 0; i < 45; i++) { + const res = await fetch(`${baseUrl}/register`, { + method: 'POST', + headers: { ...asIp(ip), 'content-type': 'application/json' }, + body: JSON.stringify({ client_name: 'flood', redirect_uris: ['http://127.0.0.1:7777/cb'] }), + }) + if (res.status === 429) throttled++ + } + expect(throttled).toBeGreaterThan(0) + }) + + it('throttles the token endpoint', async () => { + const ip = '203.0.113.13' + let throttled = 0 + for (let i = 0; i < 45; i++) { + const res = await fetch(`${baseUrl}/token`, { + method: 'POST', + headers: { ...asIp(ip), 'content-type': 'application/x-www-form-urlencoded' }, + body: 'grant_type=authorization_code&code=guess', + }) + if (res.status === 429) throttled++ + } + expect(throttled).toBeGreaterThan(0) + }) + + it('lets a normal authorization flow through unthrottled', async () => { + const ip = '203.0.113.20' + const reg = await fetch(`${baseUrl}/register`, { + method: 'POST', + headers: { ...asIp(ip), 'content-type': 'application/json' }, + body: JSON.stringify({ client_name: 'Honest', redirect_uris: ['http://127.0.0.1:7777/cb'] }), + }) + expect(reg.status).toBe(201) + const clientId = (await reg.json()).client_id + + const form = await fetch( + `${baseUrl}/authorize?response_type=code&client_id=${encodeURIComponent(clientId)}&redirect_uri=${encodeURIComponent('http://127.0.0.1:7777/cb')}&code_challenge=abc&code_challenge_method=S256`, + { headers: asIp(ip) }, + ) + expect(form.status).not.toBe(429) + + const meta = await fetch(`${baseUrl}/.well-known/oauth-authorization-server`, { headers: asIp(ip) }) + expect(meta.status).toBe(200) + }) + + it('does not throttle the health endpoint', async () => { + const ip = '203.0.113.30' + for (let i = 0; i < 40; i++) { + const res = await fetch(`${baseUrl}/health`, { headers: asIp(ip) }) + expect(res.status).toBe(200) + } + }) +}) diff --git a/tests/oauthRedirectPolicy.spec.ts b/tests/oauthRedirectPolicy.spec.ts new file mode 100644 index 0000000..57e7faa --- /dev/null +++ b/tests/oauthRedirectPolicy.spec.ts @@ -0,0 +1,264 @@ +import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http' +import { afterAll, beforeAll, describe, expect, it } from 'vitest' + +import { OAuthProvider } from '../src/auth/oauthProvider.js' + +/** + * Which callbacks may receive an authorization code, and how much friction the + * user meets on the way. + * + * With open dynamic registration, "registered client" is not a trust statement — + * an attacker registers honestly. The dividing line is whether the code leaves + * the user's machine: + * + * loopback / private-use scheme stays local, safe by construction + * trusted remote host curated, no friction + * unknown remote host allowed, but only after an explicit, + * un-skippable acknowledgement + * + * The attacker controls the entry link, so nothing in that link may switch the + * acknowledgement off. + */ + +const SECRET = 'oauth-redirect-policy-spec-secret-long-enough' +const CHALLENGE = 'Zm9vYmFyLWNoYWxsZW5nZS12YWx1ZS1oZXJlLXh4eHh4' +const UNKNOWN = 'https://unknown-service.example/cb' + +let server: Server +let port: number +let provider: OAuthProvider + +const base = (p: string) => `http://localhost:${port}${p}` + +async function register(uri: string, clientName = 'Client') { + return fetch(base('/register'), { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ client_name: clientName, redirect_uris: [uri] }), + }) +} + +async function registerOk(uri: string, clientName = 'Client'): Promise { + const res = await register(uri, clientName) + expect(res.status).toBe(201) + return (await res.json()).client_id as string +} + +function authorizeUrl(clientId: string, uri: string, extra = '') { + return base( + `/authorize?response_type=code&client_id=${encodeURIComponent(clientId)}` + + `&redirect_uri=${encodeURIComponent(uri)}` + + `&code_challenge=${CHALLENGE}&code_challenge_method=S256${extra}`, + ) +} + +function postAuthorize( + fields: Record, + fetchSite: string | null = 'same-origin', +) { + const headers: Record = { 'content-type': 'application/x-www-form-urlencoded' } + if (fetchSite) headers['sec-fetch-site'] = fetchSite + return fetch(base('/authorize'), { + method: 'POST', + headers, + body: new URLSearchParams(fields).toString(), + redirect: 'manual', + }) +} + +function flowFields(clientId: string, uri: string) { + return { + client_id: clientId, + redirect_uri: uri, + response_type: 'code', + code_challenge: CHALLENGE, + code_challenge_method: 'S256', + state: 'st', + } +} + +const hasTokenField = (html: string) => html.includes('name="api_token"') +const hasAcknowledgement = (html: string) => html.includes('name="acknowledged"') + +beforeAll(async () => { + provider = new OAuthProvider({ + secret: SECRET, + trustedRedirectHosts: ['claude.ai', 'chatgpt.com'], + validateToken: async (token) => token === 'valid-token', + }) + server = createServer(async (req: IncomingMessage, res: ServerResponse) => { + const url = new URL(req.url!, 'http://localhost') + if (url.pathname === '/authorize') await provider.handleAuthorize(req, res, url) + else if (url.pathname === '/register') await provider.handleRegister(req, res) + else res.writeHead(404).end() + }) + await new Promise((resolve) => { + server.listen(0, () => { + port = (server.address() as any).port + resolve() + }) + }) +}) + +afterAll(async () => { + provider.destroy() + await new Promise((resolve) => server.close(() => resolve())) +}) + +describe('callbacks that stay on the user machine need no friction', () => { + it.each([ + 'http://127.0.0.1:8765/cb', + 'http://localhost:3000/callback', + 'http://[::1]:9000/cb', + 'cursor://anysphere.cursor-retrieval/oauth/callback', + 'vscode://mcp/auth', + 'com.example.desktop:/oauth2redirect', + ])('goes straight to the token form for %s', async (uri) => { + const clientId = await registerOk(uri) + const html = await (await fetch(authorizeUrl(clientId, uri))).text() + expect(hasTokenField(html)).toBe(true) + expect(hasAcknowledgement(html)).toBe(false) + }) + + it('goes straight to the token form for a curated remote host', async () => { + const uri = 'https://claude.ai/api/mcp/auth_callback' + const clientId = await registerOk(uri) + const html = await (await fetch(authorizeUrl(clientId, uri))).text() + expect(hasTokenField(html)).toBe(true) + expect(hasAcknowledgement(html)).toBe(false) + }) +}) + +describe('callbacks that never work at all', () => { + it.each(['javascript:alert(1)', 'data:text/html,x', 'file:///etc/passwd', 'blob:https://x/y'])( + 'refuses to register %s', + async (uri) => { + expect((await register(uri)).status).toBe(400) + }, + ) + + it('refuses plaintext http to a remote host', async () => { + expect((await register('http://example.com/cb')).status).toBe(400) + }) +}) + +describe('an unknown remote host is allowed, but not quietly', () => { + it('registers without complaint — curation is not a gate', async () => { + expect((await register(UNKNOWN)).status).toBe(201) + }) + + it('shows the destination instead of the token field', async () => { + const clientId = await registerOk(UNKNOWN) + const res = await fetch(authorizeUrl(clientId, UNKNOWN)) + expect(res.status).toBe(200) + const html = await res.text() + expect(html).toContain('unknown-service.example') + expect(hasAcknowledgement(html)).toBe(true) + expect(hasTokenField(html)).toBe(false) + }) + + it('reveals the token field only after an acknowledgement from our own page', async () => { + const clientId = await registerOk(UNKNOWN) + const res = await postAuthorize({ ...flowFields(clientId, UNKNOWN), acknowledged: 'yes' }, 'same-origin') + expect(res.status).toBe(200) + const html = await res.text() + expect(hasTokenField(html)).toBe(true) + // the destination stays visible next to the field + expect(html).toContain('unknown-service.example') + }) + + it('issues a code once the user acknowledged and submitted a token', async () => { + const clientId = await registerOk(UNKNOWN) + const res = await postAuthorize( + { ...flowFields(clientId, UNKNOWN), acknowledged: 'yes', api_token: 'valid-token' }, + 'same-origin', + ) + expect(res.status).toBe(302) + expect(res.headers.get('location')!.startsWith(UNKNOWN)).toBe(true) + }) +}) + +describe('the acknowledgement cannot be switched off by the attacker', () => { + it('ignores an acknowledgement smuggled into the entry link', async () => { + const clientId = await registerOk(UNKNOWN) + const html = await (await fetch(authorizeUrl(clientId, UNKNOWN, '&acknowledged=yes'))).text() + expect(hasTokenField(html)).toBe(false) + expect(hasAcknowledgement(html)).toBe(true) + }) + + it('rejects a token submission that never passed the acknowledgement', async () => { + const clientId = await registerOk(UNKNOWN) + const res = await postAuthorize({ ...flowFields(clientId, UNKNOWN), api_token: 'valid-token' }, 'same-origin') + expect(res.status).toBe(400) + expect(res.headers.get('location')).toBeNull() + const html = await res.text() + expect(hasAcknowledgement(html)).toBe(true) + expect(hasTokenField(html)).toBe(false) + }) + + it('rejects an acknowledgement auto-submitted from a foreign page', async () => { + const clientId = await registerOk(UNKNOWN) + const res = await postAuthorize({ ...flowFields(clientId, UNKNOWN), acknowledged: 'yes' }, 'cross-site') + const html = await res.text() + expect(hasTokenField(html)).toBe(false) + expect(hasAcknowledgement(html)).toBe(true) + }) + + it('rejects a cross-site auto-submit that carries the token as well', async () => { + const clientId = await registerOk(UNKNOWN) + const res = await postAuthorize( + { ...flowFields(clientId, UNKNOWN), acknowledged: 'yes', api_token: 'valid-token' }, + 'cross-site', + ) + expect(res.status).toBe(400) + expect(res.headers.get('location')).toBeNull() + }) + + it('falls back to friction when the browser sends no Sec-Fetch-Site at all', async () => { + const clientId = await registerOk(UNKNOWN) + const res = await postAuthorize({ ...flowFields(clientId, UNKNOWN), acknowledged: 'yes' }, null) + const html = await res.text() + expect(hasTokenField(html)).toBe(false) + }) +}) + +describe('the consent screen does not lend credibility it cannot verify', () => { + it('marks the application name as self-reported', async () => { + const uri = 'http://127.0.0.1:4321/cb' + const clientId = await registerOk(uri, 'Claude') + const html = await (await fetch(authorizeUrl(clientId, uri))).text() + expect(html).toContain('Claude') + expect(html.toLowerCase()).toContain('self-reported') + }) + + it('names the destination on the warning page, not just the claimed identity', async () => { + const clientId = await registerOk(UNKNOWN, 'Totally Legit Sync') + const html = await (await fetch(authorizeUrl(clientId, UNKNOWN))).text() + const destinationAt = html.indexOf('unknown-service.example') + const nameAt = html.indexOf('Totally Legit Sync') + expect(destinationAt).toBeGreaterThan(-1) + // the destination is introduced before the self-declared name + expect(destinationAt).toBeLessThan(nameAt) + }) +}) + +describe('policy helpers', () => { + it('separates "usable at all" from "needs no friction"', () => { + expect(provider.isPermittedRedirectUri(UNKNOWN)).toBe(true) + expect(provider.isTrustedRedirectUri(UNKNOWN)).toBe(false) + + expect(provider.isPermittedRedirectUri('http://127.0.0.1:1/cb')).toBe(true) + expect(provider.isTrustedRedirectUri('http://127.0.0.1:1/cb')).toBe(true) + + expect(provider.isPermittedRedirectUri('javascript:alert(1)')).toBe(false) + expect(provider.isPermittedRedirectUri('http://example.com/cb')).toBe(false) + }) + + it('matches curated hosts exactly — no suffix tricks', () => { + for (const uri of ['https://claude.ai.evil.example/cb', 'https://evilclaude.ai/cb', 'https://sub.claude.ai/cb']) { + expect(provider.isTrustedRedirectUri(uri)).toBe(false) + // still permitted — it just meets the acknowledgement + expect(provider.isPermittedRedirectUri(uri)).toBe(true) + } + }) +}) diff --git a/tests/oauthTokenInput.spec.ts b/tests/oauthTokenInput.spec.ts new file mode 100644 index 0000000..78d5e77 --- /dev/null +++ b/tests/oauthTokenInput.spec.ts @@ -0,0 +1,126 @@ +import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http' +import { afterAll, beforeAll, describe, expect, it } from 'vitest' + +import { OAuthProvider } from '../src/auth/oauthProvider.js' + +/** + * Nearly half of all real authorization attempts were rejected with a bare + * "Invalid API token". Two causes: whitespace picked up while copying, and the + * SeaTable account API token pasted where a base API token is required. + */ + +const SECRET = 'oauth-token-input-spec-secret-long-enough' +const CB = 'http://127.0.0.1:5599/cb' +const VALID = 'the-one-valid-base-token' +const CHALLENGE = 'Zm9vYmFyLWNoYWxsZW5nZS12YWx1ZS1oZXJlLXh4eHh4' + +let server: Server +let port: number +let provider: OAuthProvider +const seenByValidator: string[] = [] +let accountTokens = new Set() + +const base = (p: string) => `http://localhost:${port}${p}` + +async function registerClient(): Promise { + const res = await fetch(base('/register'), { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ client_name: 'Test Client', redirect_uris: [CB] }), + }) + return (await res.json()).client_id as string +} + +function submit(clientId: string, apiToken: string) { + return fetch(base('/authorize'), { + method: 'POST', + headers: { 'content-type': 'application/x-www-form-urlencoded', 'sec-fetch-site': 'same-origin' }, + body: new URLSearchParams({ + api_token: apiToken, + client_id: clientId, + redirect_uri: CB, + response_type: 'code', + code_challenge: CHALLENGE, + code_challenge_method: 'S256', + }).toString(), + redirect: 'manual', + }) +} + +beforeAll(async () => { + provider = new OAuthProvider({ + secret: SECRET, + validateToken: async (token) => { + seenByValidator.push(token) + return token === VALID + }, + looksLikeAccountToken: async (token) => accountTokens.has(token), + }) + server = createServer(async (req: IncomingMessage, res: ServerResponse) => { + const url = new URL(req.url!, 'http://localhost') + if (url.pathname === '/authorize') await provider.handleAuthorize(req, res, url) + else if (url.pathname === '/register') await provider.handleRegister(req, res) + else res.writeHead(404).end() + }) + await new Promise((resolve) => { + server.listen(0, () => { + port = (server.address() as any).port + resolve() + }) + }) +}) + +afterAll(async () => { + provider.destroy() + await new Promise((resolve) => server.close(() => resolve())) +}) + +describe('copy-paste whitespace', () => { + it('accepts a token pasted with surrounding whitespace and a newline', async () => { + const clientId = await registerClient() + seenByValidator.length = 0 + const res = await submit(clientId, ` ${VALID}\n`) + expect(res.status).toBe(302) + expect(seenByValidator).toContain(VALID) + }) + + it('treats a whitespace-only entry as an empty field, not an invalid token', async () => { + const clientId = await registerClient() + seenByValidator.length = 0 + const res = await submit(clientId, ' \n\t ') + expect(res.status).toBe(400) + expect(await res.text()).toContain('Please enter your API token') + expect(seenByValidator).toHaveLength(0) + }) +}) + +describe('account token pasted instead of base token', () => { + it('names the actual mistake and where to find the right token', async () => { + const clientId = await registerClient() + accountTokens = new Set(['an-account-level-token']) + const res = await submit(clientId, 'an-account-level-token') + expect(res.status).toBe(400) + const html = await res.text() + expect(html).toContain('account') + expect(html).toContain('API Tokens') + // the form stays so the user can correct it right away + expect(html).toContain('name="api_token"') + }) + + it('falls back to the generic message for a token that is neither', async () => { + const clientId = await registerClient() + accountTokens = new Set() + const res = await submit(clientId, 'complete-nonsense') + expect(res.status).toBe(400) + const html = await res.text() + expect(html).toContain('Invalid API token') + expect(html).not.toContain('account API token') + }) + + it('never echoes the submitted token back into the page', async () => { + const clientId = await registerClient() + accountTokens = new Set() + const res = await submit(clientId, 'secret-nonsense-value-42') + expect(await res.text()).not.toContain('secret-nonsense-value-42') + }) +}) diff --git a/tests/tokenCipher.spec.ts b/tests/tokenCipher.spec.ts new file mode 100644 index 0000000..150eba8 --- /dev/null +++ b/tests/tokenCipher.spec.ts @@ -0,0 +1,71 @@ +import { describe, expect, it } from 'vitest' + +import { TokenCipher } from '../src/auth/tokenCipher.js' + +const SECRET = 'test-secret-value-that-is-long-enough-for-hkdf' + +describe('TokenCipher', () => { + it('seals and opens a payload round-trip', () => { + const cipher = new TokenCipher(SECRET) + const sealed = cipher.seal('access', { apiToken: 'raw-seatable-token' }, 3600_000) + + expect(sealed).not.toContain('raw-seatable-token') + + const opened = cipher.open('access', sealed) + expect(opened?.apiToken).toBe('raw-seatable-token') + }) + + it('never emits the plaintext token in the sealed value', () => { + const cipher = new TokenCipher(SECRET) + const sealed = cipher.seal('access', { apiToken: 'super-secret-abc123' }, 3600_000) + + // Neither raw, nor base64, nor hex of the token may appear + expect(sealed).not.toContain('super-secret-abc123') + expect(sealed).not.toContain(Buffer.from('super-secret-abc123').toString('base64')) + expect(sealed).not.toContain(Buffer.from('super-secret-abc123').toString('base64url')) + expect(sealed).not.toContain(Buffer.from('super-secret-abc123').toString('hex')) + }) + + it('produces a different ciphertext each time (random nonce)', () => { + const cipher = new TokenCipher(SECRET) + const a = cipher.seal('access', { apiToken: 'same' }, 3600_000) + const b = cipher.seal('access', { apiToken: 'same' }, 3600_000) + expect(a).not.toBe(b) + expect(cipher.open('access', a)?.apiToken).toBe('same') + expect(cipher.open('access', b)?.apiToken).toBe('same') + }) + + it('rejects a token sealed with a different secret', () => { + const sealed = new TokenCipher(SECRET).seal('access', { apiToken: 'x' }, 3600_000) + const other = new TokenCipher('a-completely-different-secret-value-here') + expect(other.open('access', sealed)).toBeUndefined() + }) + + it('rejects a tampered ciphertext (AEAD integrity)', () => { + const cipher = new TokenCipher(SECRET) + const sealed = cipher.seal('access', { apiToken: 'x' }, 3600_000) + const tampered = sealed.slice(0, -3) + (sealed.slice(-3) === 'AAA' ? 'BBB' : 'AAA') + expect(cipher.open('access', tampered)).toBeUndefined() + }) + + it('rejects a value of a different kind (domain separation)', () => { + const cipher = new TokenCipher(SECRET) + const refresh = cipher.seal('refresh', { apiToken: 'x' }, 3600_000) + // A refresh token must not be usable where an access token is expected + expect(cipher.open('access', refresh)).toBeUndefined() + expect(cipher.open('refresh', refresh)?.apiToken).toBe('x') + }) + + it('rejects an expired token', () => { + const cipher = new TokenCipher(SECRET) + const sealed = cipher.seal('access', { apiToken: 'x' }, -1000) + expect(cipher.open('access', sealed)).toBeUndefined() + }) + + it('rejects arbitrary attacker-supplied garbage', () => { + const cipher = new TokenCipher(SECRET) + expect(cipher.open('access', 'not-a-token')).toBeUndefined() + expect(cipher.open('access', '')).toBeUndefined() + expect(cipher.open('access', 'stmcp1.AAAA')).toBeUndefined() + }) +})