mirror of
https://github.com/stablyai/orca.git
synced 2026-11-02 01:57:57 +00:00
* refactor(remote): one paired E2EE handshake core for desktop, CLI, web and mobile clients Every paired client now builds its hello, checks the host's ready, seals its auth frame and opens host frames through src/shared/paired-e2ee-client-handshake. Platforms inject their own NaCl/base64 primitives. The v2 client session and its pure-JS key schedule (@noble/hashes 1.8.0, the version mobile pins) move to src/shared, and the per-host v2 latch policy is shared with an injected store. No wire change: frames match the captured legacy and v2 fixtures byte for byte, and a cross-version test drives the core against the released host and the released client against this host. The auth step is now async so a device-key signer can slot in later. * fix(remote): keep @noble/hashes out of the CLI's unbundled runtime The packaged CLI loads src/shared output from disk, so a bare @noble/hashes import failed the packaged-runtime check. The v2 key schedule is now a primitive: Node clients use the node:crypto schedule (moved to src/shared so the CLI can reach it), while the browser and phone keep the pure-JS one. * fix(mobile): re-check the v2 latch before sending a legacy auth frame The auth step is async, so another socket can latch the host to v2 while a legacy auth frame is being sealed. Refuse it at send time, as before. --------- Co-authored-by: m4air <m4air@Mac.localdomain>
187 lines
5.9 KiB
TypeScript
187 lines
5.9 KiB
TypeScript
import type { PairedE2EEClientHandshake } from './paired-e2ee-client-handshake'
|
|
import type { RemoteRuntimeHandshakeState } from './remote-runtime-client-handshake'
|
|
import { RemoteRuntimeClientError } from './remote-runtime-client-error'
|
|
import {
|
|
isKeepaliveFrame,
|
|
RuntimeRpcEnvelopeSchema,
|
|
type RuntimeRpcResponse
|
|
} from './runtime-rpc-envelope'
|
|
import { parseRemoteRuntimeJsonText } from './remote-runtime-request-frames'
|
|
import type { RuntimeStatus } from './runtime-types'
|
|
|
|
type RequestResponseRouterOptions<TResult> = {
|
|
handshake: PairedE2EEClientHandshake
|
|
sealAuth: () => Promise<string>
|
|
isSettled: () => boolean
|
|
serializedStatusRequest: string | null
|
|
requestId: string
|
|
statusRequestId: string | null
|
|
validateStatus?: (response: RuntimeRpcResponse<RuntimeStatus>) => void
|
|
send: (frame: string) => void
|
|
sendRequestedRpc: () => void
|
|
refreshTimeout: () => void
|
|
finishError: (error: Error) => void
|
|
finishResponse: (response: RuntimeRpcResponse<TResult>) => void
|
|
}
|
|
|
|
export class RemoteRuntimeRequestResponseRouter<TResult> {
|
|
state: RemoteRuntimeHandshakeState = 'awaiting_ready'
|
|
private awaitingRequestId: string
|
|
private awaitingStatus: boolean
|
|
|
|
constructor(private readonly options: RequestResponseRouterOptions<TResult>) {
|
|
this.awaitingRequestId = options.statusRequestId ?? options.requestId
|
|
this.awaitingStatus = options.statusRequestId !== null
|
|
}
|
|
|
|
get pairingStage(): 'connect' | 'host-identity' | 'runtime' {
|
|
return this.state === 'awaiting_ready'
|
|
? 'connect'
|
|
: this.state === 'awaiting_authenticated'
|
|
? 'host-identity'
|
|
: 'runtime'
|
|
}
|
|
|
|
handleTextFrame(frame: string): void {
|
|
if (this.state === 'awaiting_ready') {
|
|
this.handleReadyFrame(frame)
|
|
return
|
|
}
|
|
const plaintext = this.options.handshake.channel.openText(frame)
|
|
if (plaintext === null) {
|
|
this.options.finishError(
|
|
new RemoteRuntimeClientError(
|
|
'invalid_runtime_response',
|
|
'Remote Orca runtime returned an undecryptable frame.',
|
|
{
|
|
pairingStage:
|
|
this.state === 'awaiting_authenticated' ? 'host-identity' : this.pairingStage
|
|
}
|
|
)
|
|
)
|
|
return
|
|
}
|
|
if (this.state === 'awaiting_authenticated') {
|
|
this.handleAuthenticatedFrame(plaintext)
|
|
return
|
|
}
|
|
this.handleRpcFrame(plaintext)
|
|
}
|
|
|
|
private handleReadyFrame(frame: string): void {
|
|
const readyFrame = this.options.handshake.acceptReadyText(frame)
|
|
if (readyFrame !== 'accepted') {
|
|
this.options.finishError(
|
|
new RemoteRuntimeClientError(
|
|
'invalid_runtime_response',
|
|
readyFrame === 'invalid'
|
|
? 'Remote Orca runtime returned an invalid E2EE handshake frame.'
|
|
: 'Remote Orca runtime returned an unexpected E2EE handshake frame.',
|
|
{ pairingStage: 'host-identity' }
|
|
)
|
|
)
|
|
return
|
|
}
|
|
this.state = 'awaiting_authenticated'
|
|
void this.sendAuth()
|
|
}
|
|
|
|
private async sendAuth(): Promise<void> {
|
|
let frame: string
|
|
try {
|
|
frame = await this.options.sealAuth()
|
|
} catch (error) {
|
|
this.options.finishError(
|
|
error instanceof Error
|
|
? error
|
|
: new RemoteRuntimeClientError('runtime_error', String(error))
|
|
)
|
|
return
|
|
}
|
|
if (!this.options.isSettled()) {
|
|
this.options.send(frame)
|
|
}
|
|
}
|
|
|
|
private handleAuthenticatedFrame(plaintext: string): void {
|
|
const authenticated = this.options.handshake.classifyAuthReply(plaintext)
|
|
if (authenticated.kind === 'invalid') {
|
|
this.options.finishError(
|
|
new RemoteRuntimeClientError(
|
|
'invalid_runtime_response',
|
|
'Remote Orca runtime returned an invalid E2EE auth frame.',
|
|
{ pairingStage: 'host-identity' }
|
|
)
|
|
)
|
|
return
|
|
}
|
|
if (authenticated.kind !== 'authenticated') {
|
|
const code =
|
|
authenticated.errorCode === 'unauthorized' ? 'unauthorized' : 'invalid_runtime_response'
|
|
this.options.finishError(
|
|
new RemoteRuntimeClientError(code, 'Remote Orca runtime rejected the pairing token.', {
|
|
pairingStage: code === 'unauthorized' ? 'access-grant' : 'host-identity'
|
|
})
|
|
)
|
|
return
|
|
}
|
|
this.state = 'ready'
|
|
if (this.options.serializedStatusRequest) {
|
|
this.options.send(
|
|
this.options.handshake.channel.sealText(this.options.serializedStatusRequest)
|
|
)
|
|
return
|
|
}
|
|
this.options.sendRequestedRpc()
|
|
}
|
|
|
|
private handleRpcFrame(plaintext: string): void {
|
|
let raw: unknown
|
|
try {
|
|
raw = parseRemoteRuntimeJsonText(plaintext)
|
|
} catch {
|
|
this.invalidResponse('Remote Orca runtime returned an invalid response frame.')
|
|
return
|
|
}
|
|
if (isKeepaliveFrame(raw)) {
|
|
this.options.refreshTimeout()
|
|
return
|
|
}
|
|
const parsed = RuntimeRpcEnvelopeSchema.safeParse(raw)
|
|
if (!parsed.success || '_keepalive' in parsed.data) {
|
|
this.invalidResponse('Remote Orca runtime returned an invalid response frame.')
|
|
return
|
|
}
|
|
if (parsed.data.id !== this.awaitingRequestId) {
|
|
this.invalidResponse('Remote Orca runtime returned a mismatched response id.')
|
|
return
|
|
}
|
|
if (this.awaitingStatus && this.options.validateStatus) {
|
|
try {
|
|
this.options.validateStatus(parsed.data as RuntimeRpcResponse<RuntimeStatus>)
|
|
} catch (error) {
|
|
this.options.finishError(
|
|
error instanceof Error
|
|
? error
|
|
: new RemoteRuntimeClientError('runtime_error', String(error))
|
|
)
|
|
return
|
|
}
|
|
this.awaitingStatus = false
|
|
this.awaitingRequestId = this.options.requestId
|
|
this.options.refreshTimeout()
|
|
this.options.sendRequestedRpc()
|
|
return
|
|
}
|
|
this.options.finishResponse(parsed.data as RuntimeRpcResponse<TResult>)
|
|
}
|
|
|
|
private invalidResponse(message: string): void {
|
|
this.options.finishError(
|
|
new RemoteRuntimeClientError('invalid_runtime_response', message, {
|
|
pairingStage: 'runtime'
|
|
})
|
|
)
|
|
}
|
|
}
|