Files
orca/src/shared/runtime-environment-ssh-access-store.ts
T
Neil f3ffc4dbb8 fix(ssh): keep a paired server's SSH access link across its restarts, and let it relink (#33001)
* fix(ssh): keep a paired server's SSH access link across its restarts, and let it relink

orcad mints a new runtimeId at every start. A linked server refused to record it,
and an unlinked one hid its sidecar entry behind the stale id, so relinking failed.
A new paired device or a pending link is still refused.

Fixes #32957. Reported by @badaz.

* fix(ssh): keep SSH access operations valid across a server restart

runtimeId is per-process, so it no longer binds an SSH access operation. A lost
completion retry now checks the runtime the link verified, and a pending unlink no
longer refuses a restarted runtime.
2026-10-23 18:36:21 -07:00

319 lines
12 KiB
TypeScript

import type { PairedRuntimeConnection } from './paired-runtime-connection'
import { randomUUID } from 'node:crypto'
import { runtimeEnvironmentSshAccessBinding } from './runtime-environment-authority-binding'
export { runtimeEnvironmentSshAccessBinding } from './runtime-environment-authority-binding'
import {
getPreferredPairingOffer,
RuntimeSshAccessOperationSchema,
type KnownRuntimeEnvironment,
type PersistedRuntimeEnvironment,
type RuntimeSshAccessOperation,
type RuntimeSshTunnelLink
} from './runtime-environments'
import {
readEnvironmentStore,
readPersistedEnvironmentStore,
RuntimeEnvironmentStoreError
} from './runtime-environment-store-file'
import {
overlayRuntimeEnvironmentSidecar,
readCurrentRuntimeEnvironmentSidecarEntry,
runtimeEnvironmentSidecarBinding,
writeRuntimeEnvironmentSidecarEntry,
type RuntimeEnvironmentSidecarEntry
} from './runtime-environment-sidecar'
type SidecarState = Omit<RuntimeEnvironmentSidecarEntry, 'binding'>
type AccessContext = {
environments: PersistedRuntimeEnvironment[]
persisted: PersistedRuntimeEnvironment
entry: SidecarState
view: KnownRuntimeEnvironment
}
export function prepareRuntimeEnvironmentSshAccessLink(
userDataPath: string,
args: {
expectedEnvironment: KnownRuntimeEnvironment
requestId: string
sshTargetId: string
sshTargetGeneration: number
remotePort: number
targetFingerprint: string
}
): KnownRuntimeEnvironment {
const { expectedEnvironment: _expected, ...fields } = args
const intent = RuntimeSshAccessOperationSchema.parse({ ...fields, operation: 'link' })
const context = readAccessContext(userDataPath, args.expectedEnvironment, true)
const existing = context.view
if (existing.pendingSshAccessOperation) {
requireMatchingIntent(existing, intent)
return existing
}
if (existing.sshAccess) {
throw invalid('This server already has SSH access.')
}
if (existing.connectionDependency) {
throw invalid('Unlink the existing external tunnel before adding SSH access.')
}
return commit(userDataPath, context, { ...context.entry, pendingSshAccessOperation: intent })
}
export function linkVerifiedRuntimeEnvironmentSshAccess(
userDataPath: string,
args: {
expectedEnvironment: KnownRuntimeEnvironment
requestId: string
verifiedRuntimeId: string
verifiedConnection: PairedRuntimeConnection
tunnel: RuntimeSshTunnelLink
now?: number
}
): KnownRuntimeEnvironment {
const views = readEnvironmentStore(userDataPath).environments
const completed = views.find((entry) => entry.id === args.expectedEnvironment.id)
if (completed?.sshAccess?.requestId && completed.sshAccess.requestId === args.requestId) {
return requireMatchingCompletedLink(completed, linkedRuntimeId(userDataPath, completed), args)
}
const context = readAccessContext(userDataPath, args.expectedEnvironment)
const existing = context.view
const intent = existing.pendingSshAccessOperation
if (
!intent ||
intent.operation !== 'link' ||
intent.requestId !== args.requestId ||
intent.sshTargetId !== args.tunnel.sshTargetId ||
intent.sshTargetGeneration !== args.tunnel.sshTargetGeneration ||
intent.remotePort !== args.tunnel.remotePort
) {
throw invalid('SSH access completion does not match its pending link intent.')
}
const offer = getPreferredPairingOffer(existing)
if (
!args.verifiedRuntimeId.trim() ||
(existing.runtimeId !== null && existing.runtimeId !== args.verifiedRuntimeId) ||
args.verifiedConnection.hostPublicKeyB64 !== offer.publicKeyB64 ||
args.verifiedConnection.credential.deviceToken !== offer.deviceToken ||
args.verifiedConnection.pairedDeviceId !== offer.pairedDeviceId
) {
throw invalid('The SSH endpoint did not verify as this paired server.')
}
if (
views.some((entry) => entry.id !== existing.id && entry.runtimeId === args.verifiedRuntimeId)
) {
throw invalid(
'This runtime is registered more than once. Reconcile its existing registrations first.'
)
}
const endpointId = `ssh-${randomUUID()}`
const { pendingSshAccessOperation: _intent, ...remaining } = context.entry
return commit(userDataPath, context, {
...remaining,
pairingRevisionFloor: nextPairingRevision(existing, args.now ?? Date.now()),
runtimeId: args.verifiedRuntimeId,
sshAccess: {
...args.tunnel,
requestId: intent.requestId,
targetFingerprint: intent.targetFingerprint,
endpointId,
previousPreferredEndpointId: existing.preferredEndpointId,
endpoint: {
id: endpointId,
kind: 'websocket',
label: 'SSH tunnel',
endpoint: args.verifiedConnection.endpoint,
publicKeyB64: offer.publicKeyB64,
deviceToken: offer.deviceToken
}
}
})
}
export function prepareRuntimeEnvironmentSshAccessUnlink(
userDataPath: string,
args: { expectedEnvironment: KnownRuntimeEnvironment; requestId: string; now?: number }
): KnownRuntimeEnvironment {
const context = readAccessContext(userDataPath, args.expectedEnvironment)
const existing = context.view
if (existing.pendingSshAccessOperation) {
if (
existing.pendingSshAccessOperation.operation === 'unlink' &&
existing.pendingSshAccessOperation.requestId === args.requestId
) {
return existing
}
throw invalid('Another SSH access operation is pending.')
}
const sshAccess = existing.sshAccess
if (!sshAccess) {
throw invalid('This server does not have independently linked SSH access.')
}
const { sshAccess: _access, ...remaining } = context.entry
return commit(userDataPath, context, {
...remaining,
pairingRevisionFloor: nextPairingRevision(existing, args.now ?? Date.now()),
pendingSshAccessOperation: {
requestId: args.requestId,
operation: 'unlink',
sshTargetId: sshAccess.sshTargetId,
sshTargetGeneration: sshAccess.sshTargetGeneration,
remotePort: sshAccess.remotePort,
targetFingerprint: sshAccess.targetFingerprint
}
})
}
export function completeRuntimeEnvironmentSshAccessUnlink(
userDataPath: string,
args: { expectedEnvironment: KnownRuntimeEnvironment; requestId: string }
): KnownRuntimeEnvironment {
const context = readAccessContext(userDataPath, args.expectedEnvironment)
const intent = context.view.pendingSshAccessOperation
if (!intent || intent.operation !== 'unlink' || intent.requestId !== args.requestId) {
throw invalid('SSH access completion does not match its pending unlink intent.')
}
const { pendingSshAccessOperation: _intent, ...remaining } = context.entry
return commit(userDataPath, context, remaining)
}
export function cancelRuntimeEnvironmentSshAccessLink(
userDataPath: string,
args: { expectedEnvironment: KnownRuntimeEnvironment; requestId: string }
): KnownRuntimeEnvironment {
const context = readAccessContext(userDataPath, args.expectedEnvironment)
const intent = context.view.pendingSshAccessOperation
if (!intent || intent.requestId !== args.requestId) {
throw invalid('SSH access cancellation does not match its pending link intent.')
}
if (intent.operation === 'unlink') {
return context.view
}
return commit(userDataPath, context, {
...context.entry,
pendingSshAccessOperation: { ...intent, operation: 'unlink' }
})
}
/** A retried completion must name exactly the link it already completed. */
function requireMatchingCompletedLink(
completed: KnownRuntimeEnvironment,
linkedRuntime: string | undefined,
args: Parameters<typeof linkVerifiedRuntimeEnvironmentSshAccess>[1]
): KnownRuntimeEnvironment {
const access = completed.sshAccess!
const expectedIntent = args.expectedEnvironment.pendingSshAccessOperation
const prior = expectedIntent
? {
...completed,
pairingRevision: args.expectedEnvironment.pairingRevision,
sshAccess: undefined,
connectionDependency: undefined,
pendingSshAccessOperation: expectedIntent,
preferredEndpointId: access.previousPreferredEndpointId,
endpoints: completed.endpoints.filter((entry) => entry.id !== access.endpointId)
}
: completed
requireUnchangedEnvironment([prior], args.expectedEnvironment)
const offer = getPreferredPairingOffer(completed)
if (
linkedRuntime !== args.verifiedRuntimeId ||
offer.endpoint !== args.verifiedConnection.endpoint ||
offer.deviceToken !== args.verifiedConnection.credential.deviceToken ||
offer.publicKeyB64 !== args.verifiedConnection.hostPublicKeyB64 ||
offer.pairedDeviceId !== args.verifiedConnection.pairedDeviceId ||
access.sshTargetId !== args.tunnel.sshTargetId ||
access.sshTargetGeneration !== args.tunnel.sshTargetGeneration ||
access.localPort !== args.tunnel.localPort ||
access.remotePort !== args.tunnel.remotePort ||
(expectedIntent &&
(expectedIntent.operation !== 'link' ||
expectedIntent.requestId !== args.requestId ||
expectedIntent.targetFingerprint !== access.targetFingerprint))
) {
throw invalid('SSH access retry does not match its completed link.')
}
return completed
}
/** The runtime the link verified; the store's runtimeId moves on at every server restart. */
function linkedRuntimeId(
userDataPath: string,
completed: KnownRuntimeEnvironment
): string | undefined {
const persisted = readPersistedEnvironmentStore(userDataPath).environments.find(
(entry) => entry.id === completed.id
)
return persisted && readCurrentRuntimeEnvironmentSidecarEntry(userDataPath, persisted)?.runtimeId
}
function readAccessContext(
userDataPath: string,
expected: KnownRuntimeEnvironment,
ignoreIntent = false
): AccessContext {
const { environments } = readPersistedEnvironmentStore(userDataPath)
const views = readEnvironmentStore(userDataPath).environments
const view = requireUnchangedEnvironment(views, expected, ignoreIntent)
const persisted = environments.find((entry) => entry.id === view.id)!
const current = readCurrentRuntimeEnvironmentSidecarEntry(userDataPath, persisted)
const { binding: _binding, ...entry } = current ?? { binding: undefined }
return { environments, persisted, entry, view }
}
/** Writes only after the overlay proves every requested field survives validation. */
function commit(
userDataPath: string,
context: AccessContext,
next: SidecarState
): KnownRuntimeEnvironment {
const view = overlayRuntimeEnvironmentSidecar(context.persisted, {
...next,
binding: runtimeEnvironmentSidecarBinding(context.persisted)
})
if (
(next.sshAccess !== undefined) !== (view.sshAccess !== undefined) ||
(next.pendingSshAccessOperation !== undefined) !==
(view.pendingSshAccessOperation !== undefined)
) {
throw invalid('The SSH access state is inconsistent with this paired server.')
}
writeRuntimeEnvironmentSidecarEntry(userDataPath, context.environments, context.persisted, next)
return view
}
function requireMatchingIntent(
environment: KnownRuntimeEnvironment,
intent: RuntimeSshAccessOperation
): void {
if (JSON.stringify(environment.pendingSshAccessOperation) !== JSON.stringify(intent)) {
throw invalid('Another SSH access operation is pending.')
}
}
function requireUnchangedEnvironment(
environments: KnownRuntimeEnvironment[],
expected: KnownRuntimeEnvironment,
ignoreIntent = false
): KnownRuntimeEnvironment {
const existing = environments.find((entry) => entry.id === expected.id)
if (
!existing ||
JSON.stringify(runtimeEnvironmentSshAccessBinding(existing, ignoreIntent)) !==
JSON.stringify(runtimeEnvironmentSshAccessBinding(expected, ignoreIntent))
) {
throw invalid(
'The paired server changed while SSH access was being verified. Retry with its current pairing.'
)
}
return existing
}
function nextPairingRevision(environment: KnownRuntimeEnvironment, now: number): number {
return Math.max(now, (environment.pairingRevision ?? environment.createdAt) + 1)
}
function invalid(message: string): RuntimeEnvironmentStoreError {
return new RuntimeEnvironmentStoreError('invalid_argument', message)
}