import { randomUUID } from 'node:crypto' import { parsePairingCode, type PairingOffer } from './pairing' import { classifyRemotePairingHostname } from './remote-pairing-address' import { isProfileRuntimePublicKey } from './runtime-e2ee-keypair-file' import { createEnvironmentFromPairingOffer, getPreferredPairingOffer, type KnownRuntimeEnvironment, type PersistedRuntimeEnvironment, type RuntimeEnvironmentSource } from './runtime-environments' import { readEnvironmentStore, readPersistedEnvironmentStore, RuntimeEnvironmentStoreError, writeEnvironmentStore } from './runtime-environment-store-file' import { writeRuntimeEnvironmentSidecarEntry } from './runtime-environment-sidecar' import { findCollidingEnvironmentName } from './runtime-environment-name-collision' import { findEnvironmentWithHostKey, savedHostKeySpelling } from './runtime-environment-host-key-match' import { assertNoIndependentSshAccess, assertPairingReplaceableByAdd } from './runtime-environment-pairing-replace-guards' export { getEnvironmentStorePath, MAX_RUNTIME_ENVIRONMENT_STORE_FILE_BYTES, RuntimeEnvironmentStoreError, type RuntimeEnvironmentStoreErrorCode } from './runtime-environment-store-file' export function listEnvironments( userDataPath: string, options: { requireStoreFile?: boolean } = {} ): KnownRuntimeEnvironment[] { return readEnvironmentStore(userDataPath, options).environments } export type AddEnvironmentArgs = { name: string pairingCode: string now?: number source?: RuntimeEnvironmentSource connectionDependency?: 'ssh-tunnel' /** Recorded only when the code turns out to be for an already saved server. */ runtimeId?: string } export type AddEnvironmentOutcome = { environment: KnownRuntimeEnvironment /** The code was for a saved server, whose pairing it replaced instead of adding a second entry. */ updatedExisting: boolean } /** * Saves a pairing, refusing one for a server already saved. Why never a second entry: two entries * for one server publish the same workspaces, which makes every write to them ambiguous. */ export function addEnvironmentFromPairingCode( userDataPath: string, args: AddEnvironmentArgs ): KnownRuntimeEnvironment { return saveEnvironmentFromPairingCode(userDataPath, args, false).environment } /** * Saves a pairing, or refreshes the saved server it is for. Only for callers that then retire the * running app's connections to that server, which still authenticate with the replaced pairing. */ export function addOrUpdateEnvironmentFromPairingCode( userDataPath: string, args: AddEnvironmentArgs ): AddEnvironmentOutcome { return saveEnvironmentFromPairingCode(userDataPath, args, true) } function saveEnvironmentFromPairingCode( userDataPath: string, args: AddEnvironmentArgs, refreshSameServer: boolean ): AddEnvironmentOutcome { const offer = parseRemotePairingCode(userDataPath, args.pairingCode) const now = args.now ?? Date.now() const store = readPersistedEnvironmentStore(userDataPath) const samePersisted = findEnvironmentWithHostKey(store.environments, offer.publicKeyB64) // Why the overlaid entry: SSH access or a deployment lives only there, and the guard reads it. const samePaired = samePersisted && readEnvironmentStore(userDataPath).environments.find((entry) => entry.id === samePersisted.id) if (samePersisted && !samePaired) { throw new RuntimeEnvironmentStoreError( 'invalid_argument', `This server is already paired as "${samePersisted.name}", whose saved settings this version of Orca cannot read.` ) } if (samePaired) { assertPairingReplaceableByAdd(samePaired, args.source, refreshSameServer) const sameSpelling = { ...offer, publicKeyB64: savedHostKeySpelling(samePaired, offer) } const environment = replaceEnvironmentPairing(userDataPath, samePaired.id, sameSpelling, { now, runtimeId: args.runtimeId, connectionDependency: args.connectionDependency }) return { environment, updatedExisting: true } } const existing = findCollidingEnvironmentName(store.environments, args.name) if (existing) { throw new RuntimeEnvironmentStoreError( 'invalid_argument', `A server named "${existing.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, updatedExisting: false } } function parseRemotePairingCode(userDataPath: string, pairingCode: string): PairingOffer { const offer = parsePairingCode(pairingCode) if (!offer) { throw new RuntimeEnvironmentStoreError( 'invalid_argument', 'Invalid pairing code. Expected an orca://pair?... URL or bare pairing payload.' ) } // Why the key, not the address: a loopback or LAN address proves nothing either way. if (isProfileRuntimePublicKey(userDataPath, offer.publicKeyB64)) { throw new RuntimeEnvironmentStoreError( 'invalid_argument', 'This pairing code is for this Orca itself. Generate the code on the other computer instead.' ) } return offer } export function removeEnvironment(userDataPath: string, selector: string): KnownRuntimeEnvironment { const environment = resolveEnvironmentFromStore(readEnvironmentStore(userDataPath), selector) assertNoIndependentSshAccess(environment) const store = readPersistedEnvironmentStore(userDataPath) const persisted = resolveEnvironmentFromStore(store, environment.id) const remaining = store.environments.filter((entry) => entry.id !== environment.id) writeEnvironmentStore(userDataPath, { version: 1, environments: remaining }) // A leftover entry would read as stale anyway; dropping it keeps the sidecar from growing. writeRuntimeEnvironmentSidecarEntry(userDataPath, remaining, persisted, null) return environment } export function updateEnvironmentFromPairingCode( userDataPath: string, selector: string, args: { pairingCode: string; now?: number; runtimeId?: string } ): KnownRuntimeEnvironment { const offer = parseRemotePairingCode(userDataPath, args.pairingCode) const current = resolveEnvironmentFromStore(readEnvironmentStore(userDataPath), selector) assertNoIndependentSshAccess(current) return replaceEnvironmentPairing(userDataPath, selector, offer, { now: args.now ?? Date.now(), runtimeId: args.runtimeId, connectionDependency: current.connectionDependency }) } /** Swaps a saved server's pairing in place, keeping its id, name and history. */ function replaceEnvironmentPairing( userDataPath: string, selector: string, offer: PairingOffer, args: { now: number; runtimeId?: string; connectionDependency?: 'ssh-tunnel' } ): KnownRuntimeEnvironment { const store = readPersistedEnvironmentStore(userDataPath) const existing = resolveEnvironmentFromStore(store, selector) const previousPairingRevision = existing.pairingRevision ?? existing.createdAt const environment = createEnvironmentFromPairingOffer({ id: existing.id, name: existing.name, now: existing.createdAt, offer, runtimeId: args.runtimeId ?? existing.runtimeId, ...(existing.source ? { source: existing.source } : {}), ...getPairingConnectionDependency(args.connectionDependency, offer) }) const next = { ...environment, createdAt: existing.createdAt, updatedAt: args.now, pairingRevision: Math.max(args.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 /** Recorded only with `pairingDeviceToken`, and only while that token is still the saved one. */ pairedDeviceId?: string /** The token the reply was authenticated with; identity from any other pairing is ignored. */ pairingDeviceToken?: string now?: number } = {} ): void { const store = readPersistedEnvironmentStore(userDataPath) const environment = resolveEnvironmentFromStore(store, selector) const now = args.now ?? Date.now() // Why: a reply that raced a re-pair carries the previous device's identity; recording it beside // the new token makes every later identity check fail. const fromSavedPairing = args.pairingDeviceToken === undefined || preferredDeviceToken(environment) === args.pairingDeviceToken const runtimeIdChanged = fromSavedPairing && args.runtimeId != null && args.runtimeId !== environment.runtimeId const pairedDeviceIdChanged = fromSavedPairing && args.pairingDeviceToken !== undefined && 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 } if (runtimeIdChanged || pairedDeviceIdChanged) { const current = resolveEnvironmentFromStore(readEnvironmentStore(userDataPath), environment.id) if (current.sshAccess || current.pendingSshAccessOperation) { throw new RuntimeEnvironmentStoreError( 'invalid_argument', 'SSH access operation cannot change the paired runtime identity.' ) } } const next = store.environments.map((entry) => entry.id === environment.id ? { ...entry, runtimeId: (runtimeIdChanged ? args.runtimeId : null) ?? entry.runtimeId, ...(pairedDeviceIdChanged && args.pairedDeviceId ? { pairedDeviceId: args.pairedDeviceId } : {}), lastUsedAt: now, updatedAt: now } : entry ) writeEnvironmentStore(userDataPath, { version: 1, environments: next }) } function preferredDeviceToken(environment: PersistedRuntimeEnvironment): string | undefined { return ( environment.endpoints.find((entry) => entry.id === environment.preferredEndpointId) ?? environment.endpoints[0] )?.deviceToken } export function resolveEnvironmentFromStore( store: { environments: T[] }, selector: string ): T { 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}`) }