Files
orca/src/shared/runtime-environments.ts
T

341 lines
12 KiB
TypeScript

import { z } from 'zod'
import { PAIRING_OFFER_VERSION, type PairingOffer } from './pairing'
import {
pairedRuntimeConnectionFromOffer,
type PairedRuntimeConnection
} from './paired-runtime-connection'
import { classifyRemotePairingHostname } from './remote-pairing-address'
import { RuntimeEnvironmentReconciliationRecordSchema } from './runtime-environment-reconciliation-record'
import {
OrcadWslDeploymentLinkSchema,
type OrcadWslDeploymentLink
} from './wsl-host-deployment-link'
export const RuntimeAccessEndpointSchema = z.object({
id: z.string().min(1),
kind: z.literal('websocket'),
label: z.string().min(1),
endpoint: z.string().min(1),
deviceToken: z.string().min(1),
publicKeyB64: z.string().min(1)
})
export const PublicRuntimeAccessEndpointSchema = RuntimeAccessEndpointSchema.omit({
deviceToken: true,
publicKeyB64: true
})
export type PublicRuntimeAccessEndpoint = z.infer<typeof PublicRuntimeAccessEndpointSchema>
export const RuntimeEnvironmentSourceSchema = z.enum(['manual', 'ephemeral-vm'])
export type RuntimeEnvironmentSource = z.infer<typeof RuntimeEnvironmentSourceSchema>
export const RuntimeSshTunnelLinkSchema = z.object({
sshTargetId: z.string().min(1),
sshTargetGeneration: z.number().int().positive(),
localPort: z.number().int().min(1).max(65_535),
remotePort: z.number().int().min(1).max(65_535)
})
export type RuntimeSshTunnelLink = z.infer<typeof RuntimeSshTunnelLinkSchema>
/** A server Orca deployed and owns over SSH; unlike sshAccess, it grants lifecycle ownership. */
export const OrcadSshDeploymentLinkSchema = RuntimeSshTunnelLinkSchema.extend({
// Absent on disk: the sidecar persists SSH links without it, so older builds read them unchanged.
kind: z.literal('ssh').optional()
})
export type OrcadSshDeploymentLink = z.infer<typeof OrcadSshDeploymentLinkSchema>
/** A server Orca deployed and owns; the WSL kind is persisted only in wsl-host-deployment-store. */
export const OrcadDeploymentLinkSchema = z.union([
OrcadSshDeploymentLinkSchema,
OrcadWslDeploymentLinkSchema
])
export type OrcadDeploymentLink = z.infer<typeof OrcadDeploymentLinkSchema>
/** The managed deployment Orca owns over SSH; undefined for none or a WSL one. */
export function getOrcadSshDeployment(environment: {
orcadDeployment?: OrcadDeploymentLink | null
}): OrcadSshDeploymentLink | undefined {
const deployment = environment.orcadDeployment
return deployment && deployment.kind !== 'wsl' ? deployment : undefined
}
export const RuntimeSshAccessLinkSchema = RuntimeSshTunnelLinkSchema.extend({
requestId: z.string().min(1).optional(),
targetFingerprint: z.string().min(1).optional(),
endpointId: z.string().min(1),
previousPreferredEndpointId: z.string().min(1)
})
export const RuntimeSshAccessOperationSchema = RuntimeSshTunnelLinkSchema.omit({ localPort: true })
.extend({
requestId: z.string().min(1),
operation: z.enum(['link', 'unlink']),
targetFingerprint: z.string().min(1).optional()
})
.refine((intent) => intent.operation !== 'link' || !!intent.targetFingerprint, {
message: 'Link intent requires a target fingerprint.'
})
export type RuntimeSshAccessOperation = z.infer<typeof RuntimeSshAccessOperationSchema>
/** The fields shipped builds read and rewrite in orca-environments.json. */
export const PersistedRuntimeEnvironmentSchema = z.object({
id: z.string().min(1),
name: z.string().min(1),
createdAt: z.number().finite(),
updatedAt: z.number().finite(),
pairingRevision: z.number().finite().optional(),
pairedDeviceId: z.string().min(1).optional(),
lastUsedAt: z.number().finite().nullable(),
runtimeId: z.string().min(1).nullable(),
source: RuntimeEnvironmentSourceSchema.optional(),
connectionDependency: z.literal('ssh-tunnel').optional(),
endpoints: z.array(RuntimeAccessEndpointSchema).min(1),
preferredEndpointId: z.string().min(1)
})
export type PersistedRuntimeEnvironment = z.infer<typeof PersistedRuntimeEnvironmentSchema>
/**
* A persisted environment with its sidecar state overlaid (runtime-environment-sidecar). These
* fields never enter orca-environments.json: shipped builds strip unknown keys when they rewrite it.
*/
export const KnownRuntimeEnvironmentSchema = PersistedRuntimeEnvironmentSchema.extend({
sshAccess: RuntimeSshAccessLinkSchema.optional(),
pendingSshAccessOperation: RuntimeSshAccessOperationSchema.optional(),
orcadDeployment: OrcadDeploymentLinkSchema.optional(),
/** When a migration into this managed server began committing; older snapshots predate it. */
orcadMigratedAt: z.string().datetime().optional(),
reconciliation: RuntimeEnvironmentReconciliationRecordSchema.optional()
})
.refine(
({ pendingSshAccessOperation, sshAccess, orcadDeployment, connectionDependency }) =>
!pendingSshAccessOperation || (!sshAccess && !orcadDeployment && !connectionDependency),
{
message: 'Pending SSH access operations cannot coexist with active SSH access.',
path: ['pendingSshAccessOperation']
}
)
.refine(
(environment) => {
const access = environment.sshAccess
return (
!access ||
(environment.connectionDependency === 'ssh-tunnel' &&
environment.preferredEndpointId === access.endpointId &&
access.previousPreferredEndpointId !== access.endpointId &&
environment.endpoints.some(
(endpoint) => endpoint.id === access.previousPreferredEndpointId
) &&
getPreferredLoopbackRuntimePort(environment) === access.localPort)
)
},
{
message:
'Runtime SSH access must preserve its previous endpoint and prefer its loopback endpoint.',
path: ['sshAccess']
}
)
.refine(
(environment) => {
const deployment = environment.orcadDeployment
// Why no WSL dependency value: older builds reject any other value in orca-environments.json;
// a WSL link's kind, kept in its own file, is the bridge dependency.
const dependency = deployment?.kind === 'wsl' ? undefined : 'ssh-tunnel'
return (
!deployment ||
(!environment.sshAccess &&
environment.connectionDependency === dependency &&
getPreferredLoopbackRuntimePort(environment) === deployment.localPort)
)
},
{
message:
'A managed deployment must prefer its own tunnel and exclude independent SSH access.',
path: ['orcadDeployment']
}
)
export type KnownRuntimeEnvironment = z.infer<typeof KnownRuntimeEnvironmentSchema>
export type PublicKnownRuntimeEnvironment = Omit<KnownRuntimeEnvironment, 'endpoints'> & {
endpoints: PublicRuntimeAccessEndpoint[]
/** Digest of the host's persisted E2EE public key, which its pairing handshake proves. */
hostKeyFingerprint?: string
}
export function redactRuntimeEnvironment(
environment: KnownRuntimeEnvironment
): PublicKnownRuntimeEnvironment {
return {
...environment,
endpoints: environment.endpoints.map(
({ deviceToken: _deviceToken, publicKeyB64: _key, ...rest }) => rest
)
}
}
// Why version 1 and the persisted schema: shipped builds accept only this shape, so every
// T5 field lives in the sidecar and a downgrade rewrite cannot lose or reject it.
export const RuntimeEnvironmentStoreSchema = z.object({
version: z.literal(1),
environments: z.array(PersistedRuntimeEnvironmentSchema)
})
export type RuntimeEnvironmentStore = z.infer<typeof RuntimeEnvironmentStoreSchema>
export function createEnvironmentFromPairingOffer(args: {
id: string
name: string
now: number
offer: PairingOffer
runtimeId?: string | null
source?: RuntimeEnvironmentSource
connectionDependency?: 'ssh-tunnel'
}): KnownRuntimeEnvironment {
const endpointId = `ws-${args.id}`
return KnownRuntimeEnvironmentSchema.parse({
id: args.id,
name: args.name,
createdAt: args.now,
updatedAt: args.now,
pairingRevision: args.now,
...(args.offer.pairedDeviceId ? { pairedDeviceId: args.offer.pairedDeviceId } : {}),
lastUsedAt: null,
runtimeId: args.runtimeId ?? null,
...(args.source ? { source: args.source } : {}),
...(args.connectionDependency ? { connectionDependency: args.connectionDependency } : {}),
endpoints: [
{
id: endpointId,
kind: 'websocket',
label: 'WebSocket',
endpoint: args.offer.endpoint,
deviceToken: args.offer.deviceToken,
publicKeyB64: args.offer.publicKeyB64
}
],
preferredEndpointId: endpointId
})
}
export function isEphemeralVmRuntimeEnvironment(
environment: Pick<PublicKnownRuntimeEnvironment, 'source'>
): boolean {
return environment.source === 'ephemeral-vm'
}
export function isUserManagedRuntimeEnvironment(
environment: Pick<PublicKnownRuntimeEnvironment, 'source'>
): boolean {
return !isEphemeralVmRuntimeEnvironment(environment)
}
/** Only the user re-pairs these, with an access link; Orca pairs managed and cloud servers itself. */
export function isRepairableWithAccessLink(
environment: Pick<PublicKnownRuntimeEnvironment, 'source' | 'orcadDeployment'>
): boolean {
return !environment.orcadDeployment && isUserManagedRuntimeEnvironment(environment)
}
/** Who pairs a server that refused this desktop: the user with a link, Orca over SSH, or Orca on Connect. */
export type RuntimePairingRepairRoute = 'access-link' | 'ssh' | 'connect'
export function runtimePairingRepairRoute(
environment: Pick<PublicKnownRuntimeEnvironment, 'source' | 'orcadDeployment'>
): RuntimePairingRepairRoute {
if (getOrcadSshDeployment(environment)) {
return 'ssh'
}
return isRepairableWithAccessLink(environment) ? 'access-link' : 'connect'
}
/** A WSL distro added as a host; keyed by its deployment record's kind, never by its name. */
export function isWslHostRuntimeEnvironment<
T extends { orcadDeployment?: OrcadDeploymentLink | null }
>(environment: T): environment is T & { orcadDeployment: OrcadWslDeploymentLink } {
return environment.orcadDeployment?.kind === 'wsl'
}
/**
* A server the generic remote-server surfaces list (Settings, status bar, server updates):
* neither a recipe VM nor a WSL host, which each have their own place.
*/
export function isListedRemoteServer(
environment: Pick<PublicKnownRuntimeEnvironment, 'source' | 'orcadDeployment'>
): boolean {
return isUserManagedRuntimeEnvironment(environment) && !isWslHostRuntimeEnvironment(environment)
}
type PreferredEndpointSource = Pick<
KnownRuntimeEnvironment,
'name' | 'endpoints' | 'preferredEndpointId' | 'pairedDeviceId'
>
function preferredEndpoint(environment: PreferredEndpointSource) {
const endpoint =
environment.endpoints.find((entry) => entry.id === environment.preferredEndpointId) ??
environment.endpoints[0]
if (!endpoint) {
throw new Error(`Environment ${environment.name} has no access endpoints`)
}
return endpoint
}
export function getPreferredPairedRuntimeConnection(
environment: PreferredEndpointSource
): PairedRuntimeConnection {
const endpoint = preferredEndpoint(environment)
return pairedRuntimeConnectionFromOffer({
endpoint: endpoint.endpoint,
deviceToken: endpoint.deviceToken,
publicKeyB64: endpoint.publicKeyB64,
pairedDeviceId: environment.pairedDeviceId
})
}
export function getPreferredPairingOffer(environment: PreferredEndpointSource): PairingOffer {
const endpoint = preferredEndpoint(environment)
return {
v: PAIRING_OFFER_VERSION,
endpoint: endpoint.endpoint,
deviceToken: endpoint.deviceToken,
publicKeyB64: endpoint.publicKeyB64,
...(environment.pairedDeviceId ? { pairedDeviceId: environment.pairedDeviceId } : {})
}
}
/** The SSH tunnel a server is reached through; only orcadDeployment also grants lifecycle ownership. */
export function getRuntimeSshAccess(
environment: Pick<KnownRuntimeEnvironment, 'orcadDeployment' | 'sshAccess'>
): RuntimeSshTunnelLink | undefined {
return getOrcadSshDeployment(environment) ?? environment.sshAccess
}
export function getPreferredLoopbackRuntimePort(environment: {
endpoints: { id: string; endpoint: string }[]
preferredEndpointId: string
}): number | null {
const endpoint = environment.endpoints.find(
(entry) => entry.id === environment.preferredEndpointId
)
if (!endpoint) {
return null
}
try {
const url = new URL(endpoint.endpoint)
const port = Number(url.port)
return (url.protocol === 'ws:' || url.protocol === 'wss:') &&
classifyRemotePairingHostname(url.hostname) === 'loopback' &&
Number.isInteger(port) &&
port >= 1 &&
port <= 65_535
? port
: null
} catch {
return null
}
}