mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 00:57:45 +00:00
* 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.
319 lines
12 KiB
TypeScript
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)
|
|
}
|