import { stat } from "node:fs/promises"; import { getErrorMessage, isRecord } from "./auth-error-utils.js"; import { acquireFileLock, ensureFileExists, ensureParentDir, readTextSnapshotWithBackupRecovery, writeTextFileAtomically, } from "./file-utils.js"; import { isRetryableFileAccessError, writeTextSnapshotWithRetries } from "./file-retry.js"; import { RollingMetricSeries, type MetricSeriesSnapshot } from "./performance-metrics.js"; import { cloneProviderState } from "./provider-state-utils.js"; import { resolveDefaultRotationMode } from "./rotation-modes.js"; import { resolveAgentRuntimePath } from "./runtime-paths.js"; import { DEFAULT_PROVIDER_POOL_CONFIG, type ProviderPoolConfig } from "./types-pool.js"; import { type MultiAuthState, type ProviderCredentialLeaseState, type ProviderRotationState, type RotationMode, type SupportedProviderId, } from "./types.js"; type LockResult = { result: T; next?: MultiAuthState; }; type CachedStorageSnapshot = { state: MultiAuthState; fingerprint: string; serializedState: string; }; export interface MultiAuthStorageMetrics { readLatencyMs: MetricSeriesSnapshot; writeLatencyMs: MetricSeriesSnapshot; lockAcquisitionLatencyMs: MetricSeriesSnapshot; statePayloadBytes: MetricSeriesSnapshot; cacheHitCount: number; cacheMissCount: number; cacheHitRate: number; lockRetryCount: number; lastKnownStateSizeBytes: number; } /** * Creates a blank provider rotation state. */ export function createEmptyProviderState( rotationMode: RotationMode = "round-robin", ): ProviderRotationState { return { credentialIds: [], activeIndex: 0, rotationMode, manualActiveCredentialId: undefined, lastUsedAt: {}, usageCount: {}, quotaErrorCount: {}, quotaErrorLastSeenAt: {}, quotaRecoverySuccessCount: {}, quotaExhaustedUntil: {}, lastQuotaError: {}, lastTransientError: {}, transientErrorCount: {}, weeklyQuotaAttempts: {}, friendlyNames: {}, disabledCredentials: {}, cascadeState: undefined, healthState: undefined, oauthRefreshScheduled: {}, pools: undefined, poolConfig: undefined, poolState: undefined, chains: undefined, activeChain: undefined, quotaStates: undefined, quotaDrainStates: undefined, modelIncompatibilities: undefined, credentialLeases: undefined, backgroundCredentialExclusions: undefined, }; } function createProviderStateMap(providers: readonly string[]): Record { const result: Record = {}; for (const provider of providers) { result[provider] = createEmptyProviderState(resolveDefaultRotationMode(provider)); } return result; } /** * Creates the default top-level pi-multi-auth state. */ export function createDefaultMultiAuthState(providers: readonly string[] = []): MultiAuthState { return { version: 1, providers: createProviderStateMap(providers), }; } function getDefaultMultiAuthPath(): string { return resolveAgentRuntimePath("multi-auth.json"); } function parseProviderState( provider: SupportedProviderId, value: unknown, ): ProviderRotationState { if (!isRecord(value)) { return createEmptyProviderState(resolveDefaultRotationMode(provider)); } const credentialIds = Array.isArray(value.credentialIds) ? value.credentialIds.filter((item): item is string => typeof item === "string") : []; const activeIndexRaw = value.activeIndex; const activeIndex = typeof activeIndexRaw === "number" && Number.isInteger(activeIndexRaw) && activeIndexRaw >= 0 ? activeIndexRaw : 0; const rotationModeRaw = value.rotationMode; const rotationMode: RotationMode = rotationModeRaw === "usage-based" || rotationModeRaw === "round-robin" || rotationModeRaw === "balancer" ? rotationModeRaw : resolveDefaultRotationMode(provider); const manualActiveCredentialId = typeof value.manualActiveCredentialId === "string" && value.manualActiveCredentialId.trim().length > 0 ? value.manualActiveCredentialId.trim() : undefined; return { credentialIds, activeIndex, rotationMode, manualActiveCredentialId, lastUsedAt: parseNumberMap(value.lastUsedAt), usageCount: parseNumberMap(value.usageCount), quotaErrorCount: parseNumberMap(value.quotaErrorCount), quotaErrorLastSeenAt: parseNumberMap(value.quotaErrorLastSeenAt), quotaRecoverySuccessCount: parseNumberMap(value.quotaRecoverySuccessCount), quotaExhaustedUntil: parseNumberMap(value.quotaExhaustedUntil), lastQuotaError: parseStringMap(value.lastQuotaError), lastTransientError: parseStringMap(value.lastTransientError), transientErrorCount: parseNumberMap(value.transientErrorCount), weeklyQuotaAttempts: parseNumberMap(value.weeklyQuotaAttempts), friendlyNames: parseStringMap(value.friendlyNames), disabledCredentials: parseDisabledCredentials(value.disabledCredentials), cascadeState: parseCascadeState(value.cascadeState), healthState: parseHealthState(value.healthState), oauthRefreshScheduled: parseNumberMap(value.oauthRefreshScheduled), pools: parsePools(value.pools), poolConfig: parsePoolConfig(value.poolConfig), poolState: parsePoolState(value.poolState), chains: parseChains(value.chains), activeChain: parseActiveChain(value.activeChain), quotaStates: parseQuotaStates(value.quotaStates), quotaDrainStates: parseQuotaDrainStates(value.quotaDrainStates), modelIncompatibilities: parseModelIncompatibilities(value.modelIncompatibilities), credentialLeases: parseCredentialLeases(value.credentialLeases), backgroundCredentialExclusions: parseBackgroundCredentialExclusions(value.backgroundCredentialExclusions), }; } function parseDisabledCredentials( value: unknown, ): ProviderRotationState["disabledCredentials"] { if (!isRecord(value)) { return {}; } const result: ProviderRotationState["disabledCredentials"] = {}; for (const [credentialId, entry] of Object.entries(value)) { if (!isRecord(entry)) { continue; } const error = typeof entry.error === "string" ? entry.error.trim() : ""; const disabledAt = typeof entry.disabledAt === "number" && Number.isFinite(entry.disabledAt) ? entry.disabledAt : Date.now(); const planType = typeof entry.planType === "string" ? entry.planType.trim() : ""; if (error) { result[credentialId] = { error, disabledAt, ...(planType ? { planType } : {}), }; } } return result; } function parseBackgroundCredentialExclusions( value: unknown, ): ProviderRotationState["backgroundCredentialExclusions"] { if (!isRecord(value)) { return undefined; } const result: NonNullable = {}; for (const [credentialId, entry] of Object.entries(value)) { if (!isRecord(entry)) { continue; } if (entry.reason !== "missing_refresh_token_on_import") { continue; } result[credentialId] = { reason: "missing_refresh_token_on_import", excludedAt: typeof entry.excludedAt === "number" && Number.isFinite(entry.excludedAt) ? entry.excludedAt : Date.now(), }; } return Object.keys(result).length > 0 ? result : undefined; } function parseCascadeState(value: unknown): ProviderRotationState["cascadeState"] { if (!isRecord(value)) { return undefined; } const cascadeState: NonNullable = {}; for (const [providerId, providerState] of Object.entries(value)) { if (!isRecord(providerState)) { continue; } cascadeState[providerId] = JSON.parse( JSON.stringify({ active: providerState.active, history: Array.isArray(providerState.history) ? providerState.history : [], }), ) as NonNullable[string]; } return Object.keys(cascadeState).length > 0 ? cascadeState : undefined; } function parseHealthState(value: unknown): ProviderRotationState["healthState"] { if (!isRecord(value)) { return undefined; } return JSON.parse( JSON.stringify({ scores: isRecord(value.scores) ? value.scores : {}, history: isRecord(value.history) ? value.history : undefined, configHash: typeof value.configHash === "string" ? value.configHash : undefined, }), ) as ProviderRotationState["healthState"]; } function parsePools(value: unknown): ProviderRotationState["pools"] { if (!Array.isArray(value)) { return undefined; } const pools = value .filter((entry): entry is Record => isRecord(entry)) .map((entry) => ({ poolId: typeof entry.poolId === "string" ? entry.poolId.trim() : "", displayName: typeof entry.displayName === "string" ? entry.displayName.trim() : undefined, credentialIds: Array.isArray(entry.credentialIds) ? entry.credentialIds.filter((item): item is string => typeof item === "string") : [], priority: typeof entry.priority === "number" && Number.isFinite(entry.priority) ? entry.priority : 0, poolMode: (entry.poolMode === "usage-based" || entry.poolMode === "balancer" ? entry.poolMode : "round-robin") as RotationMode, maxConcurrent: typeof entry.maxConcurrent === "number" && Number.isFinite(entry.maxConcurrent) ? entry.maxConcurrent : undefined, healthThreshold: typeof entry.healthThreshold === "number" && Number.isFinite(entry.healthThreshold) ? entry.healthThreshold : undefined, config: isRecord(entry.config) ? { cooldownMs: typeof entry.config.cooldownMs === "number" && Number.isFinite(entry.config.cooldownMs) ? entry.config.cooldownMs : undefined, backoffMultiplier: typeof entry.config.backoffMultiplier === "number" && Number.isFinite(entry.config.backoffMultiplier) ? entry.config.backoffMultiplier : undefined, } : undefined, })) .filter((pool) => pool.poolId.length > 0 && pool.credentialIds.length > 0); return pools.length > 0 ? pools : undefined; } function parsePoolConfig(value: unknown): ProviderRotationState["poolConfig"] { if (!isRecord(value)) { return undefined; } const failoverStrategy = value.failoverStrategy === "round-robin" || value.failoverStrategy === "health-based" || value.failoverStrategy === "priority" ? value.failoverStrategy : DEFAULT_PROVIDER_POOL_CONFIG.failoverStrategy; const enablePools = typeof value.enablePools === "boolean" ? value.enablePools : DEFAULT_PROVIDER_POOL_CONFIG.enablePools; const preferHealthyWithinPool = typeof value.preferHealthyWithinPool === "boolean" ? value.preferHealthyWithinPool : DEFAULT_PROVIDER_POOL_CONFIG.preferHealthyWithinPool; const config: ProviderPoolConfig = { enablePools, failoverStrategy, preferHealthyWithinPool, }; return JSON.stringify(config) === JSON.stringify(DEFAULT_PROVIDER_POOL_CONFIG) ? undefined : config; } function parsePoolState(value: unknown): ProviderRotationState["poolState"] { if (!isRecord(value)) { return undefined; } const activePoolId = typeof value.activePoolId === "string" ? value.activePoolId.trim() : ""; const poolIndex = typeof value.poolIndex === "number" && Number.isInteger(value.poolIndex) && value.poolIndex >= 0 ? value.poolIndex : undefined; if (!activePoolId && poolIndex === undefined) { return undefined; } return { activePoolId: activePoolId || undefined, poolIndex, }; } function parseChains(value: unknown): ProviderRotationState["chains"] { if (!Array.isArray(value)) { return undefined; } const chains = value .filter((entry): entry is Record => isRecord(entry)) .map((entry) => ({ chainId: typeof entry.chainId === "string" ? entry.chainId.trim() : "", displayName: typeof entry.displayName === "string" ? entry.displayName.trim() : undefined, providers: Array.isArray(entry.providers) ? entry.providers .filter((provider): provider is Record => isRecord(provider)) .map((provider) => ({ providerId: typeof provider.providerId === "string" ? provider.providerId.trim() : "", modelMapping: isRecord(provider.modelMapping) ? Object.fromEntries( Object.entries(provider.modelMapping).filter( (entry): entry is [string, string] => typeof entry[1] === "string", ), ) : undefined, healthThreshold: typeof provider.healthThreshold === "number" && Number.isFinite(provider.healthThreshold) ? provider.healthThreshold : undefined, maxAttempts: typeof provider.maxAttempts === "number" && Number.isFinite(provider.maxAttempts) ? provider.maxAttempts : undefined, })) .filter((provider) => provider.providerId.length > 0) : [], maxAttemptsPerProvider: typeof entry.maxAttemptsPerProvider === "number" && Number.isFinite(entry.maxAttemptsPerProvider) ? entry.maxAttemptsPerProvider : 1, failoverTriggers: (Array.isArray(entry.failoverTriggers) ? entry.failoverTriggers.filter((trigger): trigger is string => typeof trigger === "string") : []) as NonNullable[number]["failoverTriggers"], modelMapping: isRecord(entry.modelMapping) ? Object.fromEntries( Object.entries(entry.modelMapping) .filter((modelEntry): modelEntry is [string, Record] => isRecord(modelEntry[1]), ) .map(([modelId, mapping]) => [ modelId, Object.fromEntries( Object.entries(mapping).filter( (entry): entry is [string, string] => typeof entry[1] === "string", ), ), ]), ) : undefined, })) .filter((chain) => chain.chainId.length > 0 && chain.providers.length > 1); return chains.length > 0 ? chains : undefined; } function parseActiveChain(value: unknown): ProviderRotationState["activeChain"] { if (!isRecord(value)) { return undefined; } const chainId = typeof value.chainId === "string" ? value.chainId.trim() : ""; const currentProviderId = typeof value.currentProviderId === "string" ? value.currentProviderId.trim() : ""; if (!chainId || !currentProviderId) { return undefined; } return { chainId, position: typeof value.position === "number" && Number.isInteger(value.position) && value.position >= 0 ? value.position : 0, currentProviderId, attemptsOnCurrentProvider: typeof value.attemptsOnCurrentProvider === "number" && Number.isInteger(value.attemptsOnCurrentProvider) && value.attemptsOnCurrentProvider >= 0 ? value.attemptsOnCurrentProvider : 0, failoverReason: typeof value.failoverReason === "string" ? value.failoverReason : "unknown", failoverStartedAt: typeof value.failoverStartedAt === "number" && Number.isFinite(value.failoverStartedAt) ? value.failoverStartedAt : Date.now(), failedProviders: Array.isArray(value.failedProviders) ? value.failedProviders .filter((entry): entry is Record => isRecord(entry)) .map((entry) => ({ providerId: typeof entry.providerId === "string" ? entry.providerId.trim() : "", failedAt: typeof entry.failedAt === "number" && Number.isFinite(entry.failedAt) ? entry.failedAt : Date.now(), reason: typeof entry.reason === "string" ? entry.reason : "", errorKind: (typeof entry.errorKind === "string" ? entry.errorKind : "unknown") as NonNullable< ProviderRotationState["activeChain"] >["failedProviders"][number]["errorKind"], })) .filter((entry) => entry.providerId.length > 0) : [], }; } function parseQuotaStates(value: unknown): ProviderRotationState["quotaStates"] { if (!isRecord(value)) { return undefined; } const result: NonNullable = {}; for (const [credentialId, state] of Object.entries(value)) { if (!isRecord(state)) { continue; } const classification = typeof state.classification === "string" ? state.classification.trim() : "unknown"; const errorMessage = typeof state.errorMessage === "string" ? state.errorMessage.trim() : ""; const recoveryAction = isRecord(state.recoveryAction) ? { action: (typeof state.recoveryAction.action === "string" ? state.recoveryAction.action : "switch_credential") as NonNullable< ProviderRotationState["quotaStates"] >[string]["recoveryAction"]["action"], requiresManual: state.recoveryAction.requiresManual === true, estimatedWaitMs: typeof state.recoveryAction.estimatedWaitMs === "number" && Number.isFinite(state.recoveryAction.estimatedWaitMs) ? state.recoveryAction.estimatedWaitMs : undefined, description: typeof state.recoveryAction.description === "string" ? state.recoveryAction.description : "Try another credential or provider.", } : undefined; if (!errorMessage || !recoveryAction) { continue; } result[credentialId] = { credentialId, classification: classification as NonNullable[string]["classification"], detectedAt: typeof state.detectedAt === "number" && Number.isFinite(state.detectedAt) ? state.detectedAt : Date.now(), resetAt: typeof state.resetAt === "number" && Number.isFinite(state.resetAt) ? state.resetAt : undefined, errorMessage, recoveryAction, }; } return Object.keys(result).length > 0 ? result : undefined; } function parseQuotaDrainStates(value: unknown): ProviderRotationState["quotaDrainStates"] { if (!isRecord(value)) { return undefined; } const result: NonNullable = {}; for (const [credentialId, state] of Object.entries(value)) { if (!isRecord(state) || state.draining !== true) { continue; } const updatedAt = typeof state.updatedAt === "number" && Number.isFinite(state.updatedAt) ? state.updatedAt : Date.now(); result[credentialId] = { draining: true, enteredAt: typeof state.enteredAt === "number" && Number.isFinite(state.enteredAt) ? state.enteredAt : updatedAt, lastUsedPercent: typeof state.lastUsedPercent === "number" && Number.isFinite(state.lastUsedPercent) ? Math.max(0, Math.min(100, state.lastUsedPercent)) : undefined, updatedAt, }; } return Object.keys(result).length > 0 ? result : undefined; } function parseModelIncompatibilities( value: unknown, ): ProviderRotationState["modelIncompatibilities"] { if (!isRecord(value)) { return undefined; } const result: NonNullable = {}; for (const [credentialId, models] of Object.entries(value)) { if (!isRecord(models)) { continue; } const modelEntries: Record[string][string]> = {}; for (const [modelId, entry] of Object.entries(models)) { if (!isRecord(entry)) { continue; } const normalizedModelId = modelId.trim().toLowerCase(); const error = typeof entry.error === "string" ? entry.error.trim() : ""; const blockedUntil = typeof entry.blockedUntil === "number" && Number.isFinite(entry.blockedUntil) ? entry.blockedUntil : 0; if (!normalizedModelId || !error || blockedUntil <= 0) { continue; } modelEntries[normalizedModelId] = { modelId: typeof entry.modelId === "string" && entry.modelId.trim().length > 0 ? entry.modelId.trim().toLowerCase() : normalizedModelId, blockedUntil, blockedAt: typeof entry.blockedAt === "number" && Number.isFinite(entry.blockedAt) ? entry.blockedAt : Date.now(), error, }; } if (Object.keys(modelEntries).length > 0) { result[credentialId] = modelEntries; } } return Object.keys(result).length > 0 ? result : undefined; } export function parseCredentialLeases(value: unknown): ProviderRotationState["credentialLeases"] { if (!isRecord(value)) { return undefined; } const result: Record = {}; for (const [ownerId, entry] of Object.entries(value)) { if (!isRecord(entry)) { continue; } const normalizedOwnerId = typeof entry.ownerId === "string" && entry.ownerId.trim().length > 0 ? entry.ownerId.trim() : ownerId.trim(); const credentialId = typeof entry.credentialId === "string" ? entry.credentialId.trim() : ""; const acquiredAt = typeof entry.acquiredAt === "number" && Number.isFinite(entry.acquiredAt) ? entry.acquiredAt : Date.now(); const lastSeenAt = typeof entry.lastSeenAt === "number" && Number.isFinite(entry.lastSeenAt) ? entry.lastSeenAt : acquiredAt; const expiresAt = typeof entry.expiresAt === "number" && Number.isFinite(entry.expiresAt) ? entry.expiresAt : 0; if (!normalizedOwnerId || !credentialId || expiresAt <= 0) { continue; } result[normalizedOwnerId] = { ownerId: normalizedOwnerId, credentialId, acquiredAt, lastSeenAt, expiresAt, }; } return Object.keys(result).length > 0 ? result : undefined; } function parseNumberMap(value: unknown): Record { if (!isRecord(value)) { return {}; } const result: Record = {}; for (const [key, maybeNumber] of Object.entries(value)) { if (typeof maybeNumber === "number" && Number.isFinite(maybeNumber)) { result[key] = maybeNumber; } } return result; } function parseStringMap(value: unknown): Record { if (!isRecord(value)) { return {}; } const result: Record = {}; for (const [key, maybeString] of Object.entries(value)) { if (typeof maybeString !== "string") { continue; } const normalized = maybeString.trim(); if (!normalized) { continue; } result[key] = normalized; } return result; } function parseState(content: string | undefined): MultiAuthState { if (!content || content.trim() === "") { return createDefaultMultiAuthState(); } let parsed: unknown; try { parsed = JSON.parse(content); } catch (error) { throw new Error(`Invalid JSON in multi-auth.json: ${getErrorMessage(error)}`); } if (!isRecord(parsed)) { return createDefaultMultiAuthState(); } const providersRaw = isRecord(parsed.providers) ? parsed.providers : {}; const state = createDefaultMultiAuthState(); for (const [providerId, providerValue] of Object.entries(providersRaw)) { state.providers[providerId] = parseProviderState(providerId as SupportedProviderId, providerValue); } return state; } async function readMultiAuthStateSnapshot(storagePath: string): Promise { return readTextSnapshotWithBackupRecovery({ filePath: storagePath, parse: parseState, createDefault: () => createDefaultMultiAuthState(), }); } function hasObjectEntries(value: unknown): value is Record { return isRecord(value) && Object.keys(value).length > 0; } function hasArrayEntries(value: readonly TValue[] | undefined): value is readonly TValue[] { return Array.isArray(value) && value.length > 0; } function hasMapEntries(value: unknown): value is Record { return isRecord(value) && Object.keys(value).length > 0; } function sparsifyProviderState( provider: string, state: ProviderRotationState, ): Record { const sparse: Record = {}; if (hasArrayEntries(state.credentialIds)) { sparse.credentialIds = [...state.credentialIds]; } sparse.activeIndex = state.activeIndex; sparse.rotationMode = state.rotationMode; if (state.manualActiveCredentialId) { sparse.manualActiveCredentialId = state.manualActiveCredentialId; } for (const field of [ "lastUsedAt", "usageCount", "quotaErrorCount", "quotaErrorLastSeenAt", "quotaRecoverySuccessCount", "quotaExhaustedUntil", "lastQuotaError", "lastTransientError", "transientErrorCount", "weeklyQuotaAttempts", "friendlyNames", "disabledCredentials", "oauthRefreshScheduled", "quotaStates", "quotaDrainStates", "modelIncompatibilities", "credentialLeases", "backgroundCredentialExclusions", ] as const) { const value = state[field]; if (hasMapEntries(value)) { sparse[field] = value; } } if (hasObjectEntries(state.cascadeState)) { sparse.cascadeState = state.cascadeState; } if (state.healthState) { sparse.healthState = state.healthState; } if (hasArrayEntries(state.pools)) { sparse.pools = state.pools; } if (state.poolConfig) { sparse.poolConfig = state.poolConfig; } if (state.poolState) { sparse.poolState = state.poolState; } if (hasArrayEntries(state.chains)) { sparse.chains = state.chains; } if (state.activeChain) { sparse.activeChain = state.activeChain; } return sparse; } function serializeState(state: MultiAuthState): string { const providers: Record> = {}; for (const [provider, providerState] of Object.entries(state.providers)) { providers[provider] = sparsifyProviderState(provider, providerState); } return JSON.stringify({ version: 1, providers }, null, 2); } function createStorageFingerprint(fileStats: { mtimeMs: number; size: number }): string { return `${Math.round(fileStats.mtimeMs)}:${fileStats.size}`; } async function writeSerializedMultiAuthStateSnapshot( storagePath: string, serializedState: string, ): Promise { await writeTextSnapshotWithRetries({ filePath: storagePath, failureMessage: `Failed to persist multi-auth.json to '${storagePath}'.`, write: async () => { await writeTextFileAtomically(storagePath, serializedState); }, isRetryableError: isRetryableFileAccessError, }); } function cloneState(state: MultiAuthState): MultiAuthState { const providers: Record = {}; for (const [provider, providerState] of Object.entries(state.providers)) { providers[provider] = cloneProviderState(providerState); } return { version: 1, providers, }; } function buildProviderCredentialIndex( state: MultiAuthState, ): Map { const providerByCredentialId = new Map(); for (const [providerId, providerState] of Object.entries(state.providers)) { for (const credentialId of providerState.credentialIds) { providerByCredentialId.set(credentialId, providerId as SupportedProviderId); } } return providerByCredentialId; } /** * Persistence layer for the active agent runtime multi-auth.json file with file locking. */ export class MultiAuthStorage { private cachedState: MultiAuthState | null = null; private cachedFingerprint: string | null = null; private cachedSerializedState: string | null = null; private cachedProviderByCredentialId = new Map(); private readonly readLatencyMs = new RollingMetricSeries(); private readonly writeLatencyMs = new RollingMetricSeries(); private readonly lockAcquisitionLatencyMs = new RollingMetricSeries(); private readonly statePayloadBytes = new RollingMetricSeries(); private cacheHitCount = 0; private cacheMissCount = 0; private lockRetryCount = 0; private lastKnownStateSizeBytes = 0; private readonly storagePath: string; constructor(storagePath: string = getDefaultMultiAuthPath()) { this.storagePath = storagePath; } /** * Returns the configured storage path. */ getPath(): string { return this.storagePath; } getMetrics(): MultiAuthStorageMetrics { const totalCacheReads = this.cacheHitCount + this.cacheMissCount; return { readLatencyMs: this.readLatencyMs.snapshot(), writeLatencyMs: this.writeLatencyMs.snapshot(), lockAcquisitionLatencyMs: this.lockAcquisitionLatencyMs.snapshot(), statePayloadBytes: this.statePayloadBytes.snapshot(), cacheHitCount: this.cacheHitCount, cacheMissCount: this.cacheMissCount, cacheHitRate: totalCacheReads === 0 ? 0 : Math.round((this.cacheHitCount / totalCacheReads) * 1000) / 1000, lockRetryCount: this.lockRetryCount, lastKnownStateSizeBytes: this.lastKnownStateSizeBytes, }; } /** * Reads the current pi-multi-auth state from an optimistic snapshot. * Snapshot retries tolerate concurrent writes without taking the extension lock. */ async read(): Promise { await this.ensureStorageReady(); const snapshot = await this.readCachedSnapshotReference(); return cloneState(snapshot.state); } /** * Reads one provider state without cloning the full multi-auth document. */ async readProviderState(provider: SupportedProviderId): Promise { await this.ensureStorageReady(); const snapshot = await this.readCachedSnapshotReference(); const providerState = snapshot.state.providers[provider]; return providerState ? cloneProviderState(providerState) : createEmptyProviderState(resolveDefaultRotationMode(provider)); } /** * Resolves the provider that owns one credential from the cached storage snapshot. */ async findProviderForCredential(credentialId: string): Promise { const normalizedCredentialId = credentialId.trim(); if (normalizedCredentialId.length === 0) { return null; } await this.ensureStorageReady(); await this.readCachedSnapshotReference(); return this.cachedProviderByCredentialId.get(normalizedCredentialId) ?? null; } /** * Executes a read-modify-write transaction under lock. */ async withLock( fn: (state: MultiAuthState) => Promise> | LockResult, ): Promise { await this.ensureStorageReady(); let release: (() => Promise) | undefined; try { release = await acquireFileLock( this.storagePath, { realpath: false, retries: { retries: 10, factor: 2, minTimeout: 100, maxTimeout: 10_000, randomize: true, }, stale: 30_000, onCompromised: () => { // Stale lock cleanup happened; continue transaction under the new lock. }, }, { onRetry: () => { this.lockRetryCount += 1; }, onAcquired: (latencyMs) => { this.lockAcquisitionLatencyMs.record(latencyMs); }, }, ); const snapshot = await this.readCachedSnapshotReference(); const result = await fn(cloneState(snapshot.state)); if (result.next) { const serializedNext = serializeState(result.next); if (serializedNext !== snapshot.serializedState) { await this.persistSerializedState(result.next, serializedNext); } else { this.updateCache(snapshot.state, snapshot.fingerprint, snapshot.serializedState); } } else { this.updateCache(snapshot.state, snapshot.fingerprint, snapshot.serializedState); } return result.result; } finally { if (release) { try { await release(); } catch { // Ignore unlock failures when compromised. } } } } private async ensureStorageReady(): Promise { await ensureParentDir(this.storagePath); await ensureFileExists(this.storagePath, serializeState(createDefaultMultiAuthState())); } private async readCachedSnapshotReference(): Promise { const startedAt = Date.now(); const fileStats = await stat(this.storagePath); const fingerprint = await this.buildSnapshotFingerprint(fileStats); this.lastKnownStateSizeBytes = fileStats.size; this.statePayloadBytes.record(fileStats.size); if ( this.cachedState && this.cachedFingerprint === fingerprint && this.cachedSerializedState !== null ) { this.cacheHitCount += 1; this.readLatencyMs.record(Date.now() - startedAt); return { state: this.cachedState, fingerprint, serializedState: this.cachedSerializedState, }; } this.cacheMissCount += 1; const parsed = await readMultiAuthStateSnapshot(this.storagePath); const serializedState = serializeState(parsed); this.updateCache(parsed, fingerprint, serializedState, fileStats.size); this.readLatencyMs.record(Date.now() - startedAt); return { state: this.cachedState ?? cloneState(parsed), fingerprint, serializedState, }; } private async persistSerializedState( state: MultiAuthState, serializedState: string, ): Promise { const startedAt = Date.now(); await writeSerializedMultiAuthStateSnapshot(this.storagePath, serializedState); const fileStats = await stat(this.storagePath); const fingerprint = await this.buildSnapshotFingerprint(fileStats); this.writeLatencyMs.record(Date.now() - startedAt); this.updateCache(state, fingerprint, serializedState, fileStats.size); } private async buildSnapshotFingerprint(fileStats: { mtimeMs: number; size: number }): Promise { return createStorageFingerprint(fileStats); } private updateCache( state: MultiAuthState, fingerprint: string | null, serializedState: string, persistedSizeBytes?: number, ): void { const cachedState = cloneState(state); this.cachedState = cachedState; this.cachedFingerprint = fingerprint; this.cachedSerializedState = serializedState; this.cachedProviderByCredentialId = buildProviderCredentialIndex(cachedState); const sizeBytes = persistedSizeBytes ?? Buffer.byteLength(serializedState, "utf-8"); this.lastKnownStateSizeBytes = sizeBytes; this.statePayloadBytes.record(sizeBytes); } } /** * Ensures a provider state exists and returns it. */ export function getProviderState( state: MultiAuthState, provider: SupportedProviderId, ): ProviderRotationState { const providerState = state.providers[provider]; if (!providerState) { state.providers[provider] = createEmptyProviderState(resolveDefaultRotationMode(provider)); return state.providers[provider]; } return providerState; }