import { randomUUID } from 'node:crypto' import { existsSync } from 'node:fs' import { join } from 'node:path' import { JsonStringifyByteLimitError } from './node-bounded-json-stringify' import { readNodeFileSyncWithinLimit } from './node-bounded-file-reader' import { parsePairingCode, type PairingOffer } from './pairing' import { classifyRemotePairingHostname } from './remote-pairing-address' import { writeSecureJsonFileWithinLimit } from './bounded-secure-json-file' import { hardenExistingSecureFile } from './secure-file' import { createEnvironmentFromPairingOffer, getPreferredPairingOffer, KnownRuntimeEnvironmentSchema, RuntimeEnvironmentStoreSchema, type KnownRuntimeEnvironment, type RuntimeEnvironmentSource, type RuntimeEnvironmentStore } from './runtime-environments' const ENVIRONMENTS_FILE = 'orca-environments.json' export const MAX_RUNTIME_ENVIRONMENT_STORE_FILE_BYTES = 1024 * 1024 export type RuntimeEnvironmentStoreErrorCode = 'invalid_argument' | 'runtime_error' export class RuntimeEnvironmentStoreError extends Error { readonly code: RuntimeEnvironmentStoreErrorCode constructor(code: RuntimeEnvironmentStoreErrorCode, message: string) { super(message) this.name = 'RuntimeEnvironmentStoreError' this.code = code } } export function getEnvironmentStorePath(userDataPath: string): string { return join(userDataPath, ENVIRONMENTS_FILE) } export function listEnvironments(userDataPath: string): KnownRuntimeEnvironment[] { return readEnvironmentStore(userDataPath).environments } export function addEnvironmentFromPairingCode( userDataPath: string, args: { name: string pairingCode: string now?: number source?: RuntimeEnvironmentSource connectionDependency?: 'ssh-tunnel' } ): KnownRuntimeEnvironment { const offer = parsePairingCode(args.pairingCode) if (!offer) { throw new RuntimeEnvironmentStoreError( 'invalid_argument', 'Invalid pairing code. Expected an orca://pair?... URL or bare pairing payload.' ) } const store = readEnvironmentStore(userDataPath) const now = args.now ?? Date.now() const existing = store.environments.find((entry) => entry.name === args.name) if (existing) { throw new RuntimeEnvironmentStoreError( 'invalid_argument', `A server named "${args.name}" already exists.` ) } const environment = createEnvironmentFromPairingOffer({ id: randomUUID(), name: args.name, now, offer, runtimeId: null, ...(args.source ? { source: args.source } : {}), ...getPairingConnectionDependency(args.connectionDependency, offer) }) const next = { version: 1 as const, environments: [ ...store.environments.filter((entry) => entry.id !== environment.id), environment ].sort((a, b) => a.name.localeCompare(b.name)) } writeEnvironmentStore(userDataPath, next) return environment } export function removeEnvironment(userDataPath: string, selector: string): KnownRuntimeEnvironment { const store = readEnvironmentStore(userDataPath) const environment = resolveEnvironmentFromStore(store, selector) writeEnvironmentStore(userDataPath, { version: 1, environments: store.environments.filter((entry) => entry.id !== environment.id) }) return environment } export function updateEnvironmentFromPairingCode( userDataPath: string, selector: string, args: { pairingCode: string; now?: number } ): KnownRuntimeEnvironment { const offer = parsePairingCode(args.pairingCode) if (!offer) { throw new RuntimeEnvironmentStoreError( 'invalid_argument', 'Invalid pairing code. Expected an orca://pair?... URL or bare pairing payload.' ) } const store = readEnvironmentStore(userDataPath) const existing = resolveEnvironmentFromStore(store, selector) const now = args.now ?? Date.now() const previousPairingRevision = existing.pairingRevision ?? existing.createdAt const environment = createEnvironmentFromPairingOffer({ id: existing.id, name: existing.name, now: existing.createdAt, offer, runtimeId: existing.runtimeId, ...(existing.source ? { source: existing.source } : {}), ...getPairingConnectionDependency(existing.connectionDependency, offer) }) const next = { ...environment, createdAt: existing.createdAt, updatedAt: now, pairingRevision: Math.max(now, previousPairingRevision + 1), lastUsedAt: existing.lastUsedAt } writeEnvironmentStore(userDataPath, { version: 1, environments: store.environments .map((entry) => (entry.id === existing.id ? next : entry)) .sort((a, b) => a.name.localeCompare(b.name)) }) return next } function getPairingConnectionDependency( dependency: 'ssh-tunnel' | undefined, offer: PairingOffer ): { connectionDependency?: 'ssh-tunnel' } { if (!dependency) { return {} } try { const endpoint = new URL(offer.endpoint) return classifyRemotePairingHostname(endpoint.hostname) === 'loopback' ? { connectionDependency: dependency } : {} } catch { return {} } } export function resolveEnvironment( userDataPath: string, selector: string ): KnownRuntimeEnvironment { return resolveEnvironmentFromStore(readEnvironmentStore(userDataPath), selector) } export function resolveEnvironmentPairingOffer( userDataPath: string, selector: string ): PairingOffer { return getPreferredPairingOffer(resolveEnvironment(userDataPath, selector)) } // Why: markEnvironmentUsed runs on every runtime round-trip; persisting lastUsedAt each // time forces a secure-file rewrite (ACL hardening), which blocks the main thread on // Windows. lastUsedAt only needs coarse freshness, so skip writes within this window. const LAST_USED_PERSIST_INTERVAL_MS = 60_000 export function markEnvironmentUsed( userDataPath: string, selector: string, args: { runtimeId?: string | null; pairedDeviceId?: string; now?: number } = {} ): void { const store = readEnvironmentStore(userDataPath) const environment = resolveEnvironmentFromStore(store, selector) const now = args.now ?? Date.now() const runtimeIdChanged = args.runtimeId != null && args.runtimeId !== environment.runtimeId const pairedDeviceIdChanged = args.pairedDeviceId != null && args.pairedDeviceId !== environment.pairedDeviceId const lastUsedIsFresh = environment.lastUsedAt != null && now >= environment.lastUsedAt && now - environment.lastUsedAt < LAST_USED_PERSIST_INTERVAL_MS if (!runtimeIdChanged && !pairedDeviceIdChanged && lastUsedIsFresh) { return } const next = store.environments.map((entry) => entry.id === environment.id ? { ...entry, runtimeId: args.runtimeId ?? entry.runtimeId, ...(args.pairedDeviceId ? { pairedDeviceId: args.pairedDeviceId } : {}), lastUsedAt: now, updatedAt: now } : entry ) writeEnvironmentStore(userDataPath, { version: 1, environments: next }) } function resolveEnvironmentFromStore( store: RuntimeEnvironmentStore, selector: string ): KnownRuntimeEnvironment { const byId = store.environments.find((entry) => entry.id === selector) if (byId) { return byId } const matches = store.environments.filter((entry) => entry.name === selector) if (matches.length === 1) { return matches[0]! } if (matches.length > 1) { throw new RuntimeEnvironmentStoreError( 'invalid_argument', `Environment name "${selector}" is ambiguous; use the environment id.` ) } throw new RuntimeEnvironmentStoreError('invalid_argument', `Unknown environment: ${selector}`) } function readEnvironmentStore(userDataPath: string): RuntimeEnvironmentStore { const path = getEnvironmentStorePath(userDataPath) if (!existsSync(path)) { return { version: 1, environments: [] } } try { hardenExistingSecureFile(path) const parsed = RuntimeEnvironmentStoreSchema.parse( JSON.parse( readNodeFileSyncWithinLimit(path, MAX_RUNTIME_ENVIRONMENT_STORE_FILE_BYTES).buffer.toString( 'utf8' ) ) ) return { version: 1, environments: parsed.environments .map((entry) => KnownRuntimeEnvironmentSchema.parse(entry)) .sort((a, b) => a.name.localeCompare(b.name)) } } catch { throw new RuntimeEnvironmentStoreError( 'runtime_error', `Could not read Orca environments at ${path}; the file is invalid.` ) } } function writeEnvironmentStore(userDataPath: string, store: RuntimeEnvironmentStore): void { const path = getEnvironmentStorePath(userDataPath) try { writeSecureJsonFileWithinLimit( path, RuntimeEnvironmentStoreSchema.parse(store), MAX_RUNTIME_ENVIRONMENT_STORE_FILE_BYTES ) } catch (error) { if (error instanceof JsonStringifyByteLimitError) { throw new RuntimeEnvironmentStoreError( 'runtime_error', `Could not write Orca environments at ${path}; the store exceeds its durable capacity.` ) } throw error } }