diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 44c53726..744e06b4 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "codex-multi-auth", - "version": "2.1.13-beta.2", + "version": "2.1.13-beta.3", "description": "Install and operate codex-multi-auth for the official @openai/codex CLI with multi-account OAuth rotation, switching, health checks, and recovery tools.", "interface": { "composerIcon": "./assets/codex-multi-auth-icon.svg" diff --git a/AGENTS.md b/AGENTS.md index 314cb4a2..6dce0fa6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,7 +3,7 @@ Generated: 2026-04-25 Commit: a87e005 Branch: main -Package version: 2.1.13-beta.2 +Package version: 2.1.13-beta.3 ## OVERVIEW diff --git a/README.md b/README.md index 55e7cc5a..56c7d487 100644 --- a/README.md +++ b/README.md @@ -383,7 +383,7 @@ codex-multi-auth doctor --json ## Release Notes -- Current prerelease: [docs/releases/v2.1.13-beta.2.md](docs/releases/v2.1.13-beta.2.md) — install via `npm i -g codex-multi-auth@beta` +- Current prerelease: [docs/releases/v2.1.13-beta.3.md](docs/releases/v2.1.13-beta.3.md) — install via `npm i -g codex-multi-auth@beta` - Current stable: [docs/releases/v2.1.12.md](docs/releases/v2.1.12.md) - Previous stable: [docs/releases/v2.1.11.md](docs/releases/v2.1.11.md) - Earlier stable: [docs/releases/v2.1.10.md](docs/releases/v2.1.10.md) diff --git a/docs/README.md b/docs/README.md index 51f9253b..c82a6817 100644 --- a/docs/README.md +++ b/docs/README.md @@ -32,7 +32,7 @@ Public documentation for the `codex-multi-auth` Codex CLI multi-account OAuth ma | Document | Focus | | --- | --- | -| [releases/v2.1.13-beta.2.md](releases/v2.1.13-beta.2.md) | Current prerelease notes (install via `npm i -g codex-multi-auth@beta`) | +| [releases/v2.1.13-beta.3.md](releases/v2.1.13-beta.3.md) | Current prerelease notes (install via `npm i -g codex-multi-auth@beta`) | | [releases/v2.1.12.md](releases/v2.1.12.md) | Current stable release notes | | [releases/v2.1.11.md](releases/v2.1.11.md) | Prior stable release notes | | [releases/v2.1.10.md](releases/v2.1.10.md) | Earlier stable release notes | diff --git a/docs/releases/v2.1.13-beta.3.md b/docs/releases/v2.1.13-beta.3.md new file mode 100644 index 00000000..49d4cb53 --- /dev/null +++ b/docs/releases/v2.1.13-beta.3.md @@ -0,0 +1,107 @@ +# v2.1.13-beta.3 + +Beta prerelease that lands the Phase 1 correctness/security audit (#499), the new +`mcodex` launcher + cached statusline (#500), and a round of pre-release +hardening surfaced by a whole-tree stress test (#503). It carries forward the +cascade OAuth token-invalidation fix from v2.1.13-beta.2, the multi-workspace +support from v2.1.13-beta.1, and the pinned-account 503 diagnostic from +v2.1.13-beta.0. + +This is a **prerelease**. Stable `v2.1.13` will land once the issue #486 root +cause is identified and patched. + +## Install + +```bash +npm i -g codex-multi-auth@beta +``` + +## mcodex launcher (#500) + +A lightweight launcher and cached multi-auth status display for Codex sessions. + +- `mcodex` — launch Codex with a cached status line printed before startup + (model, reasoning effort, cwd, active account, quota usage, plan, cache age). +- `mcodex --tmux` — launch inside a tmux session with mouse scrollback. +- `mcodex --tmux --live-accounts` — add a live `codex-multi-auth list` monitor pane. +- `mcodex --monitor` — monitor-only mode. + +### Status line + +- Reads local `quota-cache.json`, `runtime-observability.json`, and account + storage; never calls OpenAI on launch. +- Refreshes quota data in the background only when the cache is stale (default + 10 min, `CODEX_MULTI_AUTH_STATUS_QUOTA_REFRESH_INTERVAL_MS`), behind a lock so + concurrent launches don't double-refresh. Stale refresh locks recover after + 10 min. +- Resolves the **per-project** account pool when `perProjectAccounts` is enabled + and Codex CLI sync is off (mirrors the runtime's own account scoping), falling + back to the global pool otherwise. Quota/observability stay global. +- Toggle with `CODEX_MULTI_AUTH_STATUSLINE=0`. + +### Hardening + +- `MCODEX_MONITOR_INTERVAL` / `MCODEX_TMUX_HISTORY_LIMIT` are validated as numeric + before being interpolated into `watch` / tmux commands (no shell injection). +- `--monitor` / `--live-accounts` fail fast with a clear message when `watch` + isn't installed instead of spawning a broken pane. +- The status path resolves `~` correctly on Windows (`path.sep`, not a hardcoded + `/`), and reads the account pool without blocking the event loop. + +## Phase 1 audit remediation (#499) + +Security and correctness hardening across the runtime, storage, and prompt layers. + +### Security + +- **Prompt cache integrity.** Cached Codex instructions are SHA-256 verified; a + tampered cache is discarded, and a legacy entry with no recorded digest is + treated as unverified — it is never fast-path served and never drives a + conditional `304` revalidation (which could otherwise launder un-vetted bytes). + A full fetch mints the first digest; old bytes are kept only as an offline + fallback. +- **Path-traversal defense in recovery.** Stored message/part records are + validated before their ids are used to build filesystem paths; a + parseable-but-unsafe id (e.g. `../poison`) or a non-numeric `time.created` is + quarantined instead of escaping into a traversal read or a `NaN` sort. +- **Loopback-only egress.** The runtime rotation proxy and local bridge bind + loopback-only with no opt-out, and never forward inbound client credentials + (`authorization`, `x-api-key`, `cookie`, `proxy-authorization`) upstream + alongside the managed token. IPv6 loopback (`::1` / `[::1]`) is normalized + consistently for both the socket bind and the emitted base URL. +- **Token/email redaction.** Log, debug-bundle, and status sinks mask tokens and + emails; the debug bundle redacts the home prefix (case- and + separator-insensitive on Windows), strips credentials from config values, and + masks the account id. + +### Correctness & resilience + +- **No event-loop blocking.** Removed synchronous `Atomics.wait` sleeps from the + config load path and the logger's directory-creation retry; both now retry + without freezing the event loop. +- **Bounded network reads.** Prompt and release-metadata fetches are bounded by + connect+body timeouts that actually cancel a stalled body. +- **Windows filesystem resilience.** Account-store WAL/temp writes, temp cleanup, + backup copy/rename, quota-cache, flagged-store, and export operations retry the + shared transient-lock taxonomy (EBUSY/EPERM/ENOTEMPTY/EACCES/EAGAIN) so an + antivirus/indexer/concurrent-reader lock doesn't fail a valid operation or + strand a secret-bearing temp file. +- **Atomic, self-healing account store.** Writes go through a checksummed WAL + + temp-and-rename; a torn write self-heals on the next read. + +### CLI + +- `codex-multi-auth status` / `list` gained `--json` for machine-readable output, + with a stable shape whether or not accounts are configured. + +## Pre-release hardening (#503) + +- Strip inbound `cookie` / `proxy-authorization` on both egress paths. +- Bound the proxy's upstream error-body read (previously unbounded on 4xx/5xx). +- Persist `runtime-observability.json` owner-only (`0o600` / dir `0o700`) on POSIX. +- Bump `vitest` to `^4.1.8` (dev-only) to clear GHSA-5xrq-8626-4rwp. + +## Verification + +Full test suite (4,200+ tests) green; `npm run audit:ci` clean; typecheck and +lint pass. diff --git a/lib/local-bridge.ts b/lib/local-bridge.ts index e4f07c20..2366448c 100644 --- a/lib/local-bridge.ts +++ b/lib/local-bridge.ts @@ -98,6 +98,10 @@ function forwardHeaders(headers: Headers, runtimeClientApiKey?: string): Headers // caller's local credential across the bridge boundary and could change which // auth the runtime proxy evaluates — strip it unconditionally. result.delete("x-api-key"); + // Same contract: never carry an inbound Cookie / proxy-auth header upstream + // alongside the managed token. + result.delete("cookie"); + result.delete("proxy-authorization"); // runtime-proxy-03: present the runtime proxy's client token. We replace the // inbound client's Authorization (already validated by the bridge) rather than // forwarding it verbatim, so the bridge can authenticate to an auth-enabled diff --git a/lib/quota-cache.ts b/lib/quota-cache.ts index 82263dc4..514de11d 100644 --- a/lib/quota-cache.ts +++ b/lib/quota-cache.ts @@ -32,7 +32,16 @@ interface QuotaCacheFile { const QUOTA_CACHE_PATH = join(getCodexMultiAuthDir(), "quota-cache.json"); const QUOTA_CACHE_LABEL = basename(QUOTA_CACHE_PATH); -const RETRYABLE_FS_CODES = new Set(["EBUSY", "EPERM"]); +// Align with the shared FILE_RETRY_CODES taxonomy (lib/fs-retry.ts) so a +// transient Windows lock (AV/indexer/concurrent reader) on the quota cache is +// retried consistently with every other fs path, not just EBUSY/EPERM. +const RETRYABLE_FS_CODES = new Set([ + "EBUSY", + "EPERM", + "EAGAIN", + "ENOTEMPTY", + "EACCES", +]); let quotaCacheWriteQueue: Promise = Promise.resolve(); function isRetryableFsError(error: unknown): boolean { diff --git a/lib/runtime-rotation-proxy.ts b/lib/runtime-rotation-proxy.ts index 660867a5..887e90b9 100644 --- a/lib/runtime-rotation-proxy.ts +++ b/lib/runtime-rotation-proxy.ts @@ -268,6 +268,10 @@ function createOutboundHeaders( } headers.delete("host"); headers.delete("x-api-key"); + // Never forward inbound client credentials upstream: a Cookie / proxy-auth + // header would ride along with the managed OAuth Bearer to OpenAI. + headers.delete("cookie"); + headers.delete("proxy-authorization"); headers.set("authorization", `Bearer ${accessToken}`); headers.set(OPENAI_HEADERS.ACCOUNT_ID, accountId); headers.set(OPENAI_HEADERS.BETA, OPENAI_HEADER_VALUES.BETA_RESPONSES); @@ -892,9 +896,62 @@ function parseRetryAfterBodyMs(bodyText: string, now: number): number | null { return null; } -async function readErrorBody(response: Response): Promise { +async function readErrorBody( + response: Response, + timeoutMs: number, + maxBytes = 1024 * 1024, +): Promise { + // The outbound fetch's abort timer is cleared once headers arrive, so a + // stalled error body would otherwise hang this handler forever (the success + // path is per-chunk stall-bounded; the error path was not). Read the body via + // a reader, bound it by an idle timeout AND a size cap, and cancel the stream + // on timeout/overflow so the socket is released. + const body = response.body; + if (!body || typeof body.getReader !== "function") { + // Fallback for impls without a streamable body: race text() against a timer. + try { + return await withTimeout( + response.text(), + timeoutMs, + () => undefined, + "error body stalled", + ); + } catch { + return ""; + } + } + const reader = body.getReader(); + const chunks: Uint8Array[] = []; + let total = 0; + try { + for (;;) { + let idleTimer: ReturnType | undefined; + const idle = new Promise((_resolve, reject) => { + idleTimer = setTimeout( + () => reject(new Error("error body stalled")), + Math.max(1, timeoutMs), + ); + }); + let result: Awaited>; + try { + result = await Promise.race([reader.read(), idle]); + } finally { + if (idleTimer) clearTimeout(idleTimer); + } + if (result.done) break; + if (result.value) { + total += result.value.byteLength; + if (total > maxBytes) break; // cap: enough for diagnostics, no OOM + chunks.push(result.value); + } + } + } catch { + // stalled or errored — fall through with whatever we have + } finally { + await reader.cancel().catch(() => undefined); + } try { - return await response.text(); + return Buffer.concat(chunks).toString("utf8"); } catch { return ""; } @@ -1734,7 +1791,7 @@ export async function startRuntimeRotationProxy( } if (upstream.status === HTTP_STATUS.TOO_MANY_REQUESTS) { - const bodyText = await readErrorBody(upstream); + const bodyText = await readErrorBody(upstream, streamStallTimeoutMs); const retryAfterMs = parseRetryAfterHeaderMs(upstream.headers, now()) ?? parseRetryAfterBodyMs(bodyText, now()) ?? @@ -1759,7 +1816,7 @@ export async function startRuntimeRotationProxy( } if (upstream.status === 402 || upstream.status === HTTP_STATUS.FORBIDDEN) { - const bodyText = await readErrorBody(upstream); + const bodyText = await readErrorBody(upstream, streamStallTimeoutMs); const errorCode = extractErrorCodeFromBody(bodyText); if (isWorkspaceDisabledError(upstream.status, errorCode, bodyText)) { const accountWasEnabled = @@ -1856,7 +1913,7 @@ export async function startRuntimeRotationProxy( } if (upstream.status === HTTP_STATUS.UNAUTHORIZED) { - const bodyText = await readErrorBody(upstream); + const bodyText = await readErrorBody(upstream, streamStallTimeoutMs); accountManager.refundToken(refreshed.account, context.family, context.model); accountManager.recordFailure(refreshed.account, context.family, context.model); if (isTokenInvalidationError(bodyText)) { @@ -1902,7 +1959,7 @@ export async function startRuntimeRotationProxy( } if (upstream.status >= 500) { - await readErrorBody(upstream); + await readErrorBody(upstream, streamStallTimeoutMs); accountManager.refundToken(refreshed.account, context.family, context.model); accountManager.recordFailure(refreshed.account, context.family, context.model); accountManager.markAccountCoolingDown( diff --git a/lib/runtime/runtime-observability.ts b/lib/runtime/runtime-observability.ts index 9ed4d162..ada77a93 100644 --- a/lib/runtime/runtime-observability.ts +++ b/lib/runtime/runtime-observability.ts @@ -195,13 +195,34 @@ function ensureSnapshotState(): RuntimeObservabilitySnapshot { async function writeSnapshot(snapshot: RuntimeObservabilitySnapshot): Promise { const dir = getCodexMultiAuthDir(); const path = getSnapshotPath(); - await fs.mkdir(dir, { recursive: true }); + // The snapshot persists account identifiers (lastAccountId/label/index), so + // keep it owner-only on POSIX like the other sensitive writers (logger, + // local-client-tokens). mode is a no-op on win32 (ACL-based). + await fs.mkdir(dir, { recursive: true, mode: 0o700 }); + // mkdir's mode only applies to a freshly-created dir; an upgrade path with a + // pre-existing multi-auth dir keeps its old (possibly permissive) perms, so + // re-assert 0o700 on POSIX. Only ENOENT is swallowed (the dir was removed by a + // concurrent process — the snapshot write below will recreate/fail as needed); + // any other chmod failure is surfaced rather than silently leaving a + // world-readable dir to hold account ids/labels. + if (process.platform !== "win32") { + try { + await fs.chmod(dir, 0o700); + } catch (error) { + if ((error as NodeJS.ErrnoException | undefined)?.code !== "ENOENT") { + throw error; + } + } + } let lastError: unknown = null; for (let attempt = 0; attempt < 3; attempt += 1) { const tempPath = `${path}.${process.pid}.${Date.now()}.${attempt}.tmp`; let moved = false; try { - await fs.writeFile(tempPath, JSON.stringify(snapshot, null, 2), "utf-8"); + await fs.writeFile(tempPath, JSON.stringify(snapshot, null, 2), { + encoding: "utf-8", + mode: 0o600, + }); await fs.rename(tempPath, path); moved = true; return; diff --git a/lib/storage.ts b/lib/storage.ts index 351f407a..ef185987 100644 --- a/lib/storage.ts +++ b/lib/storage.ts @@ -2,6 +2,7 @@ import { existsSync, promises as fs, readFileSync } from "node:fs"; import { basename, dirname, join } from "node:path"; import { ACCOUNT_LIMITS } from "./constants.js"; import { StorageError } from "./errors.js"; +import { shouldRetryFileOperation, withFileOperationRetry } from "./fs-retry.js"; import { createLogger } from "./logger.js"; import { exportNamedBackupFile, @@ -273,8 +274,7 @@ async function copyFileWithRetry( return; } const canRetry = - (code === "EPERM" || code === "EBUSY") && - attempt + 1 < BACKUP_COPY_MAX_ATTEMPTS; + shouldRetryFileOperation(error) && attempt + 1 < BACKUP_COPY_MAX_ATTEMPTS; if (canRetry) { await new Promise((resolve) => setTimeout(resolve, BACKUP_COPY_BASE_DELAY_MS * 2 ** attempt), @@ -295,10 +295,8 @@ async function renameFileWithRetry( await fs.rename(sourcePath, destinationPath); return; } catch (error) { - const code = (error as NodeJS.ErrnoException).code; const canRetry = - (code === "EPERM" || code === "EBUSY" || code === "EAGAIN") && - attempt + 1 < BACKUP_COPY_MAX_ATTEMPTS; + shouldRetryFileOperation(error) && attempt + 1 < BACKUP_COPY_MAX_ATTEMPTS; if (!canRetry) { throw error; } @@ -1834,13 +1832,20 @@ async function saveAccountsUnlocked(storage: AccountStorageV3): Promise { checksum: computeSha256(content), content, }; - await fs.writeFile(walPath, JSON.stringify(journalEntry), { - encoding: "utf-8", - mode: 0o600, - }); + // Secret-bearing WAL write: retry transient Windows locks via the shared + // taxonomy so a momentary AV/indexer/concurrent-reader lock can't fail an + // otherwise-valid save (EBUSY/EPERM/EAGAIN/ENOTEMPTY/EACCES). + await withFileOperationRetry(() => + fs.writeFile(walPath, JSON.stringify(journalEntry), { + encoding: "utf-8", + mode: 0o600, + }), + ); }, writeTemp: (tempPath: string, content: string) => - fs.writeFile(tempPath, content, { encoding: "utf-8", mode: 0o600 }), + withFileOperationRetry(() => + fs.writeFile(tempPath, content, { encoding: "utf-8", mode: 0o600 }), + ), statTemp: (tempPath: string) => fs.stat(tempPath), renameTempToPath: async (tempPath: string) => { let lastError: NodeJS.ErrnoException | null = null; @@ -1875,8 +1880,11 @@ async function saveAccountsUnlocked(storage: AccountStorageV3): Promise { } }, cleanupTemp: async (tempPath: string) => { + // The temp file holds the full account store (refresh tokens, 0o600). + // Retry cleanup so a transient lock can't strand a secret-bearing *.tmp + // next to the destination; swallow a persistent failure (best effort). try { - await fs.unlink(tempPath); + await withFileOperationRetry(() => fs.unlink(tempPath)); } catch { // Ignore cleanup failure. } diff --git a/package-lock.json b/package-lock.json index 2e15f06c..f9a03303 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,22 +24,23 @@ "bin": { "codex-multi-auth": "scripts/codex-multi-auth.js", "codex-multi-auth-app-launcher": "scripts/codex-app-launcher.js", - "codex-multi-auth-codex": "scripts/codex.js" + "codex-multi-auth-codex": "scripts/codex.js", + "mcodex": "scripts/mcodex" }, "devDependencies": { "@fast-check/vitest": "^0.2.4", "@types/node": "^25.3.0", "@typescript-eslint/eslint-plugin": "^8.56.0", "@typescript-eslint/parser": "^8.56.0", - "@vitest/coverage-v8": "^4.0.18", - "@vitest/ui": "^4.0.18", + "@vitest/coverage-v8": "^4.1.8", + "@vitest/ui": "^4.1.8", "eslint": "^10.0.0", "fast-check": "^4.5.3", "husky": "^9.1.7", "lint-staged": "^16.2.7", "typescript": "^5.9.3", "typescript-language-server": "^5.1.3", - "vitest": "^4.0.18" + "vitest": "^4.1.8" }, "engines": { "node": ">=18.0.0" @@ -49,9 +50,9 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "dev": true, "license": "MIT", "engines": { @@ -59,9 +60,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "dev": true, "license": "MIT", "engines": { @@ -69,13 +70,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", - "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.28.6" + "@babel/types": "^7.29.7" }, "bin": { "parser": "bin/babel-parser.js" @@ -85,14 +86,14 @@ } }, "node_modules/@babel/types": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", - "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -117,9 +118,9 @@ "link": true }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", - "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", "cpu": [ "ppc64" ], @@ -134,9 +135,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", - "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", "cpu": [ "arm" ], @@ -151,9 +152,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", - "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", "cpu": [ "arm64" ], @@ -168,9 +169,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", - "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", "cpu": [ "x64" ], @@ -185,9 +186,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", - "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", "cpu": [ "arm64" ], @@ -202,9 +203,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", - "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", "cpu": [ "x64" ], @@ -219,9 +220,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", - "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", "cpu": [ "arm64" ], @@ -236,9 +237,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", - "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", "cpu": [ "x64" ], @@ -253,9 +254,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", - "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", "cpu": [ "arm" ], @@ -270,9 +271,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", - "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", "cpu": [ "arm64" ], @@ -287,9 +288,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", - "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", "cpu": [ "ia32" ], @@ -304,9 +305,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", - "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", "cpu": [ "loong64" ], @@ -321,9 +322,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", - "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", "cpu": [ "mips64el" ], @@ -338,9 +339,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", - "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", "cpu": [ "ppc64" ], @@ -355,9 +356,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", - "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", "cpu": [ "riscv64" ], @@ -372,9 +373,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", - "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", "cpu": [ "s390x" ], @@ -389,9 +390,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", - "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", "cpu": [ "x64" ], @@ -406,9 +407,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", - "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", "cpu": [ "arm64" ], @@ -423,9 +424,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", - "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", "cpu": [ "x64" ], @@ -440,9 +441,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", - "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", "cpu": [ "arm64" ], @@ -457,9 +458,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", - "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", "cpu": [ "x64" ], @@ -474,9 +475,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", - "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", "cpu": [ "arm64" ], @@ -491,9 +492,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", - "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", "cpu": [ "x64" ], @@ -508,9 +509,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", - "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", "cpu": [ "arm64" ], @@ -525,9 +526,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", - "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", "cpu": [ "ia32" ], @@ -542,9 +543,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", - "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", "cpu": [ "x64" ], @@ -920,6 +921,9 @@ "arm" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -934,6 +938,9 @@ "arm" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -948,6 +955,9 @@ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -962,6 +972,9 @@ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -976,6 +989,9 @@ "loong64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -990,6 +1006,9 @@ "loong64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1004,6 +1023,9 @@ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1018,6 +1040,9 @@ "ppc64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1032,6 +1057,9 @@ "riscv64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1046,6 +1074,9 @@ "riscv64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1060,6 +1091,9 @@ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1074,6 +1108,9 @@ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1088,6 +1125,9 @@ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1500,29 +1540,29 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.18.tgz", - "integrity": "sha512-7i+N2i0+ME+2JFZhfuz7Tg/FqKtilHjGyGvoHYQ6iLV0zahbsJ9sljC9OcFcPDbhYKCet+sG8SsVqlyGvPflZg==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.8.tgz", + "integrity": "sha512-lt3kovsyHwYe00wq4D1ti0Z974fWj4NLp6siqiyEufUpyFwK9Yhi7rBhac9JL5aA0zoMrJqc4vYPZRUnI7l7nw==", "dev": true, "license": "MIT", "dependencies": { "@bcoe/v8-coverage": "^1.0.2", - "@vitest/utils": "4.0.18", - "ast-v8-to-istanbul": "^0.3.10", + "@vitest/utils": "4.1.8", + "ast-v8-to-istanbul": "^1.0.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-reports": "^3.2.0", - "magicast": "^0.5.1", + "magicast": "^0.5.2", "obug": "^2.1.1", - "std-env": "^3.10.0", - "tinyrainbow": "^3.0.3" + "std-env": "^4.0.0-rc.1", + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "4.0.18", - "vitest": "4.0.18" + "@vitest/browser": "4.1.8", + "vitest": "4.1.8" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -1531,18 +1571,18 @@ } }, "node_modules/@vitest/expect": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", - "integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.8.tgz", + "integrity": "sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==", "dev": true, "license": "MIT", "dependencies": { - "@standard-schema/spec": "^1.0.0", + "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.0.18", - "@vitest/utils": "4.0.18", - "chai": "^6.2.1", - "tinyrainbow": "^3.0.3" + "@vitest/spy": "4.1.8", + "@vitest/utils": "4.1.8", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" @@ -1556,13 +1596,13 @@ "license": "MIT" }, "node_modules/@vitest/mocker": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz", - "integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.8.tgz", + "integrity": "sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.0.18", + "@vitest/spy": "4.1.8", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -1571,7 +1611,7 @@ }, "peerDependencies": { "msw": "^2.4.9", - "vite": "^6.0.0 || ^7.0.0-0" + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "msw": { @@ -1583,26 +1623,26 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz", - "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.8.tgz", + "integrity": "sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==", "dev": true, "license": "MIT", "dependencies": { - "tinyrainbow": "^3.0.3" + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/runner": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz", - "integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.8.tgz", + "integrity": "sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.0.18", + "@vitest/utils": "4.1.8", "pathe": "^2.0.3" }, "funding": { @@ -1610,13 +1650,14 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz", - "integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.8.tgz", + "integrity": "sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.0.18", + "@vitest/pretty-format": "4.1.8", + "@vitest/utils": "4.1.8", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -1625,9 +1666,9 @@ } }, "node_modules/@vitest/spy": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz", - "integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.8.tgz", + "integrity": "sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==", "dev": true, "license": "MIT", "funding": { @@ -1635,36 +1676,37 @@ } }, "node_modules/@vitest/ui": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-4.0.18.tgz", - "integrity": "sha512-CGJ25bc8fRi8Lod/3GHSvXRKi7nBo3kxh0ApW4yCjmrWmRmlT53B5E08XRSZRliygG0aVNxLrBEqPYdz/KcCtQ==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-4.1.8.tgz", + "integrity": "sha512-RUS2ZU2TsduVrI+9c12uTNaKrNUTsm6yFt3fueEUB9iKvyC2UP83F+sqIz00HQIah4UOL1TMoDAki8K0NjGvsA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.0.18", + "@vitest/utils": "4.1.8", "fflate": "^0.8.2", - "flatted": "^3.3.3", + "flatted": "^3.4.2", "pathe": "^2.0.3", "sirv": "^3.0.2", "tinyglobby": "^0.2.15", - "tinyrainbow": "^3.0.3" + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "vitest": "4.0.18" + "vitest": "4.1.8" } }, "node_modules/@vitest/utils": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz", - "integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.8.tgz", + "integrity": "sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.0.18", - "tinyrainbow": "^3.0.3" + "@vitest/pretty-format": "4.1.8", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" @@ -1762,15 +1804,15 @@ } }, "node_modules/ast-v8-to-istanbul": { - "version": "0.3.10", - "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.10.tgz", - "integrity": "sha512-p4K7vMz2ZSk3wN8l5o3y2bJAoZXT3VuJI5OLTATY/01CYWumWvwkUw0SqDBnNq6IiTO3qDa1eSQDibAV8g7XOQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.3.tgz", + "integrity": "sha512-jCMQ6ZylLPudp0CDfBmQBZUsrh1/8psbmu9ibeVWKuHWD0YrH9YABwlKu5kVEFoT0GCQQW9Z/SxfuEbbkGQCRg==", "dev": true, "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "^0.3.31", "estree-walker": "^3.0.3", - "js-tokens": "^9.0.1" + "js-tokens": "^10.0.0" } }, "node_modules/aws4fetch": { @@ -1875,6 +1917,13 @@ "node": ">=20" } }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -1936,16 +1985,16 @@ } }, "node_modules/es-module-lexer": { - "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==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", "dev": true, "license": "MIT" }, "node_modules/esbuild": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", - "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -1956,32 +2005,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.2", - "@esbuild/android-arm": "0.27.2", - "@esbuild/android-arm64": "0.27.2", - "@esbuild/android-x64": "0.27.2", - "@esbuild/darwin-arm64": "0.27.2", - "@esbuild/darwin-x64": "0.27.2", - "@esbuild/freebsd-arm64": "0.27.2", - "@esbuild/freebsd-x64": "0.27.2", - "@esbuild/linux-arm": "0.27.2", - "@esbuild/linux-arm64": "0.27.2", - "@esbuild/linux-ia32": "0.27.2", - "@esbuild/linux-loong64": "0.27.2", - "@esbuild/linux-mips64el": "0.27.2", - "@esbuild/linux-ppc64": "0.27.2", - "@esbuild/linux-riscv64": "0.27.2", - "@esbuild/linux-s390x": "0.27.2", - "@esbuild/linux-x64": "0.27.2", - "@esbuild/netbsd-arm64": "0.27.2", - "@esbuild/netbsd-x64": "0.27.2", - "@esbuild/openbsd-arm64": "0.27.2", - "@esbuild/openbsd-x64": "0.27.2", - "@esbuild/openharmony-arm64": "0.27.2", - "@esbuild/sunos-x64": "0.27.2", - "@esbuild/win32-arm64": "0.27.2", - "@esbuild/win32-ia32": "0.27.2", - "@esbuild/win32-x64": "0.27.2" + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" } }, "node_modules/escape-string-regexp": { @@ -2257,9 +2306,9 @@ "license": "MIT" }, "node_modules/fflate": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", - "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", "dev": true, "license": "MIT" }, @@ -2535,9 +2584,9 @@ } }, "node_modules/js-tokens": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", - "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", "dev": true, "license": "MIT" }, @@ -2676,14 +2725,14 @@ } }, "node_modules/magicast": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.1.tgz", - "integrity": "sha512-xrHS24IxaLrvuo613F719wvOIv9xPHFWQHuvGUBmPnCA/3MQxKI3b+r7n1jAoDHmsbC5bRhTZYR77invLAxVnw==", + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.3.tgz", + "integrity": "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.5", - "@babel/types": "^7.28.5", + "@babel/parser": "^7.29.3", + "@babel/types": "^7.29.0", "source-map-js": "^1.2.1" } }, @@ -2777,9 +2826,9 @@ } }, "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.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", "dev": true, "funding": [ { @@ -2940,9 +2989,9 @@ } }, "node_modules/postcss": { - "version": "8.5.10", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", - "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", "dev": true, "funding": [ { @@ -2960,7 +3009,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -3193,9 +3242,9 @@ "license": "MIT" }, "node_modules/std-env": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", - "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", "dev": true, "license": "MIT" }, @@ -3321,9 +3370,9 @@ } }, "node_modules/tinyrainbow": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", - "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", "dev": true, "license": "MIT", "engines": { @@ -3433,9 +3482,9 @@ } }, "node_modules/vite": { - "version": "7.3.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", - "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.5.tgz", + "integrity": "sha512-KuOaNhcnGFN2zIPGA7wRmzF+lJA1sea7rHq17aiJ++9lzY1WWG6Jpwqwe1KNbRVPIqHmr8GLYx7jbrQcN/7/ww==", "dev": true, "license": "MIT", "dependencies": { @@ -3539,31 +3588,31 @@ } }, "node_modules/vitest": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz", - "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.8.tgz", + "integrity": "sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.0.18", - "@vitest/mocker": "4.0.18", - "@vitest/pretty-format": "4.0.18", - "@vitest/runner": "4.0.18", - "@vitest/snapshot": "4.0.18", - "@vitest/spy": "4.0.18", - "@vitest/utils": "4.0.18", - "es-module-lexer": "^1.7.0", - "expect-type": "^1.2.2", + "@vitest/expect": "4.1.8", + "@vitest/mocker": "4.1.8", + "@vitest/pretty-format": "4.1.8", + "@vitest/runner": "4.1.8", + "@vitest/snapshot": "4.1.8", + "@vitest/spy": "4.1.8", + "@vitest/utils": "4.1.8", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", - "std-env": "^3.10.0", + "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", - "tinyrainbow": "^3.0.3", - "vite": "^6.0.0 || ^7.0.0", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "bin": { @@ -3579,12 +3628,15 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.0.18", - "@vitest/browser-preview": "4.0.18", - "@vitest/browser-webdriverio": "4.0.18", - "@vitest/ui": "4.0.18", + "@vitest/browser-playwright": "4.1.8", + "@vitest/browser-preview": "4.1.8", + "@vitest/browser-webdriverio": "4.1.8", + "@vitest/coverage-istanbul": "4.1.8", + "@vitest/coverage-v8": "4.1.8", + "@vitest/ui": "4.1.8", "happy-dom": "*", - "jsdom": "*" + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "@edge-runtime/vm": { @@ -3605,6 +3657,12 @@ "@vitest/browser-webdriverio": { "optional": true }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, "@vitest/ui": { "optional": true }, @@ -3613,6 +3671,9 @@ }, "jsdom": { "optional": true + }, + "vite": { + "optional": false } } }, diff --git a/package.json b/package.json index 6e0a95d4..ff5132e8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codex-multi-auth", - "version": "2.1.13-beta.2", + "version": "2.1.13-beta.3", "description": "Codex CLI multi-account OAuth manager with account switching, health checks, runtime rotation, diagnostics, and recovery tools for @openai/codex", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -161,15 +161,15 @@ "@types/node": "^25.3.0", "@typescript-eslint/eslint-plugin": "^8.56.0", "@typescript-eslint/parser": "^8.56.0", - "@vitest/coverage-v8": "^4.0.18", - "@vitest/ui": "^4.0.18", + "@vitest/coverage-v8": "^4.1.8", + "@vitest/ui": "^4.1.8", "eslint": "^10.0.0", "fast-check": "^4.5.3", "husky": "^9.1.7", "lint-staged": "^16.2.7", "typescript": "^5.9.3", "typescript-language-server": "^5.1.3", - "vitest": "^4.0.18" + "vitest": "^4.1.8" }, "dependencies": { "@codex-ai/plugin": "file:vendor/codex-ai-plugin", diff --git a/test/local-bridge.test.ts b/test/local-bridge.test.ts index 9fecf63a..e7d069a7 100644 --- a/test/local-bridge.test.ts +++ b/test/local-bridge.test.ts @@ -260,15 +260,19 @@ describe("local bridge", () => { headers: { authorization: "Bearer inbound-client-token", "x-api-key": "inbound-secret-key", + cookie: "session=inbound-cookie-secret", + "proxy-authorization": "Basic inbound-proxy-cred", }, }); const forwarded = calls.find((c) => c.url.endsWith("/v1/models")); const headers = new Headers(forwarded?.init?.headers as HeadersInit); - // The runtime key is injected as Authorization, but the inbound x-api-key is - // still stripped — it must never cross the bridge, runtime key or not. + // The runtime key is injected as Authorization, but every inbound credential + // header is still stripped — none may cross the bridge, runtime key or not. expect(headers.get("authorization")).toBe("Bearer runtime-secret-key"); expect(headers.get("x-api-key")).toBeNull(); + expect(headers.get("cookie")).toBeNull(); + expect(headers.get("proxy-authorization")).toBeNull(); }); it("refuses to start with a runtimeClientApiKey when auth is disabled", async () => { @@ -317,13 +321,17 @@ describe("local bridge", () => { headers: { authorization: "Bearer inbound-client-token", "x-api-key": "inbound-secret-key", + cookie: "session=inbound-cookie-secret", + "proxy-authorization": "Basic inbound-proxy-cred", }, }); const forwarded = calls.find((c) => c.url.endsWith("/v1/models")); const headers = new Headers(forwarded?.init?.headers as HeadersInit); - // runtime-proxy-02: neither inbound credential header crosses the bridge. + // runtime-proxy-02: no inbound credential header crosses the bridge. expect(headers.get("authorization")).toBeNull(); expect(headers.get("x-api-key")).toBeNull(); + expect(headers.get("cookie")).toBeNull(); + expect(headers.get("proxy-authorization")).toBeNull(); }); }); diff --git a/test/mcodex-statusline-scope.test.ts b/test/mcodex-statusline-scope.test.ts index 21f42ec1..c739b442 100644 --- a/test/mcodex-statusline-scope.test.ts +++ b/test/mcodex-statusline-scope.test.ts @@ -1,12 +1,12 @@ import { spawnSync } from "node:child_process"; import { + existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync, } from "node:fs"; import { tmpdir } from "node:os"; -import { createHash } from "node:crypto"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import { afterEach, describe, expect, it } from "vitest"; @@ -82,13 +82,8 @@ function runWrapper(cwd: string, env: NodeJS.ProcessEnv) { } function hasDistBuild(): boolean { - try { - // paths.js is emitted by `npm run build`; skip if the tree isn't built. - // eslint-disable-next-line @typescript-eslint/no-require-imports - return require("node:fs").existsSync(distPathsModule); - } catch { - return false; - } + // paths.js is emitted by `npm run build`; skip if the tree isn't built. + return existsSync(distPathsModule); } describe.runIf(hasDistBuild())("mcodex statusline per-project accounts (PR #500)", () => { diff --git a/test/quota-cache.test.ts b/test/quota-cache.test.ts index 2066d2f2..da8b2c1c 100644 --- a/test/quota-cache.test.ts +++ b/test/quota-cache.test.ts @@ -430,4 +430,117 @@ describe("quota cache", () => { vi.doUnmock("../lib/logger.js"); } }); + + it("retries transient EACCES while loading cache", async () => { + const { loadQuotaCache, getQuotaCachePath } = + await import("../lib/quota-cache.js"); + await fs.writeFile( + getQuotaCachePath(), + JSON.stringify({ + version: 1, + byAccountId: { + acc_1: { + updatedAt: Date.now(), + status: 200, + model: "gpt-5-codex", + primary: { usedPercent: 12 }, + secondary: { usedPercent: 7 }, + }, + }, + byEmail: {}, + }), + "utf8", + ); + + const realRead = fs.readFile.bind(fs); + let attempts = 0; + const readSpy = vi.spyOn(fs, "readFile"); + readSpy.mockImplementation(async (...args) => { + if (String(args[0]) === getQuotaCachePath()) { + attempts += 1; + if (attempts === 1) { + const error = new Error("permission denied") as NodeJS.ErrnoException; + error.code = "EACCES"; + throw error; + } + } + return realRead(...args); + }); + + try { + const loaded = await loadQuotaCache(); + expect(loaded.byAccountId.acc_1?.model).toBe("gpt-5-codex"); + expect(attempts).toBeGreaterThan(1); + } finally { + readSpy.mockRestore(); + } + }); + + it("retries atomic rename on transient ENOTEMPTY errors", async () => { + const { saveQuotaCache, loadQuotaCache } = + await import("../lib/quota-cache.js"); + const realRename = fs.rename.bind(fs); + let attempts = 0; + const renameSpy = vi.spyOn(fs, "rename"); + renameSpy.mockImplementation(async (...args) => { + attempts += 1; + if (attempts === 1) { + const error = new Error("dir not empty") as NodeJS.ErrnoException; + error.code = "ENOTEMPTY"; + throw error; + } + return realRename(...args); + }); + + try { + await saveQuotaCache({ + byAccountId: { + acc_1: { + updatedAt: Date.now(), + status: 200, + model: "gpt-5-codex", + primary: { usedPercent: 40, windowMinutes: 300 }, + secondary: { usedPercent: 20, windowMinutes: 10080 }, + }, + }, + byEmail: {}, + }); + const loaded = await loadQuotaCache(); + expect(loaded.byAccountId.acc_1?.model).toBe("gpt-5-codex"); + expect(attempts).toBeGreaterThan(1); + } finally { + renameSpy.mockRestore(); + } + }); + + it("serializes concurrent saves so the last write wins", async () => { + const { saveQuotaCache, loadQuotaCache, getQuotaCachePath } = + await import("../lib/quota-cache.js"); + + const makePayload = (usedPercent: number) => ({ + byAccountId: { + acc_1: { + updatedAt: Date.now(), + status: 200, + model: "gpt-5-codex", + planType: "plus", + primary: { usedPercent, windowMinutes: 300 }, + secondary: { usedPercent: 5, windowMinutes: 10080 }, + }, + }, + byEmail: {}, + }); + + // Fire two writes back-to-back without awaiting the first; the internal + // quotaCacheWriteQueue serializes them so the second (last) call wins and + // the file is never left torn/interleaved. + const first = saveQuotaCache(makePayload(11)); + const second = saveQuotaCache(makePayload(99)); + await Promise.all([first, second]); + + const raw = await fs.readFile(getQuotaCachePath(), "utf8"); + expect(() => JSON.parse(raw)).not.toThrow(); + const loaded = await loadQuotaCache(); + expect(loaded.byAccountId.acc_1?.primary.usedPercent).toBe(99); + }); }); diff --git a/test/runtime-observability-dir-mode.test.ts b/test/runtime-observability-dir-mode.test.ts new file mode 100644 index 00000000..8345e20e --- /dev/null +++ b/test/runtime-observability-dir-mode.test.ts @@ -0,0 +1,96 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { promises as fs, rmSync, statSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +import { sleep } from "../lib/utils.js"; + +// Windows-safe recursive remove with EBUSY/EPERM backoff, mirroring the +// removeDirectoryWithRetry helper in codex-bin-wrapper.test.ts. A bare fs.rm +// can flake on Windows when an antivirus/indexer briefly locks the just-written +// snapshot file. +function isRetriableFsError(error: unknown): boolean { + if (!error || typeof error !== "object" || !("code" in error)) { + return false; + } + const { code } = error as { code?: unknown }; + return code === "EBUSY" || code === "EPERM"; +} + +async function removeDirectoryWithRetry(dir: string): Promise { + const backoffMs = [20, 60, 120]; + let lastError: unknown; + for (let attempt = 0; attempt <= backoffMs.length; attempt += 1) { + try { + rmSync(dir, { recursive: true, force: true }); + return; + } catch (error) { + lastError = error; + if (!isRetriableFsError(error) || attempt === backoffMs.length) { + break; + } + await sleep(backoffMs[attempt]); + } + } + throw lastError; +} + +describe("runtime observability snapshot dir hardening", () => { + let tempRoot: string; + let snapshotDir: string; + let originalDir: string | undefined; + let originalVitest: string | undefined; + + beforeEach(async () => { + originalDir = process.env.CODEX_MULTI_AUTH_DIR; + originalVitest = process.env.VITEST; + tempRoot = await fs.mkdtemp(join(tmpdir(), "codex-obs-dirmode-")); + // Pin the multi-auth dir to a deterministic temp subdir; getCodexMultiAuthDir + // returns CODEX_MULTI_AUTH_DIR verbatim when set. + snapshotDir = join(tempRoot, "multi-auth"); + process.env.CODEX_MULTI_AUTH_DIR = snapshotDir; + // PERSIST_RUNTIME_SNAPSHOT is computed at module load as VITEST !== "true", + // so blank it before the dynamic import to enable on-disk persistence. + process.env.VITEST = ""; + vi.resetModules(); + }); + + afterEach(async () => { + if (originalDir === undefined) { + delete process.env.CODEX_MULTI_AUTH_DIR; + } else { + process.env.CODEX_MULTI_AUTH_DIR = originalDir; + } + if (originalVitest === undefined) { + delete process.env.VITEST; + } else { + process.env.VITEST = originalVitest; + } + await removeDirectoryWithRetry(tempRoot); + }); + + it.skipIf(process.platform === "win32")( + "re-asserts owner-only (0o700) dir mode on a pre-existing permissive dir", + async () => { + // Upgrade path: the multi-auth dir already exists with permissive perms, + // so mkdir({ mode: 0o700 }) is a no-op and only the explicit chmod can + // tighten it. + await fs.mkdir(snapshotDir, { recursive: true }); + await fs.chmod(snapshotDir, 0o777); + expect(statSync(snapshotDir).mode & 0o777).toBe(0o777); + + const mod = await import("../lib/runtime/runtime-observability.js"); + mod.mutateRuntimeObservabilitySnapshot((snapshot) => { + snapshot.responsesRequests = 1; + }); + + const snapshotPath = join(snapshotDir, "runtime-observability.json"); + await vi.waitFor(() => { + expect(statSync(snapshotPath).isFile()).toBe(true); + }); + + // chmod runs before the snapshot file is written, so the dir is tightened + // by the time the file exists. + expect(statSync(snapshotDir).mode & 0o777).toBe(0o700); + }, + ); +}); diff --git a/test/runtime-observability.test.ts b/test/runtime-observability.test.ts index 515a4bfb..524acd82 100644 --- a/test/runtime-observability.test.ts +++ b/test/runtime-observability.test.ts @@ -6,6 +6,7 @@ const writeFileMock = vi.fn(async () => undefined); const renameMock = vi.fn(async () => undefined); const unlinkMock = vi.fn(async () => undefined); const mkdirMock = vi.fn(async () => undefined); +const chmodMock = vi.fn(async () => undefined); vi.mock("node:fs", () => ({ existsSync: vi.fn(() => true), readFileSync: readFileSyncMock, @@ -15,6 +16,7 @@ vi.mock("node:fs", () => ({ rename: renameMock, unlink: unlinkMock, mkdir: mkdirMock, + chmod: chmodMock, }, })); @@ -37,6 +39,7 @@ describe("runtime observability snapshot versioning", () => { renameMock.mockReset(); unlinkMock.mockReset(); mkdirMock.mockReset(); + chmodMock.mockReset(); if (originalVitestEnv === undefined) { delete process.env.VITEST; } else { @@ -98,6 +101,34 @@ describe("runtime observability snapshot versioning", () => { expect(unlinkMock).toHaveBeenCalled(); }); + it("does not chmod the dir on win32 and still persists the snapshot", async () => { + // The 0o700 re-assert is POSIX-only (win32 perms are ACL-based, mode is a + // no-op). Persistence must still succeed without calling chmod. + process.env.VITEST = ""; + const platformSpy = vi + .spyOn(process, "platform", "get") + .mockReturnValue("win32"); + renameMock.mockResolvedValue(undefined); + try { + const mod = await import("../lib/runtime/runtime-observability.js"); + mod.mutateRuntimeObservabilitySnapshot((snapshot) => { + snapshot.responsesRequests = 7; + }); + await vi.waitFor(() => { + expect(renameMock).toHaveBeenCalled(); + }); + expect(chmodMock).not.toHaveBeenCalled(); + // The snapshot temp file is written owner-only regardless of platform. + expect(writeFileMock).toHaveBeenCalledWith( + expect.any(String), + expect.any(String), + expect.objectContaining({ mode: 0o600 }), + ); + } finally { + platformSpy.mockRestore(); + } + }); + it("contains permanent snapshot write failures without leaving pending writes rejected", async () => { process.env.VITEST = ""; renameMock.mockImplementation(async () => { diff --git a/test/runtime-rotation-proxy.test.ts b/test/runtime-rotation-proxy.test.ts index f092aa18..87f61f0b 100644 --- a/test/runtime-rotation-proxy.test.ts +++ b/test/runtime-rotation-proxy.test.ts @@ -1120,6 +1120,8 @@ describe("runtime rotation proxy", () => { accept: "application/json", connection: "close", "x-custom-trace": "trace-1", + cookie: "session=inbound-cookie-secret", + "proxy-authorization": "Basic inbound-proxy-cred", }, ) ).text(); @@ -1130,6 +1132,9 @@ describe("runtime rotation proxy", () => { expect(calls[0]?.headers.get("connection")).toBeNull(); expect(calls[0]?.headers.get("authorization")).toBe("Bearer access-1"); expect(calls[0]?.headers.get("x-api-key")).toBeNull(); + // Inbound client credentials must never ride upstream with the managed token. + expect(calls[0]?.headers.get("cookie")).toBeNull(); + expect(calls[0]?.headers.get("proxy-authorization")).toBeNull(); }); it("strips expect before forwarding to fetch", async () => {