From fe8f70260a8359698e499b74310775cba0b3e5ea Mon Sep 17 00:00:00 2001 From: Christoph Dyllick-Brenzinger Date: Tue, 25 Aug 2026 22:24:54 +0200 Subject: [PATCH 1/8] feat(auth): add TokenCipher for sealed, self-expiring values An authenticated AES-256-GCM envelope, keyed from a server secret via HKDF, for values the server hands out but must be able to read back: OAuth access and refresh tokens, and client registrations. The envelope carries its own expiry and is bound to a kind ('access', 'refresh', 'client') through the AEAD additional data, so a value sealed as one kind can never be opened as another. Because it is authenticated and self-describing, no server-side store is needed: the values survive a restart and work across instances that share the secret. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_014k5RGUNUDegp7Fhsotwiyi --- src/auth/tokenCipher.ts | 78 +++++++++++++++++++++++++++++++++++++++ tests/tokenCipher.spec.ts | 71 +++++++++++++++++++++++++++++++++++ 2 files changed, 149 insertions(+) create mode 100644 src/auth/tokenCipher.ts create mode 100644 tests/tokenCipher.spec.ts 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/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() + }) +}) From 2c6e667dd5ba9285a56a5165dabfec876381f110 Mon Sep 17 00:00:00 2001 From: Christoph Dyllick-Brenzinger Date: Tue, 25 Aug 2026 22:25:23 +0200 Subject: [PATCH 2/8] fix(auth)!: close OAuth token theft and session-ID-only authorization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses an external security report against managed mode v1.5.2 (ST-01, ST-02). Managed mode only; the selfhosted default has no OAuth endpoints and no per-request auth, and is unchanged. ST-02 — the OAuth bridge handed out the user's raw SeaTable API token and enforced none of its bindings: * /register generated a client_id and discarded the registration, so /authorize had nothing to check a callback against. The client_id is now a sealed envelope carrying the client's name and its redirect_uris; a client_id we did not issue cannot be opened, and the callback list inside it cannot be edited. * redirect_uri was compared only when the token request happened to send it -- omitting the field skipped the check. It is now required. * PKCE was verified only when the authorization request had supplied a challenge. S256 is now mandatory and 'plain' is gone, including from the advertised metadata. * The authorization code was not bound to a client. It now is, and the exchanging client must match. * access_token and refresh_token were the raw SeaTable API token, byte for byte. They are now sealed envelopes -- one hour for access, fourteen days for a rotating refresh token -- and the API token never leaves the process. resolveAccessToken() unseals it server-side. * The refresh grant echoed back whatever it was given. It now rejects any value it did not issue. With open dynamic registration, "registered client" is not a trust statement: an attacker can register honestly. So the callback policy asks a different question -- does the code leave the user's machine? Loopback and private-use app schemes (cursor://, vscode://) stay local and pass without friction; a remote https destination that is not curated is still allowed, but only after the user acknowledges where their token is about to be sent. That acknowledgement is read from our own form body and requires Sec-Fetch-Site: same-origin, so neither the entry link nor a foreign auto-submit can skip it. The consent screen now leads with the destination and marks the application name as self-reported, because it is chosen by whoever registered the client and cannot be verified. ST-01 — after initialization, POST/GET/DELETE /mcp authorized on the mcp-session-id header alone. A request with no token, an invalid token, or another account's token was routed to the session owner's client. Every request now carries a credential, it is validated, and it must resolve to the identity that created the session (401 / 403). The session ID is a routing value and is logged only as a fingerprint. The positive validation cache drops from five minutes to one, which is the window in which a revoked token still passes. Also in this change: * The OAuth endpoints bypassed the rate limiter entirely, leaving POST /authorize usable as an unthrottled oracle for testing SeaTable API tokens -- and forwarding every attempt to the SeaTable backend. Now 30 requests/min per IP across the OAuth endpoints, 10/min for token submissions. * Roughly half of all real authorization attempts failed with a bare "Invalid API token". The token is now trimmed before use, and on failure the server distinguishes an account API token from a base API token and says which one is needed and where to find it. * The logs could not answer the report's forensic questions: no client IP, no callback destination, no link between an issued code and its exchange, and several rejection paths logged nothing at all. Every OAuth event now carries ip, clientName and the callback origin; a derived flow id pairs the authorization with its exchange; no rejection path is silent. Neither the code, nor a prefix of it, nor the API token is ever written to the log. BREAKING CHANGE: managed mode requires SEATABLE_TOKEN_SECRET (min. 32 chars, stable across restarts) and refuses to start without it. Existing OAuth clients must register and authorize again: their stored client_id is not a sealed envelope and will be rejected. Clients that omit redirect_uri at the token endpoint, or that do not use PKCE S256, no longer complete the flow. Raw SeaTable API tokens continue to be accepted as bearer credentials. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_014k5RGUNUDegp7Fhsotwiyi --- src/auth/oauthProvider.ts | 738 ++++++++++++++++++++------- src/auth/tokenValidator.ts | 22 +- src/config/env.ts | 13 + src/http/httpServer.ts | 121 ++++- src/ratelimit/index.ts | 10 + tests/managedSessionAuth.spec.ts | 154 ++++++ tests/oauthObservability.spec.ts | 184 +++++++ tests/oauthProvider.security.spec.ts | 403 +++++++++++++++ tests/oauthProvider.spec.ts | 324 ++++++------ tests/oauthRateLimit.spec.ts | 141 +++++ tests/oauthRedirectPolicy.spec.ts | 264 ++++++++++ tests/oauthTokenInput.spec.ts | 126 +++++ 12 files changed, 2160 insertions(+), 340 deletions(-) create mode 100644 tests/managedSessionAuth.spec.ts create mode 100644 tests/oauthObservability.spec.ts create mode 100644 tests/oauthProvider.security.spec.ts create mode 100644 tests/oauthRateLimit.spec.ts create mode 100644 tests/oauthRedirectPolicy.spec.ts create mode 100644 tests/oauthTokenInput.spec.ts diff --git a/src/auth/oauthProvider.ts b/src/auth/oauthProvider.ts index 04df0d7..4a6292f 100644 --- a/src/auth/oauthProvider.ts +++ b/src/auth/oauthProvider.ts @@ -1,23 +1,108 @@ -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 ACCESS_TOKEN_TTL_MS = 60 * 60 * 1000 // 1 hour +const 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[] + /** 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 +110,91 @@ 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 looksLikeAccountToken?: (token: string) => Promise constructor(options?: OAuthProviderOptions) { this.configuredHostname = options?.hostname this.validateToken = options?.validateToken + this.getClientIp = options?.getClientIp + 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 +220,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 +241,193 @@ 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')) + + if (redirectUris.length === 0) { + this.registrationError(res, 'invalid_redirect_uri', 'At least one redirect_uri is required') + return + } - const clientId = randomBytes(16).toString('hex') + 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 registration: ClientRegistration = { n: clientName, r: redirectUris } + const clientId = this.cipher.seal('client', registration, CLIENT_TTL_MS) logger.info({ clientName }, 'OAuth dynamic client registration') - const response: Record = { + 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: redirectUri.slice(0, 200), 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: safeOrigin(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: callbackOrigin, 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: callbackOrigin, 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 +437,140 @@ 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') + this.issueTokens(res, payload.t, payload.c) + } + + private issueTokens(res: ServerResponse, apiToken: string, clientId: string): void { + const accessToken = this.cipher.seal('access', { t: apiToken }, ACCESS_TOKEN_TTL_MS) + const refreshToken = this.cipher.seal('refresh', { t: apiToken, c: clientId }, REFRESH_TOKEN_TTL_MS) - // 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(ACCESS_TOKEN_TTL_MS / 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 +624,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 +766,34 @@ 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. + */ +/** Origin of a callback for logging; falls back to a truncated raw value. */ +function safeOrigin(raw: string): string { + try { + return new URL(raw).origin + } catch { + return raw.slice(0, 200) + } +} + +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/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..9a59cb8 100644 --- a/src/config/env.ts +++ b/src/config/env.ts @@ -20,6 +20,9 @@ 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(), 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 +59,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..f1f8388 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,11 @@ 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(), validateToken: tokenValidator ? (token) => tokenValidator.validate(token) : undefined, + looksLikeAccountToken: tokenValidator ? (token) => tokenValidator.looksLikeAccountToken(token) : undefined, + getClientIp: (req) => getClientIp(req), }) : undefined const toolDefinitions = getStaticToolDefinitions() @@ -101,6 +126,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 +150,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 +216,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 +245,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 +290,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 +407,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 +420,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 +433,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 +463,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 +479,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/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..5e8f154 --- /dev/null +++ b/tests/oauthObservability.spec.ts @@ -0,0 +1,184 @@ +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 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') + expect(issued[0].fields.callback).toBe('http://127.0.0.1:6611') + 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('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') + 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') + }) +}) From 9af9cb674f5063c60e47db0c0fde9351a97313c2 Mon Sep 17 00:00:00 2001 From: Christoph Dyllick-Brenzinger Date: Tue, 25 Aug 2026 22:25:23 +0200 Subject: [PATCH 3/8] docs: document managed-mode auth changes, release 1.6.0 Covers SEATABLE_TOKEN_SECRET, the callback policy and its confirmation step, SEATABLE_OAUTH_TRUSTED_REDIRECT_HOSTS, and the per-request credential rule. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_014k5RGUNUDegp7Fhsotwiyi --- .env.example | 13 +++++++++++++ CLAUDE.md | 12 +++++++++++- README.md | 26 ++++++++++++++++++++++++-- package.json | 2 +- 4 files changed, 49 insertions(+), 4 deletions(-) diff --git a/.env.example b/.env.example index 8e98547..e030283 100644 --- a/.env.example +++ b/.env.example @@ -6,9 +6,22 @@ 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 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..d1fcbcd 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,7 @@ 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_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.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", From d56cfbbfc4b49750074a62dc5ac89ccb46493716 Mon Sep 17 00:00:00 2001 From: Christoph Dyllick-Brenzinger Date: Tue, 25 Aug 2026 22:34:00 +0200 Subject: [PATCH 4/8] chore(deps): lift dependencies flagged by the image scan Trivy fails the build on HIGH/CRITICAL findings, and six npm packages had accumulated advisories since the 1.5.2 image was built in March. All resolve within their existing semver ranges, so package.json is unchanged: axios 1.13.6 -> 1.19.0 (direct; prototype pollution, DoS) fast-uri 3.1.0 -> 3.1.6 (host confusion, policy bypass) form-data 4.0.5 -> 4.0.6 (CRLF field override) hono 4.12.4 -> 4.13.4 (CORS reflects any origin) ip-address 10.1.0 -> 10.5.0 (SSRF via parsing inconsistency) path-to-regexp 8.3.0 -> 8.4.2 (regex DoS) vite and vitest still carry advisories but are devDependencies; the image runs npm prune --production, so they are not shipped. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_014k5RGUNUDegp7Fhsotwiyi --- package-lock.json | 2535 +++++++++++++++++++++++++++------------------ 1 file changed, 1547 insertions(+), 988 deletions(-) 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" } } } From 43685096bf3dfa58bcb851a6b5fb64e1a02409ad Mon Sep 17 00:00:00 2001 From: Christoph Dyllick-Brenzinger Date: Tue, 25 Aug 2026 23:08:44 +0200 Subject: [PATCH 5/8] feat(logging): record the callbacks a client registered The registration log carried only clientName. When a real client authorized against the staging instance, the log could not answer whether the callback it used matched the one it registered -- which is exactly what /authorize decides on, and the only way to tell whether the RFC 8252 loopback port carve-out was exercised or merely present. Both fields come from an unauthenticated caller, so the list is capped at five entries of 120 characters. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_014k5RGUNUDegp7Fhsotwiyi --- src/auth/oauthProvider.ts | 12 +++++++++++- tests/oauthObservability.spec.ts | 25 +++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/auth/oauthProvider.ts b/src/auth/oauthProvider.ts index 4a6292f..3df2a49 100644 --- a/src/auth/oauthProvider.ts +++ b/src/auth/oauthProvider.ts @@ -263,7 +263,17 @@ export class OAuthProvider { const registration: ClientRegistration = { n: clientName, r: redirectUris } const clientId = this.cipher.seal('client', registration, CLIENT_TTL_MS) - logger.info({ clientName }, 'OAuth dynamic client registration') + // 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, diff --git a/tests/oauthObservability.spec.ts b/tests/oauthObservability.spec.ts index 5e8f154..d9489c2 100644 --- a/tests/oauthObservability.spec.ts +++ b/tests/oauthObservability.spec.ts @@ -150,6 +150,31 @@ describe('an issued code is attributable', () => { }) }) +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() From fe90cd9d5df98b015de22cd9d85b748f61d3b113 Mon Sep 17 00:00:00 2001 From: Christoph Dyllick-Brenzinger Date: Tue, 25 Aug 2026 23:16:37 +0200 Subject: [PATCH 6/8] feat(logging): make a successful token refresh visible Only failed refreshes were logged, so the healthy case was invisible. That matters now: 1.5.2 returned no expires_in and clients treated the token as permanent, while an access token now lives one hour. A client that does not renew cleanly would prompt its user for the API token every hour -- and without this line, that would only surface as user complaints. The refresh token itself is never logged; the client is identified by a truncated digest. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_014k5RGUNUDegp7Fhsotwiyi --- src/auth/oauthProvider.ts | 9 +++++ tests/oauthObservability.spec.ts | 57 ++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/src/auth/oauthProvider.ts b/src/auth/oauthProvider.ts index 3df2a49..7894b2b 100644 --- a/src/auth/oauthProvider.ts +++ b/src/auth/oauthProvider.ts @@ -543,6 +543,10 @@ export class OAuthProvider { return } + // 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) } @@ -790,6 +794,11 @@ function safeOrigin(raw: string): string { } } +/** 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) } diff --git a/tests/oauthObservability.spec.ts b/tests/oauthObservability.spec.ts index d9489c2..23313fd 100644 --- a/tests/oauthObservability.spec.ts +++ b/tests/oauthObservability.spec.ts @@ -1,3 +1,4 @@ +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' @@ -56,6 +57,35 @@ async function issueCode(clientId: string): Promise { 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', @@ -150,6 +180,33 @@ describe('an issued code is attributable', () => { }) }) +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') From 68b59f5444dc53265e9fb0ccf95bf649460dfc9a Mon Sep 17 00:00:00 2001 From: Christoph Dyllick-Brenzinger Date: Tue, 25 Aug 2026 23:23:27 +0200 Subject: [PATCH 7/8] feat(auth): make the access token lifetime configurable SEATABLE_ACCESS_TOKEN_TTL, in seconds, default 3600, range 30..2592000. One hour is a compromise between "a stolen token expires soon" and "the user is not asked for their API token again", and the right value depends on how real clients behave. 1.5.2 returned no expires_in at all, so whether ChatGPT and Claude renew silently is still unknown; a short value makes that observable in minutes instead of an hour. The refresh token is never issued shorter-lived than the access token, so a long access lifetime cannot silently invert the relationship. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_014k5RGUNUDegp7Fhsotwiyi --- .env.example | 6 ++ README.md | 1 + src/auth/oauthProvider.ts | 22 +++-- src/config/env.ts | 8 ++ src/http/httpServer.ts | 1 + tests/accessTokenTtl.spec.ts | 151 +++++++++++++++++++++++++++++++++++ 6 files changed, 184 insertions(+), 5 deletions(-) create mode 100644 tests/accessTokenTtl.spec.ts diff --git a/.env.example b/.env.example index e030283..a9af54b 100644 --- a/.env.example +++ b/.env.example @@ -25,3 +25,9 @@ SEATABLE_MODE=selfhosted # 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/README.md b/README.md index d1fcbcd..f4f030d 100644 --- a/README.md +++ b/README.md @@ -229,6 +229,7 @@ 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/src/auth/oauthProvider.ts b/src/auth/oauthProvider.ts index 7894b2b..dc4814c 100644 --- a/src/auth/oauthProvider.ts +++ b/src/auth/oauthProvider.ts @@ -32,8 +32,8 @@ interface ClientRegistration extends Record { } const CODE_TTL_MS = 5 * 60 * 1000 // 5 minutes -const ACCESS_TOKEN_TTL_MS = 60 * 60 * 1000 // 1 hour -const REFRESH_TOKEN_TTL_MS = 14 * 24 * 60 * 60 * 1000 // 14 days +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 @@ -49,6 +49,12 @@ export interface OAuthProviderOptions { * 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 /** @@ -113,12 +119,18 @@ export class OAuthProvider { 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) @@ -551,14 +563,14 @@ export class OAuthProvider { } private issueTokens(res: ServerResponse, apiToken: string, clientId: string): void { - const accessToken = this.cipher.seal('access', { t: apiToken }, ACCESS_TOKEN_TTL_MS) - const refreshToken = this.cipher.seal('refresh', { t: apiToken, c: clientId }, REFRESH_TOKEN_TTL_MS) + const accessToken = this.cipher.seal('access', { t: apiToken }, this.accessTokenTtlMs) + const refreshToken = this.cipher.seal('refresh', { t: apiToken, c: clientId }, this.refreshTokenTtlMs) res.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store' }) res.end(JSON.stringify({ access_token: accessToken, token_type: 'Bearer', - expires_in: Math.floor(ACCESS_TOKEN_TTL_MS / 1000), + expires_in: Math.floor(this.accessTokenTtlMs / 1000), refresh_token: refreshToken, })) } diff --git a/src/config/env.ts b/src/config/env.ts index 9a59cb8..a59829d 100644 --- a/src/config/env.ts +++ b/src/config/env.ts @@ -23,6 +23,14 @@ const EnvSchema = z // 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(), diff --git a/src/http/httpServer.ts b/src/http/httpServer.ts index f1f8388..0f6d356 100644 --- a/src/http/httpServer.ts +++ b/src/http/httpServer.ts @@ -112,6 +112,7 @@ export async function startHttpServer(options: StartHttpServerOptions = {}) { 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), 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/) + }) +}) From 57da6699e7204467cf451110713f450fb7badbcd Mon Sep 17 00:00:00 2001 From: Christoph Dyllick-Brenzinger Date: Tue, 25 Aug 2026 23:44:34 +0200 Subject: [PATCH 8/8] fix(logging): record the full callback, not only its origin Registration logged full redirect_uris while code issuance logged only the origin, so the two lines could not be compared -- and the origin is the wrong half: for an incident the question is where the code was actually delivered, path included. All callback fields now carry the full destination, capped at 200 characters because the value comes from an unauthenticated caller. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_014k5RGUNUDegp7Fhsotwiyi --- src/auth/oauthProvider.ts | 22 +++++++++++----------- tests/oauthObservability.spec.ts | 21 +++++++++++++++++++-- 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/src/auth/oauthProvider.ts b/src/auth/oauthProvider.ts index dc4814c..154d87f 100644 --- a/src/auth/oauthProvider.ts +++ b/src/auth/oauthProvider.ts @@ -334,14 +334,14 @@ export class OAuthProvider { } if (redirectUri && !this.isPermittedRedirectUri(redirectUri)) { - logger.warn({ clientName: client.n, callback: redirectUri.slice(0, 200), ip: this.clientIp(req) }, 'OAuth authorize rejected: callback not permitted') + 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 } if (!redirectUri || !client.r.some((registered) => redirectUriMatches(registered, redirectUri))) { logger.warn( - { clientName: client.n, callback: safeOrigin(redirectUri), ip: this.clientIp(req) }, + { 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.') @@ -370,7 +370,7 @@ export class OAuthProvider { if (!acknowledged) { if (req.method === 'POST') { - logger.warn({ clientName: client.n, callback: callbackOrigin, ip: this.clientIp(req) }, 'OAuth authorize: unacknowledged destination') + 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' }) @@ -431,7 +431,7 @@ export class OAuthProvider { }) logger.info( - { flow: flowId(code), clientName: client.n, callback: callbackOrigin, ip: this.clientIp(req) }, + { flow: flowId(code), clientName: client.n, callback: safeCallback(redirectUri), ip: this.clientIp(req) }, 'OAuth authorization code issued', ) @@ -797,13 +797,13 @@ export class OAuthProvider { * /authorize and /token log lines can be paired without ever writing the code * itself (or a prefix of it) to disk. */ -/** Origin of a callback for logging; falls back to a truncated raw value. */ -function safeOrigin(raw: string): string { - try { - return new URL(raw).origin - } catch { - return raw.slice(0, 200) - } +/** + * 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. */ diff --git a/tests/oauthObservability.spec.ts b/tests/oauthObservability.spec.ts index 23313fd..a7bd2b9 100644 --- a/tests/oauthObservability.spec.ts +++ b/tests/oauthObservability.spec.ts @@ -130,7 +130,9 @@ describe('an issued code is attributable', () => { const issued = find('authorization code issued') expect(issued).toHaveLength(1) expect(issued[0].fields.clientName).toBe('Acme Client') - expect(issued[0].fields.callback).toBe('http://127.0.0.1:6611') + // 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') }) @@ -180,6 +182,21 @@ describe('an issued code is attributable', () => { }) }) +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() @@ -260,7 +277,7 @@ describe('no rejection path is silent', () => { const rejected = find('redirect_uri not registered') expect(rejected).toHaveLength(1) - expect(rejected[0].fields.callback).toBe('https://attacker.example') + expect(rejected[0].fields.callback).toBe('https://attacker.example/cb') expect(rejected[0].fields.ip).toBe('198.51.100.66') }) })