diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8679f44..dd2abda 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,17 +52,31 @@ jobs: - name: Build run: npm run build + - name: Upload build artifact + if: matrix.node-version == '20.x' + uses: actions/upload-artifact@v4 + with: + name: dist-node20 + path: dist/ + - name: Run tests run: npm test coverage-gate: name: Coverage Gate runs-on: ubuntu-latest + needs: test steps: - name: Checkout code uses: actions/checkout@v4 + - name: Download build artifact + uses: actions/download-artifact@v4 + with: + name: dist-node20 + path: dist/ + - name: Setup Node.js uses: actions/setup-node@v4 with: @@ -73,7 +87,7 @@ jobs: run: npm ci - name: Run tests with coverage threshold gate - run: npm run coverage + run: npm run test:coverage lint: name: Lint diff --git a/.github/workflows/supply-chain.yml b/.github/workflows/supply-chain.yml index b2d94a4..5a461d6 100644 --- a/.github/workflows/supply-chain.yml +++ b/.github/workflows/supply-chain.yml @@ -8,6 +8,9 @@ on: schedule: - cron: "0 4 * * 1" +env: + CODEX_LICENSE_DENYLIST: "GPL-2.0,GPL-3.0,AGPL-3.0" + jobs: dependency-review: name: Dependency Review @@ -26,7 +29,7 @@ jobs: with: fail-on-severity: high fail-on-scopes: runtime - deny-licenses: GPL-2.0, GPL-3.0, AGPL-3.0 + deny-licenses: ${{ env.CODEX_LICENSE_DENYLIST }} sca-and-license: name: SCA and License Gate @@ -69,7 +72,7 @@ jobs: run: npm ci - name: Generate CycloneDX SBOM - run: npx --yes @cyclonedx/cyclonedx-npm --output-file sbom.cdx.json --omit dev + run: npm run sbom - name: Upload SBOM artifact uses: actions/upload-artifact@v4 diff --git a/docs/development/TESTING.md b/docs/development/TESTING.md index 5b29534..6695f74 100644 --- a/docs/development/TESTING.md +++ b/docs/development/TESTING.md @@ -50,6 +50,14 @@ npm run bench:edit-formats:smoke 7. `npm run license:check` 8. run docs command checks for newly documented command paths +### Upgrade Notes (PR #32) + +- Gate ordering was updated so `npm run coverage` runs before `npm run build`. +- Two required supply-chain checks were added to the standard local sequence: + - `npm run audit:ci` + - `npm run license:check` +- If you maintain local CI wrappers or pre-push scripts, update them to use the order above and rerun once to refresh baselines. + * * * ## Auth/Account Change Test Matrix diff --git a/docs/runbooks/operations.md b/docs/runbooks/operations.md index 88eb326..fd681e3 100644 --- a/docs/runbooks/operations.md +++ b/docs/runbooks/operations.md @@ -14,8 +14,11 @@ Routine operations checklist for maintainers. 2. Check recent audit and plugin logs: - `~/.codex/multi-auth/logs/audit.log` - `~/.codex/multi-auth/logs/codex-plugin/` + - `%USERPROFILE%\.codex\multi-auth\logs\audit.log` (Windows) + - `%USERPROFILE%\.codex\multi-auth\logs\codex-plugin\` (Windows) 3. Check dead-letter queue growth: - `~/.codex/multi-auth/background-job-dlq.jsonl` + - `%USERPROFILE%\.codex\multi-auth\background-job-dlq.jsonl` (Windows) --- @@ -24,7 +27,8 @@ Routine operations checklist for maintainers. 1. Rotate encryption key material: - set `CODEX_AUTH_ENCRYPTION_KEY` (new key) - set `CODEX_AUTH_PREVIOUS_ENCRYPTION_KEY` (prior key) - - run `codex auth rotate-secrets --json` + - run `codex auth rotate-secrets --json --idempotency-key ` + - prefer a stable run id source (for example `weekly-YYYYMMDD` or CI `${{ github.run_id }}`) - remove previous key after successful rotation validation 2. Review dependency and license policy reports: - `npm run audit:ci` @@ -73,6 +77,12 @@ Routine operations checklist for maintainers. 2. If writes fail under filesystem lock contention: - check stale `*.lock` files under runtime root - inspect DLQ entries for repeated write failures + - quick log triage: + - macOS/Linux: `tail -n 200 ~/.codex/multi-auth/logs/audit.log` + - Windows PowerShell: `Get-Content $env:USERPROFILE\.codex\multi-auth\logs\audit.log -Tail 200` + - quick DLQ triage: + - macOS/Linux: `tail -n 50 ~/.codex/multi-auth/background-job-dlq.jsonl` + - Windows PowerShell: `Get-Content $env:USERPROFILE\.codex\multi-auth\background-job-dlq.jsonl -Tail 50` 3. If CI secret scan fails: - rotate exposed secret - invalidate related tokens diff --git a/index.ts b/index.ts index 9e4a498..91686ca 100644 --- a/index.ts +++ b/index.ts @@ -351,6 +351,16 @@ export const OpenAIOAuthPlugin: Plugin = async ({ client }: PluginInput) => { return sanitized; }; + const toSafeAuditResource = (rawUrl: unknown): string => { + try { + const parsed = new URL(String(rawUrl)); + return `${parsed.origin}${parsed.pathname}`; + } catch { + const sanitized = String(rawUrl).split(/[?#]/, 1)[0]; + return sanitized && sanitized.length > 0 ? sanitized : "unknown"; + } + }; + type RuntimeMetrics = { startedAt: number; totalRequests: number; @@ -1684,13 +1694,14 @@ while (attempted.size < Math.max(1, accountCount)) { } else if (abortSignal && onUserAbort) { abortSignal.addEventListener("abort", onUserAbort, { once: true }); } + const safeAuditResource = toSafeAuditResource(url); try { runtimeMetrics.totalRequests++; auditLog( AuditAction.REQUEST_START, account.email ?? `account-${account.index + 1}`, - url, + safeAuditResource, AuditOutcome.SUCCESS, { model, @@ -1722,16 +1733,18 @@ while (attempted.size < Math.max(1, accountCount)) { ); } const errorMsg = networkError instanceof Error ? networkError.message : String(networkError); + const networkErrorType = + networkError instanceof Error ? networkError.name : "unknown_error"; logWarn(`Network error for account ${account.index + 1}: ${errorMsg}`); auditLog( AuditAction.REQUEST_FAILURE, account.email ?? `account-${account.index + 1}`, - url, + safeAuditResource, AuditOutcome.FAILURE, { model, accountIndex: account.index + 1, - error: errorMsg, + errorType: networkErrorType, stage: "network", }, ); @@ -2399,7 +2412,7 @@ while (attempted.size < Math.max(1, accountCount)) { auditLog( AuditAction.REQUEST_SUCCESS, successAccountForResponse.email ?? `account-${successAccountForResponse.index + 1}`, - url, + safeAuditResource, AuditOutcome.SUCCESS, { model, @@ -2450,10 +2463,11 @@ while (attempted.size < Math.max(1, accountCount)) { : `All ${count} account(s) failed (server errors or auth issues). Check account health with \`codex-health\`.`; runtimeMetrics.failedRequests++; runtimeMetrics.lastError = message; + const safePluginAuditResource = toSafeAuditResource(url); auditLog( AuditAction.REQUEST_FAILURE, "plugin", - url, + safePluginAuditResource, AuditOutcome.FAILURE, { model, diff --git a/lib/background-jobs.ts b/lib/background-jobs.ts index 34d98e7..46342dc 100644 --- a/lib/background-jobs.ts +++ b/lib/background-jobs.ts @@ -36,11 +36,24 @@ function toErrorMessage(error: unknown): string { return error instanceof Error ? error.message : String(error); } +function sanitizeErrorMessage(message: string): string { + return message + .replace(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi, "***REDACTED***") + .replace(/\b(?:access|refresh|id)?_?token(?:=|:)?\s*\S+/gi, "token=***REDACTED***") + .replace(/\b(Bearer)\s+\S+/gi, "$1 ***REDACTED***"); +} + function getDelayMs(attempt: number, baseDelayMs: number, maxDelayMs: number): number { - return Math.min(maxDelayMs, baseDelayMs * 2 ** Math.max(0, attempt - 1)); + const cappedDelay = Math.min(maxDelayMs, baseDelayMs * 2 ** Math.max(0, attempt - 1)); + const jitterFactor = 1 + (Math.random() * 0.4 - 0.2); + return Math.max(1, Math.round(cappedDelay * jitterFactor)); } function isRetryableByDefault(error: unknown): boolean { + const statusCode = (error as { statusCode?: unknown } | undefined)?.statusCode; + if (typeof statusCode === "number" && statusCode === 429) { + return true; + } const code = (error as NodeJS.ErrnoException | undefined)?.code; if (typeof code !== "string") return false; return code === "EBUSY" || code === "EPERM" || code === "EAGAIN" || code === "ETIMEDOUT"; @@ -75,7 +88,9 @@ export async function runBackgroundJobWithRetry(options: BackgroundJobRetryOp const retryable = options.retryable ?? isRetryableByDefault; let lastError: unknown; + let attemptsMade = 0; for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { + attemptsMade = attempt; try { return await options.task(); } catch (error) { @@ -87,12 +102,12 @@ export async function runBackgroundJobWithRetry(options: BackgroundJobRetryOp } } - const errorMessage = toErrorMessage(lastError); + const errorMessage = sanitizeErrorMessage(toErrorMessage(lastError)); const deadLetter: DeadLetterEntry = { version: 1, timestamp: new Date().toISOString(), job: options.name, - attempts: maxAttempts, + attempts: attemptsMade, error: errorMessage, ...(options.context ? { context: redactForExternalOutput(options.context) } : {}), }; @@ -102,13 +117,13 @@ export async function runBackgroundJobWithRetry(options: BackgroundJobRetryOp } catch (dlqError) { logWarn("Failed to append background job dead-letter", { job: options.name, - error: toErrorMessage(dlqError), + error: sanitizeErrorMessage(toErrorMessage(dlqError)), }); } logWarn("Background job failed after retries", { job: options.name, - attempts: maxAttempts, + attempts: attemptsMade, error: errorMessage, }); throw (lastError instanceof Error ? lastError : new Error(errorMessage)); diff --git a/lib/codex-manager.ts b/lib/codex-manager.ts index 2f7b54e..543aad8 100644 --- a/lib/codex-manager.ts +++ b/lib/codex-manager.ts @@ -137,16 +137,26 @@ function maybeRedactJsonOutput(value: T): T { return shouldRedact ? redactForExternalOutput(value) : value; } +function looksLikeOptionToken(value: string): boolean { + const trimmed = value.trim(); + if (trimmed.length === 0) return false; + return /^-{1,2}[A-Za-z0-9]/.test(trimmed); +} + function hasOptionFlag(args: readonly string[], flag: string): boolean { for (let index = 0; index < args.length; index += 1) { const arg = args[index]; if (arg === flag) { const next = args[index + 1]; - return typeof next === "string" && next.trim().length > 0; + return ( + typeof next === "string" && + next.trim().length > 0 && + !looksLikeOptionToken(next) + ); } if (arg?.startsWith(`${flag}=`)) { const value = arg.slice(flag.length + 1).trim(); - return value.length > 0; + return value.length > 0 && !looksLikeOptionToken(value); } } return false; @@ -249,6 +259,31 @@ function normalizeFailureDetail( return bounded.length > 0 ? bounded : "refresh failed"; } +function redactFreeformSecretText(message: string): string { + return message + .replace(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi, "***REDACTED***") + .replace( + /\b(?:access|refresh|id)?_?token(?:=|:)?\s*([A-Z0-9._~+/=-]+)/gi, + "token=***REDACTED***", + ) + .replace(/\b(Bearer)\s+[A-Z0-9._~+/=-]+\b/gi, "$1 ***REDACTED***"); +} + +function sanitizeCliErrorMessage(error: unknown): string { + const raw = error instanceof Error ? error.message : String(error); + const redacted = redactForExternalOutput(raw); + if (typeof redacted === "string") { + const normalized = collapseWhitespace(redactFreeformSecretText(redacted)); + return normalized.length > 0 ? normalized : "unknown error"; + } + try { + const normalized = collapseWhitespace(redactFreeformSecretText(JSON.stringify(redacted))); + return normalized.length > 0 ? normalized : "unknown error"; + } catch { + return "unknown error"; + } +} + function joinStyledSegments(parts: string[]): string { if (parts.length === 0) return ""; const separator = stylePromptText(" | ", "muted"); @@ -1518,6 +1553,7 @@ function encodePaginationCursor(index: number): string { function decodePaginationCursor(cursor: string): number | null { try { const decoded = Buffer.from(cursor, "base64").toString("utf8"); + if (!/^\d+$/.test(decoded)) return null; const parsed = Number.parseInt(decoded, 10); if (!Number.isFinite(parsed) || parsed < 0) return null; return parsed; @@ -1964,7 +2000,11 @@ function parseListArgs(args: string[]): ParsedArgsResult { if (!value) { return { ok: false, message: "Missing value for --page-size" }; } - const parsed = Number.parseInt(value, 10); + const normalized = value.trim(); + if (!/^\d+$/.test(normalized)) { + return { ok: false, message: "--page-size must be between 1 and 200" }; + } + const parsed = Number.parseInt(normalized, 10); if (!Number.isFinite(parsed) || parsed < 1 || parsed > 200) { return { ok: false, message: "--page-size must be between 1 and 200" }; } @@ -1974,6 +2014,9 @@ function parseListArgs(args: string[]): ParsedArgsResult { } if (arg.startsWith("--page-size=")) { const value = arg.slice("--page-size=".length).trim(); + if (!/^\d+$/.test(value)) { + return { ok: false, message: "--page-size must be between 1 and 200" }; + } const parsed = Number.parseInt(value, 10); if (!Number.isFinite(parsed) || parsed < 1 || parsed > 200) { return { ok: false, message: "--page-size must be between 1 and 200" }; @@ -2177,7 +2220,7 @@ function parseRotateSecretsArgs(args: string[]): ParsedArgsResult { ); return 0; } catch (error) { - const message = error instanceof Error ? error.message : String(error); + const message = sanitizeCliErrorMessage(error); emitAudit(AuditAction.CONFIG_CHANGE, AuditOutcome.FAILURE, "rotate-secrets", { error: message, ...(idempotencyKey ? { idempotencyKey } : {}), diff --git a/lib/data-retention.ts b/lib/data-retention.ts index 3b972ee..0e1981c 100644 --- a/lib/data-retention.ts +++ b/lib/data-retention.ts @@ -1,6 +1,8 @@ import { promises as fs, type Dirent } from "node:fs"; import { join } from "node:path"; import { getCodexCacheDir, getCodexLogDir, getCodexMultiAuthDir } from "./runtime-paths.js"; +import { createLogger } from "./logger.js"; +import { sleep } from "./utils.js"; export interface RetentionPolicy { logDays: number; @@ -17,6 +19,27 @@ const DEFAULT_POLICY: RetentionPolicy = { quotaCacheDays: 14, dlqDays: 30, }; +const logger = createLogger("data-retention"); +const RETRYABLE_RETENTION_CODES = new Set(["EBUSY", "EPERM", "EACCES", "EAGAIN", "ENOTEMPTY"]); + +function isRetryableRetentionError(error: unknown): boolean { + const code = (error as NodeJS.ErrnoException).code; + return typeof code === "string" && RETRYABLE_RETENTION_CODES.has(code); +} + +async function withRetentionIoRetry(operation: () => Promise): Promise { + for (let attempt = 0; attempt < 5; attempt += 1) { + try { + return await operation(); + } catch (error) { + if (!isRetryableRetentionError(error) || attempt >= 4) { + throw error; + } + await sleep(10 * 2 ** attempt); + } + } + throw new Error("Unreachable retention retry state"); +} function parseEnvDays(name: string, fallback: number): number { const raw = process.env[name]; @@ -43,7 +66,9 @@ async function pruneDirectoryByAge(path: string, maxAgeMs: number): Promise[] = []; try { - entries = await fs.readdir(path, { withFileTypes: true, encoding: "utf8" }); + entries = await withRetentionIoRetry(() => + fs.readdir(path, { withFileTypes: true, encoding: "utf8" }), + ); } catch (error) { const code = (error as NodeJS.ErrnoException).code; if (code === "ENOENT") return 0; @@ -56,22 +81,28 @@ async function pruneDirectoryByAge(path: string, maxAgeMs: number): Promise fs.readdir(fullPath)); if (childEntries.length === 0) { - await fs.rmdir(fullPath); + await withRetentionIoRetry(() => fs.rmdir(fullPath)); } continue; } if (!entry.isFile()) continue; - const stats = await fs.stat(fullPath); + const stats = await withRetentionIoRetry(() => fs.stat(fullPath)); if (now - stats.mtimeMs <= maxAgeMs) continue; - await fs.unlink(fullPath); + await withRetentionIoRetry(() => fs.unlink(fullPath)); removed += 1; } catch (error) { const code = (error as NodeJS.ErrnoException).code; if (code === "ENOENT") { continue; } + logger.warn("Skipping retention entry after retry exhaustion", { + path: fullPath, + code: code ?? "unknown", + error: error instanceof Error ? error.message : String(error), + }); + throw error; } } return removed; @@ -79,11 +110,11 @@ async function pruneDirectoryByAge(path: string, maxAgeMs: number): Promise { try { - const stats = await fs.stat(path); + const stats = await withRetentionIoRetry(() => fs.stat(path)); if (Date.now() - stats.mtimeMs <= maxAgeMs) { return false; } - await fs.unlink(path); + await withRetentionIoRetry(() => fs.unlink(path)); return true; } catch (error) { const code = (error as NodeJS.ErrnoException).code; diff --git a/lib/file-lock.ts b/lib/file-lock.ts index dde17af..306317d 100644 --- a/lib/file-lock.ts +++ b/lib/file-lock.ts @@ -1,4 +1,4 @@ -import { openSync, writeFileSync, closeSync, unlinkSync, statSync, promises as fs } from "node:fs"; +import { openSync, writeFileSync, closeSync, unlinkSync, statSync, readFileSync, promises as fs } from "node:fs"; export interface FileLockOptions { maxAttempts?: number; @@ -23,6 +23,28 @@ const DEFAULT_MAX_DELAY_MS = 1_000; const DEFAULT_STALE_AFTER_MS = 5 * 60_000; const RETRYABLE_CODES = new Set(["EEXIST", "EBUSY", "EPERM"]); +function isPidAlive(pid: number): boolean { + if (!Number.isInteger(pid) || pid <= 0) return false; + try { + process.kill(pid, 0); + return true; + } catch (error) { + const code = (error as NodeJS.ErrnoException | undefined)?.code; + return code === "EPERM"; + } +} + +function parseLockPid(rawLock: string): number | undefined { + const firstLine = rawLock.split(/\r?\n/, 1)[0] ?? ""; + if (!firstLine) return undefined; + try { + const parsed = JSON.parse(firstLine) as { pid?: unknown }; + return typeof parsed.pid === "number" && Number.isInteger(parsed.pid) ? parsed.pid : undefined; + } catch { + return undefined; + } +} + function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } @@ -43,6 +65,14 @@ async function removeIfStale(path: string, staleAfterMs: number): Promise { + try { + await fs.unlink(path); + } catch (error) { + const code = (error as NodeJS.ErrnoException).code; + if (code !== "ENOENT") { + if (originalError !== undefined) { + throwOriginalOrCleanupError(originalError, error); + } + throw error; + } + } +} + export async function acquireFileLock( path: string, options: FileLockOptions = {}, @@ -66,24 +123,41 @@ export async function acquireFileLock( for (let attempt = 0; attempt < maxAttempts; attempt += 1) { try { const handle = await fs.open(path, "wx", 0o600); - await handle.writeFile( - `${JSON.stringify({ pid: process.pid, acquiredAt: Date.now() })}\n`, - "utf8", - ); - await handle.close(); + let writeError: unknown; + let closeError: unknown; + try { + await handle.writeFile( + `${JSON.stringify({ pid: process.pid, acquiredAt: Date.now() })}\n`, + "utf8", + ); + } catch (error) { + writeError = error; + } + try { + await handle.close(); + } catch (error) { + closeError = error; + } + if (writeError !== undefined || closeError !== undefined) { + const originalFailure = writeError ?? closeError; + await cleanupIncompleteLockFile(path, originalFailure); + throwOriginalOrCleanupError(originalFailure, originalFailure); + } let released = false; return { path, release: async () => { if (released) return; - released = true; try { await fs.unlink(path); + released = true; } catch (error) { const code = (error as NodeJS.ErrnoException).code; - if (code !== "ENOENT") { - throw error; + if (code === "ENOENT") { + released = true; + return; } + throw error; } }, }; @@ -112,6 +186,14 @@ function removeIfStaleSync(path: string, staleAfterMs: number): boolean { if (Date.now() - stats.mtimeMs <= staleAfterMs) { return false; } + try { + const pid = parseLockPid(readFileSync(path, "utf8")); + if (pid !== undefined && isPidAlive(pid)) { + return false; + } + } catch { + // If metadata can't be read, fall back to age-only cleanup. + } unlinkSync(path); return true; } catch (error) { @@ -123,6 +205,20 @@ function removeIfStaleSync(path: string, staleAfterMs: number): boolean { } } +function cleanupIncompleteLockFileSync(path: string, originalError?: unknown): void { + try { + unlinkSync(path); + } catch (error) { + const code = (error as NodeJS.ErrnoException).code; + if (code !== "ENOENT") { + if (originalError !== undefined) { + throwOriginalOrCleanupError(originalError, error); + } + throw error; + } + } +} + export function acquireFileLockSync( path: string, options: FileLockOptions = {}, @@ -135,25 +231,42 @@ export function acquireFileLockSync( for (let attempt = 0; attempt < maxAttempts; attempt += 1) { try { const fd = openSync(path, "wx", 0o600); - writeFileSync( - fd, - `${JSON.stringify({ pid: process.pid, acquiredAt: Date.now() })}\n`, - "utf8", - ); - closeSync(fd); + let writeError: unknown; + let closeError: unknown; + try { + writeFileSync( + fd, + `${JSON.stringify({ pid: process.pid, acquiredAt: Date.now() })}\n`, + "utf8", + ); + } catch (error) { + writeError = error; + } + try { + closeSync(fd); + } catch (error) { + closeError = error; + } + if (writeError !== undefined || closeError !== undefined) { + const originalFailure = writeError ?? closeError; + cleanupIncompleteLockFileSync(path, originalFailure); + throwOriginalOrCleanupError(originalFailure, originalFailure); + } let released = false; return { path, release: () => { if (released) return; - released = true; try { unlinkSync(path); + released = true; } catch (error) { const code = (error as NodeJS.ErrnoException).code; - if (code !== "ENOENT") { - throw error; + if (code === "ENOENT") { + released = true; + return; } + throw error; } }, }; diff --git a/lib/storage.ts b/lib/storage.ts index 2b92654..548d4e3 100644 --- a/lib/storage.ts +++ b/lib/storage.ts @@ -48,6 +48,7 @@ const ACCOUNT_STORAGE_LOCK_OPTIONS = { maxDelayMs: 800, staleAfterMs: 120_000, } as const; +const STORAGE_DECRYPT_ERROR_CODE = "EDECRYPT"; let storageBackupEnabled = true; let lastAccountsSaveTimestamp = 0; @@ -109,6 +110,7 @@ export function formatStorageErrorHint(error: unknown, path: string): string { } let storageMutex: Promise = Promise.resolve(); +let accountFileMutex: Promise = Promise.resolve(); function withStorageLock(fn: () => Promise): Promise { const previousMutex = storageMutex; @@ -119,6 +121,23 @@ function withStorageLock(fn: () => Promise): Promise { return previousMutex.then(fn).finally(() => releaseLock()); } +function withAccountFileMutex(fn: () => Promise): Promise { + const previousMutex = accountFileMutex; + let releaseLock: () => void; + accountFileMutex = new Promise((resolve) => { + releaseLock = resolve; + }); + return previousMutex.then(fn).finally(() => releaseLock()); +} + +async function withStorageSerializedFileLock(path: string, fn: () => Promise): Promise { + // Serialize file-lock acquisition to keep save ordering deterministic. + // Acquisition order: file-queue mutex -> file lock -> storage mutex. + return withAccountFileMutex(() => + withAccountFileLock(path, () => withStorageLock(fn)), + ); +} + type AnyAccountStorage = AccountStorageV1 | AccountStorageV3; type AccountLike = { @@ -174,6 +193,16 @@ function decryptAccountSensitiveFields(account: AccountMetadataV3): AccountMetad }; } +function toStorageDecryptError(cause: unknown): NodeJS.ErrnoException { + const message = "Failed to decrypt account storage"; + const error = new Error( + message, + cause instanceof Error ? { cause } : undefined, + ) as NodeJS.ErrnoException; + error.code = STORAGE_DECRYPT_ERROR_CODE; + return error; +} + function encryptAccountSensitiveFields(account: AccountMetadataV3): AccountMetadataV3 { return { ...account, @@ -318,16 +347,38 @@ function getAccountsLockPath(path: string): string { return `${path}.lock`; } +async function releaseStorageLockFallback(lockPath: string): Promise { + try { + await fs.rm(lockPath, { force: true }); + } catch (error) { + log.debug("Best-effort lock cleanup fallback failed", { + path: lockPath, + error: String(error), + }); + } +} + async function withAccountFileLock(path: string, fn: () => Promise): Promise { + const lockPath = getAccountsLockPath(path); await fs.mkdir(dirname(path), { recursive: true }); - const lock = await acquireFileLock(getAccountsLockPath(path), ACCOUNT_STORAGE_LOCK_OPTIONS); + const lock = await acquireFileLock(lockPath, ACCOUNT_STORAGE_LOCK_OPTIONS); try { return await fn(); } finally { - await lock.release(); + try { + await lock.release(); + } catch (error) { + const code = (error as NodeJS.ErrnoException).code; + if (code !== "ENOENT") { + log.warn("Failed to release account storage lock", { + path: lockPath, + error: String(error), + }); + await releaseStorageLockFallback(lockPath); + } + } } } - async function copyFileWithRetry( sourcePath: string, destinationPath: string, @@ -815,6 +866,14 @@ function clampIndex(index: number, length: number): number { return Math.max(0, Math.min(index, length - 1)); } +function isStoredAccountCandidate(account: unknown): account is AccountMetadataV3 { + return ( + isRecord(account) && + typeof account.refreshToken === "string" && + account.refreshToken.trim().length > 0 + ); +} + function toAccountKey(account: Pick): string { return account.accountId || account.refreshToken; } @@ -874,12 +933,17 @@ export function normalizeAccountStorage(data: unknown): AccountStorageV3 | null ? migrateV1ToV3(data as unknown as AccountStorageV1) : (data as unknown as AccountStorageV3); - const validAccounts = rawAccounts - .filter( - (account): account is AccountMetadataV3 => - isRecord(account) && typeof account.refreshToken === "string" && !!account.refreshToken.trim(), - ) - .map((account) => decryptAccountSensitiveFields(account)); + const validAccounts: AccountMetadataV3[] = []; + for (const account of rawAccounts) { + if (!isStoredAccountCandidate(account)) { + continue; + } + try { + validAccounts.push(decryptAccountSensitiveFields(account)); + } catch (error) { + throw toStorageDecryptError(error); + } + } const deduplicatedAccounts = deduplicateAccountsByEmail( deduplicateAccountsByKey(validAccounts), @@ -1059,6 +1123,9 @@ async function loadAccountsInternal( return normalized; } catch (error) { const code = (error as NodeJS.ErrnoException).code; + if (code === STORAGE_DECRYPT_ERROR_CODE) { + throw error; + } if (code === "ENOENT" && migratedLegacyStorage) { return migratedLegacyStorage; } @@ -1242,14 +1309,13 @@ export async function withAccountStorageTransaction( ) => Promise, ): Promise { const path = getStoragePath(); - return withAccountFileLock(path, () => - withStorageLock(async () => { - const current = await loadAccountsInternal(saveAccountsUnlocked); - return handler(current, saveAccountsUnlocked); - }), - ); + return withStorageSerializedFileLock(path, async () => { + const current = await loadAccountsInternal(saveAccountsUnlocked); + return handler(current, saveAccountsUnlocked); + }); } + /** * Persists account storage to disk using atomic write (temp file + rename). * Creates the Codex multi-auth storage directory if it doesn't exist. @@ -1259,23 +1325,21 @@ export async function withAccountStorageTransaction( */ export async function saveAccounts(storage: AccountStorageV3): Promise { const path = getStoragePath(); - return withAccountFileLock(path, () => - withStorageLock(async () => { - await saveAccountsUnlocked(storage); - }), - ); + return withStorageSerializedFileLock(path, async () => { + await saveAccountsUnlocked(storage); + }); } + /** * Deletes the account storage file from disk. * Silently ignores if file doesn't exist. */ export async function clearAccounts(): Promise { const path = getStoragePath(); - return withAccountFileLock(path, () => - withStorageLock(async () => { - const walPath = getAccountsWalPath(path); - const backupPaths = getAccountsBackupRecoveryCandidates(path); + return withStorageSerializedFileLock(path, async () => { + const walPath = getAccountsWalPath(path); + const backupPaths = getAccountsBackupRecoveryCandidates(path); const clearPath = async (targetPath: string): Promise => { try { await fs.unlink(targetPath); @@ -1295,10 +1359,10 @@ export async function clearAccounts(): Promise { } catch { // Individual path cleanup is already best-effort with per-artifact logging. } - }), - ); + }); } + function normalizeFlaggedStorage(data: unknown): FlaggedAccountStorageV1 { if (!isRecord(data) || data.version !== 1 || !Array.isArray(data.accounts)) { return { version: 1, accounts: [] }; @@ -1579,12 +1643,13 @@ export async function rotateStoredSecretEncryption(): Promise<{ throw new Error("CODEX_AUTH_ENCRYPTION_KEY is required to rotate stored secrets"); } - let accountCount = 0; - const accounts = await loadAccounts(); - if (accounts) { - accountCount = accounts.accounts.length; - await saveAccounts(accounts); - } + const accountCount = await withAccountStorageTransaction(async (current, persist) => { + if (!current) { + return 0; + } + await persist(current); + return current.accounts.length; + }); const flagged = await loadFlaggedAccounts(); const flaggedCount = flagged.accounts.length; @@ -1597,3 +1662,5 @@ export async function rotateStoredSecretEncryption(): Promise<{ flaggedAccounts: flaggedCount, }; } + + diff --git a/lib/unified-settings.ts b/lib/unified-settings.ts index 9341fe8..f9c6f82 100644 --- a/lib/unified-settings.ts +++ b/lib/unified-settings.ts @@ -11,6 +11,7 @@ import { join } from "node:path"; import { getCodexMultiAuthDir } from "./runtime-paths.js"; import { sleep } from "./utils.js"; import { acquireFileLock, acquireFileLockSync } from "./file-lock.js"; +import { logWarn } from "./logger.js"; type JsonRecord = Record; @@ -19,6 +20,12 @@ export const UNIFIED_SETTINGS_VERSION = 1 as const; const UNIFIED_SETTINGS_PATH = join(getCodexMultiAuthDir(), "settings.json"); const UNIFIED_SETTINGS_LOCK_PATH = `${UNIFIED_SETTINGS_PATH}.lock`; const RETRYABLE_FS_CODES = new Set(["EBUSY", "EPERM"]); +const SETTINGS_LOCK_OPTIONS = { + maxAttempts: 80, + baseDelayMs: 15, + maxDelayMs: 800, + staleAfterMs: 120_000, +} as const; let settingsWriteQueue: Promise = Promise.resolve(); function isRetryableFsError(error: unknown): boolean { @@ -26,6 +33,40 @@ function isRetryableFsError(error: unknown): boolean { return typeof code === "string" && RETRYABLE_FS_CODES.has(code); } +function releaseUnifiedSettingsLockSync(release: () => void): void { + try { + release(); + } catch (error) { + const code = (error as NodeJS.ErrnoException | undefined)?.code; + if (code === "ENOENT") return; + if (code === "EBUSY" || code === "EPERM") { + logWarn("Unified settings lock release failed after write", { + lockPath: UNIFIED_SETTINGS_LOCK_PATH, + code, + }); + return; + } + throw error; + } +} + +async function releaseUnifiedSettingsLockAsync(release: () => Promise): Promise { + try { + await release(); + } catch (error) { + const code = (error as NodeJS.ErrnoException | undefined)?.code; + if (code === "ENOENT") return; + if (code === "EBUSY" || code === "EPERM") { + logWarn("Unified settings lock release failed after write", { + lockPath: UNIFIED_SETTINGS_LOCK_PATH, + code, + }); + return; + } + throw error; + } +} + /** * Determines whether a value is a non-null object suitable for use as a JsonRecord. * @@ -127,39 +168,29 @@ function writeSettingsRecordSync(record: JsonRecord): void { const payload = normalizeForWrite(record); const data = `${JSON.stringify(payload, null, 2)}\n`; const tempPath = `${UNIFIED_SETTINGS_PATH}.${process.pid}.${Date.now()}.tmp`; - const lock = acquireFileLockSync(UNIFIED_SETTINGS_LOCK_PATH, { - maxAttempts: 80, - baseDelayMs: 15, - maxDelayMs: 800, - staleAfterMs: 120_000, - }); + writeFileSync(tempPath, data, "utf8"); + let moved = false; try { - writeFileSync(tempPath, data, "utf8"); - let moved = false; - try { - for (let attempt = 0; attempt < 5; attempt += 1) { - try { - renameSync(tempPath, UNIFIED_SETTINGS_PATH); - moved = true; - return; - } catch (error) { - if (!isRetryableFsError(error) || attempt >= 4) { - throw error; - } - Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10 * 2 ** attempt); - } - } - } finally { - if (!moved) { - try { - unlinkSync(tempPath); - } catch { - // Best-effort temp cleanup. + for (let attempt = 0; attempt < 5; attempt += 1) { + try { + renameSync(tempPath, UNIFIED_SETTINGS_PATH); + moved = true; + return; + } catch (error) { + if (!isRetryableFsError(error) || attempt >= 4) { + throw error; } + Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10 * 2 ** attempt); } } } finally { - lock.release(); + if (!moved) { + try { + unlinkSync(tempPath); + } catch { + // Best-effort temp cleanup. + } + } } } @@ -188,39 +219,29 @@ async function writeSettingsRecordAsync(record: JsonRecord): Promise { const payload = normalizeForWrite(record); const data = `${JSON.stringify(payload, null, 2)}\n`; const tempPath = `${UNIFIED_SETTINGS_PATH}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`; - const lock = await acquireFileLock(UNIFIED_SETTINGS_LOCK_PATH, { - maxAttempts: 80, - baseDelayMs: 15, - maxDelayMs: 800, - staleAfterMs: 120_000, - }); + await fs.writeFile(tempPath, data, "utf8"); + let moved = false; try { - await fs.writeFile(tempPath, data, "utf8"); - let moved = false; - try { - for (let attempt = 0; attempt < 5; attempt += 1) { - try { - await fs.rename(tempPath, UNIFIED_SETTINGS_PATH); - moved = true; - return; - } catch (error) { - if (!isRetryableFsError(error) || attempt >= 4) { - throw error; - } - await sleep(10 * 2 ** attempt); - } - } - } finally { - if (!moved) { - try { - await fs.unlink(tempPath); - } catch { - // Best-effort temp cleanup. + for (let attempt = 0; attempt < 5; attempt += 1) { + try { + await fs.rename(tempPath, UNIFIED_SETTINGS_PATH); + moved = true; + return; + } catch (error) { + if (!isRetryableFsError(error) || attempt >= 4) { + throw error; } + await sleep(10 * 2 ** attempt); } } } finally { - await lock.release(); + if (!moved) { + try { + await fs.unlink(tempPath); + } catch { + // Best-effort temp cleanup. + } + } } } @@ -276,9 +297,14 @@ export function loadUnifiedPluginConfigSync(): JsonRecord | null { * @param pluginConfig - Key/value map representing plugin configuration to persist */ export function saveUnifiedPluginConfigSync(pluginConfig: JsonRecord): void { - const record = readSettingsRecordSync() ?? {}; - record.pluginConfig = { ...pluginConfig }; - writeSettingsRecordSync(record); + const lock = acquireFileLockSync(UNIFIED_SETTINGS_LOCK_PATH, SETTINGS_LOCK_OPTIONS); + try { + const record = readSettingsRecordSync() ?? {}; + record.pluginConfig = { ...pluginConfig }; + writeSettingsRecordSync(record); + } finally { + releaseUnifiedSettingsLockSync(() => lock.release()); + } } /** @@ -293,9 +319,14 @@ export function saveUnifiedPluginConfigSync(pluginConfig: JsonRecord): void { */ export async function saveUnifiedPluginConfig(pluginConfig: JsonRecord): Promise { await enqueueSettingsWrite(async () => { - const record = await readSettingsRecordAsync() ?? {}; - record.pluginConfig = { ...pluginConfig }; - await writeSettingsRecordAsync(record); + const lock = await acquireFileLock(UNIFIED_SETTINGS_LOCK_PATH, SETTINGS_LOCK_OPTIONS); + try { + const record = (await readSettingsRecordAsync()) ?? {}; + record.pluginConfig = { ...pluginConfig }; + await writeSettingsRecordAsync(record); + } finally { + await releaseUnifiedSettingsLockAsync(() => lock.release()); + } }); } @@ -336,8 +367,13 @@ export async function saveUnifiedDashboardSettings( dashboardDisplaySettings: JsonRecord, ): Promise { await enqueueSettingsWrite(async () => { - const record = await readSettingsRecordAsync() ?? {}; - record.dashboardDisplaySettings = { ...dashboardDisplaySettings }; - await writeSettingsRecordAsync(record); + const lock = await acquireFileLock(UNIFIED_SETTINGS_LOCK_PATH, SETTINGS_LOCK_OPTIONS); + try { + const record = (await readSettingsRecordAsync()) ?? {}; + record.dashboardDisplaySettings = { ...dashboardDisplaySettings }; + await writeSettingsRecordAsync(record); + } finally { + await releaseUnifiedSettingsLockAsync(() => lock.release()); + } }); } diff --git a/package-lock.json b/package-lock.json index 93ee2ca..cf02555 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ }, "devDependencies": { "@codex-ai/sdk": "file:vendor/codex-ai-sdk", + "@cyclonedx/cyclonedx-npm": "4.2.0", "@fast-check/vitest": "^0.2.4", "@types/node": "^25.3.0", "@typescript-eslint/eslint-plugin": "^8.56.0", @@ -112,6 +113,114 @@ "resolved": "vendor/codex-ai-sdk", "link": true }, + "node_modules/@cyclonedx/cyclonedx-npm": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@cyclonedx/cyclonedx-npm/-/cyclonedx-npm-4.2.0.tgz", + "integrity": "sha512-TDGb85XzPMagN8WxIGgn24sCMdgQ51UdhwvaGmAIje/3P78+vyf2s9pAU0KMSDBVz3JQ66OpHaIDpR+dpRS1Ww==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://owasp.org/donate/?reponame=www-project-cyclonedx&title=OWASP+CycloneDX" + } + ], + "license": "Apache-2.0", + "dependencies": { + "@cyclonedx/cyclonedx-library": "^10.0.0", + "commander": "^14.0.0", + "normalize-package-data": "^7.0.0 || ^8.0.0", + "packageurl-js": "^2.0.1", + "spdx-expression-parse": "^3.0.1 || ^4.0.0", + "xmlbuilder2": "^3.0.2 || ^4.0.3" + }, + "bin": { + "cyclonedx-npm": "bin/cyclonedx-npm-cli.js" + }, + "engines": { + "node": ">=20.18.0", + "npm": ">=9" + }, + "optionalDependencies": { + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "ajv-formats-draft2019": "^1.6.1", + "libxmljs2": "^0.35||^0.37" + } + }, + "node_modules/@cyclonedx/cyclonedx-npm/node_modules/@cyclonedx/cyclonedx-library": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@cyclonedx/cyclonedx-library/-/cyclonedx-library-10.0.0.tgz", + "integrity": "sha512-xDXf2eqzeFHdjamj6oBV3duRSfrlmsJ5+2z9tXp7q5qxJP5Awmjf4ABSutS4qkVHHj7JzKFL/EM0V0Nihc7zPg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://owasp.org/donate/?reponame=www-project-cyclonedx&title=OWASP+CycloneDX" + } + ], + "license": "Apache-2.0", + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "ajv-formats-draft2019": "^1.6.1", + "libxmljs2": "^0.35||^0.37", + "packageurl-js": "*", + "spdx-expression-parse": "*", + "xmlbuilder2": "^3.0.2||^4.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + }, + "ajv-formats": { + "optional": true + }, + "ajv-formats-draft2019": { + "optional": true + }, + "libxmljs2": { + "optional": true + }, + "packageurl-js": { + "optional": true + }, + "spdx-expression-parse": { + "optional": true + }, + "xmlbuilder2": { + "optional": true + } + } + }, + "node_modules/@cyclonedx/cyclonedx-npm/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "optional": true, + "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/@cyclonedx/cyclonedx-npm/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==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.27.2", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", @@ -723,6 +832,99 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -751,6 +953,90 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@npmcli/agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-3.0.0.tgz", + "integrity": "sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^10.0.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/fs": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-4.0.0.tgz", + "integrity": "sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@oozcitak/dom": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@oozcitak/dom/-/dom-2.0.2.tgz", + "integrity": "sha512-GjpKhkSYC3Mj4+lfwEyI1dqnsKTgwGy48ytZEhm4A/xnH/8z9M3ZVXKr/YGQi3uCLs1AEBS+x5T2JPiueEDW8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oozcitak/infra": "^2.0.2", + "@oozcitak/url": "^3.0.0", + "@oozcitak/util": "^10.0.0" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@oozcitak/infra": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@oozcitak/infra/-/infra-2.0.2.tgz", + "integrity": "sha512-2g+E7hoE2dgCz/APPOEK5s3rMhJvNxSMBrP+U+j1OWsIbtSpWxxlUjq1lU8RIsFJNYv7NMlnVsCuHcUzJW+8vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oozcitak/util": "^10.0.0" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@oozcitak/url": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@oozcitak/url/-/url-3.0.0.tgz", + "integrity": "sha512-ZKfET8Ak1wsLAiLWNfFkZc/BraDccuTJKR6svTYc7sVjbR+Iu0vtXdiDMY4o6jaFl5TW2TlS7jbLl4VovtAJWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oozcitak/infra": "^2.0.2", + "@oozcitak/util": "^10.0.0" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@oozcitak/util": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@oozcitak/util/-/util-10.0.0.tgz", + "integrity": "sha512-hAX0pT/73190NLqBPPWSdBVGtbY6VOhWYK3qqHqtXQ1gK7kS2yz4+ivsN07hpJ6I3aeMtKP6J6npsEKOAzuTLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0" + } + }, "node_modules/@openauthjs/openauth": { "version": "0.4.3", "resolved": "https://registry.npmjs.org/@openauthjs/openauth/-/openauth-0.4.3.tgz", @@ -817,6 +1103,17 @@ "license": "MIT", "peer": true }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@polka/url": { "version": "1.0.0-next.29", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", @@ -1666,6 +1963,17 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/abbrev": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", + "integrity": "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==", + "dev": true, + "license": "ISC", + "optional": true, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/acorn": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", @@ -1689,6 +1997,17 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 14" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -1706,6 +2025,68 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats-draft2019": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/ajv-formats-draft2019/-/ajv-formats-draft2019-1.6.1.tgz", + "integrity": "sha512-JQPvavpkWDvIsBp2Z33UkYCtXCSpW4HD3tAZ+oL4iEFOk9obQZffx0yANwECt6vzr6ET+7HN5czRyqXbnq/u0Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "punycode": "^2.1.1", + "schemes": "^1.4.0", + "smtp-address-parser": "^1.0.3", + "uri-js": "^4.4.1" + }, + "peerDependencies": { + "ajv": "*" + } + }, + "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==", + "dev": true, + "license": "MIT", + "optional": true, + "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==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/ansi-escapes": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.2.0.tgz", @@ -1735,6 +2116,23 @@ "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, + "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", + "optional": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/arctic": { "version": "2.3.4", "resolved": "https://registry.npmjs.org/arctic/-/arctic-2.3.4.tgz", @@ -1747,6 +2145,13 @@ "@oslojs/jwt": "0.2.0" } }, + "node_modules/argparse": { + "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, + "license": "Python-2.0" + }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -1785,15 +2190,61 @@ "node": "20 || >=22" } }, - "node_modules/brace-expansion": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.2.tgz", - "integrity": "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==", + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", "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" + } + ], "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, + "optional": true + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.2.tgz", + "integrity": "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, "engines": { "node": "20 || >=22" } @@ -1811,6 +2262,57 @@ "node": ">=8" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "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" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/cacache": { + "version": "19.0.1", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-19.0.1.tgz", + "integrity": "sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "@npmcli/fs": "^4.0.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^10.0.1", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^7.0.2", + "ssri": "^12.0.0", + "tar": "^7.4.3", + "unique-filename": "^4.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/chai": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", @@ -1821,6 +2323,17 @@ "node": ">=18" } }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "optional": true, + "engines": { + "node": ">=18" + } + }, "node_modules/cli-cursor": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", @@ -1854,6 +2367,28 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "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, + "license": "MIT", + "optional": true + }, "node_modules/colorette": { "version": "2.0.20", "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", @@ -1904,6 +2439,34 @@ } } }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -1911,6 +2474,33 @@ "dev": true, "license": "MIT" }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/discontinuous-range": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz", + "integrity": "sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/emoji-regex": { "version": "10.6.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", @@ -1918,6 +2508,39 @@ "dev": true, "license": "MIT" }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, "node_modules/environment": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", @@ -1931,6 +2554,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/es-module-lexer": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", @@ -2198,6 +2829,17 @@ "dev": true, "license": "MIT" }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "dev": true, + "license": "(MIT OR WTFPL)", + "optional": true, + "engines": { + "node": ">=6" + } + }, "node_modules/expect-type": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", @@ -2208,6 +2850,22 @@ "node": ">=12.0.0" } }, + "node_modules/exponential-backoff": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", + "dev": true, + "license": "Apache-2.0", + "optional": true + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/fast-check": { "version": "4.5.3", "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-4.5.3.tgz", @@ -2252,6 +2910,24 @@ "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==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause", + "optional": true + }, "node_modules/fflate": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", @@ -2272,6 +2948,14 @@ "node": ">=16.0.0" } }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -2323,6 +3007,46 @@ "dev": true, "license": "ISC" }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2351,6 +3075,37 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -2364,6 +3119,14 @@ "node": ">=10.13.0" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC", + "optional": true + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -2383,6 +3146,29 @@ "node": ">=16.9.0" } }, + "node_modules/hosted-git-info": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.2.tgz", + "integrity": "sha512-M422h7o/BR3rmCQ8UHi7cyyMqKltdP9Uo+J2fXK+RSAY+wTcKOIRyhTuKv4qn+DJf3g+PL890AzId5KZpX+CBg==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^11.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -2390,6 +3176,44 @@ "dev": true, "license": "MIT" }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/husky": { "version": "9.1.7", "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", @@ -2406,6 +3230,42 @@ "url": "https://github.com/sponsors/typicode" } }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "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" + } + ], + "license": "BSD-3-Clause", + "optional": true + }, "node_modules/ignore": { "version": "7.0.5", "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", @@ -2426,8 +3286,35 @@ "node": ">=0.8.19" } }, - "node_modules/is-extglob": { - "version": "2.1.1", + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC", + "optional": true + }, + "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==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 12" + } + }, + "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, @@ -2521,6 +3408,23 @@ "node": ">=8" } }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "optional": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jose": { "version": "5.9.6", "resolved": "https://registry.npmjs.org/jose/-/jose-5.9.6.tgz", @@ -2537,6 +3441,19 @@ "dev": true, "license": "MIT" }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -2582,6 +3499,24 @@ "node": ">= 0.8.0" } }, + "node_modules/libxmljs2": { + "version": "0.37.0", + "resolved": "https://registry.npmjs.org/libxmljs2/-/libxmljs2-0.37.0.tgz", + "integrity": "sha512-Xb78V8GZouoZFrq8cCwx7+G3WYOcJG0xb3YUbweSyE4z2EIrQCZMr3Ye/dHn4mESs6YxUMeQeUZm5IXg+iLHog==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "bindings": "~1.5.0", + "nan": "~2.22.2", + "node-gyp": "^11.2.0", + "prebuild-install": "^7.1.3" + }, + "engines": { + "node": ">=22" + } + }, "node_modules/lint-staged": { "version": "16.2.7", "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.2.7.tgz", @@ -2661,6 +3596,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC", + "optional": true + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -2699,6 +3642,30 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/make-fetch-happen": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-14.0.3.tgz", + "integrity": "sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "@npmcli/agent": "^3.0.0", + "cacache": "^19.0.1", + "http-cache-semantics": "^4.1.1", + "minipass": "^7.0.2", + "minipass-fetch": "^4.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^1.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "ssri": "^12.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -2726,6 +3693,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimatch": { "version": "10.2.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", @@ -2742,6 +3723,199 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "optional": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "optional": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-collect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", + "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-fetch": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-4.0.1.tgz", + "integrity": "sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^1.0.3", + "minizlib": "^3.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-flush/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/moo": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.3.tgz", + "integrity": "sha512-m2fmM2dDm7GZQsY7KK2cme8agi+AAljILjQnof7p1ZMDe6dQ4bdnSMx0cPppudoeNv5hEFQirN6u+O4fDE0IWA==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true + }, "node_modules/mrmime": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", @@ -2759,6 +3933,14 @@ "dev": true, "license": "MIT" }, + "node_modules/nan": { + "version": "2.22.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.2.tgz", + "integrity": "sha512-DANghxFkS1plDdRsX0X9pm0Z6SJNN6gBdtXfanwoZ8hooC5gosGFSBGRYHUVPz1asKA/kMRqDRdHrluZ61SpBQ==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/nano-spawn": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-2.0.0.tgz", @@ -2791,6 +3973,14 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -2798,6 +3988,149 @@ "dev": true, "license": "MIT" }, + "node_modules/nearley": { + "version": "2.20.1", + "resolved": "https://registry.npmjs.org/nearley/-/nearley-2.20.1.tgz", + "integrity": "sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "commander": "^2.19.0", + "moo": "^0.5.0", + "railroad-diagrams": "^1.0.0", + "randexp": "0.4.6" + }, + "bin": { + "nearley-railroad": "bin/nearley-railroad.js", + "nearley-test": "bin/nearley-test.js", + "nearley-unparse": "bin/nearley-unparse.js", + "nearleyc": "bin/nearleyc.js" + }, + "funding": { + "type": "individual", + "url": "https://nearley.js.org/#give-to-nearley" + } + }, + "node_modules/nearley/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "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==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-abi": { + "version": "3.87.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.87.0.tgz", + "integrity": "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-gyp": { + "version": "11.5.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-11.5.0.tgz", + "integrity": "sha512-ra7Kvlhxn5V9Slyus0ygMa2h+UqExPqUIkfk7Pc8QTLT956JLSy51uWFwHtIYy0vI8cB4BDhc/S03+880My/LQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^14.0.3", + "nopt": "^8.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5", + "tar": "^7.4.3", + "tinyglobby": "^0.2.12", + "which": "^5.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/node-gyp/node_modules/isexe": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", + "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", + "dev": true, + "license": "BlueOak-1.0.0", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/node-gyp/node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/nopt": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-8.1.0.tgz", + "integrity": "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "abbrev": "^3.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/normalize-package-data": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-8.0.0.tgz", + "integrity": "sha512-RWk+PI433eESQ7ounYxIp67CYuVsS1uYSonX3kA6ps/3LWfjVQa/ptEg6Y3T6uAMq1mWpX9PQ+qx+QaHpsc7gQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^9.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, "node_modules/obug": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", @@ -2809,6 +4142,17 @@ ], "license": "MIT" }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "wrappy": "1" + } + }, "node_modules/onetime": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", @@ -2875,6 +4219,35 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-map": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", + "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0", + "optional": true + }, + "node_modules/packageurl-js": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/packageurl-js/-/packageurl-js-2.0.1.tgz", + "integrity": "sha512-N5ixXjzTy4QDQH0Q9YFjqIWd6zH6936Djpl2m9QNFmDv5Fum8q8BjkpAcHNMzOFE0IwQrFhJWex3AN6kS0OSwg==", + "dev": true, + "license": "MIT" + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -2895,6 +4268,24 @@ "node": ">=8" } }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "optional": true, + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -2964,6 +4355,35 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -2974,6 +4394,44 @@ "node": ">= 0.8.0" } }, + "node_modules/proc-log": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", + "integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==", + "dev": true, + "license": "ISC", + "optional": true, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -3001,6 +4459,73 @@ ], "license": "MIT" }, + "node_modules/railroad-diagrams": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz", + "integrity": "sha512-cz93DjNeLY0idrCNOH6PviZGRN9GJhsdm9hpn1YCS879fj4W+x5IFJhhkRZcwVgMmFF7R82UA/7Oh+R8lLZg6A==", + "dev": true, + "license": "CC0-1.0", + "optional": true + }, + "node_modules/randexp": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.4.6.tgz", + "integrity": "sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "discontinuous-range": "1.0.0", + "ret": "~0.1.10" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "optional": true, + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/restore-cursor": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", @@ -3018,6 +4543,28 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 4" + } + }, "node_modules/rfdc": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", @@ -3070,6 +4617,47 @@ "fsevents": "~2.3.2" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "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" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/schemes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/schemes/-/schemes-1.4.0.tgz", + "integrity": "sha512-ImFy9FbCsQlVgnE3TCWmLPCFnVzx0lHL/l+umHplDqAKd0dzFpnS6lFZIpagBlYhKwzVmlV36ec0Y1XTu8JBAQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "extend": "^3.0.0" + } + }, "node_modules/semver": { "version": "7.7.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", @@ -3126,6 +4714,55 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "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" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "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" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/sirv": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", @@ -3152,23 +4789,81 @@ "is-fullwidth-code-point": "^5.0.0" }, "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/smtp-address-parser": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/smtp-address-parser/-/smtp-address-parser-1.1.0.tgz", + "integrity": "sha512-Gz11jbNU0plrReU9Sj7fmshSBxxJ9ShdD2q4ktHIHo/rpTH6lFyQoYHYKINPJtPe8aHFnsbtW46Ls0tCCBsIZg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "nearley": "^2.20.1" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" } }, - "node_modules/slice-ansi/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", "dev": true, "license": "MIT", - "engines": { - "node": ">=12" + "optional": true, + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "engines": { + "node": ">= 14" } }, "node_modules/source-map-js": { @@ -3181,6 +4876,67 @@ "node": ">=0.10.0" } }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-correct/node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz", + "integrity": "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", + "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/ssri": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-12.0.0.tgz", + "integrity": "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -3195,6 +4951,17 @@ "dev": true, "license": "MIT" }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string-argv": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", @@ -3222,6 +4989,67 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-ansi": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", @@ -3238,6 +5066,43 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -3251,6 +5116,64 @@ "node": ">=8" } }, + "node_modules/tar": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.10.tgz", + "integrity": "sha512-8mOPs1//5q/rlkNSPcCegA6hiHJYDmSLEI8aMH/CdSQJNWztHC9WHNam5zdQlfpTwB9Xp7IBEsHfV5LKMJGVAw==", + "dev": true, + "license": "BlueOak-1.0.0", + "optional": true, + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-fs/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -3362,6 +5285,20 @@ "typescript": ">=4.8.4" } }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -3409,6 +5346,34 @@ "dev": true, "license": "MIT" }, + "node_modules/unique-filename": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-4.0.0.tgz", + "integrity": "sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "unique-slug": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/unique-slug": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-5.0.0.tgz", + "integrity": "sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -3419,6 +5384,36 @@ "punycode": "^2.1.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/validate-npm-package-license/node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, "node_modules/vite": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", @@ -3677,6 +5672,86 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/wrap-ansi-cjs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrap-ansi/node_modules/ansi-styles": { "version": "6.2.3", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", @@ -3708,6 +5783,41 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/xmlbuilder2": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/xmlbuilder2/-/xmlbuilder2-4.0.3.tgz", + "integrity": "sha512-bx8Q1STctnNaaDymWnkfQLKofs0mGNN7rLLapJlGuV3VlvegD7Ls4ggMjE3aUSWItCCzU0PEv45lI87iSigiCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oozcitak/dom": "^2.0.2", + "@oozcitak/infra": "^2.0.2", + "@oozcitak/util": "^10.0.0", + "js-yaml": "^4.1.1" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "optional": true, + "engines": { + "node": ">=18" + } + }, "node_modules/yaml": { "version": "2.8.2", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", diff --git a/package.json b/package.json index 857207f..dc74c3e 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "audit:dev:allowlist": "node scripts/audit-dev-allowlist.js", "audit:ci": "npm run audit:prod && npm run audit:dev:allowlist", "license:check": "node scripts/license-policy-check.js", - "sbom": "npx --yes @cyclonedx/cyclonedx-npm --output-file sbom.cdx.json --omit dev", + "sbom": "cyclonedx-npm --output-file sbom.cdx.json --omit dev", "prepublishOnly": "npm run build", "prepare": "husky" }, @@ -104,6 +104,7 @@ "devDependencies": { "@fast-check/vitest": "^0.2.4", "@codex-ai/sdk": "file:vendor/codex-ai-sdk", + "@cyclonedx/cyclonedx-npm": "4.2.0", "@types/node": "^25.3.0", "@typescript-eslint/eslint-plugin": "^8.56.0", "@typescript-eslint/parser": "^8.56.0", diff --git a/scripts/license-policy-check.js b/scripts/license-policy-check.js index b00a0fe..818926c 100644 --- a/scripts/license-policy-check.js +++ b/scripts/license-policy-check.js @@ -4,9 +4,17 @@ import { readFileSync } from "node:fs"; import { resolve } from "node:path"; const lockPath = resolve(process.cwd(), "package-lock.json"); +function normalizeSpdxToken(value) { + return value + .trim() + .toUpperCase() + .replace(/\+$/g, "") + .replace(/-(ONLY|OR-LATER)$/g, ""); +} + const denyList = (process.env.CODEX_LICENSE_DENYLIST ?? "GPL-2.0,GPL-3.0,AGPL-3.0") .split(",") - .map((value) => value.trim().toUpperCase()) + .map((value) => normalizeSpdxToken(value)) .filter((value) => value.length > 0); const failOnUnknown = process.env.CODEX_LICENSE_FAIL_ON_UNKNOWN === "1"; @@ -16,25 +24,58 @@ const packages = packageLock.packages ?? {}; const violations = []; const unknown = []; +function normalizeLicenseValue(value) { + if (typeof value === "string") { + return value; + } + if (value && typeof value === "object" && typeof value.type === "string") { + return value.type; + } + return ""; +} + +function extractRawLicense(record) { + const direct = normalizeLicenseValue(record.license); + if (direct) return direct; + + if (typeof record.licenses === "string") { + return record.licenses; + } + if (Array.isArray(record.licenses)) { + const values = record.licenses + .map((entry) => normalizeLicenseValue(entry)) + .filter((entry) => entry.length > 0); + if (values.length > 0) { + return values.join(" OR "); + } + } + + return ""; +} + +function extractLicenseTokens(rawLicense) { + return rawLicense + .toUpperCase() + .split(/[^A-Z0-9.+-]+/) + .map((value) => normalizeSpdxToken(value)) + .filter((value) => value.length > 0); +} + for (const [packagePath, metadata] of Object.entries(packages)) { if (!metadata || typeof metadata !== "object") continue; if (packagePath === "") continue; const record = metadata; const name = typeof record.name === "string" ? record.name : packagePath; const version = typeof record.version === "string" ? record.version : "0.0.0"; - const rawLicense = - typeof record.license === "string" - ? record.license - : typeof record.licenses === "string" - ? record.licenses - : ""; + const rawLicense = extractRawLicense(record); const normalized = rawLicense.trim().toUpperCase(); if (!normalized) { unknown.push(`${name}@${version}`); continue; } + const tokens = new Set(extractLicenseTokens(normalized)); for (const denied of denyList) { - if (normalized.includes(denied)) { + if (tokens.has(denied)) { violations.push(`${name}@${version} (${rawLicense})`); break; } diff --git a/test/authorization.test.ts b/test/authorization.test.ts index b48aaaf..83f2e74 100644 --- a/test/authorization.test.ts +++ b/test/authorization.test.ts @@ -56,40 +56,36 @@ async function removeWithRetry( describe("authorization", () => { it("defaults to admin role", () => { - const previousRole = process.env.CODEX_AUTH_ROLE; + const previous = captureAuthEnv(); try { - delete process.env.CODEX_AUTH_ROLE; + for (const key of AUTH_ENV_KEYS) { + delete process.env[key]; + } expect(getAuthorizationRole()).toBe("admin"); expect(authorizeAction("secrets:rotate").allowed).toBe(true); } finally { - if (previousRole === undefined) { - delete process.env.CODEX_AUTH_ROLE; - } else { - process.env.CODEX_AUTH_ROLE = previousRole; - } + restoreAuthEnv(previous); } }); it("denies write actions for viewer role", () => { - const previousRole = process.env.CODEX_AUTH_ROLE; + const previous = captureAuthEnv(); try { + for (const key of AUTH_ENV_KEYS) { + delete process.env[key]; + } process.env.CODEX_AUTH_ROLE = "viewer"; const auth = authorizeAction("accounts:write"); expect(auth.allowed).toBe(false); expect(auth.role).toBe("viewer"); expect(auth.reason).toContain("accounts:write"); } finally { - if (previousRole === undefined) { - delete process.env.CODEX_AUTH_ROLE; - } else { - process.env.CODEX_AUTH_ROLE = previousRole; - } + restoreAuthEnv(previous); } }); it("allows all actions when break-glass is enabled and audits the bypass", async () => { - const previousRole = process.env.CODEX_AUTH_ROLE; - const previousBreakGlass = process.env.CODEX_AUTH_BREAK_GLASS; + const previous = captureAuthEnv(); const previousAuditConfig = getAuditConfig(); const auditDir = await fs.mkdtemp(join(tmpdir(), "codex-auth-audit-")); try { @@ -99,6 +95,9 @@ describe("authorization", () => { maxFileSizeBytes: 1024 * 1024, maxFiles: 2, }); + for (const key of AUTH_ENV_KEYS) { + delete process.env[key]; + } process.env.CODEX_AUTH_ROLE = "viewer"; process.env.CODEX_AUTH_BREAK_GLASS = "1"; const result = authorizeAction("secrets:rotate"); @@ -125,16 +124,7 @@ describe("authorization", () => { } finally { configureAudit(previousAuditConfig); await removeWithRetry(auditDir, { recursive: true, force: true }); - if (previousRole === undefined) { - delete process.env.CODEX_AUTH_ROLE; - } else { - process.env.CODEX_AUTH_ROLE = previousRole; - } - if (previousBreakGlass === undefined) { - delete process.env.CODEX_AUTH_BREAK_GLASS; - } else { - process.env.CODEX_AUTH_BREAK_GLASS = previousBreakGlass; - } + restoreAuthEnv(previous); } }); diff --git a/test/background-jobs.test.ts b/test/background-jobs.test.ts index 6062c7b..f3209d1 100644 --- a/test/background-jobs.test.ts +++ b/test/background-jobs.test.ts @@ -3,6 +3,27 @@ import { promises as fs } from "node:fs"; import { join } from "node:path"; import { tmpdir } from "node:os"; +const RETRYABLE_REMOVE_CODES = new Set(["EBUSY", "EPERM", "ENOTEMPTY"]); + +async function removeWithRetry( + targetPath: string, + options: { recursive?: boolean; force?: boolean }, +): Promise { + for (let attempt = 0; attempt < 6; attempt += 1) { + try { + await fs.rm(targetPath, options); + return; + } catch (error) { + const code = (error as NodeJS.ErrnoException).code; + if (code === "ENOENT") return; + if (!code || !RETRYABLE_REMOVE_CODES.has(code) || attempt === 5) { + throw error; + } + await new Promise((resolve) => setTimeout(resolve, 25 * 2 ** attempt)); + } + } +} + describe("background jobs", () => { let tempDir: string; let originalDir: string | undefined; @@ -20,7 +41,7 @@ describe("background jobs", () => { } else { process.env.CODEX_MULTI_AUTH_DIR = originalDir; } - await fs.rm(tempDir, { recursive: true, force: true }); + await removeWithRetry(tempDir, { recursive: true, force: true }); }); it("retries and succeeds before exhausting attempts", async () => { @@ -86,4 +107,131 @@ describe("background jobs", () => { note: "keep-visible", }); }); -}); \ No newline at end of file + + it("writes dead-letter after exhausting retries on 429 errors", async () => { + const { runBackgroundJobWithRetry, getBackgroundJobDlqPath } = + await import("../lib/background-jobs.js"); + let attempts = 0; + await expect( + runBackgroundJobWithRetry({ + name: "test.retry-429-fail", + task: async () => { + attempts += 1; + const error = Object.assign(new Error("rate limited"), { statusCode: 429 }); + throw error; + }, + maxAttempts: 3, + baseDelayMs: 1, + maxDelayMs: 2, + }), + ).rejects.toThrow("rate limited"); + expect(attempts).toBe(3); + + const content = await fs.readFile(getBackgroundJobDlqPath(), "utf8"); + const lines = content.trim().split("\n"); + expect(lines).toHaveLength(1); + const entry = JSON.parse(lines[0] ?? "{}") as { job?: string; attempts?: number }; + expect(entry.job).toBe("test.retry-429-fail"); + expect(entry.attempts).toBe(3); + }); + + it("adds jitter to retry delays to avoid synchronized retries", async () => { + vi.resetModules(); + const sleepMock = vi.fn(async () => {}); + vi.doMock("../lib/utils.js", () => ({ + sleep: sleepMock, + })); + const randomSpy = vi.spyOn(Math, "random"); + randomSpy.mockReturnValueOnce(0).mockReturnValueOnce(1); + + try { + const { runBackgroundJobWithRetry } = await import("../lib/background-jobs.js"); + let attempts = 0; + await expect( + runBackgroundJobWithRetry({ + name: "test.retry-jitter", + task: async () => { + attempts += 1; + const error = new Error("busy") as NodeJS.ErrnoException; + error.code = "EBUSY"; + throw error; + }, + maxAttempts: 3, + baseDelayMs: 100, + maxDelayMs: 100, + }), + ).rejects.toThrow("busy"); + expect(attempts).toBe(3); + expect(sleepMock.mock.calls.map(([ms]) => ms)).toEqual([80, 120]); + } finally { + randomSpy.mockRestore(); + vi.doUnmock("../lib/utils.js"); + } + }); + + it("records actual attempts for non-retryable failures", async () => { + const { runBackgroundJobWithRetry, getBackgroundJobDlqPath } = + await import("../lib/background-jobs.js"); + let attempts = 0; + await expect( + runBackgroundJobWithRetry({ + name: "test.non-retryable", + task: async () => { + attempts += 1; + const error = Object.assign(new Error("bad request"), { statusCode: 400 }); + throw error; + }, + maxAttempts: 5, + retryable: () => false, + }), + ).rejects.toThrow("bad request"); + expect(attempts).toBe(1); + + const content = await fs.readFile(getBackgroundJobDlqPath(), "utf8"); + const lines = content.trim().split("\n"); + expect(lines).toHaveLength(1); + const entry = JSON.parse(lines[0] ?? "{}") as { job?: string; attempts?: number }; + expect(entry.job).toBe("test.non-retryable"); + expect(entry.attempts).toBe(1); + }); + + it("redacts sensitive error text in dead-letter entries and warning logs", async () => { + vi.resetModules(); + const warnMock = vi.fn(); + vi.doMock("../lib/logger.js", () => ({ + logWarn: warnMock, + })); + try { + const { runBackgroundJobWithRetry, getBackgroundJobDlqPath } = + await import("../lib/background-jobs.js"); + await expect( + runBackgroundJobWithRetry({ + name: "test.retry-sensitive-error", + task: async () => { + throw new Error( + "network failed for person@example.com Bearer sk_test+/123== refresh_token=rt+/456==", + ); + }, + maxAttempts: 1, + }), + ).rejects.toThrow("person@example.com"); + + const content = await fs.readFile(getBackgroundJobDlqPath(), "utf8"); + const lines = content.trim().split("\n"); + expect(lines).toHaveLength(1); + const entry = JSON.parse(lines[0] ?? "{}") as { error?: string }; + expect(entry.error).toContain("***REDACTED***"); + expect(entry.error).not.toContain("person@example.com"); + expect(entry.error).not.toContain("sk_test+/123=="); + expect(entry.error).not.toContain("rt+/456=="); + + const warningPayloads = warnMock.mock.calls.map((args) => args[1]); + const serializedWarnings = JSON.stringify(warningPayloads); + expect(serializedWarnings).not.toContain("person@example.com"); + expect(serializedWarnings).not.toContain("sk_test+/123=="); + expect(serializedWarnings).not.toContain("rt+/456=="); + } finally { + vi.doUnmock("../lib/logger.js"); + } + }); +}); diff --git a/test/codex-manager-cli.test.ts b/test/codex-manager-cli.test.ts index 7cc0b09..22e515c 100644 --- a/test/codex-manager-cli.test.ts +++ b/test/codex-manager-cli.test.ts @@ -2050,9 +2050,18 @@ describe("codex manager cli commands", () => { ]); expect(firstExitCode).toBe(0); const firstPayload = JSON.parse(String(logSpy.mock.calls[0]?.[0])) as { + schemaVersion: number; accounts: Array<{ email: string }>; - pagination: { hasMore: boolean; nextCursor: string | null }; + pagination: { + cursor: string | null; + nextCursor: string | null; + hasMore: boolean; + pageSize: number; + }; }; + expect(typeof firstPayload.schemaVersion).toBe("number"); + expect(firstPayload.pagination.cursor).toBeNull(); + expect(firstPayload.pagination.pageSize).toBe(1); expect(firstPayload.accounts).toHaveLength(1); expect(firstPayload.accounts[0]?.email).toBe("one@example.com"); expect(firstPayload.pagination.hasMore).toBe(true); @@ -2070,9 +2079,18 @@ describe("codex manager cli commands", () => { ]); expect(secondExitCode).toBe(0); const secondPayload = JSON.parse(String(logSpy.mock.calls[0]?.[0])) as { + schemaVersion: number; accounts: Array<{ email: string }>; - pagination: { hasMore: boolean; nextCursor: string | null }; + pagination: { + cursor: string | null; + nextCursor: string | null; + hasMore: boolean; + pageSize: number; + }; }; + expect(typeof secondPayload.schemaVersion).toBe("number"); + expect(secondPayload.pagination.cursor).toBe(String(firstPayload.pagination.nextCursor)); + expect(secondPayload.pagination.pageSize).toBe(1); expect(secondPayload.accounts).toHaveLength(1); expect(secondPayload.accounts[0]?.email).toBe("two@example.com"); expect(secondPayload.pagination.hasMore).toBe(false); @@ -2082,6 +2100,71 @@ describe("codex manager cli commands", () => { } }); + it("rejects malformed pagination cursors in JSON list mode", async () => { + loadAccountsMock.mockResolvedValue({ + version: 3, + activeIndex: 0, + activeIndexByFamily: {}, + accounts: [ + { + email: "one@example.com", + accountId: "acc_one", + refreshToken: "refresh-one", + addedAt: Date.now() - 2_000, + lastUsed: Date.now() - 2_000, + enabled: true, + }, + ], + }); + + const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + try { + const { runCodexMultiAuthCli } = await import("../lib/codex-manager.js"); + const malformedPayloadCursor = Buffer.from("12junk", "utf8").toString("base64"); + const invalidPayloadExitCode = await runCodexMultiAuthCli([ + "auth", + "list", + "--json", + "--cursor", + malformedPayloadCursor, + ]); + expect(invalidPayloadExitCode).toBe(1); + expect(errorSpy).toHaveBeenCalledWith("Invalid --cursor value"); + + errorSpy.mockClear(); + const invalidBase64ExitCode = await runCodexMultiAuthCli([ + "auth", + "list", + "--json", + "--cursor", + "%%%invalid-base64%%%", + ]); + expect(invalidBase64ExitCode).toBe(1); + expect(errorSpy).toHaveBeenCalledWith("Invalid --cursor value"); + } finally { + errorSpy.mockRestore(); + } + }); + + it("rejects partially numeric --page-size values", async () => { + const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + try { + const { runCodexMultiAuthCli } = await import("../lib/codex-manager.js"); + const exitCode = await runCodexMultiAuthCli([ + "auth", + "list", + "--json", + "--page-size", + "10junk", + ]); + expect(exitCode).toBe(1); + expect(errorSpy).toHaveBeenCalledWith("--page-size must be between 1 and 200"); + expect(loadAccountsMock).not.toHaveBeenCalled(); + } finally { + errorSpy.mockRestore(); + } + }); + it("applies idempotency key for rotate-secrets automation retries", async () => { checkAndRecordIdempotencyKeyMock .mockResolvedValueOnce({ replayed: false }) @@ -2129,6 +2212,46 @@ describe("codex manager cli commands", () => { } }); + it("rejects option-smuggling for --idempotency-key", async () => { + const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + try { + const { runCodexMultiAuthCli } = await import("../lib/codex-manager.js"); + const exitCode = await runCodexMultiAuthCli([ + "auth", + "rotate-secrets", + "--idempotency-key", + "--json", + ]); + expect(exitCode).toBe(1); + expect(errorSpy).toHaveBeenCalledWith("Missing value for --idempotency-key"); + expect(rotateStoredSecretEncryptionMock).not.toHaveBeenCalled(); + } finally { + errorSpy.mockRestore(); + } + }); + + it("redacts rotate-secrets failure details in non-json output", async () => { + rotateStoredSecretEncryptionMock.mockRejectedValue( + new Error( + "failed for person@example.com Bearer sk_test+/123== refresh_token=rt+/456==", + ), + ); + const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + try { + const { runCodexMultiAuthCli } = await import("../lib/codex-manager.js"); + const exitCode = await runCodexMultiAuthCli(["auth", "rotate-secrets"]); + expect(exitCode).toBe(1); + const rendered = String(errorSpy.mock.calls[0]?.[0] ?? ""); + expect(rendered).toContain("Failed to rotate stored secrets:"); + expect(rendered).toContain("***REDACTED***"); + expect(rendered).not.toContain("person@example.com"); + expect(rendered).not.toContain("sk_test+/123=="); + expect(rendered).not.toContain("rt+/456=="); + } finally { + errorSpy.mockRestore(); + } + }); + it("enforces ABAC idempotency-key requirement for rotate-secrets", async () => { const previousRole = process.env.CODEX_AUTH_ROLE; const previousAbacRequirement = process.env.CODEX_AUTH_ABAC_REQUIRE_IDEMPOTENCY_KEY; diff --git a/test/data-retention.test.ts b/test/data-retention.test.ts index b572f22..1c95a10 100644 --- a/test/data-retention.test.ts +++ b/test/data-retention.test.ts @@ -3,6 +3,7 @@ import { promises as fs } from "node:fs"; import { join } from "node:path"; import { tmpdir } from "node:os"; import type { RetentionPolicy } from "../lib/data-retention.js"; +import { removeWithRetry } from "./helpers/remove-with-retry.js"; describe("data retention", () => { let tempDir: string; @@ -21,7 +22,7 @@ describe("data retention", () => { } else { process.env.CODEX_MULTI_AUTH_DIR = originalDir; } - await fs.rm(tempDir, { recursive: true, force: true }); + await removeWithRetry(tempDir, { recursive: true, force: true }); }); it("prunes stale log/cache/state files", async () => { @@ -76,4 +77,219 @@ describe("data retention", () => { expect(await fs.readFile(freshLog, "utf8")).toBe("fresh"); expect(await fs.readFile(freshCache, "utf8")).toBe("fresh"); }); -}); \ No newline at end of file + + it("retries transient EBUSY during directory entry retention pruning", async () => { + const { enforceDataRetention } = await import("../lib/data-retention.js"); + const oldDate = new Date(Date.now() - 3 * 24 * 60 * 60_000); + const logsDir = join(tempDir, "logs"); + const nestedDir = join(logsDir, "nested"); + const staleLog = join(nestedDir, "stale.log"); + + await fs.mkdir(nestedDir, { recursive: true }); + await fs.writeFile(staleLog, "old", "utf8"); + await fs.utimes(staleLog, oldDate, oldDate); + + const originalStat = fs.stat.bind(fs); + const statSpy = vi.spyOn(fs, "stat"); + let statBusyInjected = false; + statSpy.mockImplementation(async (path, options) => { + if (!statBusyInjected && path === staleLog) { + statBusyInjected = true; + const error = new Error("busy") as NodeJS.ErrnoException; + error.code = "EBUSY"; + throw error; + } + return originalStat(path, options as { bigint?: boolean }); + }); + + const originalRmdir = fs.rmdir.bind(fs); + const rmdirSpy = vi.spyOn(fs, "rmdir"); + let rmdirBusyInjected = false; + rmdirSpy.mockImplementation(async (path) => { + if (!rmdirBusyInjected && path === nestedDir) { + rmdirBusyInjected = true; + const error = new Error("busy") as NodeJS.ErrnoException; + error.code = "EBUSY"; + throw error; + } + return originalRmdir(path); + }); + + try { + const policy: RetentionPolicy = { + logDays: 1, + cacheDays: 90, + flaggedDays: 90, + quotaCacheDays: 90, + dlqDays: 90, + }; + const result = await enforceDataRetention(policy); + expect(result.removedLogs).toBe(1); + expect(statBusyInjected).toBe(true); + expect(rmdirBusyInjected).toBe(true); + await expect(fs.stat(staleLog)).rejects.toMatchObject({ code: "ENOENT" }); + await expect(fs.stat(nestedDir)).rejects.toMatchObject({ code: "ENOENT" }); + } finally { + statSpy.mockRestore(); + rmdirSpy.mockRestore(); + } + }); + + it.each(["EPERM", "EACCES", "EAGAIN"] as const)( + "retries transient %s during directory entry retention pruning", + async (code) => { + const { enforceDataRetention } = await import("../lib/data-retention.js"); + const oldDate = new Date(Date.now() - 3 * 24 * 60 * 60_000); + const logsDir = join(tempDir, "logs"); + const nestedDir = join(logsDir, "nested"); + const staleLog = join(nestedDir, "stale.log"); + + await fs.mkdir(nestedDir, { recursive: true }); + await fs.writeFile(staleLog, "old", "utf8"); + await fs.utimes(staleLog, oldDate, oldDate); + + const originalStat = fs.stat.bind(fs); + const statSpy = vi.spyOn(fs, "stat"); + let injected = false; + statSpy.mockImplementation(async (path, options) => { + if (!injected && path === staleLog) { + injected = true; + const error = new Error(code.toLowerCase()) as NodeJS.ErrnoException; + error.code = code; + throw error; + } + return originalStat(path, options as { bigint?: boolean }); + }); + + try { + const policy: RetentionPolicy = { + logDays: 1, + cacheDays: 90, + flaggedDays: 90, + quotaCacheDays: 90, + dlqDays: 90, + }; + const result = await enforceDataRetention(policy); + expect(result.removedLogs).toBe(1); + expect(injected).toBe(true); + await expect(fs.stat(staleLog)).rejects.toMatchObject({ code: "ENOENT" }); + } finally { + statSpy.mockRestore(); + } + }, + ); + + it("throws after max retries for persistent EBUSY during directory entry pruning", async () => { + const { enforceDataRetention } = await import("../lib/data-retention.js"); + const oldDate = new Date(Date.now() - 3 * 24 * 60 * 60_000); + const logsDir = join(tempDir, "logs"); + const nestedDir = join(logsDir, "nested"); + const staleLog = join(nestedDir, "stale.log"); + + await fs.mkdir(nestedDir, { recursive: true }); + await fs.writeFile(staleLog, "old", "utf8"); + await fs.utimes(staleLog, oldDate, oldDate); + + const originalStat = fs.stat.bind(fs); + const statSpy = vi.spyOn(fs, "stat"); + statSpy.mockImplementation(async (path, options) => { + if (path === staleLog) { + const error = new Error("busy") as NodeJS.ErrnoException; + error.code = "EBUSY"; + throw error; + } + return originalStat(path, options as { bigint?: boolean }); + }); + + try { + const policy: RetentionPolicy = { + logDays: 1, + cacheDays: 90, + flaggedDays: 90, + quotaCacheDays: 90, + dlqDays: 90, + }; + await expect(enforceDataRetention(policy)).rejects.toMatchObject({ code: "EBUSY" }); + } finally { + statSpy.mockRestore(); + } + }); + + it("retries transient unlink failures during single-file retention pruning", async () => { + const { enforceDataRetention } = await import("../lib/data-retention.js"); + const oldDate = new Date(Date.now() - 3 * 24 * 60 * 60_000); + const flagged = join(tempDir, "openai-codex-flagged-accounts.json"); + await fs.writeFile(flagged, "{}", "utf8"); + await fs.utimes(flagged, oldDate, oldDate); + + const originalUnlink = fs.unlink.bind(fs); + const unlinkSpy = vi.spyOn(fs, "unlink"); + let injected = false; + unlinkSpy.mockImplementation(async (path) => { + if (!injected && path === flagged) { + injected = true; + const error = new Error("access denied") as NodeJS.ErrnoException; + error.code = "EACCES"; + throw error; + } + return originalUnlink(path); + }); + + try { + const policy: RetentionPolicy = { + logDays: 90, + cacheDays: 90, + flaggedDays: 1, + quotaCacheDays: 90, + dlqDays: 90, + }; + const result = await enforceDataRetention(policy); + expect(result.removedStateFiles).toBe(1); + expect(injected).toBe(true); + await expect(fs.stat(flagged)).rejects.toMatchObject({ code: "ENOENT" }); + } finally { + unlinkSpy.mockRestore(); + } + }); + + it("treats ENOTEMPTY on directory cleanup as a non-fatal race", async () => { + const { enforceDataRetention } = await import("../lib/data-retention.js"); + const oldDate = new Date(Date.now() - 3 * 24 * 60 * 60_000); + const logsDir = join(tempDir, "logs"); + const nestedDir = join(logsDir, "nested"); + const staleLog = join(nestedDir, "stale.log"); + + await fs.mkdir(nestedDir, { recursive: true }); + await fs.writeFile(staleLog, "old", "utf8"); + await fs.utimes(staleLog, oldDate, oldDate); + + const originalRmdir = fs.rmdir.bind(fs); + const rmdirSpy = vi.spyOn(fs, "rmdir"); + let enotemptyInjected = false; + rmdirSpy.mockImplementation(async (path) => { + if (!enotemptyInjected && path === nestedDir) { + enotemptyInjected = true; + const error = new Error("directory recreated") as NodeJS.ErrnoException; + error.code = "ENOTEMPTY"; + throw error; + } + return originalRmdir(path); + }); + + try { + const policy: RetentionPolicy = { + logDays: 1, + cacheDays: 90, + flaggedDays: 90, + quotaCacheDays: 90, + dlqDays: 90, + }; + const result = await enforceDataRetention(policy); + expect(result.removedLogs).toBe(1); + expect(enotemptyInjected).toBe(true); + await expect(fs.stat(staleLog)).rejects.toMatchObject({ code: "ENOENT" }); + } finally { + rmdirSpy.mockRestore(); + } + }); +}); diff --git a/test/file-lock.test.ts b/test/file-lock.test.ts index d8d5919..f8633a6 100644 --- a/test/file-lock.test.ts +++ b/test/file-lock.test.ts @@ -1,30 +1,11 @@ -import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { promises as fs } from "node:fs"; import { spawn } from "node:child_process"; import { join } from "node:path"; import { tmpdir } from "node:os"; +import { pathToFileURL } from "node:url"; import { acquireFileLock } from "../lib/file-lock.js"; - -const RETRYABLE_REMOVE_CODES = new Set(["EBUSY", "EPERM", "ENOTEMPTY"]); - -async function removeWithRetry( - targetPath: string, - options: { recursive?: boolean; force?: boolean }, -): Promise { - for (let attempt = 0; attempt < 6; attempt += 1) { - try { - await fs.rm(targetPath, options); - return; - } catch (error) { - const code = (error as NodeJS.ErrnoException).code; - if (code === "ENOENT") return; - if (!code || !RETRYABLE_REMOVE_CODES.has(code) || attempt === 5) { - throw error; - } - await new Promise((resolve) => setTimeout(resolve, 25 * 2 ** attempt)); - } - } -} +import { removeWithRetry } from "./helpers/remove-with-retry.js"; describe("file lock", () => { let tempDir: string; @@ -84,50 +65,39 @@ describe("file lock", () => { const lockPath = join(tempDir, "contention.lock"); const sharedFilePath = join(tempDir, "shared.txt"); const workerScriptPath = join(tempDir, "lock-writer-worker.mjs"); + const transpiledModulePath = join(tempDir, "file-lock-worker-module.mjs"); + const ts = await import("typescript"); + const sourcePath = join(process.cwd(), "lib", "file-lock.ts"); + const source = await fs.readFile(sourcePath, "utf8"); + const transpiled = ts.transpileModule(source, { + compilerOptions: { + module: ts.ModuleKind.ESNext, + target: ts.ScriptTarget.ES2022, + }, + }).outputText; + await fs.writeFile(transpiledModulePath, transpiled, "utf8"); + const moduleUrl = pathToFileURL(transpiledModulePath).href; const workerCount = 6; - const iterationsPerWorker = 10; + const iterationsPerWorker = 50; await fs.writeFile(sharedFilePath, "", "utf8"); await fs.writeFile( workerScriptPath, `import { promises as fs } from "node:fs"; -const [lockPath, sharedFilePath, workerId, iterationsRaw] = process.argv.slice(2); +const [moduleUrl, lockPath, sharedFilePath, workerId, iterationsRaw] = process.argv.slice(2); const iterations = Number(iterationsRaw); -const RETRYABLE = new Set(["EEXIST", "EBUSY", "EPERM"]); -function sleep(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)); -} -async function acquireLock(path) { - for (let attempt = 0; attempt < 1000; attempt += 1) { - try { - const handle = await fs.open(path, "wx", 0o600); - await handle.writeFile(String(process.pid), "utf8"); - await handle.close(); - return async () => { - try { - await fs.unlink(path); - } catch (error) { - if (error && error.code !== "ENOENT") { - throw error; - } - } - }; - } catch (error) { - if (!error || !RETRYABLE.has(error.code)) { - throw error; - } - await sleep(2); - } - } - throw new Error("Failed to acquire lock under contention"); -} +const { acquireFileLock } = await import(moduleUrl); for (let i = 0; i < iterations; i += 1) { - const release = await acquireLock(lockPath); + const lock = await acquireFileLock(lockPath, { + maxAttempts: 500, + baseDelayMs: 1, + maxDelayMs: 8, + staleAfterMs: 10_000, + }); try { const existing = await fs.readFile(sharedFilePath, "utf8"); - await sleep(2); await fs.writeFile(sharedFilePath, existing + workerId + ":" + i + "\\n", "utf8"); } finally { - await release(); + await lock.release(); } } `, @@ -140,6 +110,7 @@ for (let i = 0; i < iterations; i += 1) { process.execPath, [ workerScriptPath, + moduleUrl, lockPath, sharedFilePath, String(workerId), @@ -172,4 +143,141 @@ for (let i = 0; i < iterations; i += 1) { expect(lines).toHaveLength(workerCount * iterationsPerWorker); expect(new Set(lines).size).toBe(lines.length); }); + + it.each(["EBUSY", "EPERM"] as const)( + "allows release retries after transient unlink failures (%s)", + async (transientCode) => { + const lockPath = join(tempDir, "release-retry.lock"); + const lock = await acquireFileLock(lockPath, { + maxAttempts: 3, + baseDelayMs: 1, + maxDelayMs: 2, + staleAfterMs: 10_000, + }); + + const originalUnlink = fs.unlink.bind(fs); + const unlinkSpy = vi.spyOn(fs, "unlink"); + let injectedBusy = false; + unlinkSpy.mockImplementation(async (path) => { + if (!injectedBusy && path === lockPath) { + injectedBusy = true; + const error = new Error("busy") as NodeJS.ErrnoException; + error.code = transientCode; + throw error; + } + return originalUnlink(path); + }); + + try { + await expect(lock.release()).rejects.toMatchObject({ code: transientCode }); + await expect(lock.release()).resolves.toBeUndefined(); + expect(injectedBusy).toBe(true); + await expect(fs.stat(lockPath)).rejects.toMatchObject({ code: "ENOENT" }); + } finally { + unlinkSpy.mockRestore(); + } + }, + ); + + it("does not evict stale lock files when lock owner PID is still alive", async () => { + const lockPath = join(tempDir, "live-owner.lock"); + await fs.writeFile(lockPath, `${JSON.stringify({ pid: process.pid, acquiredAt: Date.now() - 120_000 })}\n`, "utf8"); + const staleDate = new Date(Date.now() - 120_000); + await fs.utimes(lockPath, staleDate, staleDate); + + await expect( + acquireFileLock(lockPath, { + maxAttempts: 1, + baseDelayMs: 1, + maxDelayMs: 2, + staleAfterMs: 1_000, + }), + ).rejects.toMatchObject({ code: "EEXIST" }); + await expect(fs.stat(lockPath)).resolves.toBeTruthy(); + }); + + it("cleans up lock files when metadata write fails", async () => { + const lockPath = join(tempDir, "incomplete.lock"); + const originalOpen = fs.open.bind(fs); + type OpenFn = typeof fs.open; + type OpenHandle = Awaited>; + const openSpy = vi.spyOn(fs, "open"); + const mockOpen: OpenFn = (async (...args: Parameters) => { + const [path] = args; + if (String(path) === lockPath) { + await fs.writeFile(lockPath, "partial", "utf8"); + return { + writeFile: async () => { + const error = new Error("metadata write failed") as NodeJS.ErrnoException; + error.code = "EIO"; + throw error; + }, + close: async () => {}, + } as unknown as OpenHandle; + } + return originalOpen(...args); + }) as OpenFn; + openSpy.mockImplementation(mockOpen); + + try { + await expect( + acquireFileLock(lockPath, { + maxAttempts: 1, + baseDelayMs: 1, + maxDelayMs: 2, + staleAfterMs: 10_000, + }), + ).rejects.toThrow("metadata write failed"); + await expect(fs.stat(lockPath)).rejects.toMatchObject({ code: "ENOENT" }); + } finally { + openSpy.mockRestore(); + } + }); + + it("preserves metadata write failure when cleanup unlink also fails", async () => { + const lockPath = join(tempDir, "cleanup-error-precedence.lock"); + const originalOpen = fs.open.bind(fs); + type OpenFn = typeof fs.open; + type OpenHandle = Awaited>; + const openSpy = vi.spyOn(fs, "open"); + const unlinkSpy = vi.spyOn(fs, "unlink"); + const mockOpen: OpenFn = (async (...args: Parameters) => { + const [path] = args; + if (String(path) === lockPath) { + await fs.writeFile(lockPath, "partial", "utf8"); + return { + writeFile: async () => { + const error = new Error("metadata write failed") as NodeJS.ErrnoException; + error.code = "EIO"; + throw error; + }, + close: async () => {}, + } as unknown as OpenHandle; + } + return originalOpen(...args); + }) as OpenFn; + openSpy.mockImplementation(mockOpen); + unlinkSpy.mockImplementation(async (path) => { + if (String(path) === lockPath) { + const error = new Error("cleanup failed") as NodeJS.ErrnoException; + error.code = "EPERM"; + throw error; + } + return Promise.resolve(); + }); + + try { + await expect( + acquireFileLock(lockPath, { + maxAttempts: 1, + baseDelayMs: 1, + maxDelayMs: 2, + staleAfterMs: 10_000, + }), + ).rejects.toThrow("metadata write failed"); + } finally { + openSpy.mockRestore(); + unlinkSpy.mockRestore(); + } + }); }); diff --git a/test/helpers/remove-with-retry.ts b/test/helpers/remove-with-retry.ts new file mode 100644 index 0000000..6bd8415 --- /dev/null +++ b/test/helpers/remove-with-retry.ts @@ -0,0 +1,22 @@ +import { promises as fs } from "node:fs"; + +const RETRYABLE_REMOVE_CODES = new Set(["EBUSY", "EPERM", "ENOTEMPTY", "EACCES", "EAGAIN"]); + +export async function removeWithRetry( + targetPath: string, + options: { recursive?: boolean; force?: boolean }, +): Promise { + for (let attempt = 0; attempt < 6; attempt += 1) { + try { + await fs.rm(targetPath, options); + return; + } catch (error) { + const code = (error as NodeJS.ErrnoException).code; + if (code === "ENOENT") return; + if (!code || !RETRYABLE_REMOVE_CODES.has(code) || attempt === 5) { + throw error; + } + await new Promise((resolve) => setTimeout(resolve, 25 * 2 ** attempt)); + } + } +} diff --git a/test/index-retry.test.ts b/test/index-retry.test.ts index 3813edc..47397a4 100644 --- a/test/index-retry.test.ts +++ b/test/index-retry.test.ts @@ -213,5 +213,51 @@ describe("OpenAIAuthPlugin rate-limit retry", () => { expect(globalThis.fetch).toHaveBeenCalledTimes(1); expect(response.status).toBe(200); }); + + it("strips query and fragment when audit URL parsing falls back", async () => { + vi.resetModules(); + const auditLogMock = vi.fn(); + vi.doMock("../lib/audit.js", async () => { + const actual = await vi.importActual("../lib/audit.js"); + return { + ...(actual as Record), + auditLog: auditLogMock, + }; + }); + + try { + const { OpenAIAuthPlugin } = await import("../index.js"); + const client = { + tui: { showToast: vi.fn() }, + auth: { set: vi.fn() }, + } as any; + + const plugin = await OpenAIAuthPlugin({ client }); + const getAuth = async () => ({ + type: "oauth" as const, + access: "a", + refresh: "r", + expires: Date.now() + 60_000, + multiAccount: true, + }); + + const sdk = (await plugin.auth.loader(getAuth, { options: {}, models: {} })) as any; + const responsePromise = sdk.fetch("relative/path?token=super-secret#frag", {}); + await vi.advanceTimersByTimeAsync(1500); + const response = await responsePromise; + expect(response.status).toBe(200); + + const resources = auditLogMock.mock.calls.map((args) => String(args[2] ?? "")); + expect(resources.length).toBeGreaterThan(0); + for (const resource of resources) { + expect(resource).not.toContain("?"); + expect(resource).not.toContain("#"); + expect(resource).not.toContain("super-secret"); + } + } finally { + vi.doUnmock("../lib/audit.js"); + vi.resetModules(); + } + }); }); diff --git a/test/license-policy-check.test.ts b/test/license-policy-check.test.ts new file mode 100644 index 0000000..b97d82a --- /dev/null +++ b/test/license-policy-check.test.ts @@ -0,0 +1,87 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { promises as fs } from "node:fs"; +import { spawnSync } from "node:child_process"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +import { removeWithRetry } from "./helpers/remove-with-retry.js"; + +function createRetryableFsError(code: string): NodeJS.ErrnoException { + const error = new Error(code.toLowerCase()) as NodeJS.ErrnoException; + error.code = code; + return error; +} + +describe("license policy check", () => { + let tempDir: string; + + beforeEach(async () => { + tempDir = await fs.mkdtemp(join(tmpdir(), "codex-license-policy-")); + }); + + afterEach(async () => { + await removeWithRetry(tempDir, { recursive: true, force: true }); + }); + + it("retries transient windows rm errors before succeeding", async () => { + vi.useFakeTimers(); + const rmSpy = vi.spyOn(fs, "rm"); + rmSpy + .mockRejectedValueOnce(createRetryableFsError("EBUSY")) + .mockRejectedValueOnce(createRetryableFsError("EPERM")) + .mockResolvedValueOnce(undefined); + try { + const cleanupPromise = removeWithRetry("C:\\temp\\codex-license-policy", { + recursive: true, + force: true, + }); + await vi.runAllTimersAsync(); + await expect(cleanupPromise).resolves.toBeUndefined(); + expect(rmSpy).toHaveBeenCalledTimes(3); + } finally { + rmSpy.mockRestore(); + vi.useRealTimers(); + } + }); + + it.each([ + { denyList: "GPL-2.0+", metadata: { license: "GPL-2.0+" } }, + { denyList: "LGPL-2.1+", metadata: { license: "MIT OR LGPL-2.1+" } }, + { denyList: "GPL-2.0+", metadata: { license: { type: "GPL-2.0+" } } }, + { + denyList: "LGPL-2.1+", + metadata: { licenses: [{ type: "MIT OR LGPL-2.1+" }] }, + }, + ])("blocks denylisted SPDX plus-form (%o)", async ({ denyList, metadata }) => { + const lock = { + name: "license-test", + lockfileVersion: 3, + packages: { + "": { + name: "license-test", + version: "1.0.0", + }, + "node_modules/blocked-package": { + name: "blocked-package", + version: "1.0.0", + ...metadata, + }, + }, + }; + + await fs.writeFile(join(tempDir, "package-lock.json"), JSON.stringify(lock, null, 2), "utf8"); + const scriptPath = join(process.cwd(), "scripts", "license-policy-check.js"); + const result = spawnSync(process.execPath, [scriptPath], { + cwd: tempDir, + encoding: "utf8", + env: { + ...process.env, + CODEX_LICENSE_DENYLIST: denyList, + }, + }); + + expect(result.status).toBe(1); + const stderr = String(result.stderr ?? ""); + expect(stderr).toContain("License policy violations detected:"); + expect(stderr).toContain("blocked-package@1.0.0"); + }); +}); diff --git a/test/storage.test.ts b/test/storage.test.ts index 3c3157e..398b076 100644 --- a/test/storage.test.ts +++ b/test/storage.test.ts @@ -2,6 +2,7 @@ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import { promises as fs, existsSync } from "node:fs"; import { dirname, join } from "node:path"; import { tmpdir } from "node:os"; +import * as fileLock from "../lib/file-lock.js"; import { getConfigDir, getProjectStorageKey } from "../lib/storage/paths.js"; import { deduplicateAccounts, @@ -676,6 +677,42 @@ describe("storage", () => { expect(saved.version).toBe(3); }); + it("fails fast when encrypted account storage cannot be decrypted", async () => { + const previousPrimary = process.env.CODEX_AUTH_ENCRYPTION_KEY; + const previousSecondary = process.env.CODEX_AUTH_PREVIOUS_ENCRYPTION_KEY; + try { + process.env.CODEX_AUTH_ENCRYPTION_KEY = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + delete process.env.CODEX_AUTH_PREVIOUS_ENCRYPTION_KEY; + await saveAccounts({ + version: 3, + activeIndex: 0, + activeIndexByFamily: {}, + accounts: [ + { + refreshToken: "refresh-token-sensitive", + accountId: "encrypted-account", + addedAt: Date.now(), + lastUsed: Date.now(), + }, + ], + }); + + process.env.CODEX_AUTH_ENCRYPTION_KEY = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; + await expect(loadAccounts()).rejects.toThrow("Failed to decrypt account storage"); + } finally { + if (previousPrimary === undefined) { + delete process.env.CODEX_AUTH_ENCRYPTION_KEY; + } else { + process.env.CODEX_AUTH_ENCRYPTION_KEY = previousPrimary; + } + if (previousSecondary === undefined) { + delete process.env.CODEX_AUTH_PREVIOUS_ENCRYPTION_KEY; + } else { + process.env.CODEX_AUTH_PREVIOUS_ENCRYPTION_KEY = previousSecondary; + } + } + }); + it("returns migrated data even when save fails (line 422-423 coverage)", async () => { const v1Storage = { version: 1, @@ -1539,6 +1576,105 @@ describe("storage", () => { statSpy.mockRestore(); }); + it("falls back to best-effort cleanup when lock.release throws EBUSY", async () => { + const now = Date.now(); + const storage = { + version: 3 as const, + activeIndex: 0, + accounts: [{ refreshToken: "token", addedAt: now, lastUsed: now }], + }; + const lockPath = `${testStoragePath}.lock`; + + const acquireSpy = vi.spyOn(fileLock, "acquireFileLock").mockResolvedValue({ + path: lockPath, + release: async () => { + const err = new Error("EBUSY release") as NodeJS.ErrnoException; + err.code = "EBUSY"; + throw err; + }, + }); + const rmSpy = vi.spyOn(fs, "rm"); + try { + await saveAccounts(storage); + + expect(existsSync(testStoragePath)).toBe(true); + expect(rmSpy).toHaveBeenCalledWith(lockPath, { force: true }); + } finally { + rmSpy.mockRestore(); + acquireSpy.mockRestore(); + } + }); + + it("falls back to best-effort cleanup when lock.release throws EPERM", async () => { + const now = Date.now(); + const storage = { + version: 3 as const, + activeIndex: 0, + accounts: [{ refreshToken: "token", addedAt: now, lastUsed: now }], + }; + const lockPath = `${testStoragePath}.lock`; + + const acquireSpy = vi.spyOn(fileLock, "acquireFileLock").mockResolvedValue({ + path: lockPath, + release: async () => { + const err = new Error("EPERM release") as NodeJS.ErrnoException; + err.code = "EPERM"; + throw err; + }, + }); + const rmSpy = vi.spyOn(fs, "rm"); + try { + await saveAccounts(storage); + + expect(existsSync(testStoragePath)).toBe(true); + expect(rmSpy).toHaveBeenCalledWith(lockPath, { force: true }); + } finally { + rmSpy.mockRestore(); + acquireSpy.mockRestore(); + } + }); + + it("serializes lock acquisition across concurrent saves", async () => { + const now = Date.now(); + let activeLocks = 0; + let maxActiveLocks = 0; + + const acquireSpy = vi.spyOn(fileLock, "acquireFileLock").mockImplementation(async (path) => { + activeLocks += 1; + maxActiveLocks = Math.max(maxActiveLocks, activeLocks); + return { + path, + release: async () => { + await new Promise((resolve) => setTimeout(resolve, 20)); + activeLocks -= 1; + }, + }; + }); + try { + await Promise.all([ + saveAccounts({ + version: 3 as const, + activeIndex: 0, + accounts: [{ refreshToken: "token-1", addedAt: now + 1, lastUsed: now + 1 }], + }), + saveAccounts({ + version: 3 as const, + activeIndex: 0, + accounts: [{ refreshToken: "token-2", addedAt: now + 2, lastUsed: now + 2 }], + }), + saveAccounts({ + version: 3 as const, + activeIndex: 0, + accounts: [{ refreshToken: "token-3", addedAt: now + 3, lastUsed: now + 3 }], + }), + ]); + + expect(maxActiveLocks).toBe(1); + } finally { + acquireSpy.mockRestore(); + } + }); + it("retries backup copyFile on transient EBUSY and succeeds", async () => { const now = Date.now(); const storage = { diff --git a/test/unified-settings.test.ts b/test/unified-settings.test.ts index 6eff59e..58e5151 100644 --- a/test/unified-settings.test.ts +++ b/test/unified-settings.test.ts @@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { promises as fs } from "node:fs"; import { join } from "node:path"; import { tmpdir } from "node:os"; +import { removeWithRetry } from "./helpers/remove-with-retry.js"; describe("unified settings", () => { let tempDir: string; @@ -20,7 +21,7 @@ describe("unified settings", () => { } else { process.env.CODEX_MULTI_AUTH_DIR = originalDir; } - await fs.rm(tempDir, { recursive: true, force: true }); + await removeWithRetry(tempDir, { recursive: true, force: true }); }); it("merges plugin and dashboard sections into one file", async () => { @@ -225,6 +226,123 @@ describe("unified settings", () => { }); }); + it("acquires file lock before reading during plugin config updates", async () => { + vi.resetModules(); + const lockState = { held: false }; + const acquireFileLockMock = vi.fn(async () => { + lockState.held = true; + return { + path: "mock-settings.lock", + release: async () => { + lockState.held = false; + }, + }; + }); + const acquireFileLockSyncMock = vi.fn(() => ({ + path: "mock-settings.lock", + release: () => undefined, + })); + vi.doMock("../lib/file-lock.js", () => ({ + acquireFileLock: acquireFileLockMock, + acquireFileLockSync: acquireFileLockSyncMock, + })); + + try { + const { saveUnifiedPluginConfig, getUnifiedSettingsPath } = await import( + "../lib/unified-settings.js" + ); + await fs.writeFile( + getUnifiedSettingsPath(), + JSON.stringify({ + version: 1, + dashboardDisplaySettings: { menuShowLastUsed: false }, + }), + "utf8", + ); + + const originalReadFile = fs.readFile.bind(fs); + let observedReadUnderLock = false; + const readSpy = vi.spyOn(fs, "readFile").mockImplementation(async (...args) => { + if (lockState.held) { + observedReadUnderLock = true; + } + return originalReadFile(...args); + }); + try { + await saveUnifiedPluginConfig({ codexMode: true, retries: 2 }); + expect(observedReadUnderLock).toBe(true); + expect(acquireFileLockMock).toHaveBeenCalledTimes(1); + } finally { + readSpy.mockRestore(); + } + } finally { + vi.doUnmock("../lib/file-lock.js"); + vi.resetModules(); + } + }); + + it("does not fail async writes when lock release throws EPERM", async () => { + vi.resetModules(); + const acquireFileLockMock = vi.fn(async () => ({ + path: "mock-settings.lock", + release: async () => { + const error = new Error("perm locked") as NodeJS.ErrnoException; + error.code = "EPERM"; + throw error; + }, + })); + const acquireFileLockSyncMock = vi.fn(() => ({ + path: "mock-settings.lock", + release: () => undefined, + })); + vi.doMock("../lib/file-lock.js", () => ({ + acquireFileLock: acquireFileLockMock, + acquireFileLockSync: acquireFileLockSyncMock, + })); + + try { + const { saveUnifiedPluginConfig, loadUnifiedPluginConfigSync } = await import( + "../lib/unified-settings.js" + ); + await expect(saveUnifiedPluginConfig({ codexMode: true, retries: 2 })).resolves.toBeUndefined(); + expect(loadUnifiedPluginConfigSync()).toEqual({ codexMode: true, retries: 2 }); + } finally { + vi.doUnmock("../lib/file-lock.js"); + vi.resetModules(); + } + }); + + it("does not fail sync writes when lock release throws EBUSY", async () => { + vi.resetModules(); + const acquireFileLockMock = vi.fn(async () => ({ + path: "mock-settings.lock", + release: async () => undefined, + })); + const acquireFileLockSyncMock = vi.fn(() => ({ + path: "mock-settings.lock", + release: () => { + const error = new Error("busy") as NodeJS.ErrnoException; + error.code = "EBUSY"; + throw error; + }, + })); + vi.doMock("../lib/file-lock.js", () => ({ + acquireFileLock: acquireFileLockMock, + acquireFileLockSync: acquireFileLockSyncMock, + })); + + try { + const { saveUnifiedPluginConfigSync, loadUnifiedPluginConfigSync } = await import( + "../lib/unified-settings.js" + ); + expect(() => saveUnifiedPluginConfigSync({ codexMode: false, retries: 1 })).not.toThrow(); + expect(loadUnifiedPluginConfigSync()).toEqual({ codexMode: false, retries: 1 }); + } finally { + vi.doUnmock("../lib/file-lock.js"); + vi.resetModules(); + } + }); + it("refuses overwriting settings sections when a read fails", async () => { const { saveUnifiedPluginConfig,