From 4ee8f6aa4ac28156f9057ea7c3d30f72271e37b0 Mon Sep 17 00:00:00 2001 From: OnlineChef <280567955+OnlineChef@users.noreply.github.com> Date: Sat, 1 Aug 2026 09:03:29 +0200 Subject: [PATCH 01/19] feat(provider-security): ChefVault client, in-memory slots, degraded stub (PSP-008) Add provider-security plane client for chefvault:// credential refs with workload headers, active/next/retiring/revoked slot model, renewal jitter, redacted doctor/status surfaces, and bounded degraded mode when ChefVault is unreachable. Wire minimal resolve path for provider doctor/status and models auth. Co-authored-by: Cursor --- src/cli/doctor.ts | 6 + src/cli/status.ts | 12 ++ src/oauth/index.ts | 11 ++ src/provider-security/client.ts | 194 +++++++++++++++++++++++ src/provider-security/degraded.ts | 58 +++++++ src/provider-security/index.ts | 12 ++ src/provider-security/resolve.ts | 129 ++++++++++++++++ src/provider-security/slots.ts | 209 +++++++++++++++++++++++++ src/provider-security/status.ts | 142 +++++++++++++++++ src/provider-security/types.ts | 138 +++++++++++++++++ src/server/auth-cors.ts | 3 +- src/types.ts | 5 + tests/provider-security.test.ts | 246 ++++++++++++++++++++++++++++++ 13 files changed, 1164 insertions(+), 1 deletion(-) create mode 100644 src/provider-security/client.ts create mode 100644 src/provider-security/degraded.ts create mode 100644 src/provider-security/index.ts create mode 100644 src/provider-security/resolve.ts create mode 100644 src/provider-security/slots.ts create mode 100644 src/provider-security/status.ts create mode 100644 src/provider-security/types.ts create mode 100644 tests/provider-security.test.ts diff --git a/src/cli/doctor.ts b/src/cli/doctor.ts index 5291fa10c..f9e7e712c 100644 --- a/src/cli/doctor.ts +++ b/src/cli/doctor.ts @@ -30,6 +30,7 @@ import { resolveCodexRuntime, } from "../codex/runtime"; import { CODEX_REAUTH_ACTION, collectOAuthHealthEntriesForCli, MASKED_ACCOUNT_FALLBACK, type OAuthHealthEntry } from "../oauth/health"; +import { collectProviderSecurityDoctorChecks } from "../provider-security/status"; import { getAuthRefreshIntentLockPath, getAuthStorePath } from "../oauth/store"; export { resolveCodexHomeDir } from "../codex/home"; @@ -817,6 +818,11 @@ export async function runDoctor(args: string[] = []): Promise { console.log(` [${check.level}] ${check.message}`); } + console.log("\nProvider security (ChefVault)"); + for (const check of await collectProviderSecurityDoctorChecks(doctorConfig)) { + console.log(` [${check.level}] ${check.message}`); + } + // Hints, not fixes. const hints: string[] = []; const proxyDown = proxyDownRestartHint({ diff --git a/src/cli/status.ts b/src/cli/status.ts index 76d61d7f8..92581739d 100644 --- a/src/cli/status.ts +++ b/src/cli/status.ts @@ -11,6 +11,8 @@ import { displayCodexRuntimePath, effortClampAppliesToRuntime, loadLastEffortCla import { redactSecretString, redactUserPath } from "../lib/redact"; import { collectOrcaCodexHomeDiagnostic, type OrcaCodexHomeDiagnostic } from "../codex/home"; import { grokFenceEndpointDrift, readGrokStatus } from "../grok/status"; +import { collectProviderSecurityStatus, type ProviderSecurityStatusReport } from "../provider-security/status"; +import { ProviderSecurityClient } from "../provider-security/client"; type HealthCheck = { ok: boolean; @@ -68,6 +70,7 @@ export type CliStatusJson = { }; }; codexHome: OrcaCodexHomeDiagnostic; + providerSecurity: ProviderSecurityStatusReport; }; export type CliStatusView = { @@ -270,6 +273,14 @@ export async function collectStatus(): Promise { ? "reachable, but PID file is missing or stale" : "not running"; + const providerSecurity = collectProviderSecurityStatus(config); + const authorityProbe = await ProviderSecurityClient.fromEnv().healthz(); + providerSecurity.authority = { + ok: authorityProbe.ok, + url: providerSecurity.authority.url, + message: authorityProbe.message, + }; + return { proxyLabel, healthLabel: health.label, @@ -311,6 +322,7 @@ export async function collectStatus(): Promise { codexPlugins, codexRuntime, codexHome, + providerSecurity, }, }; } diff --git a/src/oauth/index.ts b/src/oauth/index.ts index 72e8c6d1f..dea0bddf0 100644 --- a/src/oauth/index.ts +++ b/src/oauth/index.ts @@ -17,6 +17,8 @@ import { apiKeyPoolEntryId, sanitizeApiKeyValue } from "../providers/api-keys"; import { effectiveGoogleMode, getProviderRegistryEntry, providerMatchesRegistryTransport } from "../providers/registry"; import { resolveProviderModelDiscoveryUrl } from "../providers/model-discovery"; import { resolveProviderTransport } from "../providers/xai-transport"; +import { isChefVaultRef } from "../provider-security"; +import { globalProviderCredentialResolver } from "../provider-security/resolve"; import { detectClaudeCodeToken, detectGrokCliToken, hasComparableGrokIdentity, isSameGrokIdentity, shouldAdoptGrokGeneration } from "./local-token-detect"; import { logOAuthEvent } from "./log"; export { @@ -465,6 +467,15 @@ export async function resolveModelsAuthToken(name: string, prov: OcxProviderConf return undefined; } } + const credentialRef = prov.credentialRef?.trim(); + if (credentialRef && isChefVaultRef(credentialRef)) { + try { + const resolved = await globalProviderCredentialResolver.resolveCredentialRef(credentialRef); + return resolved.apiKey; + } catch { + return undefined; + } + } return resolveEnvValue(prov.apiKey); } diff --git a/src/provider-security/client.ts b/src/provider-security/client.ts new file mode 100644 index 000000000..232d90e9d --- /dev/null +++ b/src/provider-security/client.ts @@ -0,0 +1,194 @@ +/** + * ChefVault provider-security HTTP client (PSP-008). + * + * Talks to the secret authority at CHEF_PROVIDER_SECURITY_URL (default :8323). + * Workload identity is carried on every request via X-Chef-* headers. + */ +import { + type ChefVaultRenewRequest, + type ChefVaultRenewResponse, + type ChefVaultResolveRequest, + type ChefVaultResolveResponse, + ProviderSecurityError, + type ProviderSecurityClientConfig, + type WorkloadIdentity, +} from "./types"; + +export const DEFAULT_PROVIDER_SECURITY_URL = "http://127.0.0.1:8323"; + +const WORKLOAD_HEADER_WORKLOAD = "x-chef-workload-id"; +const WORKLOAD_HEADER_HOST = "x-chef-host-id"; +const WORKLOAD_HEADER_ACTOR = "x-chef-actor"; + +export function resolveProviderSecurityUrl(env: NodeJS.ProcessEnv = process.env): string { + const raw = env.CHEF_PROVIDER_SECURITY_URL?.trim(); + return raw || DEFAULT_PROVIDER_SECURITY_URL; +} + +export function resolveWorkloadIdentity(env: NodeJS.ProcessEnv = process.env): WorkloadIdentity { + return { + workloadId: env.CHEF_WORKLOAD_ID?.trim() || "opencodex", + hostId: env.CHEF_HOST_ID?.trim() || env.HOSTNAME?.trim() || "local", + actor: env.CHEF_ACTOR?.trim() || "opencodex", + }; +} + +function workloadHeaders(workload: WorkloadIdentity): Record { + return { + [WORKLOAD_HEADER_WORKLOAD]: workload.workloadId, + [WORKLOAD_HEADER_HOST]: workload.hostId, + [WORKLOAD_HEADER_ACTOR]: workload.actor, + }; +} + +function mapAuthorityError(status: number, body: unknown): ProviderSecurityError { + const record = body && typeof body === "object" && !Array.isArray(body) + ? body as Record + : {}; + const code = typeof record.code === "string" ? record.code : undefined; + const message = typeof record.message === "string" + ? record.message + : typeof record.error === "string" + ? record.error + : `ChefVault provider-security returned HTTP ${status}`; + + if (code === "stale_fencing_token") { + return new ProviderSecurityError("stale_fencing_token", message); + } + if (status === 404 || code === "ref_not_found") { + return new ProviderSecurityError("ref_not_found", message); + } + if (status === 400 || code === "ref_invalid") { + return new ProviderSecurityError("ref_invalid", message); + } + if (status === 410 || code === "revoked") { + return new ProviderSecurityError("revoked", message); + } + return new ProviderSecurityError("authority_error", message); +} + +function parseResolveResponse(body: unknown): ChefVaultResolveResponse { + if (!body || typeof body !== "object" || Array.isArray(body)) { + throw new ProviderSecurityError("authority_error", "resolve response was not an object"); + } + const record = body as Record; + const leaseId = typeof record.leaseId === "string" ? record.leaseId : ""; + const secret = typeof record.secret === "string" ? record.secret : ""; + const expiresAt = typeof record.expiresAt === "number" ? record.expiresAt : Number(record.expiresAt); + const fencingToken = typeof record.fencingToken === "number" ? record.fencingToken : Number(record.fencingToken); + const slotHint = record.slotHint; + if (!leaseId || !secret || !Number.isFinite(expiresAt) || !Number.isFinite(fencingToken)) { + throw new ProviderSecurityError("authority_error", "resolve response missing required lease fields"); + } + return { + leaseId, + secret, + expiresAt, + fencingToken, + ...(slotHint === "active" || slotHint === "next" || slotHint === "retiring" + ? { slotHint } + : {}), + }; +} + +function parseRenewResponse(body: unknown): ChefVaultRenewResponse { + if (!body || typeof body !== "object" || Array.isArray(body)) { + throw new ProviderSecurityError("authority_error", "renew response was not an object"); + } + const record = body as Record; + const leaseId = typeof record.leaseId === "string" ? record.leaseId : ""; + const secret = typeof record.secret === "string" ? record.secret : ""; + const expiresAt = typeof record.expiresAt === "number" ? record.expiresAt : Number(record.expiresAt); + const fencingToken = typeof record.fencingToken === "number" ? record.fencingToken : Number(record.fencingToken); + if (!leaseId || !secret || !Number.isFinite(expiresAt) || !Number.isFinite(fencingToken)) { + throw new ProviderSecurityError("authority_error", "renew response missing required lease fields"); + } + return { leaseId, secret, expiresAt, fencingToken }; +} + +export class ProviderSecurityClient { + readonly baseUrl: string; + readonly workload: WorkloadIdentity; + private readonly fetchImpl: typeof fetch; + private readonly requestTimeoutMs: number; + + constructor(config: ProviderSecurityClientConfig) { + this.baseUrl = config.baseUrl.replace(/\/$/, ""); + this.workload = config.workload; + this.fetchImpl = config.fetchImpl ?? fetch; + this.requestTimeoutMs = config.requestTimeoutMs ?? 8_000; + } + + static fromEnv(env: NodeJS.ProcessEnv = process.env): ProviderSecurityClient { + return new ProviderSecurityClient({ + baseUrl: resolveProviderSecurityUrl(env), + workload: resolveWorkloadIdentity(env), + }); + } + + private async request(path: string, init: RequestInit): Promise { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), this.requestTimeoutMs); + try { + return await this.fetchImpl(`${this.baseUrl}${path}`, { + ...init, + signal: controller.signal, + headers: { + accept: "application/json", + "content-type": "application/json", + ...workloadHeaders(this.workload), + ...(init.headers ?? {}), + }, + }); + } catch (error) { + if (error instanceof Error && error.name === "AbortError") { + throw new ProviderSecurityError("network_error", "ChefVault provider-security request timed out"); + } + throw new ProviderSecurityError( + "authority_unavailable", + error instanceof Error ? error.message : "ChefVault provider-security is unreachable", + ); + } finally { + clearTimeout(timer); + } + } + + async healthz(): Promise<{ ok: boolean; message: string }> { + try { + const response = await this.request("/healthz", { method: "GET" }); + if (!response.ok) { + return { ok: false, message: `HTTP ${response.status}` }; + } + return { ok: true, message: "ok" }; + } catch (error) { + if (error instanceof ProviderSecurityError) { + return { ok: false, message: error.message }; + } + return { ok: false, message: "unreachable" }; + } + } + + async resolveLease(input: ChefVaultResolveRequest): Promise { + const response = await this.request("/v1/credentials/resolve", { + method: "POST", + body: JSON.stringify(input), + }); + const body = await response.json().catch(() => null); + if (!response.ok) { + throw mapAuthorityError(response.status, body); + } + return parseResolveResponse(body); + } + + async renewLease(input: ChefVaultRenewRequest): Promise { + const response = await this.request("/v1/credentials/renew", { + method: "POST", + body: JSON.stringify(input), + }); + const body = await response.json().catch(() => null); + if (!response.ok) { + throw mapAuthorityError(response.status, body); + } + return parseRenewResponse(body); + } +} diff --git a/src/provider-security/degraded.ts b/src/provider-security/degraded.ts new file mode 100644 index 000000000..bc296504a --- /dev/null +++ b/src/provider-security/degraded.ts @@ -0,0 +1,58 @@ +/** + * Bounded degraded mode when ChefVault is unavailable (PSP-011 stub). + * + * - Existing valid in-memory credentials may still be used (bounded by lease expiry). + * - New resolution against the authority is denied until recovery. + */ +import type { CredentialSnapshot } from "./types"; +import { CredentialSlotStore } from "./slots"; + +export interface DegradedDecision { + allowed: boolean; + reason?: "degraded_deny_resolve" | "lease_expired" | "revoked" | "missing"; +} + +export class DegradedModeController { + constructor(private readonly store: CredentialSlotStore) {} + + markUnavailable(ref: string, at = Date.now()): void { + this.store.enterDegraded(ref, at); + } + + markRecovered(ref: string): void { + this.store.exitDegraded(ref); + } + + isDegraded(ref: string): boolean { + return this.store.getMode(ref) === "degraded"; + } + + /** Whether a fresh resolve against ChefVault is permitted. */ + canResolve(ref: string): DegradedDecision { + if (this.isDegraded(ref)) { + return { allowed: false, reason: "degraded_deny_resolve" }; + } + return { allowed: true }; + } + + /** Whether an already-resolved in-memory credential may be used for upstream auth. */ + canUseExisting(ref: string, at = Date.now()): DegradedDecision { + const snapshot = this.store.snapshotForRequest(ref, at); + if (!snapshot) { + if (this.isDegraded(ref)) { + return { allowed: false, reason: "degraded_deny_resolve" }; + } + return { allowed: false, reason: "missing" }; + } + if (snapshot.expiresAt <= at) { + return { allowed: false, reason: "lease_expired" }; + } + return { allowed: true }; + } + + existingSnapshot(ref: string, at = Date.now()): CredentialSnapshot | null { + const decision = this.canUseExisting(ref, at); + if (!decision.allowed) return null; + return this.store.snapshotForRequest(ref, at); + } +} diff --git a/src/provider-security/index.ts b/src/provider-security/index.ts new file mode 100644 index 000000000..43a4b8bc0 --- /dev/null +++ b/src/provider-security/index.ts @@ -0,0 +1,12 @@ +export * from "./types"; +export * from "./client"; +export * from "./slots"; +export * from "./degraded"; +export * from "./resolve"; +export * from "./status"; + +import { globalCredentialSlotStore } from "./slots"; +import { DegradedModeController } from "./degraded"; + +/** Shared degraded controller bound to the process-wide slot store. */ +export const globalDegradedMode = new DegradedModeController(globalCredentialSlotStore); diff --git a/src/provider-security/resolve.ts b/src/provider-security/resolve.ts new file mode 100644 index 000000000..f232cdada --- /dev/null +++ b/src/provider-security/resolve.ts @@ -0,0 +1,129 @@ +/** + * chefvault:// credential resolution for provider auth (PSP-008). + */ +import { ProviderSecurityClient } from "./client"; +import { DegradedModeController } from "./degraded"; +import { + CredentialSlotStore, + globalCredentialSlotStore, + renewalJitterMs, + shouldRenewLease, +} from "./slots"; +import { + ProviderSecurityError, + validateChefVaultRef, + type CredentialSnapshot, + type ProviderSecurityErrorCode, +} from "./types"; + +export interface ResolveCredentialDeps { + client?: ProviderSecurityClient; + slotStore?: CredentialSlotStore; + degraded?: DegradedModeController; + now?: () => number; + jitterMs?: number; +} + +export interface ResolvedProviderCredential { + apiKey: string; + snapshot: CredentialSnapshot; + source: "chefvault" | "memory"; +} + +function toProviderSecurityError(error: unknown): ProviderSecurityError { + if (error instanceof ProviderSecurityError) return error; + return new ProviderSecurityError( + "authority_error", + error instanceof Error ? error.message : "unknown provider-security failure", + ); +} + +export class ProviderCredentialResolver { + readonly client: ProviderSecurityClient; + private readonly slotStore: CredentialSlotStore; + private readonly degraded: DegradedModeController; + private readonly now: () => number; + + constructor(deps: ResolveCredentialDeps = {}) { + this.client = deps.client ?? ProviderSecurityClient.fromEnv(); + this.slotStore = deps.slotStore ?? globalCredentialSlotStore; + this.degraded = deps.degraded ?? new DegradedModeController(this.slotStore); + this.now = deps.now ?? (() => Date.now()); + } + + async resolveCredentialRef(ref: string, deps: { jitterMs?: number } = {}): Promise { + const invalid = validateChefVaultRef(ref); + if (invalid) throw invalid; + + const at = this.now(); + const existing = this.slotStore.snapshotForRequest(ref, at); + if (existing && existing.expiresAt > at) { + const renewTarget = this.slotStore.getState(ref)?.slots.active; + if (renewTarget && shouldRenewLease(renewTarget, at, deps.jitterMs ?? renewalJitterMs())) { + await this.tryRenew(ref, renewTarget.leaseId, renewTarget.fencingToken, "active").catch(() => { + // Renewal failure keeps the current snapshot until expiry; degraded handling applies on resolve. + }); + const refreshed = this.slotStore.snapshotForRequest(ref, this.now()); + if (refreshed) { + return { apiKey: refreshed.secret, snapshot: refreshed, source: "memory" }; + } + } + return { apiKey: existing.secret, snapshot: existing, source: "memory" }; + } + + const wasDegraded = this.degraded.isDegraded(ref); + + const state = this.slotStore.getState(ref); + try { + const response = await this.client.resolveLease({ + ref, + ...(state && state.lastFencingToken > 0 ? { fencingToken: state.lastFencingToken } : {}), + }); + this.degraded.markRecovered(ref); + const lease = this.slotStore.applyResolve(ref, response, this.now()); + const snapshot = this.slotStore.snapshotForRequest(ref, this.now()); + if (!snapshot) { + throw new ProviderSecurityError("authority_error", "resolve succeeded but no usable snapshot was stored"); + } + return { apiKey: lease.secret, snapshot, source: "chefvault" }; + } catch (error) { + this.degraded.markUnavailable(ref, at); + if (wasDegraded) { + throw new ProviderSecurityError( + "degraded_deny_resolve", + "ChefVault is unavailable; new credential resolution is denied in degraded mode", + ); + } + throw toProviderSecurityError(error); + } + } + + private async tryRenew( + ref: string, + leaseId: string, + fencingToken: number, + phase: "active" | "next" | "retiring", + ): Promise { + const response = await this.client.renewLease({ ref, leaseId, fencingToken }); + this.degraded.markRecovered(ref); + this.slotStore.applyRenew(ref, response, phase, this.now()); + } + + async probeAuthority(): Promise<{ ok: boolean; message: string }> { + return this.client.healthz(); + } +} + +export const globalProviderCredentialResolver = new ProviderCredentialResolver(); + +export async function resolveChefVaultCredential( + ref: string, + deps?: ResolveCredentialDeps, +): Promise { + return new ProviderCredentialResolver(deps).resolveCredentialRef(ref); +} + +export function providerSecurityErrorCode(error: unknown): ProviderSecurityErrorCode { + if (error instanceof ProviderSecurityError) return error.code; + return "authority_error"; +} diff --git a/src/provider-security/slots.ts b/src/provider-security/slots.ts new file mode 100644 index 000000000..da32afe80 --- /dev/null +++ b/src/provider-security/slots.ts @@ -0,0 +1,209 @@ +/** + * In-memory credential slot model (PSP-008). + * + * Slots: active / next / retiring / revoked. Raw secrets live only in process memory. + */ +import { + ProviderSecurityError, + type ChefVaultRenewResponse, + type ChefVaultResolveResponse, + type CredentialLease, + type CredentialSlotPhase, + type CredentialSnapshot, + type ProviderSecurityMode, + type RedactedProviderSecurityStatus, + type RedactedSlotSummary, + type SlotStoreState, +} from "./types"; + +const RENEWAL_LEAD_MS = 5 * 60_000; +const RENEWAL_JITTER_MS = 30_000; + +function nowMs(): number { + return Date.now(); +} + +function isUsableLease(lease: CredentialLease | undefined, at = nowMs()): lease is CredentialLease { + return !!lease && lease.phase !== "revoked" && lease.expiresAt > at; +} + +function freezeSnapshot(lease: CredentialLease): CredentialSnapshot { + if (lease.phase === "revoked") { + throw new Error("revoked leases cannot produce request snapshots"); + } + return Object.freeze({ + ref: lease.ref, + leaseId: lease.leaseId, + secret: lease.secret, + expiresAt: lease.expiresAt, + fencingToken: lease.fencingToken, + phase: lease.phase, + }); +} + +function toLease( + ref: string, + response: ChefVaultResolveResponse | ChefVaultRenewResponse, + phase: CredentialSlotPhase, + resolvedAt = nowMs(), +): CredentialLease { + return { + ref, + leaseId: response.leaseId, + secret: response.secret, + expiresAt: response.expiresAt, + fencingToken: response.fencingToken, + phase, + resolvedAt, + }; +} + +export function renewalJitterMs(seed = Math.random()): number { + return Math.floor(seed * RENEWAL_JITTER_MS); +} + +export function shouldRenewLease(lease: CredentialLease | undefined, at = nowMs(), jitterMs = 0): boolean { + if (!isUsableLease(lease, at)) return false; + return lease.expiresAt - at <= RENEWAL_LEAD_MS + jitterMs; +} + +export class CredentialSlotStore { + private readonly stores = new Map(); + + private ensure(ref: string): SlotStoreState { + const existing = this.stores.get(ref); + if (existing) return existing; + const created: SlotStoreState = { + ref, + mode: "normal", + lastFencingToken: 0, + slots: {}, + degradedSince: null, + lastRenewalAt: null, + }; + this.stores.set(ref, created); + return created; + } + + getState(ref: string): SlotStoreState | undefined { + const state = this.stores.get(ref); + if (!state) return undefined; + return { + ...state, + slots: { ...state.slots }, + }; + } + + enterDegraded(ref: string, at = nowMs()): void { + const state = this.ensure(ref); + state.mode = "degraded"; + state.degradedSince ??= at; + } + + exitDegraded(ref: string): void { + const state = this.stores.get(ref); + if (!state) return; + state.mode = "normal"; + state.degradedSince = null; + } + + getMode(ref: string): ProviderSecurityMode { + return this.ensure(ref).mode; + } + + /** Immutable snapshot for an in-flight upstream request. Prefers active, then retiring. */ + snapshotForRequest(ref: string, at = nowMs()): CredentialSnapshot | null { + const state = this.stores.get(ref); + if (!state) return null; + const active = state.slots.active; + if (isUsableLease(active, at)) return freezeSnapshot(active); + const retiring = state.slots.retiring; + if (isUsableLease(retiring, at)) return freezeSnapshot(retiring); + return null; + } + + applyResolve(ref: string, response: ChefVaultResolveResponse, at = nowMs()): CredentialLease { + const state = this.ensure(ref); + if (response.fencingToken <= state.lastFencingToken) { + throw new ProviderSecurityError("stale_fencing_token", "fencing token is stale"); + } + + const targetPhase = response.slotHint ?? "active"; + const previousActive = state.slots.active; + const lease = toLease(ref, response, targetPhase, at); + + if (targetPhase === "active") { + if (previousActive && previousActive.leaseId !== lease.leaseId) { + state.slots.retiring = { ...previousActive, phase: "retiring" }; + } + state.slots.active = lease; + } else if (targetPhase === "next") { + state.slots.next = lease; + } else { + state.slots.retiring = lease; + } + + state.lastFencingToken = response.fencingToken; + state.lastRenewalAt = at; + return lease; + } + + applyRenew(ref: string, response: ChefVaultRenewResponse, phase: CredentialSlotPhase, at = nowMs()): CredentialLease { + const state = this.ensure(ref); + if (response.fencingToken <= state.lastFencingToken) { + throw new ProviderSecurityError("stale_fencing_token", "fencing token is stale"); + } + const lease = toLease(ref, response, phase, at); + state.slots[phase] = lease; + state.lastFencingToken = response.fencingToken; + state.lastRenewalAt = at; + return lease; + } + + promoteNextToActive(ref: string, at = nowMs()): CredentialLease | null { + const state = this.stores.get(ref); + const next = state?.slots.next; + if (!state || !isUsableLease(next, at)) return null; + const previousActive = state.slots.active; + if (previousActive) { + state.slots.retiring = { ...previousActive, phase: "retiring" }; + } + const promoted: CredentialLease = { ...next, phase: "active", resolvedAt: at }; + state.slots.active = promoted; + delete state.slots.next; + return promoted; + } + + revokePhase(ref: string, phase: CredentialSlotPhase): void { + const state = this.stores.get(ref); + const lease = state?.slots[phase]; + if (!state || !lease) return; + state.slots[phase] = { ...lease, phase: "revoked" }; + } + + redactedStatus(ref: string, at = nowMs()): RedactedProviderSecurityStatus { + const state = this.ensure(ref); + const slots: RedactedSlotSummary[] = (["active", "next", "retiring", "revoked"] as const).flatMap(phase => { + const lease = state.slots[phase]; + if (!lease) return []; + return [{ + phase, + leaseId: lease.leaseId, + expiresAt: lease.expiresAt, + fencingToken: lease.fencingToken, + valid: isUsableLease(lease, at), + }]; + }); + return { + ref, + mode: state.mode, + degradedSince: state.degradedSince, + lastFencingToken: state.lastFencingToken, + slots, + hasUsableCredential: slots.some(slot => slot.valid && slot.phase !== "revoked"), + }; + } +} + +/** Process-wide slot store — secrets never leave memory or hit disk. */ +export const globalCredentialSlotStore = new CredentialSlotStore(); diff --git a/src/provider-security/status.ts b/src/provider-security/status.ts new file mode 100644 index 000000000..9e138496d --- /dev/null +++ b/src/provider-security/status.ts @@ -0,0 +1,142 @@ +/** + * Redacted provider-security status for doctor/status surfaces (PSP-008). + */ +import type { OcxConfig, OcxProviderConfig } from "../types"; +import { ProviderSecurityClient } from "./client"; +import { globalCredentialSlotStore } from "./slots"; +import { ProviderCredentialResolver } from "./resolve"; +import { isChefVaultRef, type RedactedProviderSecurityStatus } from "./types"; + +export interface ProviderSecurityDoctorCheck { + level: "OK" | "WARN"; + provider: string; + message: string; +} + +export interface ProviderSecurityStatusReport { + authority: { ok: boolean; url: string; message: string }; + providers: Array<{ + provider: string; + credentialRef: string; + status: RedactedProviderSecurityStatus; + }>; +} + +export function listChefVaultProviders(config: OcxConfig): Array<{ name: string; ref: string; provider: OcxProviderConfig }> { + return Object.entries(config.providers).flatMap(([name, provider]) => { + const ref = provider.credentialRef?.trim(); + if (!ref || !isChefVaultRef(ref)) return []; + return [{ name, ref, provider }]; + }); +} + +export function collectProviderSecurityStatus( + config: OcxConfig, + client: ProviderSecurityClient = ProviderSecurityClient.fromEnv(), + slotStore: typeof globalCredentialSlotStore = globalCredentialSlotStore, +): ProviderSecurityStatusReport { + const authority = client.baseUrl; + return { + authority: { ok: false, url: authority, message: "not probed" }, + providers: listChefVaultProviders(config).map(entry => ({ + provider: entry.name, + credentialRef: entry.ref, + status: slotStore.redactedStatus(entry.ref), + })), + }; +} + +export async function collectProviderSecurityStatusAsync( + config: OcxConfig, + resolver: ProviderCredentialResolver = new ProviderCredentialResolver(), +): Promise { + const authorityProbe = await resolver.probeAuthority(); + const base = collectProviderSecurityStatus(config, resolver.client); + base.authority = { + ok: authorityProbe.ok, + url: resolver.client.baseUrl, + message: authorityProbe.message, + }; + + for (const entry of base.providers) { + try { + await resolver.resolveCredentialRef(entry.credentialRef); + entry.status = globalCredentialSlotStore.redactedStatus(entry.credentialRef); + } catch { + entry.status = globalCredentialSlotStore.redactedStatus(entry.credentialRef); + } + } + + return base; +} + +export async function collectProviderSecurityDoctorChecks( + config: OcxConfig, + resolver: ProviderCredentialResolver = new ProviderCredentialResolver(), +): Promise { + const checks: ProviderSecurityDoctorCheck[] = []; + const refs = listChefVaultProviders(config); + const authority = await resolver.probeAuthority(); + + if (authority.ok) { + checks.push({ + level: "OK", + provider: "*", + message: `ChefVault provider-security reachable (${resolver.client.baseUrl}).`, + }); + } else { + checks.push({ + level: "WARN", + provider: "*", + message: `ChefVault provider-security unavailable (${authority.message}). Degraded mode: existing in-memory leases only; new resolve denied.`, + }); + } + + if (refs.length === 0) { + checks.push({ + level: "OK", + provider: "*", + message: "No providers configured with chefvault:// credentialRef.", + }); + return checks; + } + + for (const { name, ref } of refs) { + const status = globalCredentialSlotStore.redactedStatus(ref); + if (status.mode === "degraded") { + checks.push({ + level: "WARN", + provider: name, + message: `Provider "${name}" is in degraded mode for ${ref}; only bounded in-memory credentials may be used.`, + }); + continue; + } + if (status.hasUsableCredential) { + checks.push({ + level: "OK", + provider: name, + message: `Provider "${name}" has a usable in-memory lease for ${ref}.`, + }); + continue; + } + if (authority.ok) { + checks.push({ + level: "WARN", + provider: name, + message: `Provider "${name}" references ${ref} but has no resolved in-memory lease yet.`, + }); + } else { + checks.push({ + level: "WARN", + provider: name, + message: `Provider "${name}" cannot resolve ${ref} while ChefVault is unavailable.`, + }); + } + } + + return checks; +} + +export function serializeProviderSecurityStatus(report: ProviderSecurityStatusReport): string { + return JSON.stringify(report); +} diff --git a/src/provider-security/types.ts b/src/provider-security/types.ts new file mode 100644 index 000000000..d789b029e --- /dev/null +++ b/src/provider-security/types.ts @@ -0,0 +1,138 @@ +/** ChefVault provider-security plane — shared types and error taxonomy (PSP-008). */ + +export const CHEFVAULT_REF_PREFIX = "chefvault://"; + +export type CredentialSlotPhase = "active" | "next" | "retiring" | "revoked"; + +export type ProviderSecurityMode = "normal" | "degraded"; + +/** Stable error codes surfaced to callers and doctor/status. */ +export type ProviderSecurityErrorCode = + | "authority_unavailable" + | "stale_fencing_token" + | "ref_invalid" + | "ref_not_found" + | "lease_expired" + | "degraded_deny_resolve" + | "revoked" + | "network_error" + | "authority_error"; + +export class ProviderSecurityError extends Error { + readonly code: ProviderSecurityErrorCode; + + constructor(code: ProviderSecurityErrorCode, message: string) { + super(message); + this.name = "ProviderSecurityError"; + this.code = code; + } +} + +export interface WorkloadIdentity { + workloadId: string; + hostId: string; + actor: string; +} + +export interface ChefVaultResolveRequest { + ref: string; + fencingToken?: number; +} + +export interface ChefVaultResolveResponse { + leaseId: string; + secret: string; + expiresAt: number; + fencingToken: number; + slotHint?: Exclude; +} + +export interface ChefVaultRenewRequest { + ref: string; + leaseId: string; + fencingToken: number; +} + +export interface ChefVaultRenewResponse { + leaseId: string; + secret: string; + expiresAt: number; + fencingToken: number; +} + +/** In-memory lease material — never persisted to disk. */ +export interface CredentialLease { + ref: string; + leaseId: string; + secret: string; + expiresAt: number; + fencingToken: number; + phase: CredentialSlotPhase; + resolvedAt: number; +} + +/** Immutable credential view handed to a single upstream request. */ +export interface CredentialSnapshot { + readonly ref: string; + readonly leaseId: string; + readonly secret: string; + readonly expiresAt: number; + readonly fencingToken: number; + readonly phase: Exclude; +} + +export interface SlotStoreState { + ref: string; + mode: ProviderSecurityMode; + lastFencingToken: number; + slots: Partial>; + degradedSince: number | null; + lastRenewalAt: number | null; +} + +/** Redacted slot summary safe for doctor/status/telemetry. */ +export interface RedactedSlotSummary { + phase: CredentialSlotPhase; + leaseId: string; + expiresAt: number; + fencingToken: number; + valid: boolean; +} + +export interface RedactedProviderSecurityStatus { + ref: string; + mode: ProviderSecurityMode; + degradedSince: number | null; + lastFencingToken: number; + slots: RedactedSlotSummary[]; + hasUsableCredential: boolean; +} + +export interface ProviderSecurityClientConfig { + baseUrl: string; + workload: WorkloadIdentity; + fetchImpl?: typeof fetch; + requestTimeoutMs?: number; +} + +export function isChefVaultRef(value: string | undefined): value is string { + return typeof value === "string" && value.startsWith(CHEFVAULT_REF_PREFIX) && value.length > CHEFVAULT_REF_PREFIX.length; +} + +export function validateChefVaultRef(ref: string): ProviderSecurityError | null { + if (!isChefVaultRef(ref)) { + return new ProviderSecurityError("ref_invalid", `credential ref must start with ${CHEFVAULT_REF_PREFIX}`); + } + try { + const parsed = new URL(ref); + if (parsed.protocol !== "chefvault:") { + return new ProviderSecurityError("ref_invalid", "credential ref must use chefvault:// scheme"); + } + if (!parsed.pathname || parsed.pathname === "/") { + return new ProviderSecurityError("ref_invalid", "credential ref must include a resource path"); + } + } catch { + return new ProviderSecurityError("ref_invalid", "credential ref is not a valid chefvault:// URI"); + } + return null; +} diff --git a/src/server/auth-cors.ts b/src/server/auth-cors.ts index 4f806e87e..d24945eb9 100644 --- a/src/server/auth-cors.ts +++ b/src/server/auth-cors.ts @@ -386,7 +386,7 @@ export function safeConfigDTO(config: OcxConfig): unknown { const dto: Record = { adapter: provider.adapter, baseUrl: publicProviderBaseUrl(provider.baseUrl), - hasApiKey: !!provider.apiKey, + hasApiKey: !!provider.apiKey || !!provider.credentialRef, hasHeaders: !!provider.headers && Object.keys(provider.headers).length > 0, }; for (const key of [ @@ -395,6 +395,7 @@ export function safeConfigDTO(config: OcxConfig): unknown { "allowPrivateNetwork", "authMode", "apiKeyTransport", + "credentialRef", "keyOptional", "freeTier", "liveModels", diff --git a/src/types.ts b/src/types.ts index 4ede9c80c..ffda13e6e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -998,6 +998,11 @@ export interface OcxProviderConfig { * failover engine; "direct" pins the caller's main Codex login and never touches pool state. */ codexAccountMode?: CodexAccountMode; + /** + * ChefVault credential reference (`chefvault://…`) resolved via the provider-security plane. + * When set, raw secrets are not stored in config — only in-memory leases at runtime. + */ + credentialRef?: string; apiKey?: string; /** * Key-auth header style for Anthropic-compatible providers. diff --git a/tests/provider-security.test.ts b/tests/provider-security.test.ts new file mode 100644 index 000000000..c52c552f5 --- /dev/null +++ b/tests/provider-security.test.ts @@ -0,0 +1,246 @@ +import { describe, expect, test } from "bun:test"; +import { + CredentialSlotStore, + renewalJitterMs, + shouldRenewLease, +} from "../src/provider-security/slots"; +import { DegradedModeController } from "../src/provider-security/degraded"; +import { ProviderSecurityClient } from "../src/provider-security/client"; +import { ProviderCredentialResolver } from "../src/provider-security/resolve"; +import { + ProviderSecurityError, + validateChefVaultRef, +} from "../src/provider-security"; +import { + collectProviderSecurityStatus, + collectProviderSecurityDoctorChecks, + serializeProviderSecurityStatus, +} from "../src/provider-security/status"; +import type { OcxConfig } from "../src/types"; + +const REF = "chefvault://providers/demo/prod"; + +function leaseResponse( + overrides: Partial<{ leaseId: string; secret: string; expiresAt: number; fencingToken: number; slotHint?: "active" | "next" | "retiring" }> = {}, +) { + return { + leaseId: overrides.leaseId ?? "lease-1", + secret: overrides.secret ?? "sk-live-secret-value", + expiresAt: overrides.expiresAt ?? Date.now() + 60_000, + fencingToken: overrides.fencingToken ?? 1, + ...(overrides.slotHint ? { slotHint: overrides.slotHint } : {}), + }; +} + +function mockFetch(handlers: { + healthz?: () => Response | Promise; + resolve?: (body: unknown) => Response | Promise; + renew?: (body: unknown) => Response | Promise; +}): typeof fetch { + return (async (input: RequestInfo | URL, init?: RequestInit) => { + const url = String(input); + if (url.endsWith("/healthz")) { + return handlers.healthz?.() ?? new Response("ok", { status: 200 }); + } + if (url.endsWith("/v1/credentials/resolve")) { + const body = init?.body ? JSON.parse(String(init.body)) : {}; + return handlers.resolve?.(body) ?? Response.json(leaseResponse(), { status: 200 }); + } + if (url.endsWith("/v1/credentials/renew")) { + const body = init?.body ? JSON.parse(String(init.body)) : {}; + return handlers.renew?.(body) ?? Response.json(leaseResponse({ fencingToken: 2 }), { status: 200 }); + } + return new Response("not found", { status: 404 }); + }) as typeof fetch; +} + +describe("chefvault ref validation", () => { + test("accepts a scoped chefvault:// ref", () => { + expect(validateChefVaultRef(REF)).toBeNull(); + }); + + test("rejects bare paths", () => { + expect(validateChefVaultRef("providers/demo")?.code).toBe("ref_invalid"); + }); +}); + +describe("credential slot transitions", () => { + test("moves previous active lease to retiring on rotation", () => { + const store = new CredentialSlotStore(); + const at = 1_700_000_000_000; + store.applyResolve(REF, leaseResponse({ leaseId: "a", fencingToken: 1 }), at); + store.applyResolve(REF, leaseResponse({ leaseId: "b", fencingToken: 2, secret: "sk-next" }), at + 1); + + const state = store.getState(REF)!; + expect(state.slots.active?.leaseId).toBe("b"); + expect(state.slots.retiring?.leaseId).toBe("a"); + expect(state.slots.retiring?.phase).toBe("retiring"); + }); + + test("promotes next slot to active", () => { + const store = new CredentialSlotStore(); + const at = 1_700_000_000_000; + store.applyResolve(REF, leaseResponse({ leaseId: "active", fencingToken: 1 }), at); + store.applyResolve(REF, leaseResponse({ leaseId: "queued", fencingToken: 2, slotHint: "next" }), at + 1); + + const promoted = store.promoteNextToActive(REF, at + 2); + expect(promoted?.leaseId).toBe("queued"); + expect(store.getState(REF)?.slots.active?.leaseId).toBe("queued"); + expect(store.getState(REF)?.slots.retiring?.leaseId).toBe("active"); + }); + + test("request snapshots are immutable", () => { + const store = new CredentialSlotStore(); + store.applyResolve(REF, leaseResponse(), Date.now()); + const snapshot = store.snapshotForRequest(REF)!; + expect(Object.isFrozen(snapshot)).toBe(true); + expect(() => { + (snapshot as { secret: string }).secret = "mutated"; + }).toThrow(); + }); + + test("rejects stale fencing tokens", () => { + const store = new CredentialSlotStore(); + store.applyResolve(REF, leaseResponse({ fencingToken: 5 }), Date.now()); + expect(() => store.applyResolve(REF, leaseResponse({ fencingToken: 4 }), Date.now())) + .toThrow(ProviderSecurityError); + try { + store.applyResolve(REF, leaseResponse({ fencingToken: 3 }), Date.now()); + } catch (error) { + expect((error as ProviderSecurityError).code).toBe("stale_fencing_token"); + } + }); + + test("renewal jitter stays within bound", () => { + expect(renewalJitterMs(0)).toBe(0); + expect(renewalJitterMs(0.999)).toBeLessThan(30_000); + }); + + test("shouldRenewLease triggers inside lead window", () => { + const store = new CredentialSlotStore(); + const at = 1_000_000; + store.applyResolve(REF, leaseResponse({ expiresAt: at + 4 * 60_000 }), at); + const active = store.getState(REF)?.slots.active; + expect(shouldRenewLease(active, at)).toBe(true); + store.applyResolve(REF, leaseResponse({ expiresAt: at + 60 * 60_000, fencingToken: 2 }), at); + const fresh = store.getState(REF)?.slots.active; + expect(shouldRenewLease(fresh, at)).toBe(false); + }); +}); + +describe("degraded mode", () => { + test("denies new resolve but allows bounded existing credentials", async () => { + const store = new CredentialSlotStore(); + const degraded = new DegradedModeController(store); + const at = 1_700_000_000_000; + store.applyResolve(REF, leaseResponse({ expiresAt: at + 60 * 60_000 }), at); + degraded.markUnavailable(REF, at); + + expect(degraded.canResolve(REF).allowed).toBe(false); + expect(degraded.canUseExisting(REF, at).allowed).toBe(true); + + const resolver = new ProviderCredentialResolver({ + slotStore: store, + degraded, + now: () => at, + client: new ProviderSecurityClient({ + baseUrl: "http://vault.test", + workload: { workloadId: "t", hostId: "h", actor: "a" }, + fetchImpl: mockFetch({ + resolve: () => Response.json({ code: "stale", message: "should not resolve" }, { status: 503 }), + }), + }), + }); + + const fromMemory = await resolver.resolveCredentialRef(REF); + expect(fromMemory.source).toBe("memory"); + + store.revokePhase(REF, "active"); + store.revokePhase(REF, "retiring"); + await expect(resolver.resolveCredentialRef(REF)).rejects.toMatchObject({ + code: "degraded_deny_resolve", + }); + }); + + test("recovers after authority returns", async () => { + const store = new CredentialSlotStore(); + const degraded = new DegradedModeController(store); + let calls = 0; + const client = new ProviderSecurityClient({ + baseUrl: "http://vault.test", + workload: { workloadId: "t", hostId: "h", actor: "a" }, + fetchImpl: mockFetch({ + resolve: () => { + calls += 1; + if (calls === 1) { + return new Response(JSON.stringify({ message: "down" }), { status: 503 }); + } + return Response.json(leaseResponse({ fencingToken: 1, leaseId: "fresh" }), { status: 200 }); + }, + }), + }); + const resolver = new ProviderCredentialResolver({ slotStore: store, degraded, client }); + + await expect(resolver.resolveCredentialRef(REF)).rejects.toBeInstanceOf(ProviderSecurityError); + expect(store.getMode(REF)).toBe("degraded"); + + const resolved = await resolver.resolveCredentialRef(REF); + expect(resolved.snapshot.leaseId).toBe("fresh"); + expect(store.getMode(REF)).toBe("normal"); + }); +}); + +describe("provider-security client headers", () => { + test("sends workload identity headers on resolve", async () => { + let headers: Record = {}; + const client = new ProviderSecurityClient({ + baseUrl: "http://vault.test", + workload: { workloadId: "ocx", hostId: "sofie", actor: "doctor" }, + fetchImpl: (async (_input, init) => { + headers = Object.fromEntries(new Headers(init?.headers).entries()); + return Response.json(leaseResponse(), { status: 200 }); + }) as typeof fetch, + }); + + await client.resolveLease({ ref: REF }); + expect(headers["x-chef-workload-id"]).toBe("ocx"); + expect(headers["x-chef-host-id"]).toBe("sofie"); + expect(headers["x-chef-actor"]).toBe("doctor"); + }); +}); + +describe("redacted status serialization", () => { + test("never includes raw secret material", async () => { + const store = new CredentialSlotStore(); + store.applyResolve(REF, leaseResponse({ secret: "sk-super-secret-12345" }), Date.now()); + + const config: OcxConfig = { + port: 10100, + defaultProvider: "demo", + providers: { + demo: { + adapter: "openai-chat", + baseUrl: "https://example/v1", + credentialRef: REF, + }, + }, + }; + + const report = collectProviderSecurityStatus(config, undefined, store); + const serialized = serializeProviderSecurityStatus(report); + expect(serialized).not.toContain("sk-super-secret-12345"); + expect(serialized).not.toContain("secret"); + expect(report.providers[0]?.status.slots[0]?.leaseId).toBe("lease-1"); + + const doctor = await collectProviderSecurityDoctorChecks(config, new ProviderCredentialResolver({ + slotStore: store, + client: new ProviderSecurityClient({ + baseUrl: "http://vault.test", + workload: { workloadId: "t", hostId: "h", actor: "a" }, + fetchImpl: mockFetch({ healthz: () => new Response("ok", { status: 200 }) }), + }), + })); + const doctorText = JSON.stringify(doctor); + expect(doctorText).not.toContain("sk-super-secret-12345"); + }); +}); From 805bfd6df0bcc6e1cd9bcbde24f4720cb6779e30 Mon Sep 17 00:00:00 2001 From: OnlineChef <280567955+OnlineChef@users.noreply.github.com> Date: Sat, 1 Aug 2026 09:08:05 +0200 Subject: [PATCH 02/19] feat(provider-security): ChefVault client, slots, and degraded mode Add in-memory credential slots, workload-authenticated ChefVault client, chefvault:// resolve path, redacted doctor/status, and bounded degraded mode when the secret authority is unavailable. Co-authored-by: Cursor --- tests/provider-security.test.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/provider-security.test.ts b/tests/provider-security.test.ts index c52c552f5..587afa08a 100644 --- a/tests/provider-security.test.ts +++ b/tests/provider-security.test.ts @@ -25,7 +25,7 @@ function leaseResponse( ) { return { leaseId: overrides.leaseId ?? "lease-1", - secret: overrides.secret ?? "sk-live-secret-value", + secret: overrides.secret ?? "skfix1", expiresAt: overrides.expiresAt ?? Date.now() + 60_000, fencingToken: overrides.fencingToken ?? 1, ...(overrides.slotHint ? { slotHint: overrides.slotHint } : {}), @@ -69,7 +69,7 @@ describe("credential slot transitions", () => { const store = new CredentialSlotStore(); const at = 1_700_000_000_000; store.applyResolve(REF, leaseResponse({ leaseId: "a", fencingToken: 1 }), at); - store.applyResolve(REF, leaseResponse({ leaseId: "b", fencingToken: 2, secret: "sk-next" }), at + 1); + store.applyResolve(REF, leaseResponse({ leaseId: "b", fencingToken: 2, secret: "skn2" }), at + 1); const state = store.getState(REF)!; expect(state.slots.active?.leaseId).toBe("b"); @@ -212,7 +212,7 @@ describe("provider-security client headers", () => { describe("redacted status serialization", () => { test("never includes raw secret material", async () => { const store = new CredentialSlotStore(); - store.applyResolve(REF, leaseResponse({ secret: "sk-super-secret-12345" }), Date.now()); + store.applyResolve(REF, leaseResponse({ secret: "skfix" }), Date.now()); const config: OcxConfig = { port: 10100, @@ -228,7 +228,7 @@ describe("redacted status serialization", () => { const report = collectProviderSecurityStatus(config, undefined, store); const serialized = serializeProviderSecurityStatus(report); - expect(serialized).not.toContain("sk-super-secret-12345"); + expect(serialized).not.toContain("skfix"); expect(serialized).not.toContain("secret"); expect(report.providers[0]?.status.slots[0]?.leaseId).toBe("lease-1"); @@ -241,6 +241,6 @@ describe("redacted status serialization", () => { }), })); const doctorText = JSON.stringify(doctor); - expect(doctorText).not.toContain("sk-super-secret-12345"); + expect(doctorText).not.toContain("skfix"); }); }); From 32910a3a29cf1853247129d5b491a95406a00248 Mon Sep 17 00:00:00 2001 From: OnlineChef <280567955+OnlineChef@users.noreply.github.com> Date: Sat, 1 Aug 2026 09:40:04 +0200 Subject: [PATCH 03/19] fix(ci): refresh gui bun.lock for frozen install CI failed on bun install --frozen-lockfile after provider-security and fallback GUI dependency resolution drifted from the lockfile. Co-authored-by: Cursor --- gui/bun.lock | 150 +++++++++++++++++++++++++-------------------------- 1 file changed, 75 insertions(+), 75 deletions(-) diff --git a/gui/bun.lock b/gui/bun.lock index 56c90814e..36debcbea 100644 --- a/gui/bun.lock +++ b/gui/bun.lock @@ -35,7 +35,7 @@ "@babel/core": ["@babel/core@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-module-transforms": "^7.29.7", "@babel/helpers": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA=="], - "@babel/generator": ["@babel/generator@7.29.7", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="], + "@babel/generator": ["@babel/generator@7.29.8", "", { "dependencies": { "@babel/parser": "^7.29.8", "@babel/types": "^7.29.8", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg=="], "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.29.7", "", { "dependencies": { "@babel/compat-data": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g=="], @@ -53,27 +53,27 @@ "@babel/helpers": ["@babel/helpers@7.29.7", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg=="], - "@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], + "@babel/parser": ["@babel/parser@7.29.8", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": "./bin/babel-parser.js" }, "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA=="], "@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="], - "@babel/traverse": ["@babel/traverse@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/types": "^7.29.7", "debug": "^4.3.1" } }, "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw=="], + "@babel/traverse": ["@babel/traverse@7.29.8", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.8", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.8", "@babel/template": "^7.29.7", "@babel/types": "^7.29.8", "debug": "^4.3.1" } }, "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg=="], - "@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], + "@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="], - "@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="], + "@emnapi/core": ["@emnapi/core@2.0.0-alpha.3", "", { "dependencies": { "@emnapi/wasi-threads": "2.0.1", "tslib": "^2.4.0" } }, "sha512-AZypUeJ/yByuxyS7BlSNRDOMLMlROYtjYdIAuBmJssVz1UJDSeYxLrdizhXCFYhedC5bqd/ASy8EuNXbVVXp9g=="], - "@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="], + "@emnapi/runtime": ["@emnapi/runtime@2.0.0-alpha.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-hFPAhMUjJD9BSyCANEISPOogeXC9Zo9ZQl7L6vKnaVsMkCtzznaW/naYypeyl0Gv5rYfWYsZbpixTMpjDJzQeA=="], - "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="], + "@emnapi/wasi-threads": ["@emnapi/wasi-threads@2.0.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9DsSk+o5NBX0CCJT8s0EROGSGxjR/tKu6aBTaVyq+SjAEQH4XcdcRxPBRzsBLizTTJ49MJjF+jgu3qnO9GLQcQ=="], - "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.10.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg=="], "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], "@eslint/config-array": ["@eslint/config-array@0.23.5", "", { "dependencies": { "@eslint/object-schema": "^3.0.5", "debug": "^4.3.1", "minimatch": "^10.2.4" } }, "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA=="], - "@eslint/config-helpers": ["@eslint/config-helpers@0.6.0", "", { "dependencies": { "@eslint/core": "^1.2.1" } }, "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA=="], + "@eslint/config-helpers": ["@eslint/config-helpers@0.7.0", "", { "dependencies": { "@eslint/core": "^1.2.1" } }, "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw=="], "@eslint/core": ["@eslint/core@1.2.1", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ=="], @@ -107,45 +107,45 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], - "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.6", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg=="], + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.2.2", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.3", "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.3" } }, "sha512-JfB4kuJQjaoHuCTseIINHtHWeJnvgEcxjwA5t/Y00ZgaOO1Crz3fjT/p8kT28zA/Caz7oiUMn3d6H2yOVCVwuw=="], - "@oxc-project/types": ["@oxc-project/types@0.137.0", "", {}, "sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA=="], + "@oxc-project/types": ["@oxc-project/types@0.142.0", "", {}, "sha512-7W+2q5AKQVU36fkaryontrHn3YDt1RyUYXatw9i5H8ocYe2sPKSFB6eS8WNPeRKiN1qAWWZUPm7gwFzJGrccqQ=="], - "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.1.3", "", { "os": "android", "cpu": "arm64" }, "sha512-DT6Z3PhvioeHMvxo+xHc3KtqggrI7CCTXCmC2h/5zUlp5jVitv7XEy+9q5/7v8IolhlioawpMo8Kg0EEBy7J0g=="], + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.2.1", "", { "os": "android", "cpu": "arm64" }, "sha512-02hOeOSryYxVrOIphmLAsqnCJWxwlzFk+pEt/N/i6OgT3lShHO7xGCU5cpgchRDHboAEbSjzgGh+O/u1GswQmA=="], - "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.1.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-0NwgwsjM7LrsuVnXMK3koTpagBNOhloc/BNjKqZjv4V5zI5r13qx69uVhRx+o5Z0yy4Hzq+lpy7TAgUG/ocvrw=="], + "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.2.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-fMsTOnN0OjFm3CyppWPitKnc8UlliVARUULW6cfU6AIqjdtgmSFWSk9vecHzZduv/yMWIHDlRhM1e8Iff9uAfA=="], - "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.1.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-YtiBp4disu6V560loT6PjMdiRaWmVvDNrUunAalbiFx2ggeJwxdAsgZMcoGP17uyAsTwAj5V1niksxlHnVQ1Sw=="], + "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.2.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-1wjKdz/XLGKHaTNHjQveQ/B23TKx4ItAqm1JbyVuvNPc4Ze0Fb48s49TAd/2zcplPl8okE/UbTgmlVfwT7eFeQ=="], - "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.1.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-yD3EkEdXk2LypPxnf/kSZHirarsI8gcPzc62SukhR9VJTyvV+F9Q/GxWNuCojc7sXyuVC4DxRGhdDK4X8VSsbw=="], + "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.2.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Fa0jHR07E7YBN4vOEsbVf2briYNsuOowfLJaXULZM0ldMlaCaj2LJgLMbMe4iacRyZmvR8efFhgR9wKuGclQUg=="], - "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.1.3", "", { "os": "linux", "cpu": "arm" }, "sha512-c+8vieQbsD7HNAHKIA34w0GJ9FedFFuJGD+7E6vz7Q3uqAIugL5p45fhlsj4UaAsHpcmlqugBWMhA0/j7o0sIg=="], + "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.2.1", "", { "os": "linux", "cpu": "arm" }, "sha512-pzkgu1SSHGgRRyRZ4fbmSgmajbVt+epaLP99NDjFft69v/ypfTi6swBMiVdh2EkQ0OSnHE1lZDM7DRGkyAzUpA=="], - "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.1.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-50jD0uUwLvur7Zz9LHz17kaAdTPjn5wN93hEgjvmYFRZwiR7ZJYovTd5ipyWJDAnXKvZ+wgc+/Ika6dwSF5OcA=="], + "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-QI5SEDY8cbiYWHx0VO4vIc3UlS6a32vXHjU8Qy/17adEmZIPuByJg13UEvo9c/UCiUkdcVWY83C+b+JrwnNyUg=="], - "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.1.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-BO9+oPL8K9poZJBfYPsXNtYjPE5uM3qeehT3aFcW4LITOl+iSqhp0abzjR2nWBUNjIZeKXjAEWBZ64WjNoHd6w=="], + "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-Sm41FyCeXqmYcERoYOCbGIL5hNfd8w9LQ7Y61Bev48HkcjaJqV/iiVOaiDxjVTRMS+QKrZmD8cfPt4uMVnvM+A=="], - "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.1.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-f3VpLB1vQ0Eo6ecr/6cekLnvYMFF4YBFoVGkfkvPLq1bAkbAwHYQPZKoAmG6OJyTcxxoC+AvezGx/S1obNC0Mw=="], + "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.2.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-2x+WhXTGl9yJYPbltW/BSEPTVz9OIWQyER4N+gJEDWkkn904eRcBzELqh/Hf7K0w/ubGbKNMv0ZC+94QK/IFEg=="], - "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.1.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-AmurZ26Pqx/RI9N1gzEOCklkKXl927yjfXWUUS0O7Puh8ARM/Ob8qfrD3qnWksScdw6cSrW5PSHE9DyLu7+PtA=="], + "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.2.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-eEjmQpuRQayHPWWnywaWHkFT3ToPbP3RYy42VVd/B9aBGDA+Ol25EIWHxKQST3IiWJjikCWUF7KtbfqwZrzVwQ=="], - "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.1.3", "", { "os": "linux", "cpu": "x64" }, "sha512-JJpqs8bRGITDOdbkNKnlojzBabbOHrqjSvDr0IVsZObE1lBcPjxItUEY9eWIDbxaJ3cGrXPWGfGkIxFijg/URg=="], + "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-/Orga1fZYkLc/56jBICcHrKchl8Z2UKdDSr3LG9ToWO1lQ6a4Livk9Xz+9WN91zsz5QR3XQz2NNoSDEvP6qadw=="], - "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.1.3", "", { "os": "linux", "cpu": "x64" }, "sha512-rSJcdjPxzA/by/6/rYs+v+bXU7UjvnbUWz8MJb6kh6+knqB1dCrtHg0uu7C/4haqJvqdkYHQ5IGn+tCH9GLW/g=="], + "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-xxBJRL+0q0Kce7orznGWLuylHDY65vuARXZRpX+hPdv+DqK2c3NlCsVA98tlWzWNEE7yPqA/1NQ5nnCrj49Y5A=="], - "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.1.3", "", { "os": "none", "cpu": "arm64" }, "sha512-hQ3/PYkDJICgevvyNcVrihVeqq7k1Pp3VZ9lY+dauAYUJKO+auqApvANhvR1An9BhmqYKvW2Mu1F9u4DXSMLxQ=="], + "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.2.1", "", { "os": "none", "cpu": "arm64" }, "sha512-M6AdXIXw3s+/8XpKMzdGDEXGS1S7kwUsy+rcTIUIOx5Ge4nXKCtAFHFV9YKkXvGcC5WMoTjAteLzlsQROVI0Yw=="], - "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.1.3", "", { "dependencies": { "@emnapi/core": "1.11.1", "@emnapi/runtime": "1.11.1", "@napi-rs/wasm-runtime": "^1.1.6" }, "cpu": "none" }, "sha512-Elcv/BtML9lXrV6JuKITc/grN2kYV9gjsQpW8Jfw4ioK0TOkjBjye0nnyqQNy9STNaI20lXNaQBRrD5gSgR0Yg=="], + "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.2.1", "", { "dependencies": { "@emnapi/core": "2.0.0-alpha.3", "@emnapi/runtime": "2.0.0-alpha.3", "@napi-rs/wasm-runtime": "^1.2.0" } }, "sha512-/TX0SoRGojHzSAHpfVBbavRVSazg5U3h3Y3VXfcc0cdugq6kxdqw8LPGFiPr+/7gE/60zRcsOY2Vi9b9eT0jww=="], - "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.1.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-2DrEfhluH9yhiaFApmsjsjwrSYbNcY1oFTzYSP1a535jDbV98zCFanA/96TBUd0iDFcxGmw9QRExwGCXz3U+/g=="], + "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.2.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-EvRrivJieyHG+AO9lleZWgq+g0+S7oV2C51yuqlcyU/R9net+sI4Pj0F+lUoP2bEr6TWX3SqFaaS0SzfLxSzkw=="], - "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.1.3", "", { "os": "win32", "cpu": "x64" }, "sha512-OL4OMk7UPXOeVGGd3qo5zJyPIljf4AFgk5QAkPPS+OoLuOOozhuaQGC18MxVTnw/06q93gShAJzlwnSCY9YtqA=="], + "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.2.1", "", { "os": "win32", "cpu": "x64" }, "sha512-Z4eCmn5QJ/5+azF9knpLWKfVd9aidn0mAe9TpJgvBLId9Ax3t0+JVxBmT25Bv7NBbVW1TZyKjQjQReouMeH5UQ=="], "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="], - "@tanstack/react-virtual": ["@tanstack/react-virtual@3.14.5", "", { "dependencies": { "@tanstack/virtual-core": "3.17.3" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-4EKRXh7zBLkbKbFmG3AUVkircuHd+7OdT1pocJSepxtfBd3qnrJgJ5rtPkRYyo9fmyVb2+pI2xPy5oYvMLQy6A=="], + "@tanstack/react-virtual": ["@tanstack/react-virtual@3.14.9", "", { "dependencies": { "@tanstack/virtual-core": "3.17.7" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-qZyr0FZDP8rDC4WBhsryIZmAd9bveJvFGUJJtskWaew6/0dTRS6wZxnR6VQ5bY2KwL3LjerrHqQLk3a0GKcPXQ=="], - "@tanstack/virtual-core": ["@tanstack/virtual-core@3.17.3", "", {}, "sha512-8Np/TFELpI0ySuJoVmjvOrQYXH/8sTX0Biv9szhFhY39xOdAAY+smrMxjxOum/ux3eM8MUJQsEJ0/R0UpvC8dw=="], + "@tanstack/virtual-core": ["@tanstack/virtual-core@3.17.7", "", {}, "sha512-bp+v10y65sp2H7WpWfIMyxTNfl8ZVfxFTLRjPIFRryi6FV/J33z4IS53WO4pTk36KlvJ4iLiQz+oaydDC1xbcA=="], "@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="], @@ -155,39 +155,39 @@ "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], - "@types/node": ["@types/node@24.13.2", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA=="], + "@types/node": ["@types/node@24.13.3", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q=="], - "@types/react": ["@types/react@19.2.17", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw=="], + "@types/react": ["@types/react@19.2.18", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w=="], - "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], + "@types/react-dom": ["@types/react-dom@19.2.4", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw=="], "@types/whatwg-mimetype": ["@types/whatwg-mimetype@3.0.2", "", {}, "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA=="], "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.61.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.61.1", "@typescript-eslint/type-utils": "8.61.1", "@typescript-eslint/utils": "8.61.1", "@typescript-eslint/visitor-keys": "8.61.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.61.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-ZPlVl3PB3et/59Ne0fv/sci6ZXz4T4Hp4nTJ56i/Y0gR89ARb+KphojTq6j+56E5PIezmOIOOWyY+aWQFd+IkQ=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.65.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.65.0", "@typescript-eslint/type-utils": "8.65.0", "@typescript-eslint/utils": "8.65.0", "@typescript-eslint/visitor-keys": "8.65.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.65.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA=="], - "@typescript-eslint/parser": ["@typescript-eslint/parser@8.61.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.61.1", "@typescript-eslint/types": "8.61.1", "@typescript-eslint/typescript-estree": "8.61.1", "@typescript-eslint/visitor-keys": "8.61.1", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-PJ5vePq5/ognBbrIcoC5+SHO5dfpeLPzP9FpLkzWrguoYQEeeSjlJpVwOpo1JRSTEi7dRcwNy4h4dzV70PqHcg=="], + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.65.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.65.0", "@typescript-eslint/types": "8.65.0", "@typescript-eslint/typescript-estree": "8.65.0", "@typescript-eslint/visitor-keys": "8.65.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-CZ4nMxWwgu1HEEFNkeaCptra9QCtkmKdgf3sWh1rl1trIhmxLilgTV4cwcbQ4wemnT4sWQN8CaKOmdYx+g2gMA=="], - "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.61.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.61.1", "@typescript-eslint/types": "^8.61.1", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-PrC4JYGmR241lYnfhmKGTXkFqv8+ymbTFgSAY0fVXpY82/QkMw5TZPl+vGzuDDU2QYJk9fIDOBTntF+yDv9LEA=="], + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.65.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.65.0", "@typescript-eslint/types": "^8.65.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q=="], - "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.61.1", "", { "dependencies": { "@typescript-eslint/types": "8.61.1", "@typescript-eslint/visitor-keys": "8.61.1" } }, "sha512-L2bdIeoQS8FlKAvONAr20w6OcLXeB+qiDKbAooS9A0Ben+iSIkBef0FxqwKWYqt5sa0i4KJtxVyVmhMylKzF5w=="], + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.65.0", "", { "dependencies": { "@typescript-eslint/types": "8.65.0", "@typescript-eslint/visitor-keys": "8.65.0" } }, "sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg=="], - "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.61.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-UN/H4di+OO7EWx2ovME+8t31YO+KVnK0RRKEHR3kOt21/Ay8BOq3M1OMvWs5vNiqcFCYGYoxK3MXPZzmMUE+yg=="], + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.65.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg=="], - "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.61.1", "", { "dependencies": { "@typescript-eslint/types": "8.61.1", "@typescript-eslint/typescript-estree": "8.61.1", "@typescript-eslint/utils": "8.61.1", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-GYRicKmVK0C4fsKgaACaknOUAq9Oa2kwsjnpFhFcS/5p4Ht5IP9OVLbgIgcK4SRk92nVHFluurg1lumD9dBcLw=="], + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.65.0", "", { "dependencies": { "@typescript-eslint/types": "8.65.0", "@typescript-eslint/typescript-estree": "8.65.0", "@typescript-eslint/utils": "8.65.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-YjaZ7PRI5qY7ax2L3PbvX0rRyGtipAReCWs0mhhDBHjH/vl0g0BonaGXrKdKpMbIIsMIwDgbk/xzkBTyAltS5g=="], - "@typescript-eslint/types": ["@typescript-eslint/types@8.61.1", "", {}, "sha512-G+CRlPqLv7Bz1IZVs03x5K59F1veqL0EJUROAdGhKsEq8qOiRiZbI+HUojPq5l0fEGOKModD9br6lObhB8zkoA=="], + "@typescript-eslint/types": ["@typescript-eslint/types@8.65.0", "", {}, "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg=="], - "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.61.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.61.1", "@typescript-eslint/tsconfig-utils": "8.61.1", "@typescript-eslint/types": "8.61.1", "@typescript-eslint/visitor-keys": "8.61.1", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-u+oQD3BqYWPc8YV9Zab4vaJElJuwOLPRc10Jm1o/qS+6Qwen14HCWwx0Seo4LnSn2wxea2Ik8DxPt2/FHmuhrg=="], + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.65.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.65.0", "@typescript-eslint/tsconfig-utils": "8.65.0", "@typescript-eslint/types": "8.65.0", "@typescript-eslint/visitor-keys": "8.65.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg=="], - "@typescript-eslint/utils": ["@typescript-eslint/utils@8.61.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.61.1", "@typescript-eslint/types": "8.61.1", "@typescript-eslint/typescript-estree": "8.61.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-1+P/3Dj6jvtybE1q0HQ6yBt/gq+oKJyLdEv4HdnqasaEXRSYCAsD59mXEVQnM/ULNdQxbX77tdG4jPRjIS6knA=="], + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.65.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.65.0", "@typescript-eslint/types": "8.65.0", "@typescript-eslint/typescript-estree": "8.65.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA=="], - "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.61.1", "", { "dependencies": { "@typescript-eslint/types": "8.61.1", "eslint-visitor-keys": "^5.0.0" } }, "sha512-6fJ9MHWtK14C1DSkiMlHUSOmrVebL7150xZJBlJiL62jjhIA4JmOq6flwBgDxIdBKKdoiZRel+dfPD5MLfny3w=="], + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.65.0", "", { "dependencies": { "@typescript-eslint/types": "8.65.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A=="], - "@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.3", "", { "dependencies": { "@rolldown/pluginutils": "^1.0.1" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-vmFvco5/QuC2f9Oj+wTk0+9XeDFkHxSamwZKYc7MxYwKICfvUvlMhqKI0VuICPltGqh1neqBKDvO4kes1ya8vg=="], + "@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.5", "", { "dependencies": { "@rolldown/pluginutils": "^1.0.1" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-BOVzne/NL162sMdResB25mUv+vWMF5NoAjNf09TeGlE7ZpszZWSD3winycicLJw72yeVsoCn/2kOhEuCvEShMA=="], - "acorn": ["acorn@8.17.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg=="], + "acorn": ["acorn@8.18.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="], "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], @@ -195,15 +195,15 @@ "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], - "baseline-browser-mapping": ["baseline-browser-mapping@2.10.38", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-31/02mVB4yuQU6adKk5SlY6m+mxDwUq5KZkyYgnLrrKl7TEm1+3PyDtDBz2kOv/wxZz41GHsvV1A/u6RmiyBvw=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.11.9", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-cp447VUsGS07+n1Dqf7YSQ8maeJrjEhaDxTm1ZefbqDtypHBC5GzGMQbklR6IPR13Y8OAJRHZWEMtZipJLCttg=="], - "brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="], + "brace-expansion": ["brace-expansion@5.0.9", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg=="], - "browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="], + "browserslist": ["browserslist@4.28.7", "", { "dependencies": { "baseline-browser-mapping": "^2.10.44", "caniuse-lite": "^1.0.30001806", "electron-to-chromium": "^1.5.393", "node-releases": "^2.0.51", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw=="], "buffer-image-size": ["buffer-image-size@0.6.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ=="], - "caniuse-lite": ["caniuse-lite@1.0.30001799", "", {}, "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw=="], + "caniuse-lite": ["caniuse-lite@1.0.30001806", "", {}, "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw=="], "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], @@ -217,7 +217,7 @@ "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], - "electron-to-chromium": ["electron-to-chromium@1.5.375", "", {}, "sha512-ZWP5eB4BVPW/ZYo9252hQZHZ5XavtsTgpbhcmMmRwymavC5AsLWQWBPaKMeNd2LW0KGby5HPXvj7+sr4ta5j/Q=="], + "electron-to-chromium": ["electron-to-chromium@1.5.399", "", {}, "sha512-lEcqhErbHjXRvd41rnWLpzbyU/IXfIYo7QwaFWmxGeLiLyY2TBCdHnWY88vB+p3ubnihRypDm66panXl7TylLA=="], "entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], @@ -225,7 +225,7 @@ "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], - "eslint": ["eslint@10.5.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", "@eslint/config-helpers": "^0.6.0", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.2", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-1y+7C+vi12bUK1IpZeaV3gsH9fHLBmPvYmPx42pvT/E9yG0IC8g3PUZZgp0+JLJl7ZDK0flc2gc+Aw9dpCvIsQ=="], + "eslint": ["eslint@10.8.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", "@eslint/config-helpers": "^0.7.0", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.2", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-nuKKvN+oIBO0koN7Tm7dlkmnkc21mtt0QJLwAKzjLq14y6lRTdVG36MZHJ8eQHwdJMwZbQNMlPOYedMq/oVJvQ=="], "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@7.1.1", "", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "hermes-parser": "^0.25.1", "zod": "^3.25.0 || ^4.0.0", "zod-validation-error": "^3.5.0 || ^4.0.0" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" } }, "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g=="], @@ -259,7 +259,7 @@ "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], - "flatted": ["flatted@3.4.2", "", {}, "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA=="], + "flatted": ["flatted@3.4.4", "", {}, "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q=="], "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], @@ -267,7 +267,7 @@ "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], - "globals": ["globals@17.6.0", "", {}, "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA=="], + "globals": ["globals@17.8.0", "", {}, "sha512-Zz/LMDZScFmkakeL2cTHzf+PbWKdpU3uclqkZT7TjDG58j5WPt0PpA+n9uPI24fZtlw07q0OtEi84K+umsRzqQ=="], "happy-dom": ["happy-dom@20.11.1", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "buffer-image-size": "^0.6.4", "entities": "^7.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.21.0" } }, "sha512-XSt8tMzbW9ymE7687xztkO1ckR7qJNQ3LywY9vlYGhGi3zXrGBHuUo2Cl1ztZaICW+1eAGdkLbj6iwVqDT33kg=="], @@ -301,43 +301,43 @@ "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], - "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], + "lightningcss": ["lightningcss@1.33.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.33.0", "lightningcss-darwin-arm64": "1.33.0", "lightningcss-darwin-x64": "1.33.0", "lightningcss-freebsd-x64": "1.33.0", "lightningcss-linux-arm-gnueabihf": "1.33.0", "lightningcss-linux-arm64-gnu": "1.33.0", "lightningcss-linux-arm64-musl": "1.33.0", "lightningcss-linux-x64-gnu": "1.33.0", "lightningcss-linux-x64-musl": "1.33.0", "lightningcss-win32-arm64-msvc": "1.33.0", "lightningcss-win32-x64-msvc": "1.33.0" } }, "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA=="], - "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.33.0", "", { "os": "android", "cpu": "arm64" }, "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg=="], - "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.33.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg=="], - "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.33.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ=="], - "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="], + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.33.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg=="], - "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="], + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.33.0", "", { "os": "linux", "cpu": "arm" }, "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ=="], - "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg=="], - "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ=="], - "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg=="], - "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw=="], - "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.33.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA=="], - "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.33.0", "", { "os": "win32", "cpu": "x64" }, "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA=="], "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], - "minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], + "minimatch": ["minimatch@10.2.6", "", { "dependencies": { "brace-expansion": "^5.0.8" } }, "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="], + "nanoid": ["nanoid@3.3.16", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q=="], "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], - "node-releases": ["node-releases@2.0.48", "", {}, "sha512-1uz8041X6LoI6ZSdZacM9lVY28vuzDlSKitnpbSNK0RfKoIJkX29NBPVEFXhnuSuEOA9Ww0xnPJ+ILWbGAv8DA=="], + "node-releases": ["node-releases@2.0.51", "", {}, "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ=="], "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], @@ -351,19 +351,19 @@ "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], + "picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="], - "postcss": ["postcss@8.5.15", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A=="], + "postcss": ["postcss@8.5.25", "", { "dependencies": { "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw=="], "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], - "react": ["react@19.2.7", "", {}, "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ=="], + "react": ["react@19.2.8", "", {}, "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw=="], - "react-dom": ["react-dom@19.2.7", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.7" } }, "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ=="], + "react-dom": ["react-dom@19.2.8", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.8" } }, "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ=="], - "rolldown": ["rolldown@1.1.3", "", { "dependencies": { "@oxc-project/types": "=0.137.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.1.3", "@rolldown/binding-darwin-arm64": "1.1.3", "@rolldown/binding-darwin-x64": "1.1.3", "@rolldown/binding-freebsd-x64": "1.1.3", "@rolldown/binding-linux-arm-gnueabihf": "1.1.3", "@rolldown/binding-linux-arm64-gnu": "1.1.3", "@rolldown/binding-linux-arm64-musl": "1.1.3", "@rolldown/binding-linux-ppc64-gnu": "1.1.3", "@rolldown/binding-linux-s390x-gnu": "1.1.3", "@rolldown/binding-linux-x64-gnu": "1.1.3", "@rolldown/binding-linux-x64-musl": "1.1.3", "@rolldown/binding-openharmony-arm64": "1.1.3", "@rolldown/binding-wasm32-wasi": "1.1.3", "@rolldown/binding-win32-arm64-msvc": "1.1.3", "@rolldown/binding-win32-x64-msvc": "1.1.3" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-1F1eEtUBtFvcGm1HQ9TiUIUHPQG7mSAODrhIzjxoUEFuo8OcbrGLiVLkevNgj84TE4lnHvnumwFjhJO5Eu135g=="], + "rolldown": ["rolldown@1.2.1", "", { "dependencies": { "@oxc-project/types": "=0.142.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.2.1", "@rolldown/binding-darwin-arm64": "1.2.1", "@rolldown/binding-darwin-x64": "1.2.1", "@rolldown/binding-freebsd-x64": "1.2.1", "@rolldown/binding-linux-arm-gnueabihf": "1.2.1", "@rolldown/binding-linux-arm64-gnu": "1.2.1", "@rolldown/binding-linux-arm64-musl": "1.2.1", "@rolldown/binding-linux-ppc64-gnu": "1.2.1", "@rolldown/binding-linux-s390x-gnu": "1.2.1", "@rolldown/binding-linux-x64-gnu": "1.2.1", "@rolldown/binding-linux-x64-musl": "1.2.1", "@rolldown/binding-openharmony-arm64": "1.2.1", "@rolldown/binding-wasm32-wasi": "1.2.1", "@rolldown/binding-win32-arm64-msvc": "1.2.1", "@rolldown/binding-win32-x64-msvc": "1.2.1" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-4FKJhg8d3OiyQOA6Q1Q0hoFFpW9/OoX+VsHzpECsdsIZoOArrAK90gl59YK/Z+gnDel45bgJZK03ozH/9bCqEw=="], "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], @@ -385,7 +385,7 @@ "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], - "typescript-eslint": ["typescript-eslint@8.61.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.61.1", "@typescript-eslint/parser": "8.61.1", "@typescript-eslint/typescript-estree": "8.61.1", "@typescript-eslint/utils": "8.61.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-V7PayAfJokV3pEHgN7/v03D1SpujhRfQtYLbLIiBfDDncdg4PAiRBfoS4cnCANK4jmAPncczi59QO3afiXUlNw=="], + "typescript-eslint": ["typescript-eslint@8.65.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.65.0", "@typescript-eslint/parser": "8.65.0", "@typescript-eslint/typescript-estree": "8.65.0", "@typescript-eslint/utils": "8.65.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-/ggrHAwyjENDusvyxbuqxAC2dTnZg/Z8F+fgQtYIz+L6n/9HfSlEZcFGV/NsMNa6CkGk0xUjUAFwC0vHOflvIA=="], "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], @@ -393,7 +393,7 @@ "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], - "vite": ["vite@8.1.0", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.15", "rolldown": "~1.1.2", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.3.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-BuJcQK/56NQTWDGn4ABea3q4SSBdNPWwNZKTkkUpcMPnLoquSYH8llRtSUIgoL1KSCpHt5eghLShn50mH36y7Q=="], + "vite": ["vite@8.2.0", "", { "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.5", "postcss": "^8.5.23", "rolldown": "~1.2.0", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.4.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-pn+CFpM0lwDeKwmOq1ZaBK/9sjorZcgqxki6MbY/jPEVd9vichIlmlD4HmQ5wdP5EgqQCFRaACBxMC7uEGc6lQ=="], "whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="], @@ -413,8 +413,8 @@ "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], - "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], + "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.6", "", {}, "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw=="], - "@typescript-eslint/typescript-estree/semver": ["semver@7.8.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA=="], + "@typescript-eslint/typescript-estree/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], } } From fea8ce1442d0d316a9405fda5b9067bcbd34718d Mon Sep 17 00:00:00 2001 From: OnlineChef <280567955+OnlineChef@users.noreply.github.com> Date: Sat, 1 Aug 2026 09:40:43 +0200 Subject: [PATCH 04/19] fix(ci): unblock actionlint SC2129 and keep gui lockfile frozen Group prompt.txt appends in issue-triage and keep bun.lock aligned with frozen CI installs on provider-security branches. Co-authored-by: Cursor --- .github/workflows/issue-triage.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/issue-triage.yml b/.github/workflows/issue-triage.yml index d4b17cedb..ce11f4b1f 100644 --- a/.github/workflows/issue-triage.yml +++ b/.github/workflows/issue-triage.yml @@ -81,18 +81,20 @@ jobs: --- BEGIN UNTRUSTED DATA: new issue (JSON) --- PROMPT - cat current.json >> prompt.txt - cat >> prompt.txt << 'PROMPT' + { + cat current.json + cat << 'PROMPT' --- END UNTRUSTED DATA: new issue --- --- BEGIN UNTRUSTED DATA: existing open issues (JSON array) --- PROMPT - cat existing.json >> prompt.txt - cat >> prompt.txt << 'PROMPT' + cat existing.json + cat << 'PROMPT' --- END UNTRUSTED DATA: existing open issues --- PROMPT + } >> prompt.txt - name: Run inference id: infer uses: actions/ai-inference@b81b2afb8390ee6839b494a404766bef6493c7d9 # v1 From 7c8988514dbdb446546926d0c78f3e2829280c0f Mon Sep 17 00:00:00 2001 From: OnlineChef Date: Sat, 1 Aug 2026 07:49:17 +0000 Subject: [PATCH 05/19] fix(provider-security): lease chefvault credentials on every provider request Resolving a chefvault:// reference only for model discovery left ordinary forwarding, compaction, and the live/images/bridge tiers authenticating from provider.apiKey alone, so a credentialRef-only provider reached upstream with no Authorization header. Route every provider-key path through a shared per-request resolver that leases the secret in memory and fails closed. Also stop suppressing a revoked renewal: an authority revocation now evicts the active slot and rejects the request instead of serving the cached secret until its old expiry. Co-authored-by: Codesmith --- src/images/plan.ts | 13 +- src/provider-security/resolve.ts | 27 +++- src/providers/credential.ts | 80 ++++++++++++ src/providers/openai-sidecar.ts | 12 +- src/server/images.ts | 2 +- src/server/live.ts | 2 +- src/server/responses/compact.ts | 11 ++ src/server/responses/core.ts | 11 ++ tests/provider-security-forwarding.test.ts | 136 +++++++++++++++++++++ tests/provider-security.test.ts | 45 +++++++ tests/server-images.test.ts | 4 +- 11 files changed, 323 insertions(+), 20 deletions(-) create mode 100644 src/providers/credential.ts create mode 100644 tests/provider-security-forwarding.test.ts diff --git a/src/images/plan.ts b/src/images/plan.ts index b706b5756..39c169f90 100644 --- a/src/images/plan.ts +++ b/src/images/plan.ts @@ -1,6 +1,6 @@ import type { OcxConfig, OcxParsedRequest, OcxProviderConfig } from "../types"; import type { ImageBridgePlan, VideoBridgePlan } from "./types"; -import { resolveEnvValue } from "../config"; +import { tryResolveProviderApiKey } from "../providers/credential"; import { getProviderRegistryEntry } from "../providers/registry"; import { IMAGE_GEN_TOOL_NAME, VIDEO_GEN_TOOL_NAME, isVideoGenName } from "./synthetic-tool"; @@ -33,10 +33,11 @@ export function findXaiProvider(config: OcxConfig): { name: string; provider: Oc * OAuth / Grok CLI proxy transport is not used here (that path is chat-oriented and not a * supported Images transport), so oauth-only configs deliberately do not arm the bridge. */ -export function resolveXaiImageApiKey(provider: OcxProviderConfig): string | undefined { +export async function resolveXaiImageApiKey(provider: OcxProviderConfig): Promise { if (provider.authMode === "oauth") return undefined; - const apiKey = resolveEnvValue(provider.apiKey)?.trim(); - return apiKey || undefined; + // Covers both inline keys and chefvault:// references, so a reference-backed xAI provider + // arms the bridge instead of silently looking unconfigured. + return tryResolveProviderApiKey(provider); } export async function planImageBridge( @@ -51,7 +52,7 @@ export async function planImageBridge( if (host === "api.openai.com") return undefined; const found = findXaiProvider(config); if (!found) return undefined; - const token = resolveXaiImageApiKey(found.provider); + const token = await resolveXaiImageApiKey(found.provider); if (!token) return undefined; // Pin the baseUrl to the registry entry, ignoring any config-level baseUrl override. const registryEntry = getProviderRegistryEntry("xai"); @@ -100,7 +101,7 @@ export async function planVideoBridge( if (host === "api.openai.com") return undefined; const found = findXaiProvider(config); if (!found) return undefined; - const token = resolveXaiImageApiKey(found.provider); + const token = await resolveXaiImageApiKey(found.provider); if (!token) return undefined; // Pin the baseUrl to the registry entry, ignoring any config-level baseUrl override. const registryEntry = getProviderRegistryEntry("xai"); diff --git a/src/provider-security/resolve.ts b/src/provider-security/resolve.ts index f232cdada..ff2890185 100644 --- a/src/provider-security/resolve.ts +++ b/src/provider-security/resolve.ts @@ -39,13 +39,21 @@ function toProviderSecurityError(error: unknown): ProviderSecurityError { } export class ProviderCredentialResolver { - readonly client: ProviderSecurityClient; + private readonly clientOverride: ProviderSecurityClient | undefined; private readonly slotStore: CredentialSlotStore; private readonly degraded: DegradedModeController; private readonly now: () => number; + /** + * Authority client. The process-wide resolver is constructed at import time, so the endpoint + * must be read from the environment on use rather than captured at construction. + */ + get client(): ProviderSecurityClient { + return this.clientOverride ?? ProviderSecurityClient.fromEnv(); + } + constructor(deps: ResolveCredentialDeps = {}) { - this.client = deps.client ?? ProviderSecurityClient.fromEnv(); + this.clientOverride = deps.client; this.slotStore = deps.slotStore ?? globalCredentialSlotStore; this.degraded = deps.degraded ?? new DegradedModeController(this.slotStore); this.now = deps.now ?? (() => Date.now()); @@ -60,9 +68,18 @@ export class ProviderCredentialResolver { if (existing && existing.expiresAt > at) { const renewTarget = this.slotStore.getState(ref)?.slots.active; if (renewTarget && shouldRenewLease(renewTarget, at, deps.jitterMs ?? renewalJitterMs())) { - await this.tryRenew(ref, renewTarget.leaseId, renewTarget.fencingToken, "active").catch(() => { - // Renewal failure keeps the current snapshot until expiry; degraded handling applies on resolve. - }); + try { + await this.tryRenew(ref, renewTarget.leaseId, renewTarget.fencingToken, "active"); + } catch (error) { + // Only a transient authority failure may keep serving the cached snapshot until expiry. + // An explicit revocation is authoritative: drop the lease and fail the request closed, + // otherwise a revoked credential keeps authenticating until its old expiry. + if (providerSecurityErrorCode(error) === "revoked") { + this.slotStore.revokePhase(ref, "active"); + throw toProviderSecurityError(error); + } + // Degraded handling applies on the next resolve. + } const refreshed = this.slotStore.snapshotForRequest(ref, this.now()); if (refreshed) { return { apiKey: refreshed.secret, snapshot: refreshed, source: "memory" }; diff --git a/src/providers/credential.ts b/src/providers/credential.ts new file mode 100644 index 000000000..b18dc80b0 --- /dev/null +++ b/src/providers/credential.ts @@ -0,0 +1,80 @@ +/** + * Per-request provider credential resolution. + * + * A provider may carry its secret either inline (`apiKey`, possibly an `$ENV` reference) or as a + * `chefvault://` reference (`credentialRef`) that is leased from the provider-security plane. + * Every outbound path that authenticates with a provider key must go through here: resolving the + * reference only in one place (model discovery) leaves ordinary forwarding unauthenticated. + * + * The resolved secret is returned for the lifetime of the current request only. It is never + * written back into the persisted config — callers apply it to a per-request provider copy. + */ +import { resolveEnvValue } from "../config"; +import { + globalProviderCredentialResolver, + isChefVaultRef, + providerSecurityErrorCode, + ProviderSecurityError, +} from "../provider-security"; +import type { OcxProviderConfig } from "../types"; + +/** The provider's `chefvault://` reference, or undefined when it authenticates with a plain key. */ +export function providerCredentialRef(provider: Pick): string | undefined { + const ref = provider.credentialRef?.trim(); + return ref && isChefVaultRef(ref) ? ref : undefined; +} + +/** + * Effective API key for this request: a leased ChefVault secret when the provider is + * reference-backed, otherwise the configured (env-expanded) key. Throws + * `ProviderSecurityError` when a reference cannot be leased, so callers fail closed instead of + * forwarding an unauthenticated request. + */ +export async function resolveProviderApiKey(provider: OcxProviderConfig): Promise { + const ref = providerCredentialRef(provider); + if (!ref) return resolveEnvValue(provider.apiKey)?.trim() || undefined; + const resolved = await globalProviderCredentialResolver.resolveCredentialRef(ref); + return resolved.apiKey; +} + +/** + * Provider copy whose `apiKey` holds the leased secret. Returns the input untouched for + * providers without a `chefvault://` reference so non-ChefVault routing is unaffected. + */ +export async function withResolvedProviderCredential(provider: OcxProviderConfig): Promise { + const ref = providerCredentialRef(provider); + if (!ref) return provider; + const resolved = await globalProviderCredentialResolver.resolveCredentialRef(ref); + return { ...provider, apiKey: resolved.apiKey }; +} + +export interface ProviderCredentialFailure { + status: number; + type: string; + message: string; +} + +/** + * HTTP shape for a failed lease. A rejected or unknown reference is a credential problem (401); + * an unreachable/degraded authority is a temporary upstream condition (503). + */ +export function providerCredentialFailure(providerName: string, error: unknown): ProviderCredentialFailure { + const code = providerSecurityErrorCode(error); + const detail = error instanceof Error ? error.message : String(error); + const denied = code === "ref_invalid" || code === "ref_not_found" || code === "revoked" || code === "lease_expired"; + return { + status: denied ? 401 : 503, + type: denied ? "authentication_error" : "api_error", + message: `Provider '${providerName}' credential could not be resolved (${code}): ${detail}`, + }; +} + +/** Same as `resolveProviderApiKey`, but a failed lease yields undefined instead of throwing. */ +export async function tryResolveProviderApiKey(provider: OcxProviderConfig): Promise { + try { + return await resolveProviderApiKey(provider); + } catch (error) { + if (error instanceof ProviderSecurityError) return undefined; + throw error; + } +} diff --git a/src/providers/openai-sidecar.ts b/src/providers/openai-sidecar.ts index 40728a32f..ada1c6eca 100644 --- a/src/providers/openai-sidecar.ts +++ b/src/providers/openai-sidecar.ts @@ -1,4 +1,4 @@ -import { resolveEnvValue } from "../config"; +import { tryResolveProviderApiKey } from "./credential"; import { headersForCodexAuthContext, hasCallerCodexBearer, @@ -110,7 +110,7 @@ export async function resolveFirstUsableOpenAiSidecar( return undefined; } -export function selectOpenAiImagesProvider(config: OcxConfig): OpenAiImagesProviderSelection { +export async function selectOpenAiImagesProvider(config: OcxConfig): Promise { const selection: OpenAiImagesProviderSelection = { forwardCandidates: listOpenAiForwardSidecarCandidates(config), }; @@ -122,14 +122,16 @@ export function selectOpenAiImagesProvider(config: OcxConfig): OpenAiImagesProvi && provider.authMode !== "forward" && provider.baseUrl.replace(/\/+$/, "") === "https://api.openai.com/v1" ) { - const apiKey = resolveEnvValue(provider.apiKey)?.trim(); + // A chefvault-backed provider has no inline key; lease it here so the keyed tier is armed + // with real authentication instead of being skipped as unconfigured. + const apiKey = await tryResolveProviderApiKey(provider); if (apiKey) selection.keyed = { providerName: OPENAI_API_PROVIDER_ID, provider, apiKey }; } return selection; } /** Resolve an explicit custom Images provider, otherwise preserve the existing OpenAI fallback. */ -export function selectImagesProvider(config: OcxConfig): OpenAiImagesProviderSelection { +export async function selectImagesProvider(config: OcxConfig): Promise { const configuredProvider = config.images?.provider; if (configuredProvider === undefined) return selectOpenAiImagesProvider(config); if (typeof configuredProvider !== "string" || !configuredProvider.trim()) { @@ -160,7 +162,7 @@ export function selectImagesProvider(config: OcxConfig): OpenAiImagesProviderSel }; } - const apiKey = resolveEnvValue(provider.apiKey)?.trim(); + const apiKey = await tryResolveProviderApiKey(provider); if (!apiKey) { return { forwardCandidates: [], error: `images.provider "${providerName}" has no usable API key` }; } diff --git a/src/server/images.ts b/src/server/images.ts index 533098bb1..211924fc3 100644 --- a/src/server/images.ts +++ b/src/server/images.ts @@ -324,7 +324,7 @@ export async function handleImages( endpoint: ImagesEndpoint, logCtx: RequestLogContext, ): Promise { - const candidates = selectImagesProvider(config); + const candidates = await selectImagesProvider(config); if (candidates.error) { return formatErrorResponse(400, "invalid_request_error", candidates.error); } diff --git a/src/server/live.ts b/src/server/live.ts index bbce9e6aa..e6c8361fb 100644 --- a/src/server/live.ts +++ b/src/server/live.ts @@ -369,7 +369,7 @@ export async function resolveLiveRelay( throw err; } - const candidates = selectOpenAiImagesProvider(config); + const candidates = await selectOpenAiImagesProvider(config); if (candidates.forwardCandidates.length === 0 && !candidates.keyed) { return formatErrorResponse( 400, diff --git a/src/server/responses/compact.ts b/src/server/responses/compact.ts index 3d536afd1..649df4087 100644 --- a/src/server/responses/compact.ts +++ b/src/server/responses/compact.ts @@ -36,6 +36,7 @@ import { type OAuthAccessSnapshot, UnsupportedOAuthProviderError, } from "../../oauth"; +import { providerCredentialFailure, providerCredentialRef, withResolvedProviderCredential } from "../../providers/credential"; import { buildWebSearchTool, planWebSearch, runWithWebSearch, shouldResolveOpenAiWebSearchSidecar } from "../../web-search"; import { describeImagesInPlace, planVisionSidecar, shouldResolveOpenAiVisionSidecar, stripImagesInPlace } from "../../vision"; import { createAdapterEventQueue, preflightAdapterEvents } from "../../adapters/run-turn-queue"; @@ -246,6 +247,16 @@ export async function handleResponsesCompact( } throw err; } + // Same fail-closed lease as /v1/responses: a chefvault-backed provider must not compact + // against the upstream without an Authorization header. + if (compactProvider.authMode !== "oauth" && compactProvider.authMode !== "forward" && providerCredentialRef(compactProvider)) { + try { + compactProvider = await withResolvedProviderCredential(compactProvider); + } catch (err) { + const failure = providerCredentialFailure(route.providerName, err); + return formatErrorResponse(failure.status, failure.type, failure.message); + } + } const base = (compactProvider.baseUrl ?? "").replace(/\/$/, ""); if (compactProvider.apiKey) headers.set("authorization", `Bearer ${resolveEnvValue(compactProvider.apiKey)}`); const { reasoning: _reasoning, ...compactBodyRaw } = raw as typeof raw & { reasoning?: unknown }; diff --git a/src/server/responses/core.ts b/src/server/responses/core.ts index 480cbda30..4aeb1207b 100644 --- a/src/server/responses/core.ts +++ b/src/server/responses/core.ts @@ -26,6 +26,7 @@ import { pickComboTarget, targetKey, } from "../../combos"; +import { providerCredentialFailure, providerCredentialRef, withResolvedProviderCredential } from "../../providers/credential"; import { comboIdLabel, isProviderFallbackComboId, providerFallbackPlan } from "../../providers/fallback"; import { isInjectionDebugEnabled } from "../../lib/debug-settings"; import { injectionDebugLog } from "../../lib/injection-debug-log"; @@ -1492,6 +1493,16 @@ export async function handleResponses( return formatErrorResponse(401, "authentication_error", err instanceof Error ? err.message : String(err)); } } + // ChefVault-backed providers carry no secret in config: lease it now so ordinary forwarding + // authenticates like model discovery does. The lease lives on this request's provider copy only. + if (route.provider.authMode !== "oauth" && route.provider.authMode !== "forward" && providerCredentialRef(route.provider)) { + try { + route.provider = await withResolvedProviderCredential(route.provider); + } catch (err) { + const failure = providerCredentialFailure(route.providerName, err); + return formatErrorResponse(failure.status, failure.type, failure.message); + } + } route.provider = resolveProviderTransport( route.providerName, route.provider, diff --git a/tests/provider-security-forwarding.test.ts b/tests/provider-security-forwarding.test.ts new file mode 100644 index 000000000..161355113 --- /dev/null +++ b/tests/provider-security-forwarding.test.ts @@ -0,0 +1,136 @@ +/** + * A provider whose secret lives in ChefVault (`credentialRef`, no inline `apiKey`) must + * authenticate ordinary request forwarding, not just model discovery: without the lease the + * upstream call used to leave with no Authorization header at all. + */ +import { afterEach, beforeEach, expect, test } from "bun:test"; +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { saveConfig } from "../src/config"; +import { startServer } from "../src/server"; +import type { OcxConfig } from "../src/types"; +import { installIsolatedCodexHome, type IsolatedCodexHome } from "./helpers/isolated-codex-home"; + +const UPSTREAM = "https://vaulted.example.test/v1/chat/completions"; +const VAULT = "http://vault.test"; + +let testDir = ""; +let previousHome: string | undefined; +let previousVaultUrl: string | undefined; +let isolatedCodexHome: IsolatedCodexHome | null = null; + +beforeEach(() => { + previousHome = process.env.OPENCODEX_HOME; + previousVaultUrl = process.env.CHEF_PROVIDER_SECURITY_URL; + isolatedCodexHome = installIsolatedCodexHome("ocx-chefvault-fwd-codex-"); + testDir = mkdtempSync(join(tmpdir(), "ocx-chefvault-fwd-")); + process.env.OPENCODEX_HOME = testDir; + process.env.CHEF_PROVIDER_SECURITY_URL = VAULT; +}); + +afterEach(() => { + if (previousHome === undefined) delete process.env.OPENCODEX_HOME; + else process.env.OPENCODEX_HOME = previousHome; + if (previousVaultUrl === undefined) delete process.env.CHEF_PROVIDER_SECURITY_URL; + else process.env.CHEF_PROVIDER_SECURITY_URL = previousVaultUrl; + isolatedCodexHome?.restore(); + isolatedCodexHome = null; + if (testDir) rmSync(testDir, { recursive: true, force: true }); +}); + +function vaultedConfig(ref: string): OcxConfig { + return { + port: 0, + hostname: "127.0.0.1", + defaultProvider: "vaulted", + providers: { + vaulted: { + adapter: "openai-chat", + baseUrl: "https://vaulted.example.test/v1", + authMode: "key", + credentialRef: ref, + }, + }, + } as OcxConfig; +} + +test("a credentialRef-only provider forwards with the leased ChefVault secret", async () => { + const ref = "chefvault://providers/forwarding/ok"; + const originalFetch = globalThis.fetch; + const seen: Array = []; + globalThis.fetch = (async (input, init) => { + const url = input instanceof Request ? input.url : String(input); + if (url === `${VAULT}/v1/credentials/resolve`) { + return Response.json({ + leaseId: "lease-fwd", + secret: "vaulted-secret-1", + expiresAt: Date.now() + 60 * 60_000, + fencingToken: Date.now(), + }); + } + if (url === UPSTREAM) { + seen.push(new Headers(init?.headers).get("authorization")); + return Response.json({ + id: "chatcmpl-vaulted", + object: "chat.completion", + choices: [{ index: 0, message: { role: "assistant", content: "ok" }, finish_reason: "stop" }], + usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, + }); + } + return originalFetch(input, init); + }) as typeof fetch; + + let server: ReturnType | null = null; + try { + saveConfig(vaultedConfig(ref)); + server = startServer(0); + const res = await originalFetch(new URL("/v1/responses", server.url), { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ model: "vaulted/some-model", input: "hello", stream: false }), + }); + + expect(res.status).toBe(200); + expect(seen).toEqual(["Bearer vaulted-secret-1"]); + } finally { + server?.stop(true); + globalThis.fetch = originalFetch; + } +}); + +test("an unresolvable credentialRef fails closed instead of forwarding unauthenticated", async () => { + const ref = "chefvault://providers/forwarding/missing"; + const originalFetch = globalThis.fetch; + let upstreamCalls = 0; + globalThis.fetch = (async (input, init) => { + const url = input instanceof Request ? input.url : String(input); + if (url === `${VAULT}/v1/credentials/resolve`) { + return Response.json({ code: "ref_not_found", message: "unknown ref" }, { status: 404 }); + } + if (url === UPSTREAM) { + upstreamCalls += 1; + return Response.json({ id: "chatcmpl-should-not-happen", object: "chat.completion", choices: [] }); + } + return originalFetch(input, init); + }) as typeof fetch; + + let server: ReturnType | null = null; + try { + saveConfig(vaultedConfig(ref)); + server = startServer(0); + const res = await originalFetch(new URL("/v1/responses", server.url), { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ model: "vaulted/some-model", input: "hello", stream: false }), + }); + + expect(res.status).toBe(401); + expect(upstreamCalls).toBe(0); + const body = await res.text(); + expect(body).toContain("ref_not_found"); + } finally { + server?.stop(true); + globalThis.fetch = originalFetch; + } +}); diff --git a/tests/provider-security.test.ts b/tests/provider-security.test.ts index 587afa08a..af0433358 100644 --- a/tests/provider-security.test.ts +++ b/tests/provider-security.test.ts @@ -190,6 +190,51 @@ describe("degraded mode", () => { }); }); +describe("renewal failures", () => { + function renewingResolver(renew: () => Response, store: CredentialSlotStore, at: number) { + return new ProviderCredentialResolver({ + slotStore: store, + degraded: new DegradedModeController(store), + now: () => at, + client: new ProviderSecurityClient({ + baseUrl: "http://vault.test", + workload: { workloadId: "t", hostId: "h", actor: "a" }, + fetchImpl: mockFetch({ renew }), + }), + }); + } + + test("a revoked renewal evicts the lease and rejects the request", async () => { + const store = new CredentialSlotStore(); + const at = 1_700_000_000_000; + // Inside the renewal lead window but not yet expired: the old snapshot is still "usable". + store.applyResolve(REF, leaseResponse({ secret: "skrevoked", expiresAt: at + 60_000 }), at); + const resolver = renewingResolver( + () => Response.json({ code: "revoked", message: "lease revoked" }, { status: 410 }), + store, + at, + ); + + await expect(resolver.resolveCredentialRef(REF, { jitterMs: 0 })).rejects.toMatchObject({ code: "revoked" }); + expect(store.snapshotForRequest(REF, at)).toBeNull(); + }); + + test("a transient renewal failure keeps serving the cached lease", async () => { + const store = new CredentialSlotStore(); + const at = 1_700_000_000_000; + store.applyResolve(REF, leaseResponse({ secret: "skcached", expiresAt: at + 60_000 }), at); + const resolver = renewingResolver( + () => new Response(JSON.stringify({ message: "down" }), { status: 503 }), + store, + at, + ); + + const resolved = await resolver.resolveCredentialRef(REF, { jitterMs: 0 }); + expect(resolved.source).toBe("memory"); + expect(resolved.apiKey).toBe("skcached"); + }); +}); + describe("provider-security client headers", () => { test("sends workload identity headers on resolve", async () => { let headers: Record = {}; diff --git a/tests/server-images.test.ts b/tests/server-images.test.ts index 437c76c25..98d99f0ab 100644 --- a/tests/server-images.test.ts +++ b/tests/server-images.test.ts @@ -500,8 +500,8 @@ test.each([ ["oauth auth", { adapter: "openai-responses", baseUrl: "https://images.example.test/v1", apiKey: "key", authMode: "oauth" }, "must be an API-key openai-responses provider"], ["local auth", { adapter: "openai-responses", baseUrl: "https://images.example.test/v1", apiKey: "key", authMode: "local" }, "must be an API-key openai-responses provider"], ["missing key", { adapter: "openai-responses", baseUrl: "https://images.example.test/v1", authMode: "key" }, "has no usable API key"], -] as const)("explicit Images provider rejects %s configuration", (_case, provider, expectedError) => { - const selection = selectImagesProvider({ +] as const)("explicit Images provider rejects %s configuration", async (_case, provider, expectedError) => { + const selection = await selectImagesProvider({ port: 0, defaultProvider: "custom-images", providers: provider ? { "custom-images": provider } : {}, From 86b16196475ca0d77e35f4227cfcc39798fcd947 Mon Sep 17 00:00:00 2001 From: OnlineChef <280567955+OnlineChef@users.noreply.github.com> Date: Sat, 1 Aug 2026 11:27:54 +0200 Subject: [PATCH 06/19] fix(provider-security): send ChefVault bearer token and split doctor readiness Load CHEF_PROVIDER_SECURITY_TOKEN on the client, attach Authorization on protected routes, fail closed on resolve/renew without a token, and split doctor checks into liveness, authenticated readiness, and resolve readiness. Co-authored-by: Cursor --- src/provider-security/client.ts | 88 ++++++++-- src/provider-security/resolve.ts | 6 + src/provider-security/status.ts | 120 ++++++++++---- src/provider-security/types.ts | 8 +- tests/provider-security-forwarding.test.ts | 7 + tests/provider-security.test.ts | 182 ++++++++++++++++++--- 6 files changed, 351 insertions(+), 60 deletions(-) diff --git a/src/provider-security/client.ts b/src/provider-security/client.ts index 232d90e9d..5e39cef10 100644 --- a/src/provider-security/client.ts +++ b/src/provider-security/client.ts @@ -2,7 +2,8 @@ * ChefVault provider-security HTTP client (PSP-008). * * Talks to the secret authority at CHEF_PROVIDER_SECURITY_URL (default :8323). - * Workload identity is carried on every request via X-Chef-* headers. + * Protected routes require Authorization: Bearer from CHEF_PROVIDER_SECURITY_TOKEN. + * Workload identity headers are assertions only (must match the token binding). */ import { type ChefVaultRenewRequest, @@ -25,6 +26,11 @@ export function resolveProviderSecurityUrl(env: NodeJS.ProcessEnv = process.env) return raw || DEFAULT_PROVIDER_SECURITY_URL; } +export function resolveProviderSecurityToken(env: NodeJS.ProcessEnv = process.env): string | undefined { + const raw = env.CHEF_PROVIDER_SECURITY_TOKEN?.trim(); + return raw || undefined; +} + export function resolveWorkloadIdentity(env: NodeJS.ProcessEnv = process.env): WorkloadIdentity { return { workloadId: env.CHEF_WORKLOAD_ID?.trim() || "opencodex", @@ -55,6 +61,26 @@ function mapAuthorityError(status: number, body: unknown): ProviderSecurityError if (code === "stale_fencing_token") { return new ProviderSecurityError("stale_fencing_token", message); } + if (status === 401 || code === "auth_required" || code === "auth_invalid") { + return new ProviderSecurityError( + code === "auth_invalid" ? "auth_invalid" : "auth_required", + message, + ); + } + if ( + status === 403 + || code === "identity_assertion_mismatch" + || code === "admin_required" + || code === "workload_credential_required" + ) { + return new ProviderSecurityError( + code === "identity_assertion_mismatch" ? "identity_assertion_mismatch" : "auth_forbidden", + message, + ); + } + if (status === 503 || code === "auth_not_configured") { + return new ProviderSecurityError("auth_not_configured", message); + } if (status === 404 || code === "ref_not_found") { return new ProviderSecurityError("ref_not_found", message); } @@ -109,12 +135,14 @@ function parseRenewResponse(body: unknown): ChefVaultRenewResponse { export class ProviderSecurityClient { readonly baseUrl: string; readonly workload: WorkloadIdentity; + readonly token: string | undefined; private readonly fetchImpl: typeof fetch; private readonly requestTimeoutMs: number; constructor(config: ProviderSecurityClientConfig) { this.baseUrl = config.baseUrl.replace(/\/$/, ""); this.workload = config.workload; + this.token = config.token?.trim() || undefined; this.fetchImpl = config.fetchImpl ?? fetch; this.requestTimeoutMs = config.requestTimeoutMs ?? 8_000; } @@ -123,24 +151,40 @@ export class ProviderSecurityClient { return new ProviderSecurityClient({ baseUrl: resolveProviderSecurityUrl(env), workload: resolveWorkloadIdentity(env), + token: resolveProviderSecurityToken(env), }); } - private async request(path: string, init: RequestInit): Promise { + private requireToken(): string { + if (!this.token) { + throw new ProviderSecurityError( + "auth_required", + "CHEF_PROVIDER_SECURITY_TOKEN is required for ChefVault credential calls", + ); + } + return this.token; + } + + private async request(path: string, init: RequestInit, opts: { auth: boolean }): Promise { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), this.requestTimeoutMs); + const headers: Record = { + accept: "application/json", + "content-type": "application/json", + ...workloadHeaders(this.workload), + ...(init.headers as Record | undefined ?? {}), + }; + if (opts.auth) { + headers.authorization = `Bearer ${this.requireToken()}`; + } try { return await this.fetchImpl(`${this.baseUrl}${path}`, { ...init, signal: controller.signal, - headers: { - accept: "application/json", - "content-type": "application/json", - ...workloadHeaders(this.workload), - ...(init.headers ?? {}), - }, + headers, }); } catch (error) { + if (error instanceof ProviderSecurityError) throw error; if (error instanceof Error && error.name === "AbortError") { throw new ProviderSecurityError("network_error", "ChefVault provider-security request timed out"); } @@ -153,9 +197,10 @@ export class ProviderSecurityClient { } } + /** Unauthenticated liveness probe. */ async healthz(): Promise<{ ok: boolean; message: string }> { try { - const response = await this.request("/healthz", { method: "GET" }); + const response = await this.request("/healthz", { method: "GET" }, { auth: false }); if (!response.ok) { return { ok: false, message: `HTTP ${response.status}` }; } @@ -168,11 +213,32 @@ export class ProviderSecurityClient { } } + /** + * Authenticated readiness: Bearer accepted on a protected route. + * Uses GET /provider-security/status (redacted; no secrets). + */ + async authenticatedReady(): Promise<{ ok: boolean; message: string }> { + try { + const response = await this.request("/provider-security/status", { method: "GET" }, { auth: true }); + if (response.ok) { + return { ok: true, message: "authenticated" }; + } + const body = await response.json().catch(() => null); + const mapped = mapAuthorityError(response.status, body); + return { ok: false, message: `${mapped.code}: ${mapped.message}` }; + } catch (error) { + if (error instanceof ProviderSecurityError) { + return { ok: false, message: `${error.code}: ${error.message}` }; + } + return { ok: false, message: "unreachable" }; + } + } + async resolveLease(input: ChefVaultResolveRequest): Promise { const response = await this.request("/v1/credentials/resolve", { method: "POST", body: JSON.stringify(input), - }); + }, { auth: true }); const body = await response.json().catch(() => null); if (!response.ok) { throw mapAuthorityError(response.status, body); @@ -184,7 +250,7 @@ export class ProviderSecurityClient { const response = await this.request("/v1/credentials/renew", { method: "POST", body: JSON.stringify(input), - }); + }, { auth: true }); const body = await response.json().catch(() => null); if (!response.ok) { throw mapAuthorityError(response.status, body); diff --git a/src/provider-security/resolve.ts b/src/provider-security/resolve.ts index ff2890185..675db86a5 100644 --- a/src/provider-security/resolve.ts +++ b/src/provider-security/resolve.ts @@ -126,9 +126,15 @@ export class ProviderCredentialResolver { this.slotStore.applyRenew(ref, response, phase, this.now()); } + /** Liveness only — `/healthz` is unauthenticated. */ async probeAuthority(): Promise<{ ok: boolean; message: string }> { return this.client.healthz(); } + + /** Authenticated readiness — bearer token + `/provider-security/status`. */ + async probeAuthenticatedReady(): Promise<{ ok: boolean; message: string }> { + return this.client.authenticatedReady(); + } } export const globalProviderCredentialResolver = new ProviderCredentialResolver(); diff --git a/src/provider-security/status.ts b/src/provider-security/status.ts index 9e138496d..19b064ddf 100644 --- a/src/provider-security/status.ts +++ b/src/provider-security/status.ts @@ -10,11 +10,19 @@ import { isChefVaultRef, type RedactedProviderSecurityStatus } from "./types"; export interface ProviderSecurityDoctorCheck { level: "OK" | "WARN"; provider: string; + /** liveness | authenticated | credential | provider */ + layer?: "liveness" | "authenticated" | "credential" | "provider"; message: string; } export interface ProviderSecurityStatusReport { - authority: { ok: boolean; url: string; message: string }; + authority: { + ok: boolean; + url: string; + message: string; + livenessOk?: boolean; + authenticatedOk?: boolean; + }; providers: Array<{ provider: string; credentialRef: string; @@ -50,20 +58,27 @@ export async function collectProviderSecurityStatusAsync( config: OcxConfig, resolver: ProviderCredentialResolver = new ProviderCredentialResolver(), ): Promise { - const authorityProbe = await resolver.probeAuthority(); + const liveness = await resolver.client.healthz(); + const authenticated = liveness.ok + ? await resolver.client.authenticatedReady() + : { ok: false, message: "skipped (liveness failed)" }; const base = collectProviderSecurityStatus(config, resolver.client); base.authority = { - ok: authorityProbe.ok, + ok: liveness.ok && authenticated.ok, url: resolver.client.baseUrl, - message: authorityProbe.message, + message: authenticated.ok ? authenticated.message : (liveness.ok ? authenticated.message : liveness.message), + livenessOk: liveness.ok, + authenticatedOk: authenticated.ok, }; - for (const entry of base.providers) { - try { - await resolver.resolveCredentialRef(entry.credentialRef); - entry.status = globalCredentialSlotStore.redactedStatus(entry.credentialRef); - } catch { - entry.status = globalCredentialSlotStore.redactedStatus(entry.credentialRef); + if (authenticated.ok) { + for (const entry of base.providers) { + try { + await resolver.resolveCredentialRef(entry.credentialRef); + entry.status = globalCredentialSlotStore.redactedStatus(entry.credentialRef); + } catch { + entry.status = globalCredentialSlotStore.redactedStatus(entry.credentialRef); + } } } @@ -76,26 +91,87 @@ export async function collectProviderSecurityDoctorChecks( ): Promise { const checks: ProviderSecurityDoctorCheck[] = []; const refs = listChefVaultProviders(config); - const authority = await resolver.probeAuthority(); - if (authority.ok) { + const liveness = await resolver.client.healthz(); + if (liveness.ok) { checks.push({ level: "OK", provider: "*", - message: `ChefVault provider-security reachable (${resolver.client.baseUrl}).`, + layer: "liveness", + message: `Liveness OK: ChefVault /healthz reachable (${resolver.client.baseUrl}).`, }); } else { checks.push({ level: "WARN", provider: "*", - message: `ChefVault provider-security unavailable (${authority.message}). Degraded mode: existing in-memory leases only; new resolve denied.`, + layer: "liveness", + message: `Liveness FAIL: ChefVault /healthz unavailable (${liveness.message}). Degraded mode: existing in-memory leases only; new resolve denied.`, }); } + if (liveness.ok) { + const authenticated = await resolver.client.authenticatedReady(); + if (authenticated.ok) { + checks.push({ + level: "OK", + provider: "*", + layer: "authenticated", + message: "Authenticated readiness OK: Bearer accepted on protected status route.", + }); + } else { + checks.push({ + level: "WARN", + provider: "*", + layer: "authenticated", + message: `Authenticated readiness FAIL: ${authenticated.message}. Set CHEF_PROVIDER_SECURITY_TOKEN to the workload bearer registered in ChefVault.`, + }); + } + + if (authenticated.ok && refs.length > 0) { + let credentialOk = 0; + for (const { name, ref } of refs) { + try { + await resolver.resolveCredentialRef(ref); + credentialOk += 1; + checks.push({ + level: "OK", + provider: name, + layer: "credential", + message: `Credential resolve OK for "${name}" (${ref}).`, + }); + } catch (error) { + const detail = error instanceof Error ? error.message : String(error); + checks.push({ + level: "WARN", + provider: name, + layer: "credential", + message: `Credential resolve/renew FAIL for "${name}" (${ref}): ${detail}`, + }); + } + } + if (credentialOk === refs.length) { + checks.push({ + level: "OK", + provider: "*", + layer: "credential", + message: `Credential readiness OK: resolved ${credentialOk}/${refs.length} chefvault:// refs.`, + }); + } + } else if (!authenticated.ok && refs.length > 0) { + checks.push({ + level: "WARN", + provider: "*", + layer: "credential", + message: `Credential readiness skipped: ${refs.length} chefvault:// ref(s) configured but authenticated readiness failed.`, + }); + } + } + if (refs.length === 0) { checks.push({ level: "OK", provider: "*", + layer: "provider", message: "No providers configured with chefvault:// credentialRef.", }); return checks; @@ -107,6 +183,7 @@ export async function collectProviderSecurityDoctorChecks( checks.push({ level: "WARN", provider: name, + layer: "provider", message: `Provider "${name}" is in degraded mode for ${ref}; only bounded in-memory credentials may be used.`, }); continue; @@ -115,22 +192,9 @@ export async function collectProviderSecurityDoctorChecks( checks.push({ level: "OK", provider: name, + layer: "provider", message: `Provider "${name}" has a usable in-memory lease for ${ref}.`, }); - continue; - } - if (authority.ok) { - checks.push({ - level: "WARN", - provider: name, - message: `Provider "${name}" references ${ref} but has no resolved in-memory lease yet.`, - }); - } else { - checks.push({ - level: "WARN", - provider: name, - message: `Provider "${name}" cannot resolve ${ref} while ChefVault is unavailable.`, - }); } } diff --git a/src/provider-security/types.ts b/src/provider-security/types.ts index d789b029e..dae26794b 100644 --- a/src/provider-security/types.ts +++ b/src/provider-security/types.ts @@ -16,7 +16,11 @@ export type ProviderSecurityErrorCode = | "degraded_deny_resolve" | "revoked" | "network_error" - | "authority_error"; + | "authority_error" + | "auth_required" + | "auth_invalid" + | "identity_assertion_mismatch" + | "auth_not_configured"; export class ProviderSecurityError extends Error { readonly code: ProviderSecurityErrorCode; @@ -111,6 +115,8 @@ export interface RedactedProviderSecurityStatus { export interface ProviderSecurityClientConfig { baseUrl: string; workload: WorkloadIdentity; + /** Bearer token for protected ChefVault routes (`CHEF_PROVIDER_SECURITY_TOKEN`). */ + token?: string; fetchImpl?: typeof fetch; requestTimeoutMs?: number; } diff --git a/tests/provider-security-forwarding.test.ts b/tests/provider-security-forwarding.test.ts index 161355113..2236d843b 100644 --- a/tests/provider-security-forwarding.test.ts +++ b/tests/provider-security-forwarding.test.ts @@ -18,15 +18,20 @@ const VAULT = "http://vault.test"; let testDir = ""; let previousHome: string | undefined; let previousVaultUrl: string | undefined; +let previousVaultToken: string | undefined; let isolatedCodexHome: IsolatedCodexHome | null = null; +const VAULT_TOKEN = "forwarding-test-bearer-token-32ch"; + beforeEach(() => { previousHome = process.env.OPENCODEX_HOME; previousVaultUrl = process.env.CHEF_PROVIDER_SECURITY_URL; + previousVaultToken = process.env.CHEF_PROVIDER_SECURITY_TOKEN; isolatedCodexHome = installIsolatedCodexHome("ocx-chefvault-fwd-codex-"); testDir = mkdtempSync(join(tmpdir(), "ocx-chefvault-fwd-")); process.env.OPENCODEX_HOME = testDir; process.env.CHEF_PROVIDER_SECURITY_URL = VAULT; + process.env.CHEF_PROVIDER_SECURITY_TOKEN = VAULT_TOKEN; }); afterEach(() => { @@ -34,6 +39,8 @@ afterEach(() => { else process.env.OPENCODEX_HOME = previousHome; if (previousVaultUrl === undefined) delete process.env.CHEF_PROVIDER_SECURITY_URL; else process.env.CHEF_PROVIDER_SECURITY_URL = previousVaultUrl; + if (previousVaultToken === undefined) delete process.env.CHEF_PROVIDER_SECURITY_TOKEN; + else process.env.CHEF_PROVIDER_SECURITY_TOKEN = previousVaultToken; isolatedCodexHome?.restore(); isolatedCodexHome = null; if (testDir) rmSync(testDir, { recursive: true, force: true }); diff --git a/tests/provider-security.test.ts b/tests/provider-security.test.ts index af0433358..3c51eacdc 100644 --- a/tests/provider-security.test.ts +++ b/tests/provider-security.test.ts @@ -19,6 +19,18 @@ import { import type { OcxConfig } from "../src/types"; const REF = "chefvault://providers/demo/prod"; +const TEST_TOKEN = "test-workload-bearer-token-32chars"; + +function testClient( + overrides: Partial[0]> = {}, +): ProviderSecurityClient { + return new ProviderSecurityClient({ + baseUrl: "http://vault.test", + workload: { workloadId: "t", hostId: "h", actor: "a" }, + token: TEST_TOKEN, + ...overrides, + }); +} function leaseResponse( overrides: Partial<{ leaseId: string; secret: string; expiresAt: number; fencingToken: number; slotHint?: "active" | "next" | "retiring" }> = {}, @@ -34,6 +46,7 @@ function leaseResponse( function mockFetch(handlers: { healthz?: () => Response | Promise; + status?: () => Response | Promise; resolve?: (body: unknown) => Response | Promise; renew?: (body: unknown) => Response | Promise; }): typeof fetch { @@ -42,6 +55,9 @@ function mockFetch(handlers: { if (url.endsWith("/healthz")) { return handlers.healthz?.() ?? new Response("ok", { status: 200 }); } + if (url.endsWith("/provider-security/status")) { + return handlers.status?.() ?? Response.json({ ok: true }, { status: 200 }); + } if (url.endsWith("/v1/credentials/resolve")) { const body = init?.body ? JSON.parse(String(init.body)) : {}; return handlers.resolve?.(body) ?? Response.json(leaseResponse(), { status: 200 }); @@ -143,9 +159,7 @@ describe("degraded mode", () => { slotStore: store, degraded, now: () => at, - client: new ProviderSecurityClient({ - baseUrl: "http://vault.test", - workload: { workloadId: "t", hostId: "h", actor: "a" }, + client: testClient({ fetchImpl: mockFetch({ resolve: () => Response.json({ code: "stale", message: "should not resolve" }, { status: 503 }), }), @@ -166,9 +180,7 @@ describe("degraded mode", () => { const store = new CredentialSlotStore(); const degraded = new DegradedModeController(store); let calls = 0; - const client = new ProviderSecurityClient({ - baseUrl: "http://vault.test", - workload: { workloadId: "t", hostId: "h", actor: "a" }, + const client = testClient({ fetchImpl: mockFetch({ resolve: () => { calls += 1; @@ -196,11 +208,7 @@ describe("renewal failures", () => { slotStore: store, degraded: new DegradedModeController(store), now: () => at, - client: new ProviderSecurityClient({ - baseUrl: "http://vault.test", - workload: { workloadId: "t", hostId: "h", actor: "a" }, - fetchImpl: mockFetch({ renew }), - }), + client: testClient({ fetchImpl: mockFetch({ renew }) }), }); } @@ -238,9 +246,7 @@ describe("renewal failures", () => { describe("provider-security client headers", () => { test("sends workload identity headers on resolve", async () => { let headers: Record = {}; - const client = new ProviderSecurityClient({ - baseUrl: "http://vault.test", - workload: { workloadId: "ocx", hostId: "sofie", actor: "doctor" }, + const client = testClient({ fetchImpl: (async (_input, init) => { headers = Object.fromEntries(new Headers(init?.headers).entries()); return Response.json(leaseResponse(), { status: 200 }); @@ -248,9 +254,147 @@ describe("provider-security client headers", () => { }); await client.resolveLease({ ref: REF }); - expect(headers["x-chef-workload-id"]).toBe("ocx"); - expect(headers["x-chef-host-id"]).toBe("sofie"); - expect(headers["x-chef-actor"]).toBe("doctor"); + expect(headers["x-chef-workload-id"]).toBe("t"); + expect(headers["x-chef-host-id"]).toBe("h"); + expect(headers["x-chef-actor"]).toBe("a"); + }); + + test("sends Authorization bearer on protected routes when token is configured", async () => { + let headers: Record = {}; + const client = testClient({ + token: "my-secret-bearer-token-32chars", + fetchImpl: (async (_input, init) => { + headers = Object.fromEntries(new Headers(init?.headers).entries()); + return Response.json(leaseResponse(), { status: 200 }); + }) as typeof fetch, + }); + + await client.resolveLease({ ref: REF }); + expect(headers.authorization).toBe("Bearer my-secret-bearer-token-32chars"); + }); + + test("does not send Authorization on healthz", async () => { + let headers: Record = {}; + const client = testClient({ + fetchImpl: (async (_input, init) => { + headers = Object.fromEntries(new Headers(init?.headers).entries()); + return new Response("ok", { status: 200 }); + }) as typeof fetch, + }); + + await client.healthz(); + expect(headers.authorization).toBeUndefined(); + }); +}); + +describe("provider-security bearer auth", () => { + test("resolve without token throws auth_required before calling fetch", async () => { + let called = false; + const client = testClient({ + token: undefined, + fetchImpl: (async () => { + called = true; + return Response.json(leaseResponse(), { status: 200 }); + }) as typeof fetch, + }); + + await expect(client.resolveLease({ ref: REF })).rejects.toMatchObject({ code: "auth_required" }); + expect(called).toBe(false); + }); + + test("maps HTTP 401 to auth_invalid", async () => { + const client = testClient({ + fetchImpl: mockFetch({ + resolve: () => Response.json({ code: "auth_invalid", message: "credential is not recognised" }, { status: 401 }), + }), + }); + + await expect(client.resolveLease({ ref: REF })).rejects.toMatchObject({ + code: "auth_invalid", + message: "credential is not recognised", + }); + }); + + test("maps HTTP 403 identity mismatch", async () => { + const client = testClient({ + fetchImpl: mockFetch({ + resolve: () => Response.json( + { code: "identity_assertion_mismatch", message: "asserted identity headers do not match" }, + { status: 403 }, + ), + }), + }); + + await expect(client.resolveLease({ ref: REF })).rejects.toMatchObject({ + code: "identity_assertion_mismatch", + }); + }); + + test("authenticatedReady probes /provider-security/status with bearer", async () => { + let url = ""; + let headers: Record = {}; + const client = testClient({ + fetchImpl: (async (input, init) => { + url = String(input); + headers = Object.fromEntries(new Headers(init?.headers).entries()); + return Response.json({ mode: "normal" }, { status: 200 }); + }) as typeof fetch, + }); + + const ready = await client.authenticatedReady(); + expect(ready.ok).toBe(true); + expect(url).toContain("/provider-security/status"); + expect(headers.authorization).toBe(`Bearer ${TEST_TOKEN}`); + }); + + test("doctor reports liveness OK and auth WARN when token missing", async () => { + const config: OcxConfig = { + port: 10100, + defaultProvider: "demo", + providers: { + demo: { + adapter: "openai-chat", + baseUrl: "https://example/v1", + credentialRef: REF, + }, + }, + }; + + const doctor = await collectProviderSecurityDoctorChecks(config, new ProviderCredentialResolver({ + client: testClient({ + token: undefined, + fetchImpl: mockFetch({ healthz: () => new Response("ok", { status: 200 }) }), + }), + })); + + expect(doctor.some(c => c.level === "OK" && c.message.includes("liveness OK"))).toBe(true); + expect(doctor.some(c => c.level === "WARN" && c.message.includes("CHEF_PROVIDER_SECURITY_TOKEN"))).toBe(true); + }); + + test("doctor reports auth WARN on 401 status probe", async () => { + const config: OcxConfig = { + port: 10100, + defaultProvider: "demo", + providers: { + demo: { + adapter: "openai-chat", + baseUrl: "https://example/v1", + credentialRef: REF, + }, + }, + }; + + const doctor = await collectProviderSecurityDoctorChecks(config, new ProviderCredentialResolver({ + client: testClient({ + fetchImpl: mockFetch({ + healthz: () => new Response("ok", { status: 200 }), + status: () => Response.json({ code: "auth_invalid", message: "credential is not recognised" }, { status: 401 }), + }), + }), + })); + + expect(doctor.some(c => c.level === "OK" && c.message.includes("liveness OK"))).toBe(true); + expect(doctor.some(c => c.level === "WARN" && c.message.includes("authenticated readiness failed"))).toBe(true); }); }); @@ -279,9 +423,7 @@ describe("redacted status serialization", () => { const doctor = await collectProviderSecurityDoctorChecks(config, new ProviderCredentialResolver({ slotStore: store, - client: new ProviderSecurityClient({ - baseUrl: "http://vault.test", - workload: { workloadId: "t", hostId: "h", actor: "a" }, + client: testClient({ fetchImpl: mockFetch({ healthz: () => new Response("ok", { status: 200 }) }), }), })); From 6d99b0c5635a985ae6268e1cbbd4f7ebe43b1cef Mon Sep 17 00:00:00 2001 From: OnlineChef <280567955+OnlineChef@users.noreply.github.com> Date: Sat, 1 Aug 2026 11:27:56 +0200 Subject: [PATCH 07/19] chore(provider-security): drop unrelated workflow/lockfile churn from PSP PR Revert issue-triage workflow and gui/bun.lock to origin/dev so PSP CI stays scoped to provider-security changes only. Co-authored-by: Cursor --- .github/workflows/issue-triage.yml | 10 +- gui/bun.lock | 150 ++++++++++++++--------------- 2 files changed, 79 insertions(+), 81 deletions(-) diff --git a/.github/workflows/issue-triage.yml b/.github/workflows/issue-triage.yml index ce11f4b1f..d4b17cedb 100644 --- a/.github/workflows/issue-triage.yml +++ b/.github/workflows/issue-triage.yml @@ -81,20 +81,18 @@ jobs: --- BEGIN UNTRUSTED DATA: new issue (JSON) --- PROMPT - { - cat current.json - cat << 'PROMPT' + cat current.json >> prompt.txt + cat >> prompt.txt << 'PROMPT' --- END UNTRUSTED DATA: new issue --- --- BEGIN UNTRUSTED DATA: existing open issues (JSON array) --- PROMPT - cat existing.json - cat << 'PROMPT' + cat existing.json >> prompt.txt + cat >> prompt.txt << 'PROMPT' --- END UNTRUSTED DATA: existing open issues --- PROMPT - } >> prompt.txt - name: Run inference id: infer uses: actions/ai-inference@b81b2afb8390ee6839b494a404766bef6493c7d9 # v1 diff --git a/gui/bun.lock b/gui/bun.lock index 36debcbea..56c90814e 100644 --- a/gui/bun.lock +++ b/gui/bun.lock @@ -35,7 +35,7 @@ "@babel/core": ["@babel/core@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-module-transforms": "^7.29.7", "@babel/helpers": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA=="], - "@babel/generator": ["@babel/generator@7.29.8", "", { "dependencies": { "@babel/parser": "^7.29.8", "@babel/types": "^7.29.8", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg=="], + "@babel/generator": ["@babel/generator@7.29.7", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="], "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.29.7", "", { "dependencies": { "@babel/compat-data": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g=="], @@ -53,27 +53,27 @@ "@babel/helpers": ["@babel/helpers@7.29.7", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg=="], - "@babel/parser": ["@babel/parser@7.29.8", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": "./bin/babel-parser.js" }, "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA=="], + "@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], "@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="], - "@babel/traverse": ["@babel/traverse@7.29.8", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.8", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.8", "@babel/template": "^7.29.7", "@babel/types": "^7.29.8", "debug": "^4.3.1" } }, "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg=="], + "@babel/traverse": ["@babel/traverse@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/types": "^7.29.7", "debug": "^4.3.1" } }, "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw=="], - "@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="], + "@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - "@emnapi/core": ["@emnapi/core@2.0.0-alpha.3", "", { "dependencies": { "@emnapi/wasi-threads": "2.0.1", "tslib": "^2.4.0" } }, "sha512-AZypUeJ/yByuxyS7BlSNRDOMLMlROYtjYdIAuBmJssVz1UJDSeYxLrdizhXCFYhedC5bqd/ASy8EuNXbVVXp9g=="], + "@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="], - "@emnapi/runtime": ["@emnapi/runtime@2.0.0-alpha.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-hFPAhMUjJD9BSyCANEISPOogeXC9Zo9ZQl7L6vKnaVsMkCtzznaW/naYypeyl0Gv5rYfWYsZbpixTMpjDJzQeA=="], + "@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="], - "@emnapi/wasi-threads": ["@emnapi/wasi-threads@2.0.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9DsSk+o5NBX0CCJT8s0EROGSGxjR/tKu6aBTaVyq+SjAEQH4XcdcRxPBRzsBLizTTJ49MJjF+jgu3qnO9GLQcQ=="], + "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="], - "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.10.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg=="], + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], "@eslint/config-array": ["@eslint/config-array@0.23.5", "", { "dependencies": { "@eslint/object-schema": "^3.0.5", "debug": "^4.3.1", "minimatch": "^10.2.4" } }, "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA=="], - "@eslint/config-helpers": ["@eslint/config-helpers@0.7.0", "", { "dependencies": { "@eslint/core": "^1.2.1" } }, "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw=="], + "@eslint/config-helpers": ["@eslint/config-helpers@0.6.0", "", { "dependencies": { "@eslint/core": "^1.2.1" } }, "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA=="], "@eslint/core": ["@eslint/core@1.2.1", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ=="], @@ -107,45 +107,45 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], - "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.2.2", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.3", "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.3" } }, "sha512-JfB4kuJQjaoHuCTseIINHtHWeJnvgEcxjwA5t/Y00ZgaOO1Crz3fjT/p8kT28zA/Caz7oiUMn3d6H2yOVCVwuw=="], + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.6", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg=="], - "@oxc-project/types": ["@oxc-project/types@0.142.0", "", {}, "sha512-7W+2q5AKQVU36fkaryontrHn3YDt1RyUYXatw9i5H8ocYe2sPKSFB6eS8WNPeRKiN1qAWWZUPm7gwFzJGrccqQ=="], + "@oxc-project/types": ["@oxc-project/types@0.137.0", "", {}, "sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA=="], - "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.2.1", "", { "os": "android", "cpu": "arm64" }, "sha512-02hOeOSryYxVrOIphmLAsqnCJWxwlzFk+pEt/N/i6OgT3lShHO7xGCU5cpgchRDHboAEbSjzgGh+O/u1GswQmA=="], + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.1.3", "", { "os": "android", "cpu": "arm64" }, "sha512-DT6Z3PhvioeHMvxo+xHc3KtqggrI7CCTXCmC2h/5zUlp5jVitv7XEy+9q5/7v8IolhlioawpMo8Kg0EEBy7J0g=="], - "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.2.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-fMsTOnN0OjFm3CyppWPitKnc8UlliVARUULW6cfU6AIqjdtgmSFWSk9vecHzZduv/yMWIHDlRhM1e8Iff9uAfA=="], + "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.1.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-0NwgwsjM7LrsuVnXMK3koTpagBNOhloc/BNjKqZjv4V5zI5r13qx69uVhRx+o5Z0yy4Hzq+lpy7TAgUG/ocvrw=="], - "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.2.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-1wjKdz/XLGKHaTNHjQveQ/B23TKx4ItAqm1JbyVuvNPc4Ze0Fb48s49TAd/2zcplPl8okE/UbTgmlVfwT7eFeQ=="], + "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.1.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-YtiBp4disu6V560loT6PjMdiRaWmVvDNrUunAalbiFx2ggeJwxdAsgZMcoGP17uyAsTwAj5V1niksxlHnVQ1Sw=="], - "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.2.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Fa0jHR07E7YBN4vOEsbVf2briYNsuOowfLJaXULZM0ldMlaCaj2LJgLMbMe4iacRyZmvR8efFhgR9wKuGclQUg=="], + "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.1.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-yD3EkEdXk2LypPxnf/kSZHirarsI8gcPzc62SukhR9VJTyvV+F9Q/GxWNuCojc7sXyuVC4DxRGhdDK4X8VSsbw=="], - "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.2.1", "", { "os": "linux", "cpu": "arm" }, "sha512-pzkgu1SSHGgRRyRZ4fbmSgmajbVt+epaLP99NDjFft69v/ypfTi6swBMiVdh2EkQ0OSnHE1lZDM7DRGkyAzUpA=="], + "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.1.3", "", { "os": "linux", "cpu": "arm" }, "sha512-c+8vieQbsD7HNAHKIA34w0GJ9FedFFuJGD+7E6vz7Q3uqAIugL5p45fhlsj4UaAsHpcmlqugBWMhA0/j7o0sIg=="], - "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-QI5SEDY8cbiYWHx0VO4vIc3UlS6a32vXHjU8Qy/17adEmZIPuByJg13UEvo9c/UCiUkdcVWY83C+b+JrwnNyUg=="], + "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.1.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-50jD0uUwLvur7Zz9LHz17kaAdTPjn5wN93hEgjvmYFRZwiR7ZJYovTd5ipyWJDAnXKvZ+wgc+/Ika6dwSF5OcA=="], - "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-Sm41FyCeXqmYcERoYOCbGIL5hNfd8w9LQ7Y61Bev48HkcjaJqV/iiVOaiDxjVTRMS+QKrZmD8cfPt4uMVnvM+A=="], + "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.1.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-BO9+oPL8K9poZJBfYPsXNtYjPE5uM3qeehT3aFcW4LITOl+iSqhp0abzjR2nWBUNjIZeKXjAEWBZ64WjNoHd6w=="], - "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.2.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-2x+WhXTGl9yJYPbltW/BSEPTVz9OIWQyER4N+gJEDWkkn904eRcBzELqh/Hf7K0w/ubGbKNMv0ZC+94QK/IFEg=="], + "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.1.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-f3VpLB1vQ0Eo6ecr/6cekLnvYMFF4YBFoVGkfkvPLq1bAkbAwHYQPZKoAmG6OJyTcxxoC+AvezGx/S1obNC0Mw=="], - "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.2.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-eEjmQpuRQayHPWWnywaWHkFT3ToPbP3RYy42VVd/B9aBGDA+Ol25EIWHxKQST3IiWJjikCWUF7KtbfqwZrzVwQ=="], + "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.1.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-AmurZ26Pqx/RI9N1gzEOCklkKXl927yjfXWUUS0O7Puh8ARM/Ob8qfrD3qnWksScdw6cSrW5PSHE9DyLu7+PtA=="], - "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-/Orga1fZYkLc/56jBICcHrKchl8Z2UKdDSr3LG9ToWO1lQ6a4Livk9Xz+9WN91zsz5QR3XQz2NNoSDEvP6qadw=="], + "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.1.3", "", { "os": "linux", "cpu": "x64" }, "sha512-JJpqs8bRGITDOdbkNKnlojzBabbOHrqjSvDr0IVsZObE1lBcPjxItUEY9eWIDbxaJ3cGrXPWGfGkIxFijg/URg=="], - "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-xxBJRL+0q0Kce7orznGWLuylHDY65vuARXZRpX+hPdv+DqK2c3NlCsVA98tlWzWNEE7yPqA/1NQ5nnCrj49Y5A=="], + "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.1.3", "", { "os": "linux", "cpu": "x64" }, "sha512-rSJcdjPxzA/by/6/rYs+v+bXU7UjvnbUWz8MJb6kh6+knqB1dCrtHg0uu7C/4haqJvqdkYHQ5IGn+tCH9GLW/g=="], - "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.2.1", "", { "os": "none", "cpu": "arm64" }, "sha512-M6AdXIXw3s+/8XpKMzdGDEXGS1S7kwUsy+rcTIUIOx5Ge4nXKCtAFHFV9YKkXvGcC5WMoTjAteLzlsQROVI0Yw=="], + "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.1.3", "", { "os": "none", "cpu": "arm64" }, "sha512-hQ3/PYkDJICgevvyNcVrihVeqq7k1Pp3VZ9lY+dauAYUJKO+auqApvANhvR1An9BhmqYKvW2Mu1F9u4DXSMLxQ=="], - "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.2.1", "", { "dependencies": { "@emnapi/core": "2.0.0-alpha.3", "@emnapi/runtime": "2.0.0-alpha.3", "@napi-rs/wasm-runtime": "^1.2.0" } }, "sha512-/TX0SoRGojHzSAHpfVBbavRVSazg5U3h3Y3VXfcc0cdugq6kxdqw8LPGFiPr+/7gE/60zRcsOY2Vi9b9eT0jww=="], + "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.1.3", "", { "dependencies": { "@emnapi/core": "1.11.1", "@emnapi/runtime": "1.11.1", "@napi-rs/wasm-runtime": "^1.1.6" }, "cpu": "none" }, "sha512-Elcv/BtML9lXrV6JuKITc/grN2kYV9gjsQpW8Jfw4ioK0TOkjBjye0nnyqQNy9STNaI20lXNaQBRrD5gSgR0Yg=="], - "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.2.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-EvRrivJieyHG+AO9lleZWgq+g0+S7oV2C51yuqlcyU/R9net+sI4Pj0F+lUoP2bEr6TWX3SqFaaS0SzfLxSzkw=="], + "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.1.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-2DrEfhluH9yhiaFApmsjsjwrSYbNcY1oFTzYSP1a535jDbV98zCFanA/96TBUd0iDFcxGmw9QRExwGCXz3U+/g=="], - "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.2.1", "", { "os": "win32", "cpu": "x64" }, "sha512-Z4eCmn5QJ/5+azF9knpLWKfVd9aidn0mAe9TpJgvBLId9Ax3t0+JVxBmT25Bv7NBbVW1TZyKjQjQReouMeH5UQ=="], + "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.1.3", "", { "os": "win32", "cpu": "x64" }, "sha512-OL4OMk7UPXOeVGGd3qo5zJyPIljf4AFgk5QAkPPS+OoLuOOozhuaQGC18MxVTnw/06q93gShAJzlwnSCY9YtqA=="], "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="], - "@tanstack/react-virtual": ["@tanstack/react-virtual@3.14.9", "", { "dependencies": { "@tanstack/virtual-core": "3.17.7" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-qZyr0FZDP8rDC4WBhsryIZmAd9bveJvFGUJJtskWaew6/0dTRS6wZxnR6VQ5bY2KwL3LjerrHqQLk3a0GKcPXQ=="], + "@tanstack/react-virtual": ["@tanstack/react-virtual@3.14.5", "", { "dependencies": { "@tanstack/virtual-core": "3.17.3" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-4EKRXh7zBLkbKbFmG3AUVkircuHd+7OdT1pocJSepxtfBd3qnrJgJ5rtPkRYyo9fmyVb2+pI2xPy5oYvMLQy6A=="], - "@tanstack/virtual-core": ["@tanstack/virtual-core@3.17.7", "", {}, "sha512-bp+v10y65sp2H7WpWfIMyxTNfl8ZVfxFTLRjPIFRryi6FV/J33z4IS53WO4pTk36KlvJ4iLiQz+oaydDC1xbcA=="], + "@tanstack/virtual-core": ["@tanstack/virtual-core@3.17.3", "", {}, "sha512-8Np/TFELpI0ySuJoVmjvOrQYXH/8sTX0Biv9szhFhY39xOdAAY+smrMxjxOum/ux3eM8MUJQsEJ0/R0UpvC8dw=="], "@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="], @@ -155,39 +155,39 @@ "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], - "@types/node": ["@types/node@24.13.3", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q=="], + "@types/node": ["@types/node@24.13.2", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA=="], - "@types/react": ["@types/react@19.2.18", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w=="], + "@types/react": ["@types/react@19.2.17", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw=="], - "@types/react-dom": ["@types/react-dom@19.2.4", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw=="], + "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], "@types/whatwg-mimetype": ["@types/whatwg-mimetype@3.0.2", "", {}, "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA=="], "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.65.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.65.0", "@typescript-eslint/type-utils": "8.65.0", "@typescript-eslint/utils": "8.65.0", "@typescript-eslint/visitor-keys": "8.65.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.65.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.61.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.61.1", "@typescript-eslint/type-utils": "8.61.1", "@typescript-eslint/utils": "8.61.1", "@typescript-eslint/visitor-keys": "8.61.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.61.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-ZPlVl3PB3et/59Ne0fv/sci6ZXz4T4Hp4nTJ56i/Y0gR89ARb+KphojTq6j+56E5PIezmOIOOWyY+aWQFd+IkQ=="], - "@typescript-eslint/parser": ["@typescript-eslint/parser@8.65.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.65.0", "@typescript-eslint/types": "8.65.0", "@typescript-eslint/typescript-estree": "8.65.0", "@typescript-eslint/visitor-keys": "8.65.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-CZ4nMxWwgu1HEEFNkeaCptra9QCtkmKdgf3sWh1rl1trIhmxLilgTV4cwcbQ4wemnT4sWQN8CaKOmdYx+g2gMA=="], + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.61.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.61.1", "@typescript-eslint/types": "8.61.1", "@typescript-eslint/typescript-estree": "8.61.1", "@typescript-eslint/visitor-keys": "8.61.1", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-PJ5vePq5/ognBbrIcoC5+SHO5dfpeLPzP9FpLkzWrguoYQEeeSjlJpVwOpo1JRSTEi7dRcwNy4h4dzV70PqHcg=="], - "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.65.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.65.0", "@typescript-eslint/types": "^8.65.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q=="], + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.61.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.61.1", "@typescript-eslint/types": "^8.61.1", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-PrC4JYGmR241lYnfhmKGTXkFqv8+ymbTFgSAY0fVXpY82/QkMw5TZPl+vGzuDDU2QYJk9fIDOBTntF+yDv9LEA=="], - "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.65.0", "", { "dependencies": { "@typescript-eslint/types": "8.65.0", "@typescript-eslint/visitor-keys": "8.65.0" } }, "sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg=="], + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.61.1", "", { "dependencies": { "@typescript-eslint/types": "8.61.1", "@typescript-eslint/visitor-keys": "8.61.1" } }, "sha512-L2bdIeoQS8FlKAvONAr20w6OcLXeB+qiDKbAooS9A0Ben+iSIkBef0FxqwKWYqt5sa0i4KJtxVyVmhMylKzF5w=="], - "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.65.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg=="], + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.61.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-UN/H4di+OO7EWx2ovME+8t31YO+KVnK0RRKEHR3kOt21/Ay8BOq3M1OMvWs5vNiqcFCYGYoxK3MXPZzmMUE+yg=="], - "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.65.0", "", { "dependencies": { "@typescript-eslint/types": "8.65.0", "@typescript-eslint/typescript-estree": "8.65.0", "@typescript-eslint/utils": "8.65.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-YjaZ7PRI5qY7ax2L3PbvX0rRyGtipAReCWs0mhhDBHjH/vl0g0BonaGXrKdKpMbIIsMIwDgbk/xzkBTyAltS5g=="], + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.61.1", "", { "dependencies": { "@typescript-eslint/types": "8.61.1", "@typescript-eslint/typescript-estree": "8.61.1", "@typescript-eslint/utils": "8.61.1", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-GYRicKmVK0C4fsKgaACaknOUAq9Oa2kwsjnpFhFcS/5p4Ht5IP9OVLbgIgcK4SRk92nVHFluurg1lumD9dBcLw=="], - "@typescript-eslint/types": ["@typescript-eslint/types@8.65.0", "", {}, "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg=="], + "@typescript-eslint/types": ["@typescript-eslint/types@8.61.1", "", {}, "sha512-G+CRlPqLv7Bz1IZVs03x5K59F1veqL0EJUROAdGhKsEq8qOiRiZbI+HUojPq5l0fEGOKModD9br6lObhB8zkoA=="], - "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.65.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.65.0", "@typescript-eslint/tsconfig-utils": "8.65.0", "@typescript-eslint/types": "8.65.0", "@typescript-eslint/visitor-keys": "8.65.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg=="], + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.61.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.61.1", "@typescript-eslint/tsconfig-utils": "8.61.1", "@typescript-eslint/types": "8.61.1", "@typescript-eslint/visitor-keys": "8.61.1", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-u+oQD3BqYWPc8YV9Zab4vaJElJuwOLPRc10Jm1o/qS+6Qwen14HCWwx0Seo4LnSn2wxea2Ik8DxPt2/FHmuhrg=="], - "@typescript-eslint/utils": ["@typescript-eslint/utils@8.65.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.65.0", "@typescript-eslint/types": "8.65.0", "@typescript-eslint/typescript-estree": "8.65.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA=="], + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.61.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.61.1", "@typescript-eslint/types": "8.61.1", "@typescript-eslint/typescript-estree": "8.61.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-1+P/3Dj6jvtybE1q0HQ6yBt/gq+oKJyLdEv4HdnqasaEXRSYCAsD59mXEVQnM/ULNdQxbX77tdG4jPRjIS6knA=="], - "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.65.0", "", { "dependencies": { "@typescript-eslint/types": "8.65.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A=="], + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.61.1", "", { "dependencies": { "@typescript-eslint/types": "8.61.1", "eslint-visitor-keys": "^5.0.0" } }, "sha512-6fJ9MHWtK14C1DSkiMlHUSOmrVebL7150xZJBlJiL62jjhIA4JmOq6flwBgDxIdBKKdoiZRel+dfPD5MLfny3w=="], - "@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.5", "", { "dependencies": { "@rolldown/pluginutils": "^1.0.1" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-BOVzne/NL162sMdResB25mUv+vWMF5NoAjNf09TeGlE7ZpszZWSD3winycicLJw72yeVsoCn/2kOhEuCvEShMA=="], + "@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.3", "", { "dependencies": { "@rolldown/pluginutils": "^1.0.1" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-vmFvco5/QuC2f9Oj+wTk0+9XeDFkHxSamwZKYc7MxYwKICfvUvlMhqKI0VuICPltGqh1neqBKDvO4kes1ya8vg=="], - "acorn": ["acorn@8.18.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="], + "acorn": ["acorn@8.17.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg=="], "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], @@ -195,15 +195,15 @@ "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], - "baseline-browser-mapping": ["baseline-browser-mapping@2.11.9", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-cp447VUsGS07+n1Dqf7YSQ8maeJrjEhaDxTm1ZefbqDtypHBC5GzGMQbklR6IPR13Y8OAJRHZWEMtZipJLCttg=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.38", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-31/02mVB4yuQU6adKk5SlY6m+mxDwUq5KZkyYgnLrrKl7TEm1+3PyDtDBz2kOv/wxZz41GHsvV1A/u6RmiyBvw=="], - "brace-expansion": ["brace-expansion@5.0.9", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg=="], + "brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="], - "browserslist": ["browserslist@4.28.7", "", { "dependencies": { "baseline-browser-mapping": "^2.10.44", "caniuse-lite": "^1.0.30001806", "electron-to-chromium": "^1.5.393", "node-releases": "^2.0.51", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw=="], + "browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="], "buffer-image-size": ["buffer-image-size@0.6.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ=="], - "caniuse-lite": ["caniuse-lite@1.0.30001806", "", {}, "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw=="], + "caniuse-lite": ["caniuse-lite@1.0.30001799", "", {}, "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw=="], "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], @@ -217,7 +217,7 @@ "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], - "electron-to-chromium": ["electron-to-chromium@1.5.399", "", {}, "sha512-lEcqhErbHjXRvd41rnWLpzbyU/IXfIYo7QwaFWmxGeLiLyY2TBCdHnWY88vB+p3ubnihRypDm66panXl7TylLA=="], + "electron-to-chromium": ["electron-to-chromium@1.5.375", "", {}, "sha512-ZWP5eB4BVPW/ZYo9252hQZHZ5XavtsTgpbhcmMmRwymavC5AsLWQWBPaKMeNd2LW0KGby5HPXvj7+sr4ta5j/Q=="], "entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], @@ -225,7 +225,7 @@ "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], - "eslint": ["eslint@10.8.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", "@eslint/config-helpers": "^0.7.0", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.2", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-nuKKvN+oIBO0koN7Tm7dlkmnkc21mtt0QJLwAKzjLq14y6lRTdVG36MZHJ8eQHwdJMwZbQNMlPOYedMq/oVJvQ=="], + "eslint": ["eslint@10.5.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", "@eslint/config-helpers": "^0.6.0", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.2", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-1y+7C+vi12bUK1IpZeaV3gsH9fHLBmPvYmPx42pvT/E9yG0IC8g3PUZZgp0+JLJl7ZDK0flc2gc+Aw9dpCvIsQ=="], "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@7.1.1", "", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "hermes-parser": "^0.25.1", "zod": "^3.25.0 || ^4.0.0", "zod-validation-error": "^3.5.0 || ^4.0.0" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" } }, "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g=="], @@ -259,7 +259,7 @@ "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], - "flatted": ["flatted@3.4.4", "", {}, "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q=="], + "flatted": ["flatted@3.4.2", "", {}, "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA=="], "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], @@ -267,7 +267,7 @@ "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], - "globals": ["globals@17.8.0", "", {}, "sha512-Zz/LMDZScFmkakeL2cTHzf+PbWKdpU3uclqkZT7TjDG58j5WPt0PpA+n9uPI24fZtlw07q0OtEi84K+umsRzqQ=="], + "globals": ["globals@17.6.0", "", {}, "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA=="], "happy-dom": ["happy-dom@20.11.1", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "buffer-image-size": "^0.6.4", "entities": "^7.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.21.0" } }, "sha512-XSt8tMzbW9ymE7687xztkO1ckR7qJNQ3LywY9vlYGhGi3zXrGBHuUo2Cl1ztZaICW+1eAGdkLbj6iwVqDT33kg=="], @@ -301,43 +301,43 @@ "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], - "lightningcss": ["lightningcss@1.33.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.33.0", "lightningcss-darwin-arm64": "1.33.0", "lightningcss-darwin-x64": "1.33.0", "lightningcss-freebsd-x64": "1.33.0", "lightningcss-linux-arm-gnueabihf": "1.33.0", "lightningcss-linux-arm64-gnu": "1.33.0", "lightningcss-linux-arm64-musl": "1.33.0", "lightningcss-linux-x64-gnu": "1.33.0", "lightningcss-linux-x64-musl": "1.33.0", "lightningcss-win32-arm64-msvc": "1.33.0", "lightningcss-win32-x64-msvc": "1.33.0" } }, "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA=="], + "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], - "lightningcss-android-arm64": ["lightningcss-android-arm64@1.33.0", "", { "os": "android", "cpu": "arm64" }, "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg=="], + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], - "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.33.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg=="], + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], - "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.33.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ=="], + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], - "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.33.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg=="], + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="], - "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.33.0", "", { "os": "linux", "cpu": "arm" }, "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ=="], + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="], - "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg=="], + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], - "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ=="], + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], - "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg=="], + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], - "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw=="], + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], - "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.33.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA=="], + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], - "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.33.0", "", { "os": "win32", "cpu": "x64" }, "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA=="], + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], - "minimatch": ["minimatch@10.2.6", "", { "dependencies": { "brace-expansion": "^5.0.8" } }, "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A=="], + "minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "nanoid": ["nanoid@3.3.16", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q=="], + "nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="], "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], - "node-releases": ["node-releases@2.0.51", "", {}, "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ=="], + "node-releases": ["node-releases@2.0.48", "", {}, "sha512-1uz8041X6LoI6ZSdZacM9lVY28vuzDlSKitnpbSNK0RfKoIJkX29NBPVEFXhnuSuEOA9Ww0xnPJ+ILWbGAv8DA=="], "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], @@ -351,19 +351,19 @@ "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - "picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="], + "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], - "postcss": ["postcss@8.5.25", "", { "dependencies": { "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw=="], + "postcss": ["postcss@8.5.15", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A=="], "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], - "react": ["react@19.2.8", "", {}, "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw=="], + "react": ["react@19.2.7", "", {}, "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ=="], - "react-dom": ["react-dom@19.2.8", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.8" } }, "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ=="], + "react-dom": ["react-dom@19.2.7", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.7" } }, "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ=="], - "rolldown": ["rolldown@1.2.1", "", { "dependencies": { "@oxc-project/types": "=0.142.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.2.1", "@rolldown/binding-darwin-arm64": "1.2.1", "@rolldown/binding-darwin-x64": "1.2.1", "@rolldown/binding-freebsd-x64": "1.2.1", "@rolldown/binding-linux-arm-gnueabihf": "1.2.1", "@rolldown/binding-linux-arm64-gnu": "1.2.1", "@rolldown/binding-linux-arm64-musl": "1.2.1", "@rolldown/binding-linux-ppc64-gnu": "1.2.1", "@rolldown/binding-linux-s390x-gnu": "1.2.1", "@rolldown/binding-linux-x64-gnu": "1.2.1", "@rolldown/binding-linux-x64-musl": "1.2.1", "@rolldown/binding-openharmony-arm64": "1.2.1", "@rolldown/binding-wasm32-wasi": "1.2.1", "@rolldown/binding-win32-arm64-msvc": "1.2.1", "@rolldown/binding-win32-x64-msvc": "1.2.1" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-4FKJhg8d3OiyQOA6Q1Q0hoFFpW9/OoX+VsHzpECsdsIZoOArrAK90gl59YK/Z+gnDel45bgJZK03ozH/9bCqEw=="], + "rolldown": ["rolldown@1.1.3", "", { "dependencies": { "@oxc-project/types": "=0.137.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.1.3", "@rolldown/binding-darwin-arm64": "1.1.3", "@rolldown/binding-darwin-x64": "1.1.3", "@rolldown/binding-freebsd-x64": "1.1.3", "@rolldown/binding-linux-arm-gnueabihf": "1.1.3", "@rolldown/binding-linux-arm64-gnu": "1.1.3", "@rolldown/binding-linux-arm64-musl": "1.1.3", "@rolldown/binding-linux-ppc64-gnu": "1.1.3", "@rolldown/binding-linux-s390x-gnu": "1.1.3", "@rolldown/binding-linux-x64-gnu": "1.1.3", "@rolldown/binding-linux-x64-musl": "1.1.3", "@rolldown/binding-openharmony-arm64": "1.1.3", "@rolldown/binding-wasm32-wasi": "1.1.3", "@rolldown/binding-win32-arm64-msvc": "1.1.3", "@rolldown/binding-win32-x64-msvc": "1.1.3" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-1F1eEtUBtFvcGm1HQ9TiUIUHPQG7mSAODrhIzjxoUEFuo8OcbrGLiVLkevNgj84TE4lnHvnumwFjhJO5Eu135g=="], "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], @@ -385,7 +385,7 @@ "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], - "typescript-eslint": ["typescript-eslint@8.65.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.65.0", "@typescript-eslint/parser": "8.65.0", "@typescript-eslint/typescript-estree": "8.65.0", "@typescript-eslint/utils": "8.65.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-/ggrHAwyjENDusvyxbuqxAC2dTnZg/Z8F+fgQtYIz+L6n/9HfSlEZcFGV/NsMNa6CkGk0xUjUAFwC0vHOflvIA=="], + "typescript-eslint": ["typescript-eslint@8.61.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.61.1", "@typescript-eslint/parser": "8.61.1", "@typescript-eslint/typescript-estree": "8.61.1", "@typescript-eslint/utils": "8.61.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-V7PayAfJokV3pEHgN7/v03D1SpujhRfQtYLbLIiBfDDncdg4PAiRBfoS4cnCANK4jmAPncczi59QO3afiXUlNw=="], "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], @@ -393,7 +393,7 @@ "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], - "vite": ["vite@8.2.0", "", { "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.5", "postcss": "^8.5.23", "rolldown": "~1.2.0", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.4.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-pn+CFpM0lwDeKwmOq1ZaBK/9sjorZcgqxki6MbY/jPEVd9vichIlmlD4HmQ5wdP5EgqQCFRaACBxMC7uEGc6lQ=="], + "vite": ["vite@8.1.0", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.15", "rolldown": "~1.1.2", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.3.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-BuJcQK/56NQTWDGn4ABea3q4SSBdNPWwNZKTkkUpcMPnLoquSYH8llRtSUIgoL1KSCpHt5eghLShn50mH36y7Q=="], "whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="], @@ -413,8 +413,8 @@ "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], - "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.6", "", {}, "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw=="], + "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], - "@typescript-eslint/typescript-estree/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + "@typescript-eslint/typescript-estree/semver": ["semver@7.8.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA=="], } } From 7ddf206344d0ba9d27b4a277e1011c623958dbc3 Mon Sep 17 00:00:00 2001 From: OnlineChef <280567955+OnlineChef@users.noreply.github.com> Date: Sat, 1 Aug 2026 11:28:38 +0200 Subject: [PATCH 08/19] fix(provider-security): align doctor auth assertions and auth_forbidden code Co-authored-by: Cursor --- src/provider-security/types.ts | 1 + tests/provider-security.test.ts | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/provider-security/types.ts b/src/provider-security/types.ts index dae26794b..dd4f64ba7 100644 --- a/src/provider-security/types.ts +++ b/src/provider-security/types.ts @@ -19,6 +19,7 @@ export type ProviderSecurityErrorCode = | "authority_error" | "auth_required" | "auth_invalid" + | "auth_forbidden" | "identity_assertion_mismatch" | "auth_not_configured"; diff --git a/tests/provider-security.test.ts b/tests/provider-security.test.ts index 3c51eacdc..6bf3da4c3 100644 --- a/tests/provider-security.test.ts +++ b/tests/provider-security.test.ts @@ -367,7 +367,7 @@ describe("provider-security bearer auth", () => { }), })); - expect(doctor.some(c => c.level === "OK" && c.message.includes("liveness OK"))).toBe(true); + expect(doctor.some(c => c.level === "OK" && c.layer === "liveness")).toBe(true); expect(doctor.some(c => c.level === "WARN" && c.message.includes("CHEF_PROVIDER_SECURITY_TOKEN"))).toBe(true); }); @@ -393,8 +393,8 @@ describe("provider-security bearer auth", () => { }), })); - expect(doctor.some(c => c.level === "OK" && c.message.includes("liveness OK"))).toBe(true); - expect(doctor.some(c => c.level === "WARN" && c.message.includes("authenticated readiness failed"))).toBe(true); + expect(doctor.some(c => c.level === "OK" && c.layer === "liveness")).toBe(true); + expect(doctor.some(c => c.level === "WARN" && c.layer === "authenticated")).toBe(true); }); }); From 23cff7460ec89d12f39fd92494be162091cd61d0 Mon Sep 17 00:00:00 2001 From: OnlineChef Date: Sat, 1 Aug 2026 09:34:17 +0000 Subject: [PATCH 09/19] fix(tests): use allowlisted bearer fixture token for privacy scan Co-authored-by: Codesmith --- tests/provider-security.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/provider-security.test.ts b/tests/provider-security.test.ts index 6bf3da4c3..5dfd55986 100644 --- a/tests/provider-security.test.ts +++ b/tests/provider-security.test.ts @@ -262,7 +262,7 @@ describe("provider-security client headers", () => { test("sends Authorization bearer on protected routes when token is configured", async () => { let headers: Record = {}; const client = testClient({ - token: "my-secret-bearer-token-32chars", + token: "access-token-chefvault-bearer", fetchImpl: (async (_input, init) => { headers = Object.fromEntries(new Headers(init?.headers).entries()); return Response.json(leaseResponse(), { status: 200 }); @@ -270,7 +270,7 @@ describe("provider-security client headers", () => { }); await client.resolveLease({ ref: REF }); - expect(headers.authorization).toBe("Bearer my-secret-bearer-token-32chars"); + expect(headers.authorization).toBe("Bearer access-token-chefvault-bearer"); }); test("does not send Authorization on healthz", async () => { From 3d204a50f764b95a56db2c36b7015fd29da93fb1 Mon Sep 17 00:00:00 2001 From: GroepChef Date: Sun, 2 Aug 2026 02:36:02 +0200 Subject: [PATCH 10/19] fix(provider-security): harden lease outages, revocation, and status probes Only authority outages enter degraded mode; revoked renewals evict and fail closed; in-flight resolve coalesces concurrent cold starts; status reporting is read-only and skips probing when no refs are configured; images/compact surface credential failures with the correct status. refs #38 Co-authored-by: Cursor --- src/cli/status.ts | 11 ++------ src/provider-security/client.ts | 46 +++++++++++++++++++------------ src/provider-security/degraded.ts | 7 ----- src/provider-security/index.ts | 6 ---- src/provider-security/resolve.ts | 32 +++++++++++++++++++-- src/provider-security/slots.ts | 13 +++++---- src/provider-security/status.ts | 40 +++++++++++++++------------ src/providers/credential.ts | 12 +++++--- src/providers/openai-sidecar.ts | 23 ++++++++++++++-- src/server/images.ts | 6 +++- src/server/responses/compact.ts | 7 +++-- 11 files changed, 128 insertions(+), 75 deletions(-) diff --git a/src/cli/status.ts b/src/cli/status.ts index 92581739d..93292bcfb 100644 --- a/src/cli/status.ts +++ b/src/cli/status.ts @@ -11,8 +11,7 @@ import { displayCodexRuntimePath, effortClampAppliesToRuntime, loadLastEffortCla import { redactSecretString, redactUserPath } from "../lib/redact"; import { collectOrcaCodexHomeDiagnostic, type OrcaCodexHomeDiagnostic } from "../codex/home"; import { grokFenceEndpointDrift, readGrokStatus } from "../grok/status"; -import { collectProviderSecurityStatus, type ProviderSecurityStatusReport } from "../provider-security/status"; -import { ProviderSecurityClient } from "../provider-security/client"; +import { collectProviderSecurityStatusAsync, type ProviderSecurityStatusReport } from "../provider-security/status"; type HealthCheck = { ok: boolean; @@ -273,13 +272,7 @@ export async function collectStatus(): Promise { ? "reachable, but PID file is missing or stale" : "not running"; - const providerSecurity = collectProviderSecurityStatus(config); - const authorityProbe = await ProviderSecurityClient.fromEnv().healthz(); - providerSecurity.authority = { - ok: authorityProbe.ok, - url: providerSecurity.authority.url, - message: authorityProbe.message, - }; + const providerSecurity = await collectProviderSecurityStatusAsync(config); return { proxyLabel, diff --git a/src/provider-security/client.ts b/src/provider-security/client.ts index 5e39cef10..53243d58c 100644 --- a/src/provider-security/client.ts +++ b/src/provider-security/client.ts @@ -78,7 +78,7 @@ function mapAuthorityError(status: number, body: unknown): ProviderSecurityError message, ); } - if (status === 503 || code === "auth_not_configured") { + if (code === "auth_not_configured") { return new ProviderSecurityError("auth_not_configured", message); } if (status === 404 || code === "ref_not_found") { @@ -90,6 +90,11 @@ function mapAuthorityError(status: number, body: unknown): ProviderSecurityError if (status === 410 || code === "revoked") { return new ProviderSecurityError("revoked", message); } + // An authority that answers 5xx is up but cannot serve; that is an outage, not a rejected + // credential, and only outages are allowed to trip degraded mode. + if (status >= 500) { + return new ProviderSecurityError("authority_unavailable", message); + } return new ProviderSecurityError("authority_error", message); } @@ -165,7 +170,11 @@ export class ProviderSecurityClient { return this.token; } - private async request(path: string, init: RequestInit, opts: { auth: boolean }): Promise { + private async request( + path: string, + init: RequestInit, + opts: { auth: boolean }, + ): Promise<{ ok: boolean; status: number; body: unknown }> { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), this.requestTimeoutMs); const headers: Record = { @@ -178,11 +187,15 @@ export class ProviderSecurityClient { headers.authorization = `Bearer ${this.requireToken()}`; } try { - return await this.fetchImpl(`${this.baseUrl}${path}`, { + const response = await this.fetchImpl(`${this.baseUrl}${path}`, { ...init, signal: controller.signal, headers, }); + // Read the body while the timeout is still armed: a response that stalls mid-body would + // otherwise hang a credential call for as long as the authority keeps the socket open. + const body = await response.json().catch(() => null); + return { ok: response.ok, status: response.status, body }; } catch (error) { if (error instanceof ProviderSecurityError) throw error; if (error instanceof Error && error.name === "AbortError") { @@ -200,9 +213,9 @@ export class ProviderSecurityClient { /** Unauthenticated liveness probe. */ async healthz(): Promise<{ ok: boolean; message: string }> { try { - const response = await this.request("/healthz", { method: "GET" }, { auth: false }); - if (!response.ok) { - return { ok: false, message: `HTTP ${response.status}` }; + const { ok, status } = await this.request("/healthz", { method: "GET" }, { auth: false }); + if (!ok) { + return { ok: false, message: `HTTP ${status}` }; } return { ok: true, message: "ok" }; } catch (error) { @@ -219,12 +232,11 @@ export class ProviderSecurityClient { */ async authenticatedReady(): Promise<{ ok: boolean; message: string }> { try { - const response = await this.request("/provider-security/status", { method: "GET" }, { auth: true }); - if (response.ok) { + const { ok, status, body } = await this.request("/provider-security/status", { method: "GET" }, { auth: true }); + if (ok) { return { ok: true, message: "authenticated" }; } - const body = await response.json().catch(() => null); - const mapped = mapAuthorityError(response.status, body); + const mapped = mapAuthorityError(status, body); return { ok: false, message: `${mapped.code}: ${mapped.message}` }; } catch (error) { if (error instanceof ProviderSecurityError) { @@ -235,25 +247,23 @@ export class ProviderSecurityClient { } async resolveLease(input: ChefVaultResolveRequest): Promise { - const response = await this.request("/v1/credentials/resolve", { + const { ok, status, body } = await this.request("/v1/credentials/resolve", { method: "POST", body: JSON.stringify(input), }, { auth: true }); - const body = await response.json().catch(() => null); - if (!response.ok) { - throw mapAuthorityError(response.status, body); + if (!ok) { + throw mapAuthorityError(status, body); } return parseResolveResponse(body); } async renewLease(input: ChefVaultRenewRequest): Promise { - const response = await this.request("/v1/credentials/renew", { + const { ok, status, body } = await this.request("/v1/credentials/renew", { method: "POST", body: JSON.stringify(input), }, { auth: true }); - const body = await response.json().catch(() => null); - if (!response.ok) { - throw mapAuthorityError(response.status, body); + if (!ok) { + throw mapAuthorityError(status, body); } return parseRenewResponse(body); } diff --git a/src/provider-security/degraded.ts b/src/provider-security/degraded.ts index bc296504a..bea30f041 100644 --- a/src/provider-security/degraded.ts +++ b/src/provider-security/degraded.ts @@ -4,7 +4,6 @@ * - Existing valid in-memory credentials may still be used (bounded by lease expiry). * - New resolution against the authority is denied until recovery. */ -import type { CredentialSnapshot } from "./types"; import { CredentialSlotStore } from "./slots"; export interface DegradedDecision { @@ -49,10 +48,4 @@ export class DegradedModeController { } return { allowed: true }; } - - existingSnapshot(ref: string, at = Date.now()): CredentialSnapshot | null { - const decision = this.canUseExisting(ref, at); - if (!decision.allowed) return null; - return this.store.snapshotForRequest(ref, at); - } } diff --git a/src/provider-security/index.ts b/src/provider-security/index.ts index 43a4b8bc0..578273177 100644 --- a/src/provider-security/index.ts +++ b/src/provider-security/index.ts @@ -4,9 +4,3 @@ export * from "./slots"; export * from "./degraded"; export * from "./resolve"; export * from "./status"; - -import { globalCredentialSlotStore } from "./slots"; -import { DegradedModeController } from "./degraded"; - -/** Shared degraded controller bound to the process-wide slot store. */ -export const globalDegradedMode = new DegradedModeController(globalCredentialSlotStore); diff --git a/src/provider-security/resolve.ts b/src/provider-security/resolve.ts index 675db86a5..0ffb3ad48 100644 --- a/src/provider-security/resolve.ts +++ b/src/provider-security/resolve.ts @@ -38,11 +38,25 @@ function toProviderSecurityError(error: unknown): ProviderSecurityError { ); } +/** + * Degraded mode means "ChefVault cannot be reached", so only availability failures may enter it. + * A rejected reference (unknown, invalid, revoked) proves the authority answered: keeping those in + * normal mode is what lets the caller keep seeing the real 401 instead of a masking + * `degraded_deny_resolve` on every later request. + */ +const AUTHORITY_OUTAGE_CODES: ReadonlySet = new Set([ + "authority_unavailable", + "authority_error", + "network_error", + "auth_not_configured", +]); + export class ProviderCredentialResolver { private readonly clientOverride: ProviderSecurityClient | undefined; - private readonly slotStore: CredentialSlotStore; + readonly slotStore: CredentialSlotStore; private readonly degraded: DegradedModeController; private readonly now: () => number; + private readonly inFlight = new Map>(); /** * Authority client. The process-wide resolver is constructed at import time, so the endpoint @@ -88,8 +102,19 @@ export class ProviderCredentialResolver { return { apiKey: existing.secret, snapshot: existing, source: "memory" }; } - const wasDegraded = this.degraded.isDegraded(ref); + // One resolve per ref at a time. Concurrent cold-start requests would otherwise each mint a + // lease, and every apply after the first would be rejected as a stale fencing token. + const pending = this.inFlight.get(ref); + if (pending) return pending; + const attempt = this.resolveFromAuthority(ref, at).finally(() => { + this.inFlight.delete(ref); + }); + this.inFlight.set(ref, attempt); + return attempt; + } + private async resolveFromAuthority(ref: string, at: number): Promise { + const wasDegraded = this.degraded.isDegraded(ref); const state = this.slotStore.getState(ref); try { const response = await this.client.resolveLease({ @@ -104,6 +129,9 @@ export class ProviderCredentialResolver { } return { apiKey: lease.secret, snapshot, source: "chefvault" }; } catch (error) { + if (!AUTHORITY_OUTAGE_CODES.has(providerSecurityErrorCode(error))) { + throw toProviderSecurityError(error); + } this.degraded.markUnavailable(ref, at); if (wasDegraded) { throw new ProviderSecurityError( diff --git a/src/provider-security/slots.ts b/src/provider-security/slots.ts index da32afe80..546111353 100644 --- a/src/provider-security/slots.ts +++ b/src/provider-security/slots.ts @@ -108,7 +108,7 @@ export class CredentialSlotStore { } getMode(ref: string): ProviderSecurityMode { - return this.ensure(ref).mode; + return this.stores.get(ref)?.mode ?? "normal"; } /** Immutable snapshot for an in-flight upstream request. Prefers active, then retiring. */ @@ -181,10 +181,11 @@ export class CredentialSlotStore { state.slots[phase] = { ...lease, phase: "revoked" }; } + /** Read-only: reporting on a ref must not create slot state for it. */ redactedStatus(ref: string, at = nowMs()): RedactedProviderSecurityStatus { - const state = this.ensure(ref); + const state = this.stores.get(ref); const slots: RedactedSlotSummary[] = (["active", "next", "retiring", "revoked"] as const).flatMap(phase => { - const lease = state.slots[phase]; + const lease = state?.slots[phase]; if (!lease) return []; return [{ phase, @@ -196,9 +197,9 @@ export class CredentialSlotStore { }); return { ref, - mode: state.mode, - degradedSince: state.degradedSince, - lastFencingToken: state.lastFencingToken, + mode: state?.mode ?? "normal", + degradedSince: state?.degradedSince ?? null, + lastFencingToken: state?.lastFencingToken ?? 0, slots, hasUsableCredential: slots.some(slot => slot.valid && slot.phase !== "revoked"), }; diff --git a/src/provider-security/status.ts b/src/provider-security/status.ts index 19b064ddf..11a48bc99 100644 --- a/src/provider-security/status.ts +++ b/src/provider-security/status.ts @@ -5,7 +5,7 @@ import type { OcxConfig, OcxProviderConfig } from "../types"; import { ProviderSecurityClient } from "./client"; import { globalCredentialSlotStore } from "./slots"; import { ProviderCredentialResolver } from "./resolve"; -import { isChefVaultRef, type RedactedProviderSecurityStatus } from "./types"; +import type { RedactedProviderSecurityStatus } from "./types"; export interface ProviderSecurityDoctorCheck { level: "OK" | "WARN"; @@ -30,10 +30,15 @@ export interface ProviderSecurityStatusReport { }>; } +/** + * Providers that delegate their secret to the plane. Malformed references are included on + * purpose: a typo'd ref fails every request closed, so hiding it from doctor/status is the worst + * possible place for it to be invisible. + */ export function listChefVaultProviders(config: OcxConfig): Array<{ name: string; ref: string; provider: OcxProviderConfig }> { return Object.entries(config.providers).flatMap(([name, provider]) => { const ref = provider.credentialRef?.trim(); - if (!ref || !isChefVaultRef(ref)) return []; + if (!ref) return []; return [{ name, ref, provider }]; }); } @@ -54,34 +59,33 @@ export function collectProviderSecurityStatus( }; } +/** + * Liveness plus authenticated readiness, for status surfaces. `/healthz` alone can report green + * while every lease call is rejected, so `authority.ok` requires both probes. Nothing here + * resolves a credential: reporting status must not mint leases as a side effect, and a proxy + * with no reference-backed provider must not reach out to the authority at all. + */ export async function collectProviderSecurityStatusAsync( config: OcxConfig, resolver: ProviderCredentialResolver = new ProviderCredentialResolver(), ): Promise { - const liveness = await resolver.client.healthz(); + const base = collectProviderSecurityStatus(config, resolver.client); + if (base.providers.length === 0) { + base.authority.message = "not probed (no provider uses a credential reference)"; + return base; + } + + const liveness = await resolver.probeAuthority(); const authenticated = liveness.ok - ? await resolver.client.authenticatedReady() + ? await resolver.probeAuthenticatedReady() : { ok: false, message: "skipped (liveness failed)" }; - const base = collectProviderSecurityStatus(config, resolver.client); base.authority = { ok: liveness.ok && authenticated.ok, url: resolver.client.baseUrl, - message: authenticated.ok ? authenticated.message : (liveness.ok ? authenticated.message : liveness.message), + message: liveness.ok ? authenticated.message : liveness.message, livenessOk: liveness.ok, authenticatedOk: authenticated.ok, }; - - if (authenticated.ok) { - for (const entry of base.providers) { - try { - await resolver.resolveCredentialRef(entry.credentialRef); - entry.status = globalCredentialSlotStore.redactedStatus(entry.credentialRef); - } catch { - entry.status = globalCredentialSlotStore.redactedStatus(entry.credentialRef); - } - } - } - return base; } diff --git a/src/providers/credential.ts b/src/providers/credential.ts index b18dc80b0..4f1d19cae 100644 --- a/src/providers/credential.ts +++ b/src/providers/credential.ts @@ -12,16 +12,20 @@ import { resolveEnvValue } from "../config"; import { globalProviderCredentialResolver, - isChefVaultRef, providerSecurityErrorCode, ProviderSecurityError, } from "../provider-security"; import type { OcxProviderConfig } from "../types"; -/** The provider's `chefvault://` reference, or undefined when it authenticates with a plain key. */ +/** + * The provider's credential reference, or undefined when it authenticates with a plain key. + * + * A configured-but-malformed reference is deliberately returned rather than ignored: silently + * falling back to an absent `apiKey` would forward the request with no Authorization header at + * all. Resolution rejects it as `ref_invalid` instead, so a typo fails closed and is visible. + */ export function providerCredentialRef(provider: Pick): string | undefined { - const ref = provider.credentialRef?.trim(); - return ref && isChefVaultRef(ref) ? ref : undefined; + return provider.credentialRef?.trim() || undefined; } /** diff --git a/src/providers/openai-sidecar.ts b/src/providers/openai-sidecar.ts index ada1c6eca..eae1761f2 100644 --- a/src/providers/openai-sidecar.ts +++ b/src/providers/openai-sidecar.ts @@ -1,4 +1,5 @@ -import { tryResolveProviderApiKey } from "./credential"; +import { providerCredentialFailure, resolveProviderApiKey, tryResolveProviderApiKey } from "./credential"; +import { ProviderSecurityError } from "../provider-security"; import { headersForCodexAuthContext, hasCallerCodexBearer, @@ -37,6 +38,9 @@ export interface OpenAiImagesProviderSelection { apiKey: string; }; error?: string; + /** Response shape for `error`. Defaults to a 400 invalid_request_error (a configuration mistake). */ + errorStatus?: number; + errorType?: string; } export function listOpenAiForwardSidecarCandidates(config: OcxConfig): OpenAiForwardSidecarCandidate[] { @@ -162,7 +166,22 @@ export async function selectImagesProvider(config: OcxConfig): Promise { const candidates = await selectImagesProvider(config); if (candidates.error) { - return formatErrorResponse(400, "invalid_request_error", candidates.error); + return formatErrorResponse( + candidates.errorStatus ?? 400, + candidates.errorType ?? "invalid_request_error", + candidates.error, + ); } const explicitKeyedProvider = config.images?.provider !== undefined && candidates.keyed !== undefined; // Admission bearer is valid proxy auth (requireApiAuth already passed) but must never be diff --git a/src/server/responses/compact.ts b/src/server/responses/compact.ts index 649df4087..91c9ffb67 100644 --- a/src/server/responses/compact.ts +++ b/src/server/responses/compact.ts @@ -248,17 +248,20 @@ export async function handleResponsesCompact( throw err; } // Same fail-closed lease as /v1/responses: a chefvault-backed provider must not compact - // against the upstream without an Authorization header. + // against the upstream without an Authorization header. A configured key may be an `$ENV` + // reference; a leased secret is literal credential material and must not be expanded. + let compactAuthKey = resolveEnvValue(compactProvider.apiKey); if (compactProvider.authMode !== "oauth" && compactProvider.authMode !== "forward" && providerCredentialRef(compactProvider)) { try { compactProvider = await withResolvedProviderCredential(compactProvider); + compactAuthKey = compactProvider.apiKey; } catch (err) { const failure = providerCredentialFailure(route.providerName, err); return formatErrorResponse(failure.status, failure.type, failure.message); } } const base = (compactProvider.baseUrl ?? "").replace(/\/$/, ""); - if (compactProvider.apiKey) headers.set("authorization", `Bearer ${resolveEnvValue(compactProvider.apiKey)}`); + if (compactAuthKey) headers.set("authorization", `Bearer ${compactAuthKey}`); const { reasoning: _reasoning, ...compactBodyRaw } = raw as typeof raw & { reasoning?: unknown }; // The regular /v1/responses path applies sanitizeReasoningInputContent via the adapter's // buildRequest, but the compact endpoint forwards directly. Apply the same sanitizer here From 3cde02b1e5cd404db1eeb050aa3afad4ecee4c3a Mon Sep 17 00:00:00 2001 From: OnlineChef Date: Sun, 2 Aug 2026 01:10:13 +0000 Subject: [PATCH 11/19] fix(provider-security): enforce degraded gate, ref-wide revocation, doctor guards - degraded mode gates resolve before any authority call, with a bounded 30s recovery probe; malformed responses (authority_error) no longer enter degraded mode - revoked renewals revoke every slot for the ref (active/next/retiring) so rotation leftovers fail closed - doctor returns early when no chefvault refs are configured, uses the resolver's slot store, and redacts authority error detail in status/doctor messages - validateChefVaultRef validates the raw ref string (whitespace/control chars, traversal, empty segments) and accepts host-only refs; redactedStatus reports the lease's own phase - ChefVault client: dedupe lease parsing, build headers before arming the request timeout so early throws cannot leak the timer - resolveModelsAuthToken defers all non-empty credentialRefs to the resolver so invalid refs fail closed instead of silently falling through to apiKey - tests: negative ref cases, degraded no-probe/recovery-interval coverage, retiring-slot revocation, doctor zero-ref guard, and vault bearer assertion in forwarding fixture Co-authored-by: Codesmith --- src/oauth/index.ts | 3 +- src/provider-security/client.ts | 41 +++---- src/provider-security/degraded.ts | 33 ++++- src/provider-security/resolve.ts | 33 ++--- src/provider-security/slots.ts | 17 ++- src/provider-security/status.ts | 36 +++--- src/provider-security/types.ts | 30 +++-- tests/provider-security-forwarding.test.ts | 5 + tests/provider-security.test.ts | 135 ++++++++++++++++++++- 9 files changed, 256 insertions(+), 77 deletions(-) diff --git a/src/oauth/index.ts b/src/oauth/index.ts index dea0bddf0..868ef5e70 100644 --- a/src/oauth/index.ts +++ b/src/oauth/index.ts @@ -17,7 +17,6 @@ import { apiKeyPoolEntryId, sanitizeApiKeyValue } from "../providers/api-keys"; import { effectiveGoogleMode, getProviderRegistryEntry, providerMatchesRegistryTransport } from "../providers/registry"; import { resolveProviderModelDiscoveryUrl } from "../providers/model-discovery"; import { resolveProviderTransport } from "../providers/xai-transport"; -import { isChefVaultRef } from "../provider-security"; import { globalProviderCredentialResolver } from "../provider-security/resolve"; import { detectClaudeCodeToken, detectGrokCliToken, hasComparableGrokIdentity, isSameGrokIdentity, shouldAdoptGrokGeneration } from "./local-token-detect"; import { logOAuthEvent } from "./log"; @@ -468,7 +467,7 @@ export async function resolveModelsAuthToken(name: string, prov: OcxProviderConf } } const credentialRef = prov.credentialRef?.trim(); - if (credentialRef && isChefVaultRef(credentialRef)) { + if (credentialRef) { try { const resolved = await globalProviderCredentialResolver.resolveCredentialRef(credentialRef); return resolved.apiKey; diff --git a/src/provider-security/client.ts b/src/provider-security/client.ts index 53243d58c..24d6dff4b 100644 --- a/src/provider-security/client.ts +++ b/src/provider-security/client.ts @@ -98,24 +98,29 @@ function mapAuthorityError(status: number, body: unknown): ProviderSecurityError return new ProviderSecurityError("authority_error", message); } -function parseResolveResponse(body: unknown): ChefVaultResolveResponse { +function parseLeaseFields(body: unknown, kind: "resolve" | "renew"): { + record: Record; + lease: ChefVaultRenewResponse; +} { if (!body || typeof body !== "object" || Array.isArray(body)) { - throw new ProviderSecurityError("authority_error", "resolve response was not an object"); + throw new ProviderSecurityError("authority_error", `${kind} response was not an object`); } const record = body as Record; const leaseId = typeof record.leaseId === "string" ? record.leaseId : ""; const secret = typeof record.secret === "string" ? record.secret : ""; const expiresAt = typeof record.expiresAt === "number" ? record.expiresAt : Number(record.expiresAt); const fencingToken = typeof record.fencingToken === "number" ? record.fencingToken : Number(record.fencingToken); - const slotHint = record.slotHint; if (!leaseId || !secret || !Number.isFinite(expiresAt) || !Number.isFinite(fencingToken)) { - throw new ProviderSecurityError("authority_error", "resolve response missing required lease fields"); + throw new ProviderSecurityError("authority_error", `${kind} response missing required lease fields`); } + return { record, lease: { leaseId, secret, expiresAt, fencingToken } }; +} + +function parseResolveResponse(body: unknown): ChefVaultResolveResponse { + const { record, lease } = parseLeaseFields(body, "resolve"); + const slotHint = record.slotHint; return { - leaseId, - secret, - expiresAt, - fencingToken, + ...lease, ...(slotHint === "active" || slotHint === "next" || slotHint === "retiring" ? { slotHint } : {}), @@ -123,18 +128,7 @@ function parseResolveResponse(body: unknown): ChefVaultResolveResponse { } function parseRenewResponse(body: unknown): ChefVaultRenewResponse { - if (!body || typeof body !== "object" || Array.isArray(body)) { - throw new ProviderSecurityError("authority_error", "renew response was not an object"); - } - const record = body as Record; - const leaseId = typeof record.leaseId === "string" ? record.leaseId : ""; - const secret = typeof record.secret === "string" ? record.secret : ""; - const expiresAt = typeof record.expiresAt === "number" ? record.expiresAt : Number(record.expiresAt); - const fencingToken = typeof record.fencingToken === "number" ? record.fencingToken : Number(record.fencingToken); - if (!leaseId || !secret || !Number.isFinite(expiresAt) || !Number.isFinite(fencingToken)) { - throw new ProviderSecurityError("authority_error", "renew response missing required lease fields"); - } - return { leaseId, secret, expiresAt, fencingToken }; + return parseLeaseFields(body, "renew").lease; } export class ProviderSecurityClient { @@ -175,17 +169,20 @@ export class ProviderSecurityClient { init: RequestInit, opts: { auth: boolean }, ): Promise<{ ok: boolean; status: number; body: unknown }> { - const controller = new AbortController(); - const timer = setTimeout(() => controller.abort(), this.requestTimeoutMs); const headers: Record = { accept: "application/json", "content-type": "application/json", ...workloadHeaders(this.workload), ...(init.headers as Record | undefined ?? {}), }; + // Resolve the token before arming the abort timer: requireToken() throws for + // unauthenticated calls, and a timer armed before that throw would leak and keep a + // short-lived process (e.g. the doctor CLI) alive until it fires. if (opts.auth) { headers.authorization = `Bearer ${this.requireToken()}`; } + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), this.requestTimeoutMs); try { const response = await this.fetchImpl(`${this.baseUrl}${path}`, { ...init, diff --git a/src/provider-security/degraded.ts b/src/provider-security/degraded.ts index bea30f041..53b4fc541 100644 --- a/src/provider-security/degraded.ts +++ b/src/provider-security/degraded.ts @@ -11,27 +11,48 @@ export interface DegradedDecision { reason?: "degraded_deny_resolve" | "lease_expired" | "revoked" | "missing"; } +const RECOVERY_PROBE_INTERVAL_MS = 30_000; + export class DegradedModeController { - constructor(private readonly store: CredentialSlotStore) {} + /** Last time the authority was probed (and failed) for a ref. */ + private readonly lastAttemptAt = new Map(); + private readonly probeIntervalMs: number; + + constructor( + private readonly store: CredentialSlotStore, + probeIntervalMs = RECOVERY_PROBE_INTERVAL_MS, + ) { + this.probeIntervalMs = probeIntervalMs; + } markUnavailable(ref: string, at = Date.now()): void { this.store.enterDegraded(ref, at); + this.lastAttemptAt.set(ref, at); } markRecovered(ref: string): void { this.store.exitDegraded(ref); + this.lastAttemptAt.delete(ref); } isDegraded(ref: string): boolean { return this.store.getMode(ref) === "degraded"; } - /** Whether a fresh resolve against ChefVault is permitted. */ - canResolve(ref: string): DegradedDecision { - if (this.isDegraded(ref)) { - return { allowed: false, reason: "degraded_deny_resolve" }; + /** + * Whether a fresh resolve against ChefVault is permitted. While degraded, new resolution + * is denied except for a bounded recovery probe once per probe interval, so the gate can + * reopen without waiting for a successful renewal. + */ + canResolve(ref: string, at = Date.now()): DegradedDecision { + if (!this.isDegraded(ref)) { + return { allowed: true }; } - return { allowed: true }; + const lastAttempt = this.lastAttemptAt.get(ref) ?? 0; + if (at - lastAttempt >= this.probeIntervalMs) { + return { allowed: true }; + } + return { allowed: false, reason: "degraded_deny_resolve" }; } /** Whether an already-resolved in-memory credential may be used for upstream auth. */ diff --git a/src/provider-security/resolve.ts b/src/provider-security/resolve.ts index 0ffb3ad48..a9be384d8 100644 --- a/src/provider-security/resolve.ts +++ b/src/provider-security/resolve.ts @@ -40,13 +40,13 @@ function toProviderSecurityError(error: unknown): ProviderSecurityError { /** * Degraded mode means "ChefVault cannot be reached", so only availability failures may enter it. - * A rejected reference (unknown, invalid, revoked) proves the authority answered: keeping those in - * normal mode is what lets the caller keep seeing the real 401 instead of a masking - * `degraded_deny_resolve` on every later request. + * Any delivered answer proves the authority is reachable: a rejected reference (unknown, + * invalid, revoked) must keep surfacing its real 401 cause, and `authority_error` (a malformed + * but delivered response, or a local failure while applying it) must not let a parsing bug + * masquerade as an outage. */ const AUTHORITY_OUTAGE_CODES: ReadonlySet = new Set([ "authority_unavailable", - "authority_error", "network_error", "auth_not_configured", ]); @@ -89,7 +89,7 @@ export class ProviderCredentialResolver { // An explicit revocation is authoritative: drop the lease and fail the request closed, // otherwise a revoked credential keeps authenticating until its old expiry. if (providerSecurityErrorCode(error) === "revoked") { - this.slotStore.revokePhase(ref, "active"); + this.slotStore.revokeRef(ref); throw toProviderSecurityError(error); } // Degraded handling applies on the next resolve. @@ -114,7 +114,14 @@ export class ProviderCredentialResolver { } private async resolveFromAuthority(ref: string, at: number): Promise { - const wasDegraded = this.degraded.isDegraded(ref); + // Degraded mode denies new resolution without touching the authority, except for a + // bounded recovery probe so the gate reopens once ChefVault returns. + if (!this.degraded.canResolve(ref, at).allowed) { + throw new ProviderSecurityError( + "degraded_deny_resolve", + "ChefVault is unavailable; new credential resolution is denied in degraded mode", + ); + } const state = this.slotStore.getState(ref); try { const response = await this.client.resolveLease({ @@ -129,17 +136,11 @@ export class ProviderCredentialResolver { } return { apiKey: lease.secret, snapshot, source: "chefvault" }; } catch (error) { - if (!AUTHORITY_OUTAGE_CODES.has(providerSecurityErrorCode(error))) { - throw toProviderSecurityError(error); - } - this.degraded.markUnavailable(ref, at); - if (wasDegraded) { - throw new ProviderSecurityError( - "degraded_deny_resolve", - "ChefVault is unavailable; new credential resolution is denied in degraded mode", - ); + const mapped = toProviderSecurityError(error); + if (AUTHORITY_OUTAGE_CODES.has(mapped.code)) { + this.degraded.markUnavailable(ref, at); } - throw toProviderSecurityError(error); + throw mapped; } } diff --git a/src/provider-security/slots.ts b/src/provider-security/slots.ts index 546111353..e312e592d 100644 --- a/src/provider-security/slots.ts +++ b/src/provider-security/slots.ts @@ -181,6 +181,19 @@ export class CredentialSlotStore { state.slots[phase] = { ...lease, phase: "revoked" }; } + /** + * Revocation from the authority is authoritative for the whole ref: no slot (including a + * retiring lease copied aside during rotation) may keep serving the secret. + */ + revokeRef(ref: string): void { + const state = this.stores.get(ref); + if (!state) return; + for (const phase of ["active", "next", "retiring"] as const) { + const lease = state.slots[phase]; + if (lease) state.slots[phase] = { ...lease, phase: "revoked" }; + } + } + /** Read-only: reporting on a ref must not create slot state for it. */ redactedStatus(ref: string, at = nowMs()): RedactedProviderSecurityStatus { const state = this.stores.get(ref); @@ -188,7 +201,9 @@ export class CredentialSlotStore { const lease = state?.slots[phase]; if (!lease) return []; return [{ - phase, + // Report the lease's own phase: a revoked lease stays stored under its slot key, + // and operators must be able to tell "revoked" apart from "expired". + phase: lease.phase, leaseId: lease.leaseId, expiresAt: lease.expiresAt, fencingToken: lease.fencingToken, diff --git a/src/provider-security/status.ts b/src/provider-security/status.ts index 11a48bc99..c5e5970ca 100644 --- a/src/provider-security/status.ts +++ b/src/provider-security/status.ts @@ -5,7 +5,7 @@ import type { OcxConfig, OcxProviderConfig } from "../types"; import { ProviderSecurityClient } from "./client"; import { globalCredentialSlotStore } from "./slots"; import { ProviderCredentialResolver } from "./resolve"; -import type { RedactedProviderSecurityStatus } from "./types"; +import { redactProviderSecurityDetail, type RedactedProviderSecurityStatus } from "./types"; export interface ProviderSecurityDoctorCheck { level: "OK" | "WARN"; @@ -69,7 +69,7 @@ export async function collectProviderSecurityStatusAsync( config: OcxConfig, resolver: ProviderCredentialResolver = new ProviderCredentialResolver(), ): Promise { - const base = collectProviderSecurityStatus(config, resolver.client); + const base = collectProviderSecurityStatus(config, resolver.client, resolver.slotStore); if (base.providers.length === 0) { base.authority.message = "not probed (no provider uses a credential reference)"; return base; @@ -82,7 +82,7 @@ export async function collectProviderSecurityStatusAsync( base.authority = { ok: liveness.ok && authenticated.ok, url: resolver.client.baseUrl, - message: liveness.ok ? authenticated.message : liveness.message, + message: redactProviderSecurityDetail(liveness.ok ? authenticated.message : liveness.message), livenessOk: liveness.ok, authenticatedOk: authenticated.ok, }; @@ -96,6 +96,18 @@ export async function collectProviderSecurityDoctorChecks( const checks: ProviderSecurityDoctorCheck[] = []; const refs = listChefVaultProviders(config); + // A proxy with no reference-backed provider must not reach out to the authority at all: + // guard before any liveness/authenticated probe, not after. + if (refs.length === 0) { + checks.push({ + level: "OK", + provider: "*", + layer: "provider", + message: "No providers configured with chefvault:// credentialRef.", + }); + return checks; + } + const liveness = await resolver.client.healthz(); if (liveness.ok) { checks.push({ @@ -109,7 +121,7 @@ export async function collectProviderSecurityDoctorChecks( level: "WARN", provider: "*", layer: "liveness", - message: `Liveness FAIL: ChefVault /healthz unavailable (${liveness.message}). Degraded mode: existing in-memory leases only; new resolve denied.`, + message: `Liveness FAIL: ChefVault /healthz unavailable (${redactProviderSecurityDetail(liveness.message)}). Degraded mode: existing in-memory leases only; new resolve denied.`, }); } @@ -127,7 +139,7 @@ export async function collectProviderSecurityDoctorChecks( level: "WARN", provider: "*", layer: "authenticated", - message: `Authenticated readiness FAIL: ${authenticated.message}. Set CHEF_PROVIDER_SECURITY_TOKEN to the workload bearer registered in ChefVault.`, + message: `Authenticated readiness FAIL: ${redactProviderSecurityDetail(authenticated.message)}. Set CHEF_PROVIDER_SECURITY_TOKEN to the workload bearer registered in ChefVault.`, }); } @@ -144,7 +156,7 @@ export async function collectProviderSecurityDoctorChecks( message: `Credential resolve OK for "${name}" (${ref}).`, }); } catch (error) { - const detail = error instanceof Error ? error.message : String(error); + const detail = redactProviderSecurityDetail(error instanceof Error ? error.message : String(error)); checks.push({ level: "WARN", provider: name, @@ -171,18 +183,8 @@ export async function collectProviderSecurityDoctorChecks( } } - if (refs.length === 0) { - checks.push({ - level: "OK", - provider: "*", - layer: "provider", - message: "No providers configured with chefvault:// credentialRef.", - }); - return checks; - } - for (const { name, ref } of refs) { - const status = globalCredentialSlotStore.redactedStatus(ref); + const status = resolver.slotStore.redactedStatus(ref); if (status.mode === "degraded") { checks.push({ level: "WARN", diff --git a/src/provider-security/types.ts b/src/provider-security/types.ts index dd4f64ba7..95b8562c9 100644 --- a/src/provider-security/types.ts +++ b/src/provider-security/types.ts @@ -130,16 +130,26 @@ export function validateChefVaultRef(ref: string): ProviderSecurityError | null if (!isChefVaultRef(ref)) { return new ProviderSecurityError("ref_invalid", `credential ref must start with ${CHEFVAULT_REF_PREFIX}`); } - try { - const parsed = new URL(ref); - if (parsed.protocol !== "chefvault:") { - return new ProviderSecurityError("ref_invalid", "credential ref must use chefvault:// scheme"); - } - if (!parsed.pathname || parsed.pathname === "/") { - return new ProviderSecurityError("ref_invalid", "credential ref must include a resource path"); - } - } catch { - return new ProviderSecurityError("ref_invalid", "credential ref is not a valid chefvault:// URI"); + // The raw ref string is the authority lookup key, so validate it verbatim: URL parsing + // would normalize away traversal segments and percent-encode whitespace. + if (/[\s\u0000-\u001f\u007f]/.test(ref)) { + return new ProviderSecurityError("ref_invalid", "credential ref must not contain whitespace or control characters"); + } + // Host-only refs (chefvault://name) are accepted, matching isChefVaultRef. + const segments = ref.slice(CHEFVAULT_REF_PREFIX.length).split("/"); + if (segments.some(segment => segment === "" || segment === "." || segment === "..")) { + return new ProviderSecurityError("ref_invalid", "credential ref must use non-empty path segments without traversal"); } return null; } + +/** + * Scrub credential material from authority error text before it reaches doctor/status + * output. Authority responses and transport errors may echo header or token fragments; + * redact anything shaped like bearer/token material rather than trusting the source. + */ +export function redactProviderSecurityDetail(text: string): string { + return text + .replace(/\bbearer\s+[^\s"',;]+/gi, "Bearer [redacted]") + .replace(/\b(authorization|x-api-key|api[-_]?key|token|secret)(\s*[:=]\s*)[^\s"',;]+/gi, "$1$2[redacted]"); +} diff --git a/tests/provider-security-forwarding.test.ts b/tests/provider-security-forwarding.test.ts index 2236d843b..dcf00fad3 100644 --- a/tests/provider-security-forwarding.test.ts +++ b/tests/provider-security-forwarding.test.ts @@ -69,6 +69,11 @@ test("a credentialRef-only provider forwards with the leased ChefVault secret", globalThis.fetch = (async (input, init) => { const url = input instanceof Request ? input.url : String(input); if (url === `${VAULT}/v1/credentials/resolve`) { + // The proxy must authenticate to ChefVault with the configured workload bearer; + // an unauthenticated resolve must not mint a lease. + if (new Headers(init?.headers).get("authorization") !== `Bearer ${VAULT_TOKEN}`) { + return Response.json({ code: "auth_invalid", message: "credential is not recognised" }, { status: 401 }); + } return Response.json({ leaseId: "lease-fwd", secret: "vaulted-secret-1", diff --git a/tests/provider-security.test.ts b/tests/provider-security.test.ts index 5dfd55986..6b483b497 100644 --- a/tests/provider-security.test.ts +++ b/tests/provider-security.test.ts @@ -75,9 +75,26 @@ describe("chefvault ref validation", () => { expect(validateChefVaultRef(REF)).toBeNull(); }); + test("accepts a host-only ref", () => { + expect(validateChefVaultRef("chefvault://providers")).toBeNull(); + }); + test("rejects bare paths", () => { expect(validateChefVaultRef("providers/demo")?.code).toBe("ref_invalid"); }); + + test.each([ + ["wrong scheme", "vault://providers/demo/prod"], + ["scheme only", "chefvault://"], + ["path traversal", "chefvault://providers/../secrets"], + ["current-dir segment", "chefvault://providers/./prod"], + ["embedded whitespace", "chefvault://providers/demo prod"], + ["control character", "chefvault://providers/demo\u0000prod"], + ["empty segment", "chefvault://providers//prod"], + ["empty string", ""], + ])("rejects %s", (_label, ref) => { + expect(validateChefVaultRef(ref)?.code).toBe("ref_invalid"); + }); }); describe("credential slot transitions", () => { @@ -120,11 +137,14 @@ describe("credential slot transitions", () => { store.applyResolve(REF, leaseResponse({ fencingToken: 5 }), Date.now()); expect(() => store.applyResolve(REF, leaseResponse({ fencingToken: 4 }), Date.now())) .toThrow(ProviderSecurityError); + let caught: unknown; try { store.applyResolve(REF, leaseResponse({ fencingToken: 3 }), Date.now()); } catch (error) { - expect((error as ProviderSecurityError).code).toBe("stale_fencing_token"); + caught = error; } + expect(caught).toBeInstanceOf(ProviderSecurityError); + expect((caught as ProviderSecurityError).code).toBe("stale_fencing_token"); }); test("renewal jitter stays within bound", () => { @@ -152,7 +172,7 @@ describe("degraded mode", () => { store.applyResolve(REF, leaseResponse({ expiresAt: at + 60 * 60_000 }), at); degraded.markUnavailable(REF, at); - expect(degraded.canResolve(REF).allowed).toBe(false); + expect(degraded.canResolve(REF, at).allowed).toBe(false); expect(degraded.canUseExisting(REF, at).allowed).toBe(true); const resolver = new ProviderCredentialResolver({ @@ -176,9 +196,53 @@ describe("degraded mode", () => { }); }); + test("a resolve during degraded mode issues no authority request", async () => { + const store = new CredentialSlotStore(); + const degraded = new DegradedModeController(store); + const at = 1_700_000_000_000; + degraded.markUnavailable(REF, at); + let calls = 0; + const resolver = new ProviderCredentialResolver({ + slotStore: store, + degraded, + now: () => at + 1_000, + client: testClient({ + fetchImpl: (async () => { + calls += 1; + return Response.json(leaseResponse(), { status: 200 }); + }) as typeof fetch, + }), + }); + + await expect(resolver.resolveCredentialRef(REF)).rejects.toMatchObject({ + code: "degraded_deny_resolve", + }); + expect(calls).toBe(0); + }); + + test("a malformed authority response surfaces authority_error without entering degraded mode", async () => { + const store = new CredentialSlotStore(); + const degraded = new DegradedModeController(store); + const resolver = new ProviderCredentialResolver({ + slotStore: store, + degraded, + client: testClient({ + fetchImpl: mockFetch({ + resolve: () => Response.json({ nonsense: true }, { status: 200 }), + }), + }), + }); + + await expect(resolver.resolveCredentialRef(REF)).rejects.toMatchObject({ + code: "authority_error", + }); + expect(store.getMode(REF)).toBe("normal"); + }); + test("recovers after authority returns", async () => { const store = new CredentialSlotStore(); const degraded = new DegradedModeController(store); + let t = Date.now(); let calls = 0; const client = testClient({ fetchImpl: mockFetch({ @@ -191,11 +255,20 @@ describe("degraded mode", () => { }, }), }); - const resolver = new ProviderCredentialResolver({ slotStore: store, degraded, client }); + const resolver = new ProviderCredentialResolver({ slotStore: store, degraded, client, now: () => t }); await expect(resolver.resolveCredentialRef(REF)).rejects.toBeInstanceOf(ProviderSecurityError); expect(store.getMode(REF)).toBe("degraded"); + // Inside the recovery probe interval the gate stays closed and never touches the authority. + t += 1_000; + await expect(resolver.resolveCredentialRef(REF)).rejects.toMatchObject({ + code: "degraded_deny_resolve", + }); + expect(calls).toBe(1); + + // After the interval elapses a single probe is allowed and recovery reopens the gate. + t += 31_000; const resolved = await resolver.resolveCredentialRef(REF); expect(resolved.snapshot.leaseId).toBe("fresh"); expect(store.getMode(REF)).toBe("normal"); @@ -227,6 +300,28 @@ describe("renewal failures", () => { expect(store.snapshotForRequest(REF, at)).toBeNull(); }); + test("a revoked renewal also revokes a retiring lease so the ref fails closed", async () => { + const store = new CredentialSlotStore(); + const at = 1_700_000_000_000; + // Rotation leaves the previous active lease in the retiring slot with its secret intact. + store.applyResolve(REF, leaseResponse({ leaseId: "old", secret: "skold", expiresAt: at + 60_000 }), at); + store.applyResolve(REF, leaseResponse({ leaseId: "new", secret: "sknew", fencingToken: 2, expiresAt: at + 60_000 }), at + 1); + const resolver = renewingResolver( + () => Response.json({ code: "revoked", message: "lease revoked" }, { status: 410 }), + store, + at + 2, + ); + + await expect(resolver.resolveCredentialRef(REF, { jitterMs: 0 })).rejects.toMatchObject({ code: "revoked" }); + expect(store.snapshotForRequest(REF, at + 2)).toBeNull(); + const slots = store.getState(REF)!.slots; + for (const phase of ["active", "next", "retiring"] as const) { + const lease = slots[phase]; + if (lease) expect(lease.phase).toBe("revoked"); + } + expect(slots.retiring?.phase).toBe("revoked"); + }); + test("a transient renewal failure keeps serving the cached lease", async () => { const store = new CredentialSlotStore(); const at = 1_700_000_000_000; @@ -396,6 +491,40 @@ describe("provider-security bearer auth", () => { expect(doctor.some(c => c.level === "OK" && c.layer === "liveness")).toBe(true); expect(doctor.some(c => c.level === "WARN" && c.layer === "authenticated")).toBe(true); }); + + test("doctor makes no authority probe when no chefvault refs are configured", async () => { + const config: OcxConfig = { + port: 10100, + defaultProvider: "demo", + providers: { + demo: { + adapter: "openai-chat", + baseUrl: "https://example/v1", + apiKey: "env:DEMO_KEY", + }, + }, + }; + + let fetchCalls = 0; + const doctor = await collectProviderSecurityDoctorChecks(config, new ProviderCredentialResolver({ + client: testClient({ + fetchImpl: (async () => { + fetchCalls += 1; + return new Response("ok", { status: 200 }); + }) as typeof fetch, + }), + })); + + expect(fetchCalls).toBe(0); + expect(doctor).toEqual([ + { + level: "OK", + provider: "*", + layer: "provider", + message: "No providers configured with chefvault:// credentialRef.", + }, + ]); + }); }); describe("redacted status serialization", () => { From ff63915f2a5220df14cd300648254ef53ff90ab3 Mon Sep 17 00:00:00 2001 From: OnlineChef Date: Sun, 2 Aug 2026 01:12:48 +0000 Subject: [PATCH 12/19] test(images): cover ChefVault lease failure mapping for the Images provider A revoked lease must surface as a non-retryable 401 authentication_error and an unreachable authority as a retryable 503 api_error, covering the selectImagesProvider -> providerCredentialFailure split used by Codex. Co-authored-by: Codesmith --- tests/server-images.test.ts | 51 +++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/tests/server-images.test.ts b/tests/server-images.test.ts index 98d99f0ab..08ff01e9e 100644 --- a/tests/server-images.test.ts +++ b/tests/server-images.test.ts @@ -513,6 +513,57 @@ test.each([ expect(selection.error).toContain(expectedError); }); +// A failed ChefVault lease on an explicit Images provider must keep its credential/authority +// split: a revoked lease is a non-retryable 401 authentication_error, an unreachable authority +// is a retryable 503 api_error. Codex uses the type to decide whether to retry. +test.each([ + ["revoked", "chefvault://providers/images/revoked", 410, { code: "revoked", message: "lease revoked" }, 401, "authentication_error"], + ["authority_unavailable", "chefvault://providers/images/outage", 503, { message: "authority down" }, 503, "api_error"], +] as const)( + "a %s ChefVault lease failure maps the Images provider error to %d/%s", + async (_case, ref, vaultStatus, vaultBody, expectedStatus, expectedType) => { + const previousVaultUrl = process.env.CHEF_PROVIDER_SECURITY_URL; + const previousVaultToken = process.env.CHEF_PROVIDER_SECURITY_TOKEN; + process.env.CHEF_PROVIDER_SECURITY_URL = "http://vault.test"; + process.env.CHEF_PROVIDER_SECURITY_TOKEN = "access-token-images-vault-32chars"; + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + if (url === "http://vault.test/v1/credentials/resolve") { + return Response.json(vaultBody, { status: vaultStatus }); + } + return originalFetch(input, init); + }) as typeof fetch; + + try { + const selection = await selectImagesProvider({ + port: 0, + defaultProvider: "custom-images", + providers: { + "custom-images": { + adapter: "openai-responses", + baseUrl: "https://images.example.test/v1", + authMode: "key", + credentialRef: ref, + }, + }, + images: { provider: "custom-images" }, + } as OcxConfig); + + expect(selection.keyed).toBeUndefined(); + expect(selection.forwardCandidates).toHaveLength(0); + expect(selection.error).toContain("credential could not be resolved"); + expect(selection.errorStatus).toBe(expectedStatus); + expect(selection.errorType).toBe(expectedType); + } finally { + globalThis.fetch = originalFetch; + if (previousVaultUrl === undefined) delete process.env.CHEF_PROVIDER_SECURITY_URL; + else process.env.CHEF_PROVIDER_SECURITY_URL = previousVaultUrl; + if (previousVaultToken === undefined) delete process.env.CHEF_PROVIDER_SECURITY_TOKEN; + else process.env.CHEF_PROVIDER_SECURITY_TOKEN = previousVaultToken; + } + }, +); + test("keyed baseUrl with a /v1 suffix is normalized (no double /v1)", async () => { const captured: CapturedRequest[] = []; const upstream = fakeImagesUpstream(captured); From 1248c6ad51c41da15b7a1557e304326dd6e0de9b Mon Sep 17 00:00:00 2001 From: OnlineChef Date: Sun, 2 Aug 2026 12:08:28 +0200 Subject: [PATCH 13/19] test(provider-security): exercise real ChefVault HTTP contract --- ...ovider-security-chefvault-contract.test.ts | 149 ++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 tests/provider-security-chefvault-contract.test.ts diff --git a/tests/provider-security-chefvault-contract.test.ts b/tests/provider-security-chefvault-contract.test.ts new file mode 100644 index 000000000..bcb512745 --- /dev/null +++ b/tests/provider-security-chefvault-contract.test.ts @@ -0,0 +1,149 @@ +import { afterEach, describe, expect, test } from "bun:test"; +import { mkdtempSync, rmSync } from "node:fs"; +import type { AddressInfo } from "node:net"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; +import { pathToFileURL } from "node:url"; +import { ProviderSecurityClient } from "../src/provider-security"; + +const chefVaultDist = process.env.CHEFVAULT_PROVIDER_SECURITY_DIST?.trim(); +const contractTest = chefVaultDist ? test : test.skip; + +const WORKLOAD_TOKEN = "ocx-contract-workload-token-2026"; +const ADMIN_TOKEN = "ocx-contract-admin-token-2026"; +const WORKLOAD = { + workloadId: "opencodex", + hostId: "ocx-contract-host", + actor: "opencodex-contract", +}; + +describe("ChefVault cross-repository HTTP contract", () => { + let server: import("node:http").Server | undefined; + let stateDir = ""; + + afterEach(async () => { + if (server) { + await new Promise((resolveClose, reject) => { + server!.close(error => error ? reject(error) : resolveClose()); + }); + server = undefined; + } + if (stateDir) rmSync(stateDir, { recursive: true, force: true }); + stateDir = ""; + }); + + contractTest("enforces bearer identity and supports resolve plus renew with the real Vault server", async () => { + const moduleUrl = pathToFileURL(resolve(chefVaultDist!)).href; + const vault = await import(moduleUrl) as { + ProviderSecurityService: { + open(path: string): Promise<{ + createCredentialRef(input: { + providerId: string; + alias: string; + secret: string; + actor: string; + }): Promise<{ uri: string }>; + }>; + }; + TokenRegistry: new (input: { + admins: Array<{ subject: string; token: string }>; + workloads: Array<{ + workloadId: string; + hostId: string; + token: string; + subject: string; + }>; + }) => unknown; + createProviderSecurityServer(service: unknown, registry: unknown): import("node:http").Server; + }; + + stateDir = mkdtempSync(join(tmpdir(), "ocx-chefvault-contract-")); + const service = await vault.ProviderSecurityService.open(join(stateDir, "provider-security.json")); + const ref = await service.createCredentialRef({ + providerId: "openai", + alias: "contract-key", + secret: "vault-contract-secret", + actor: "contract-admin", + }); + const registry = new vault.TokenRegistry({ + admins: [{ subject: "contract-admin", token: ADMIN_TOKEN }], + workloads: [{ ...WORKLOAD, token: WORKLOAD_TOKEN, subject: WORKLOAD.actor }], + }); + + server = vault.createProviderSecurityServer(service, registry); + await new Promise((resolveListen, reject) => { + const onError = (error: Error) => reject(error); + server!.once("error", onError); + server!.listen(0, "127.0.0.1", () => { + server!.off("error", onError); + resolveListen(); + }); + }); + const address = server.address() as AddressInfo; + const baseUrl = `http://127.0.0.1:${address.port}`; + + // Client-asserted identity headers do not grant authority without a bearer credential. + const unauthenticated = await fetch(`${baseUrl}/v1/credentials/resolve`, { + method: "POST", + headers: { + "content-type": "application/json", + "x-chef-workload-id": WORKLOAD.workloadId, + "x-chef-host-id": WORKLOAD.hostId, + "x-chef-actor": WORKLOAD.actor, + }, + body: JSON.stringify({ ref: ref.uri, fencingToken: 0 }), + }); + expect(unauthenticated.status).toBe(401); + expect(await unauthenticated.json()).toMatchObject({ code: "auth_required" }); + + const client = new ProviderSecurityClient({ + baseUrl, + token: WORKLOAD_TOKEN, + workload: WORKLOAD, + }); + const resolved = await client.resolveLease({ ref: ref.uri, fencingToken: 0 }); + expect(resolved).toMatchObject({ + secret: "vault-contract-secret", + slotHint: "active", + }); + expect(resolved.leaseId).not.toBe(""); + expect(resolved.fencingToken).toBeGreaterThan(0); + expect(resolved.expiresAt).toBeGreaterThan(Date.now()); + + const renewed = await client.renewLease({ + ref: ref.uri, + leaseId: resolved.leaseId, + fencingToken: resolved.fencingToken, + }); + expect(renewed).toMatchObject({ + leaseId: resolved.leaseId, + secret: "vault-contract-secret", + }); + expect(renewed.expiresAt).toBeGreaterThan(Date.now()); + + // The token-bound workload and host win. Mismatched assertions are a hard 403 mapped to the + // stable OCX identity_assertion_mismatch error code. + const mismatched = new ProviderSecurityClient({ + baseUrl, + token: WORKLOAD_TOKEN, + workload: { ...WORKLOAD, workloadId: "spoofed-workload" }, + }); + await expect(mismatched.resolveLease({ ref: ref.uri, fencingToken: 0 })).rejects.toMatchObject({ + code: "identity_assertion_mismatch", + }); + + // A workload credential cannot cross into the operator/admin surface. + const forbiddenAdmin = await fetch(`${baseUrl}/provider-security/mode`, { + method: "PUT", + headers: { + authorization: `Bearer ${WORKLOAD_TOKEN}`, + "content-type": "application/json", + "x-chef-workload-id": WORKLOAD.workloadId, + "x-chef-host-id": WORKLOAD.hostId, + }, + body: JSON.stringify({ mode: "degraded" }), + }); + expect(forbiddenAdmin.status).toBe(403); + expect(await forbiddenAdmin.json()).toMatchObject({ code: "admin_required" }); + }); +}); From c9c89df83051129a43807b3321b62a8c65f963d2 Mon Sep 17 00:00:00 2001 From: OnlineChef Date: Sun, 2 Aug 2026 12:09:02 +0200 Subject: [PATCH 14/19] ci(provider-security): verify pinned ChefVault contract --- .../workflows/provider-security-contract.yml | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 .github/workflows/provider-security-contract.yml diff --git a/.github/workflows/provider-security-contract.yml b/.github/workflows/provider-security-contract.yml new file mode 100644 index 000000000..472702ee6 --- /dev/null +++ b/.github/workflows/provider-security-contract.yml @@ -0,0 +1,69 @@ +name: ChefVault Provider-Security Contract + +on: + pull_request: + branches: [dev] + paths: + - "src/provider-security/**" + - "src/providers/credential.ts" + - "src/providers/openai-sidecar.ts" + - "src/server/images.ts" + - "src/server/live.ts" + - "src/server/responses/**" + - "tests/provider-security*.test.ts" + - ".github/workflows/provider-security-contract.yml" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: provider-security-contract-${{ github.ref }} + cancel-in-progress: true + +jobs: + chefvault-contract: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout OpenCodex + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + with: + persist-credentials: false + + - name: Checkout pinned ChefVault contract + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + with: + repository: OnlineChefGroep/chefgroep-vault + # Intentional contract pin. Update only with an explicit compatibility review. + ref: 9d46095d02c078377c89e569f86b5b813a222b17 + path: .ci/chefgroep-vault + persist-credentials: false + + - name: Setup Bun + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + with: + bun-version: 1.3.14 + + - name: Setup Node + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: 20 + + - name: Install OpenCodex dependencies + run: bun install --frozen-lockfile + + - name: Build pinned ChefVault provider-security package + shell: bash + run: | + corepack enable + corepack prepare pnpm@10.33.3 --activate + cd .ci/chefgroep-vault + pnpm install --frozen-lockfile --filter @chefgroep/provider-security... + pnpm --filter @chefgroep/provider-security build + + - name: Exercise real HTTP authentication and lease contract + shell: bash + env: + CHEFVAULT_PROVIDER_SECURITY_DIST: ${{ github.workspace }}/.ci/chefgroep-vault/packages/provider-security/dist/index.js + run: bun test tests/provider-security-chefvault-contract.test.ts From 5f058c9d9482066aa4eb6e5f293f8af14b0907e8 Mon Sep 17 00:00:00 2001 From: OnlineChef Date: Sun, 2 Aug 2026 12:11:25 +0200 Subject: [PATCH 15/19] ci(provider-security): run contract on security branches --- .github/workflows/provider-security-contract.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/provider-security-contract.yml b/.github/workflows/provider-security-contract.yml index 472702ee6..1ce19b1c3 100644 --- a/.github/workflows/provider-security-contract.yml +++ b/.github/workflows/provider-security-contract.yml @@ -12,6 +12,17 @@ on: - "src/server/responses/**" - "tests/provider-security*.test.ts" - ".github/workflows/provider-security-contract.yml" + push: + branches: [main, dev, "feat/provider-security-*"] + paths: + - "src/provider-security/**" + - "src/providers/credential.ts" + - "src/providers/openai-sidecar.ts" + - "src/server/images.ts" + - "src/server/live.ts" + - "src/server/responses/**" + - "tests/provider-security*.test.ts" + - ".github/workflows/provider-security-contract.yml" workflow_dispatch: permissions: From ac7aafb64c941747a1469e66e8196d03e48b5bdc Mon Sep 17 00:00:00 2001 From: OnlineChef Date: Sun, 2 Aug 2026 10:13:32 +0000 Subject: [PATCH 16/19] ci(provider-security): gate ChefVault contract on cross-repo token Co-authored-by: Codesmith --- .github/workflows/provider-security-contract.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.github/workflows/provider-security-contract.yml b/.github/workflows/provider-security-contract.yml index 1ce19b1c3..a0302f052 100644 --- a/.github/workflows/provider-security-contract.yml +++ b/.github/workflows/provider-security-contract.yml @@ -36,6 +36,11 @@ jobs: chefvault-contract: runs-on: ubuntu-latest timeout-minutes: 10 + env: + # chefgroep-vault is private, so the default GITHUB_TOKEN cannot read it. + # The contract only runs when a read-only cross-repository token is + # configured; fork pull requests never receive secrets and skip cleanly. + CHEFVAULT_CONTRACT_AVAILABLE: ${{ secrets.CHEFVAULT_CONTRACT_TOKEN != '' }} steps: - name: Checkout OpenCodex uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 @@ -43,28 +48,34 @@ jobs: persist-credentials: false - name: Checkout pinned ChefVault contract + if: env.CHEFVAULT_CONTRACT_AVAILABLE == 'true' uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 with: repository: OnlineChefGroep/chefgroep-vault # Intentional contract pin. Update only with an explicit compatibility review. ref: 9d46095d02c078377c89e569f86b5b813a222b17 path: .ci/chefgroep-vault + token: ${{ secrets.CHEFVAULT_CONTRACT_TOKEN }} persist-credentials: false - name: Setup Bun + if: env.CHEFVAULT_CONTRACT_AVAILABLE == 'true' uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 with: bun-version: 1.3.14 - name: Setup Node + if: env.CHEFVAULT_CONTRACT_AVAILABLE == 'true' uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: 20 - name: Install OpenCodex dependencies + if: env.CHEFVAULT_CONTRACT_AVAILABLE == 'true' run: bun install --frozen-lockfile - name: Build pinned ChefVault provider-security package + if: env.CHEFVAULT_CONTRACT_AVAILABLE == 'true' shell: bash run: | corepack enable @@ -74,7 +85,12 @@ jobs: pnpm --filter @chefgroep/provider-security build - name: Exercise real HTTP authentication and lease contract + if: env.CHEFVAULT_CONTRACT_AVAILABLE == 'true' shell: bash env: CHEFVAULT_PROVIDER_SECURITY_DIST: ${{ github.workspace }}/.ci/chefgroep-vault/packages/provider-security/dist/index.js run: bun test tests/provider-security-chefvault-contract.test.ts + + - name: Report skipped contract verification + if: env.CHEFVAULT_CONTRACT_AVAILABLE != 'true' + run: echo "::warning::CHEFVAULT_CONTRACT_TOKEN is not configured; skipping pinned ChefVault contract verification." From 4a9f139880b47ed7bd4bb133961efbbe32c5d810 Mon Sep 17 00:00:00 2001 From: OnlineChef Date: Sun, 2 Aug 2026 12:14:52 +0200 Subject: [PATCH 17/19] ci(provider-security): keep live contract in ChefVault --- .../workflows/provider-security-contract.yml | 96 ------------------- 1 file changed, 96 deletions(-) delete mode 100644 .github/workflows/provider-security-contract.yml diff --git a/.github/workflows/provider-security-contract.yml b/.github/workflows/provider-security-contract.yml deleted file mode 100644 index a0302f052..000000000 --- a/.github/workflows/provider-security-contract.yml +++ /dev/null @@ -1,96 +0,0 @@ -name: ChefVault Provider-Security Contract - -on: - pull_request: - branches: [dev] - paths: - - "src/provider-security/**" - - "src/providers/credential.ts" - - "src/providers/openai-sidecar.ts" - - "src/server/images.ts" - - "src/server/live.ts" - - "src/server/responses/**" - - "tests/provider-security*.test.ts" - - ".github/workflows/provider-security-contract.yml" - push: - branches: [main, dev, "feat/provider-security-*"] - paths: - - "src/provider-security/**" - - "src/providers/credential.ts" - - "src/providers/openai-sidecar.ts" - - "src/server/images.ts" - - "src/server/live.ts" - - "src/server/responses/**" - - "tests/provider-security*.test.ts" - - ".github/workflows/provider-security-contract.yml" - workflow_dispatch: - -permissions: - contents: read - -concurrency: - group: provider-security-contract-${{ github.ref }} - cancel-in-progress: true - -jobs: - chefvault-contract: - runs-on: ubuntu-latest - timeout-minutes: 10 - env: - # chefgroep-vault is private, so the default GITHUB_TOKEN cannot read it. - # The contract only runs when a read-only cross-repository token is - # configured; fork pull requests never receive secrets and skip cleanly. - CHEFVAULT_CONTRACT_AVAILABLE: ${{ secrets.CHEFVAULT_CONTRACT_TOKEN != '' }} - steps: - - name: Checkout OpenCodex - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - with: - persist-credentials: false - - - name: Checkout pinned ChefVault contract - if: env.CHEFVAULT_CONTRACT_AVAILABLE == 'true' - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - with: - repository: OnlineChefGroep/chefgroep-vault - # Intentional contract pin. Update only with an explicit compatibility review. - ref: 9d46095d02c078377c89e569f86b5b813a222b17 - path: .ci/chefgroep-vault - token: ${{ secrets.CHEFVAULT_CONTRACT_TOKEN }} - persist-credentials: false - - - name: Setup Bun - if: env.CHEFVAULT_CONTRACT_AVAILABLE == 'true' - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 - with: - bun-version: 1.3.14 - - - name: Setup Node - if: env.CHEFVAULT_CONTRACT_AVAILABLE == 'true' - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version: 20 - - - name: Install OpenCodex dependencies - if: env.CHEFVAULT_CONTRACT_AVAILABLE == 'true' - run: bun install --frozen-lockfile - - - name: Build pinned ChefVault provider-security package - if: env.CHEFVAULT_CONTRACT_AVAILABLE == 'true' - shell: bash - run: | - corepack enable - corepack prepare pnpm@10.33.3 --activate - cd .ci/chefgroep-vault - pnpm install --frozen-lockfile --filter @chefgroep/provider-security... - pnpm --filter @chefgroep/provider-security build - - - name: Exercise real HTTP authentication and lease contract - if: env.CHEFVAULT_CONTRACT_AVAILABLE == 'true' - shell: bash - env: - CHEFVAULT_PROVIDER_SECURITY_DIST: ${{ github.workspace }}/.ci/chefgroep-vault/packages/provider-security/dist/index.js - run: bun test tests/provider-security-chefvault-contract.test.ts - - - name: Report skipped contract verification - if: env.CHEFVAULT_CONTRACT_AVAILABLE != 'true' - run: echo "::warning::CHEFVAULT_CONTRACT_TOKEN is not configured; skipping pinned ChefVault contract verification." From cfd83f3870d7e0fa40a6f06797930c7c2905a8f5 Mon Sep 17 00:00:00 2001 From: OnlineChef Date: Sun, 2 Aug 2026 12:15:00 +0200 Subject: [PATCH 18/19] test(provider-security): move live server contract to Vault --- ...ovider-security-chefvault-contract.test.ts | 149 ------------------ 1 file changed, 149 deletions(-) delete mode 100644 tests/provider-security-chefvault-contract.test.ts diff --git a/tests/provider-security-chefvault-contract.test.ts b/tests/provider-security-chefvault-contract.test.ts deleted file mode 100644 index bcb512745..000000000 --- a/tests/provider-security-chefvault-contract.test.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { afterEach, describe, expect, test } from "bun:test"; -import { mkdtempSync, rmSync } from "node:fs"; -import type { AddressInfo } from "node:net"; -import { tmpdir } from "node:os"; -import { join, resolve } from "node:path"; -import { pathToFileURL } from "node:url"; -import { ProviderSecurityClient } from "../src/provider-security"; - -const chefVaultDist = process.env.CHEFVAULT_PROVIDER_SECURITY_DIST?.trim(); -const contractTest = chefVaultDist ? test : test.skip; - -const WORKLOAD_TOKEN = "ocx-contract-workload-token-2026"; -const ADMIN_TOKEN = "ocx-contract-admin-token-2026"; -const WORKLOAD = { - workloadId: "opencodex", - hostId: "ocx-contract-host", - actor: "opencodex-contract", -}; - -describe("ChefVault cross-repository HTTP contract", () => { - let server: import("node:http").Server | undefined; - let stateDir = ""; - - afterEach(async () => { - if (server) { - await new Promise((resolveClose, reject) => { - server!.close(error => error ? reject(error) : resolveClose()); - }); - server = undefined; - } - if (stateDir) rmSync(stateDir, { recursive: true, force: true }); - stateDir = ""; - }); - - contractTest("enforces bearer identity and supports resolve plus renew with the real Vault server", async () => { - const moduleUrl = pathToFileURL(resolve(chefVaultDist!)).href; - const vault = await import(moduleUrl) as { - ProviderSecurityService: { - open(path: string): Promise<{ - createCredentialRef(input: { - providerId: string; - alias: string; - secret: string; - actor: string; - }): Promise<{ uri: string }>; - }>; - }; - TokenRegistry: new (input: { - admins: Array<{ subject: string; token: string }>; - workloads: Array<{ - workloadId: string; - hostId: string; - token: string; - subject: string; - }>; - }) => unknown; - createProviderSecurityServer(service: unknown, registry: unknown): import("node:http").Server; - }; - - stateDir = mkdtempSync(join(tmpdir(), "ocx-chefvault-contract-")); - const service = await vault.ProviderSecurityService.open(join(stateDir, "provider-security.json")); - const ref = await service.createCredentialRef({ - providerId: "openai", - alias: "contract-key", - secret: "vault-contract-secret", - actor: "contract-admin", - }); - const registry = new vault.TokenRegistry({ - admins: [{ subject: "contract-admin", token: ADMIN_TOKEN }], - workloads: [{ ...WORKLOAD, token: WORKLOAD_TOKEN, subject: WORKLOAD.actor }], - }); - - server = vault.createProviderSecurityServer(service, registry); - await new Promise((resolveListen, reject) => { - const onError = (error: Error) => reject(error); - server!.once("error", onError); - server!.listen(0, "127.0.0.1", () => { - server!.off("error", onError); - resolveListen(); - }); - }); - const address = server.address() as AddressInfo; - const baseUrl = `http://127.0.0.1:${address.port}`; - - // Client-asserted identity headers do not grant authority without a bearer credential. - const unauthenticated = await fetch(`${baseUrl}/v1/credentials/resolve`, { - method: "POST", - headers: { - "content-type": "application/json", - "x-chef-workload-id": WORKLOAD.workloadId, - "x-chef-host-id": WORKLOAD.hostId, - "x-chef-actor": WORKLOAD.actor, - }, - body: JSON.stringify({ ref: ref.uri, fencingToken: 0 }), - }); - expect(unauthenticated.status).toBe(401); - expect(await unauthenticated.json()).toMatchObject({ code: "auth_required" }); - - const client = new ProviderSecurityClient({ - baseUrl, - token: WORKLOAD_TOKEN, - workload: WORKLOAD, - }); - const resolved = await client.resolveLease({ ref: ref.uri, fencingToken: 0 }); - expect(resolved).toMatchObject({ - secret: "vault-contract-secret", - slotHint: "active", - }); - expect(resolved.leaseId).not.toBe(""); - expect(resolved.fencingToken).toBeGreaterThan(0); - expect(resolved.expiresAt).toBeGreaterThan(Date.now()); - - const renewed = await client.renewLease({ - ref: ref.uri, - leaseId: resolved.leaseId, - fencingToken: resolved.fencingToken, - }); - expect(renewed).toMatchObject({ - leaseId: resolved.leaseId, - secret: "vault-contract-secret", - }); - expect(renewed.expiresAt).toBeGreaterThan(Date.now()); - - // The token-bound workload and host win. Mismatched assertions are a hard 403 mapped to the - // stable OCX identity_assertion_mismatch error code. - const mismatched = new ProviderSecurityClient({ - baseUrl, - token: WORKLOAD_TOKEN, - workload: { ...WORKLOAD, workloadId: "spoofed-workload" }, - }); - await expect(mismatched.resolveLease({ ref: ref.uri, fencingToken: 0 })).rejects.toMatchObject({ - code: "identity_assertion_mismatch", - }); - - // A workload credential cannot cross into the operator/admin surface. - const forbiddenAdmin = await fetch(`${baseUrl}/provider-security/mode`, { - method: "PUT", - headers: { - authorization: `Bearer ${WORKLOAD_TOKEN}`, - "content-type": "application/json", - "x-chef-workload-id": WORKLOAD.workloadId, - "x-chef-host-id": WORKLOAD.hostId, - }, - body: JSON.stringify({ mode: "degraded" }), - }); - expect(forbiddenAdmin.status).toBe(403); - expect(await forbiddenAdmin.json()).toMatchObject({ code: "admin_required" }); - }); -}); From acffee8254be5a8a06567ed453c4e6ab23a3f2c2 Mon Sep 17 00:00:00 2001 From: OnlineChef Date: Sun, 2 Aug 2026 10:30:36 +0000 Subject: [PATCH 19/19] fix(provider-security): gate renewal outages and redact data-plane lease errors Co-authored-by: Codesmith --- src/provider-security/resolve.ts | 18 ++++++-- src/providers/credential.ts | 5 ++- tests/provider-security.test.ts | 72 ++++++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 4 deletions(-) diff --git a/src/provider-security/resolve.ts b/src/provider-security/resolve.ts index a9be384d8..b15281b36 100644 --- a/src/provider-security/resolve.ts +++ b/src/provider-security/resolve.ts @@ -81,18 +81,30 @@ export class ProviderCredentialResolver { const existing = this.slotStore.snapshotForRequest(ref, at); if (existing && existing.expiresAt > at) { const renewTarget = this.slotStore.getState(ref)?.slots.active; - if (renewTarget && shouldRenewLease(renewTarget, at, deps.jitterMs ?? renewalJitterMs())) { + if ( + renewTarget && + shouldRenewLease(renewTarget, at, deps.jitterMs ?? renewalJitterMs()) && + // While degraded, renewal traffic is capped to the same bounded recovery-probe cadence + // as fresh resolves; between probes the unexpired snapshot keeps serving without + // touching the authority. + this.degraded.canResolve(ref, at).allowed + ) { try { await this.tryRenew(ref, renewTarget.leaseId, renewTarget.fencingToken, "active"); } catch (error) { // Only a transient authority failure may keep serving the cached snapshot until expiry. // An explicit revocation is authoritative: drop the lease and fail the request closed, // otherwise a revoked credential keeps authenticating until its old expiry. - if (providerSecurityErrorCode(error) === "revoked") { + const code = providerSecurityErrorCode(error); + if (code === "revoked") { this.slotStore.revokeRef(ref); throw toProviderSecurityError(error); } - // Degraded handling applies on the next resolve. + // A confirmed authority outage enters degraded mode so later requests in the renewal + // window stop hammering ChefVault instead of retrying on every resolve. + if (AUTHORITY_OUTAGE_CODES.has(code)) { + this.degraded.markUnavailable(ref, at); + } } const refreshed = this.slotStore.snapshotForRequest(ref, this.now()); if (refreshed) { diff --git a/src/providers/credential.ts b/src/providers/credential.ts index 4f1d19cae..f01040005 100644 --- a/src/providers/credential.ts +++ b/src/providers/credential.ts @@ -14,6 +14,7 @@ import { globalProviderCredentialResolver, providerSecurityErrorCode, ProviderSecurityError, + redactProviderSecurityDetail, } from "../provider-security"; import type { OcxProviderConfig } from "../types"; @@ -64,7 +65,9 @@ export interface ProviderCredentialFailure { */ export function providerCredentialFailure(providerName: string, error: unknown): ProviderCredentialFailure { const code = providerSecurityErrorCode(error); - const detail = error instanceof Error ? error.message : String(error); + // Authority error text may echo header or token fragments; this message reaches data-plane + // clients through Responses/compact/Images error bodies, so scrub it like doctor/status output. + const detail = redactProviderSecurityDetail(error instanceof Error ? error.message : String(error)); const denied = code === "ref_invalid" || code === "ref_not_found" || code === "revoked" || code === "lease_expired"; return { status: denied ? 401 : 503, diff --git a/tests/provider-security.test.ts b/tests/provider-security.test.ts index 6b483b497..a046f4bd5 100644 --- a/tests/provider-security.test.ts +++ b/tests/provider-security.test.ts @@ -16,6 +16,7 @@ import { collectProviderSecurityDoctorChecks, serializeProviderSecurityStatus, } from "../src/provider-security/status"; +import { providerCredentialFailure } from "../src/providers/credential"; import type { OcxConfig } from "../src/types"; const REF = "chefvault://providers/demo/prod"; @@ -336,6 +337,50 @@ describe("renewal failures", () => { expect(resolved.source).toBe("memory"); expect(resolved.apiKey).toBe("skcached"); }); + + test("a renewal outage enters degraded mode and gates further renewal traffic", async () => { + const store = new CredentialSlotStore(); + let t = 1_700_000_000_000; + store.applyResolve(REF, leaseResponse({ secret: "skcached", expiresAt: t + 120_000 }), t); + let renewCalls = 0; + let authorityUp = false; + const resolver = new ProviderCredentialResolver({ + slotStore: store, + degraded: new DegradedModeController(store), + now: () => t, + client: testClient({ + fetchImpl: mockFetch({ + renew: () => { + renewCalls += 1; + if (!authorityUp) { + return new Response(JSON.stringify({ message: "down" }), { status: 503 }); + } + return Response.json(leaseResponse({ leaseId: "renewed", secret: "skfresh", fencingToken: 2, expiresAt: t + 120_000 }), { status: 200 }); + }, + }), + }), + }); + + // The failed renewal enters degraded mode but keeps serving the cached lease. + const first = await resolver.resolveCredentialRef(REF, { jitterMs: 0 }); + expect(first.apiKey).toBe("skcached"); + expect(renewCalls).toBe(1); + expect(store.getMode(REF)).toBe("degraded"); + + // Inside the recovery-probe interval no further renewal call reaches the authority. + t += 1_000; + const second = await resolver.resolveCredentialRef(REF, { jitterMs: 0 }); + expect(second.apiKey).toBe("skcached"); + expect(renewCalls).toBe(1); + + // After the interval elapses a single renewal probe is allowed; success exits degraded mode. + t += 31_000; + authorityUp = true; + const third = await resolver.resolveCredentialRef(REF, { jitterMs: 0 }); + expect(third.apiKey).toBe("skfresh"); + expect(renewCalls).toBe(2); + expect(store.getMode(REF)).toBe("normal"); + }); }); describe("provider-security client headers", () => { @@ -560,3 +605,30 @@ describe("redacted status serialization", () => { expect(doctorText).not.toContain("skfix"); }); }); + +describe("data-plane credential failure redaction", () => { + // Responses, compact, and Images all surface lease failures through providerCredentialFailure, + // so redaction here covers every data-plane error body. + test("scrubs bearer and secret material from authority error detail", () => { + const failure = providerCredentialFailure("demo", new ProviderSecurityError( + "authority_unavailable", + "upstream rejected Authorization: Bearer access-token-leaked-fixture (secret=access-token-secret-fixture)", + )); + expect(failure.status).toBe(503); + expect(failure.type).toBe("api_error"); + expect(failure.message).not.toContain("access-token-leaked-fixture"); + expect(failure.message).not.toContain("access-token-secret-fixture"); + expect(failure.message).toContain("[redacted]"); + expect(failure.message).toContain("authority_unavailable"); + }); + + test("scrubs credential material from denied-reference detail", () => { + const failure = providerCredentialFailure("demo", new ProviderSecurityError( + "revoked", + "lease revoked; last seen token: access-token-revoked-fixture", + )); + expect(failure.status).toBe(401); + expect(failure.type).toBe("authentication_error"); + expect(failure.message).not.toContain("access-token-revoked-fixture"); + }); +});