Files
orca/src/shared/remote-runtime-client.test.ts
T
Brennan Benson 757736628f fix(native-chat): a paired server admits structured chat by client capability, not its own chat setting (#24203)
* fix(native-chat): a host admits structured sessions by client capability, not its own chat setting

A host's experimentalStructuredNativeChat decided whether any paired client could reach
agentSession.* at all, and whether session.tabs.* showed it structured tabs. That setting is the
host user's own launch preference: whether a new agent opens as a chat or a terminal is decided by
whoever launches it. Using it as admission control meant a client whose own preference was
"structured chat" was refused on a host whose preference was "terminal", and chats opened while
the setting was on were withheld from mobile once it was turned off.

The gate now asks one thing: did the client advertise agent-session.structured.v1 (in-process
callers negotiate nothing and are always admitted). Tab projection and restore follow the same
rule. With the setting no longer gating anything, the separate cleanup gate (close, cancel,
unsubscribe, release), which existed only so those kept working after the setting was switched
off, is identical to the main gate and is folded into it. The settings listener that republished
tabs when the setting changed is removed, since projection no longer depends on it.

The host setting still picks the default for launches that start on the host itself
(agent.launch from mobile, orchestration worker-start).

* fix(native-chat): negotiate client-chosen launch mode so released phones and old servers keep terminals

Hosts advertise agent-session.structured.client-launch-mode.v1: they admit
structured sessions by client capability alone. A remote client that does
not advertise it (phones released before agent.launch) asks createSupport
to pick the launch mode, so the host keeps answering that with its own
setting, exactly as before. Cleanup methods keep their own named gate so a
future admission condition cannot make close or cancel refusable.

* chore(native-chat): justify the two type assertions this change's lines touch

* fix(native-chat): chats that already exist keep showing whatever the chat setting says

The structured chat setting decides only what new agents open as. With it
off, this machine's structured chats used to be hidden while the host,
which no longer reads the setting, still reported them to the workspace
activation gate, so a workspace holding only a chat opened empty. The
local chat mirror and its startup restore now run whatever the setting
says, the continue-after-restart offer follows the chats that exist, and
the setting's copy says it applies to new agents.

* test(native-chat): pin that a host advertises the client-chosen launch mode

* fix(native-chat): mirror this machine's chats only where it holds them

Round 1 ran the local chat mirror for everyone so existing chats show
whatever the setting says. That gave every desktop a permanent
session-tabs listener, which turns on the runtime's phone replication
paths, plus two full session-tab censuses at startup, and made the
browser client mirror its remote host a second time.

The runtime now says whether it holds structured chats: its structured
host is built only when saved chats were restored at startup or a client
created one here, and it announces the moment one is built. The mirror,
the startup restore and the continue-after-restart offer run only when
the setting launches chats or the host holds some, and never in the
browser client. A chat a paired client creates here with the setting off
still appears at once. The chat behaviour settings show wherever chats
exist, and the setting's copy says it picks what new agents open as. The
toggle-off teardown this made dead is removed.

* test(native-chat): record install listeners without a cast

* fix(native-chat): mirror this machine's chats only once it holds one, not once its host is built

Session history, resume preparation, terminal resume commands and replay-safe phone launches all
build the structured host for users who never had a chat, which turned on the chat mirror and the
structured-only settings rows until the next restart. The signal is now derived from the host's
records (or a records file still owed its import) and pushed when the first chat is restored or
created. A throwing listener no longer fails the install that fired it.

* feat(native-chat): createSupport reports the saved selection a new chat on this host starts with

A chat on a paired server starts with the server's saved model and options, which the desktop could
not read, so its picker showed a guess. createSupport's answer, which the desktop already waits for
before a paired launch, now also carries that seed as a new optional field (older clients ignore it).
Create and createSupport read it through one resolver so they cannot drift.

* refactor(protocol): move the Electron remote client capability list into its own module

Merging main left protocol-version.ts one line over the max-lines limit on this branch. The list of
capabilities the desktop advertises to a paired host moves, unchanged, into
electron-remote-runtime-client-capabilities.ts, the module the next PR in the stack already uses
for it; importers point there.

* test(cross-version): stub the launch seed resolver createSupport now reads

* fix(native-chat): the desktop tells its own host it picks each launch mode, so retrying an existing chat works with the setting off

* docs(native-chat): name the real exit for the released-phone createSupport rule

* test(cross-version): a released client still gets the host-setting createSupport answer; a launch-mode client gets supported plus the seed
2026-10-01 18:55:32 -07:00

763 lines
24 KiB
TypeScript

import type { AddressInfo } from 'node:net'
import { afterEach, describe, expect, it, vi } from 'vitest'
import WebSocketClient, { WebSocketServer, type WebSocket } from 'ws'
import { encodePairingOffer, parsePairingCode, type PairingOffer } from './pairing'
import {
decrypt,
decryptBytes,
deriveSharedKey,
encrypt,
generateKeyPair,
publicKeyFromBase64,
publicKeyToBase64
} from './e2ee-crypto'
import { sendRemoteRuntimeRequest, subscribeRemoteRuntimeRequest } from './remote-runtime-client'
import { remoteRuntimeClientCapabilities } from './remote-runtime-client-capabilities'
import { MAX_TIMER_DELAY_MS } from './timer-delay'
import {
BROWSER_CLIENT_HOST_RUNTIME_CAPABILITY,
BROWSER_CLIENT_PAGE_METADATA_RUNTIME_CAPABILITY,
BROWSER_NETWORK_TUNNEL_RUNTIME_CAPABILITY
} from './protocol-version'
import { ELECTRON_REMOTE_RUNTIME_CLIENT_CAPABILITIES } from './electron-remote-runtime-client-capabilities'
const servers: WebSocketServer[] = []
afterEach(async () => {
await Promise.all(
servers.splice(0).map(
(server) =>
new Promise<void>((resolve) => {
for (const client of server.clients) {
client.close()
}
server.close(() => resolve())
})
)
)
})
describe('subscribeRemoteRuntimeRequest', () => {
it('includes WebSocket close details when subscription admission is rejected', async () => {
const server = await createClosingServer(1013, 'Maximum connections reached')
await expect(
subscribeRemoteRuntimeRequest(server.pairing, 'terminal.subscribe', {}, 1000, {
onResponse: vi.fn(),
onError: vi.fn()
})
).rejects.toThrow(
'Remote Orca runtime closed the connection (1013: Maximum connections reached).'
)
})
it('sends encrypted binary frames on an established subscription socket', async () => {
const server = await createSubscriptionServer()
const onResponse = vi.fn()
const onError = vi.fn()
const subscription = await subscribeRemoteRuntimeRequest(
server.pairing,
'terminal.subscribe',
{ terminal: 't1' },
1000,
{
onResponse,
onError
}
)
await vi.waitFor(() =>
expect(onResponse).toHaveBeenCalledWith(
expect.objectContaining({ ok: true, result: { type: 'subscribed' } })
)
)
await expect(server.nextAuth).resolves.toEqual({
type: 'e2ee_auth',
deviceToken: 'device-token',
clientCapabilities: remoteRuntimeClientCapabilities()
})
const bytes = new Uint8Array([1, 2, 3])
expect(subscription.sendBinary(bytes)).toBe(true)
await expect(server.nextBinary).resolves.toEqual(bytes)
expect(onError).not.toHaveBeenCalled()
subscription.close()
})
it('binds optional capabilities and rejects a hard outbound queue overflow', async () => {
const server = await createSubscriptionServer()
const onResponse = vi.fn()
const onError = vi.fn()
const subscription = await subscribeRemoteRuntimeRequest(
server.pairing,
'network.browserTunnel',
{},
1000,
{ onResponse, onError },
{
clientCapabilities: [BROWSER_NETWORK_TUNNEL_RUNTIME_CAPABILITY],
outboundQueue: { softCapBytes: 0, maxQueuedBytes: 1, maxQueuedFrames: 1 }
}
)
await vi.waitFor(() => expect(onResponse).toHaveBeenCalled())
await expect(server.nextAuth).resolves.toEqual({
type: 'e2ee_auth',
deviceToken: 'device-token',
clientCapabilities: remoteRuntimeClientCapabilities([
BROWSER_NETWORK_TUNNEL_RUNTIME_CAPABILITY
])
})
expect(subscription.sendBinary(new Uint8Array([9]))).toBe(false)
expect(onError).toHaveBeenCalledWith(
expect.objectContaining({ code: 'remote_runtime_unavailable' })
)
})
it('accounts encrypted queues and native socket buffers in an injected aggregate budget', async () => {
const server = await createSubscriptionServer()
const releaseQueued = vi.fn()
const releaseSocket = vi.fn()
const canSend = vi.fn(() => false)
let readBufferedAmount: (() => number) | undefined
const outboundMemoryBudget = {
claimQueuedBytes: vi.fn(() => releaseQueued),
registerBufferedAmount: vi.fn((read: () => number) => {
readBufferedAmount = read
return { canSend, release: releaseSocket }
})
}
const onResponse = vi.fn()
const onClose = vi.fn()
const subscription = await subscribeRemoteRuntimeRequest(
server.pairing,
'network.browserTunnel',
{},
1000,
{ onResponse, onError: vi.fn(), onClose },
{
outboundMemoryBudget,
outboundQueue: { softCapBytes: 1, maxQueuedBytes: 1024, maxQueuedFrames: 8 }
}
)
await vi.waitFor(() => expect(onResponse).toHaveBeenCalled())
expect(subscription.sendBinary(new Uint8Array([9]))).toBe(true)
expect(outboundMemoryBudget.registerBufferedAmount).toHaveBeenCalledOnce()
expect(readBufferedAmount?.()).toBeGreaterThanOrEqual(0)
expect(canSend).toHaveBeenCalledWith(expect.any(Number), false)
expect(outboundMemoryBudget.claimQueuedBytes).toHaveBeenCalledWith(expect.any(Number))
subscription.close()
subscription.close()
expect(releaseQueued).toHaveBeenCalledOnce()
expect(releaseSocket).not.toHaveBeenCalled()
await vi.waitFor(() => expect(onClose).toHaveBeenCalledOnce())
expect(releaseSocket).toHaveBeenCalledOnce()
})
it('keeps native socket bytes charged through failure cleanup and a later close', async () => {
const server = await createSubscriptionServer()
const releaseSocket = vi.fn()
const onError = vi.fn()
const subscription = await subscribeRemoteRuntimeRequest(
server.pairing,
'network.browserTunnel',
{},
1000,
{ onResponse: vi.fn(), onError },
{
outboundMemoryBudget: {
claimQueuedBytes: vi.fn(() => vi.fn()),
registerBufferedAmount: vi.fn(() => ({ canSend: () => false, release: releaseSocket }))
},
outboundQueue: { softCapBytes: 1, maxQueuedBytes: 1, maxQueuedFrames: 1 }
}
)
expect(subscription.sendBinary(new Uint8Array([9]))).toBe(false)
expect(onError).toHaveBeenCalledWith(
expect.objectContaining({ code: 'remote_runtime_unavailable' })
)
subscription.close()
expect(releaseSocket).not.toHaveBeenCalled()
await vi.waitFor(() => expect(releaseSocket).toHaveBeenCalledOnce())
})
it('detaches subscription socket listeners after close', async () => {
const offSpy = vi.spyOn(WebSocketClient.prototype, 'off')
try {
const server = await createSubscriptionServer()
const onResponse = vi.fn()
const onError = vi.fn()
const onClose = vi.fn()
const subscription = await subscribeRemoteRuntimeRequest(
server.pairing,
'terminal.subscribe',
{ terminal: 't1' },
1000,
{
onResponse,
onError,
onClose
}
)
await vi.waitFor(() => expect(onResponse).toHaveBeenCalled())
subscription.close()
await vi.waitFor(() => expect(onClose).toHaveBeenCalledOnce())
const removedEvents = offSpy.mock.calls.map(([event]) => event)
expect(removedEvents).toEqual(expect.arrayContaining(['open', 'error', 'close', 'message']))
expect(subscription.sendBinary(new Uint8Array([9]))).toBe(false)
expect(onError).not.toHaveBeenCalled()
} finally {
offSpy.mockRestore()
}
})
it('closes a half-open subscription socket via client liveness so callers can resubscribe', async () => {
// Why: dedicated stream sockets (terminal.multiplex, browser.screencast)
// must not hang forever when a tunnel goes half-open — no close frame, no
// pongs, no data (#7718). Client liveness surfaces onError/onClose so the
// renderer's onTransportClose resubscribe path can run.
const server = await createSubscriptionServer({ disableAutoPong: true })
const onResponse = vi.fn()
const onError = vi.fn()
const onClose = vi.fn()
const subscription = await subscribeRemoteRuntimeRequest(
server.pairing,
'terminal.multiplex',
{},
1000,
{ onResponse, onError, onClose },
{ pingIntervalMs: 50, livenessTimeoutMs: 200 }
)
await vi.waitFor(() => expect(onResponse).toHaveBeenCalled())
await vi.waitFor(
() =>
expect(onError).toHaveBeenCalledWith(
expect.objectContaining({ code: 'remote_runtime_unavailable' })
),
{ timeout: 5000 }
)
expect(onClose).toHaveBeenCalledOnce()
subscription.close()
})
it('closes established subscription sockets after terminal protocol errors', async () => {
const offSpy = vi.spyOn(WebSocketClient.prototype, 'off')
try {
const server = await createSubscriptionServer({ sendMismatchedResponseAfterSubscribe: true })
const onResponse = vi.fn()
const onError = vi.fn()
const onClose = vi.fn()
const subscription = await subscribeRemoteRuntimeRequest(
server.pairing,
'terminal.subscribe',
{ terminal: 't1' },
1000,
{
onResponse,
onError,
onClose
}
)
await vi.waitFor(() => expect(onResponse).toHaveBeenCalled())
await vi.waitFor(() =>
expect(onError).toHaveBeenCalledWith(
expect.objectContaining({ code: 'invalid_runtime_response' })
)
)
expect(onClose).toHaveBeenCalledOnce()
const removedEvents = offSpy.mock.calls.map(([event]) => event)
expect(removedEvents).toEqual(expect.arrayContaining(['open', 'error', 'close', 'message']))
expect(subscription.sendBinary(new Uint8Array([9]))).toBe(false)
} finally {
offSpy.mockRestore()
}
})
})
describe('sendRemoteRuntimeRequest', () => {
it('keeps generic native authentication free of Electron placement support', async () => {
let receivedAuth: Record<string, unknown> | null = null
const server = await createOneShotServer({
onAuth: (auth) => {
receivedAuth = auth
}
})
await sendRemoteRuntimeRequest(
server.pairing,
'session.tabs.list',
{ worktree: 'id:worktree-a' },
1000
)
expect(receivedAuth).toMatchObject({
clientCapabilities: remoteRuntimeClientCapabilities()
})
})
it('advertises browser placement support only when the Electron caller opts in', async () => {
let receivedAuth: Record<string, unknown> | null = null
const server = await createOneShotServer({
onAuth: (auth) => {
receivedAuth = auth
}
})
await sendRemoteRuntimeRequest(
server.pairing,
'session.tabs.list',
{ worktree: 'id:worktree-a' },
1000,
undefined,
undefined,
ELECTRON_REMOTE_RUNTIME_CLIENT_CAPABILITIES
)
expect(receivedAuth).toMatchObject({
clientCapabilities: expect.arrayContaining([
BROWSER_CLIENT_HOST_RUNTIME_CAPABILITY,
BROWSER_CLIENT_PAGE_METADATA_RUNTIME_CAPABILITY
])
})
})
it.each([-1, 1.5, MAX_TIMER_DELAY_MS + 1, Number.MAX_SAFE_INTEGER + 1])(
'rejects invalid timer delay %s before reading pairing data',
async (timeoutMs) => {
await expect(
sendRemoteRuntimeRequest({} as PairingOffer, 'status.get', {}, timeoutMs)
).rejects.toMatchObject({ code: 'invalid_argument' })
}
)
it('includes WebSocket close details when one-shot admission is rejected', async () => {
const server = await createClosingServer(1013, 'Maximum connections reached')
await expect(sendRemoteRuntimeRequest(server.pairing, 'status.get', {}, 1000)).rejects.toThrow(
'Remote Orca runtime closed the connection (1013: Maximum connections reached).'
)
})
it('classifies a non-Orca handshake as a host identity mismatch', async () => {
const server = await createInvalidHandshakeServer()
await expect(
sendRemoteRuntimeRequest(server.pairing, 'status.get', {}, 1000)
).rejects.toMatchObject({
code: 'invalid_runtime_response',
pairingStage: 'host-identity'
})
})
it('classifies an undecryptable post-auth frame as a runtime failure', async () => {
const server = await createOneShotServer({ sendUndecryptableResponse: true })
await expect(
sendRemoteRuntimeRequest(server.pairing, 'status.get', {}, 1000)
).rejects.toMatchObject({
code: 'invalid_runtime_response',
pairingStage: 'runtime'
})
})
it('refreshes the per-call timeout when the runtime sends keepalive frames', async () => {
const server = await createOneShotServer()
const response = await sendRemoteRuntimeRequest<{ satisfied: boolean }>(
server.pairing,
'terminal.wait',
{ terminal: 't1', for: 'tui-idle', timeoutMs: 550 },
300
)
expect(response).toMatchObject({
ok: true,
result: { satisfied: true }
})
})
it('aborts and closes an in-flight one-shot socket', async () => {
let requestObserved: () => void = () => {}
const observed = new Promise<void>((resolve) => {
requestObserved = resolve
})
const server = await createOneShotServer({ onRequest: requestObserved })
const closeSpy = vi.spyOn(WebSocketClient.prototype, 'close')
const controller = new AbortController()
try {
const request = sendRemoteRuntimeRequest(
server.pairing,
'skills.install',
{},
60_000,
undefined,
controller.signal
)
await observed
controller.abort()
await expect(request).rejects.toMatchObject({ name: 'AbortError' })
expect(closeSpy).toHaveBeenCalled()
} finally {
closeSpy.mockRestore()
}
})
it('preserves structured failure data for remote computer-use recovery hints', async () => {
const server = await createOneShotServer({
response: (requestId) => ({
id: requestId,
ok: false,
error: {
code: 'app_not_found',
message: 'app not found: Gmail',
data: {
nextSteps: ['Target the desktop browser app/window that contains Gmail.']
}
},
_meta: { runtimeId: 'runtime-test' }
})
})
const response = await sendRemoteRuntimeRequest(
server.pairing,
'computer.getAppState',
{ app: 'Gmail' },
1000
)
expect(response).toMatchObject({
ok: false,
error: {
code: 'app_not_found',
data: {
nextSteps: [expect.stringContaining('desktop browser app/window')]
}
}
})
})
it('sends orchestration authentication fields in the admitted encrypted request', async () => {
let receivedRequest: Record<string, unknown> | null = null
const server = await createOneShotServer({
onRequest: (request) => {
receivedRequest = request
}
})
await sendRemoteRuntimeRequest(
server.pairing,
'orchestration.federationControl',
{ dispatch: 'ctx_1' },
1000,
{
orchestrationCapability: 'capability',
orchestrationContractVersion: 1,
orchestrationRequestId: 'mutation_1'
}
)
expect(receivedRequest).toMatchObject({
method: 'orchestration.federationControl',
params: { dispatch: 'ctx_1' },
orchestrationCapability: 'capability',
orchestrationContractVersion: 1,
orchestrationRequestId: 'mutation_1'
})
})
it('omits optional request fields for hosts that predate them', async () => {
let receivedRequest: Record<string, unknown> | null = null
const server = await createOneShotServer({
onRequest: (request) => {
receivedRequest = request
}
})
await sendRemoteRuntimeRequest(server.pairing, 'status.get', undefined, 1000)
expect(receivedRequest).toMatchObject({
deviceToken: 'device-token',
method: 'status.get'
})
expect(receivedRequest).not.toHaveProperty('params')
expect(receivedRequest).not.toHaveProperty('orchestrationCapability')
expect(receivedRequest).not.toHaveProperty('orchestrationContractVersion')
expect(receivedRequest).not.toHaveProperty('orchestrationRequestId')
})
it('detaches one-shot socket listeners after a successful response', async () => {
const offSpy = vi.spyOn(WebSocketClient.prototype, 'off')
try {
const server = await createOneShotServer()
await sendRemoteRuntimeRequest<{ satisfied: boolean }>(
server.pairing,
'terminal.wait',
{ terminal: 't1', for: 'tui-idle', timeoutMs: 550 },
300
)
const removedEvents = offSpy.mock.calls.map(([event]) => event)
expect(removedEvents).toEqual(expect.arrayContaining(['open', 'error', 'close', 'message']))
} finally {
offSpy.mockRestore()
}
})
})
async function createSubscriptionServer(
options: {
sendMismatchedResponseAfterSubscribe?: boolean
// Why: half-open simulation — the socket stays open but never answers
// protocol pings, like a wedged tunnel that swallows frames silently.
disableAutoPong?: boolean
} = {}
): Promise<{
pairing: PairingOffer
nextBinary: Promise<Uint8Array>
nextAuth: Promise<unknown>
}> {
const serverKeyPair = generateKeyPair()
let resolveBinary: (bytes: Uint8Array) => void = () => {}
const nextBinary = new Promise<Uint8Array>((resolve) => {
resolveBinary = resolve
})
let resolveAuth: (auth: unknown) => void = () => {}
const nextAuth = new Promise<unknown>((resolve) => {
resolveAuth = resolve
})
// host must match the 127.0.0.1 clients dial: a wildcard bind lets a foreign loopback listener claim the port and answer here.
const wss = new WebSocketServer({
host: '127.0.0.1',
port: 0,
autoPong: options.disableAutoPong !== true
})
servers.push(wss)
wss.on('connection', (ws) => {
let sharedKey: Uint8Array | null = null
let authenticated = false
ws.on('message', (data, isBinary) => {
if (isBinary) {
if (!sharedKey) {
return
}
const plaintext = decryptBytes(new Uint8Array(data as Buffer), sharedKey)
if (plaintext) {
resolveBinary(plaintext)
}
return
}
const frame = data.toString()
if (!sharedKey) {
const hello = JSON.parse(frame) as { publicKeyB64: string }
sharedKey = deriveSharedKey(
serverKeyPair.secretKey,
publicKeyFromBase64(hello.publicKeyB64)
)
ws.send(JSON.stringify({ type: 'e2ee_ready' }))
return
}
const plaintext = decrypt(frame, sharedKey)
if (!plaintext) {
return
}
if (!authenticated) {
resolveAuth(JSON.parse(plaintext))
authenticated = true
sendEncrypted(ws, sharedKey, { type: 'e2ee_authenticated' })
return
}
const request = JSON.parse(plaintext) as { id: string }
sendEncrypted(ws, sharedKey, {
id: request.id,
ok: true,
streaming: true,
result: { type: 'subscribed' },
_meta: { runtimeId: 'runtime-test' }
})
if (options.sendMismatchedResponseAfterSubscribe) {
sendEncrypted(ws, sharedKey, {
id: `${request.id}-mismatch`,
ok: true,
streaming: true,
result: { type: 'subscribed' },
_meta: { runtimeId: 'runtime-test' }
})
}
})
})
await new Promise<void>((resolve) => wss.once('listening', resolve))
const address = wss.address() as AddressInfo
const pairing = parsePairingCode(
encodePairingOffer({
v: 2,
endpoint: `ws://127.0.0.1:${address.port}`,
deviceToken: 'device-token',
publicKeyB64: publicKeyToBase64(serverKeyPair.publicKey)
})
)
if (!pairing) {
throw new Error('Failed to create test pairing')
}
return { pairing, nextBinary, nextAuth }
}
function sendEncrypted(ws: WebSocket, sharedKey: Uint8Array, message: unknown): void {
ws.send(encrypt(JSON.stringify(message), sharedKey))
}
async function createClosingServer(
code: number,
reason: string
): Promise<{ pairing: PairingOffer }> {
const serverKeyPair = generateKeyPair()
const wss = new WebSocketServer({ host: '127.0.0.1', port: 0 })
servers.push(wss)
wss.on('connection', (ws) => {
ws.close(code, reason)
})
await new Promise<void>((resolve) => wss.once('listening', resolve))
const address = wss.address() as AddressInfo
const pairing = parsePairingCode(
encodePairingOffer({
v: 2,
endpoint: `ws://127.0.0.1:${address.port}`,
deviceToken: 'device-token',
publicKeyB64: publicKeyToBase64(serverKeyPair.publicKey)
})
)
if (!pairing) {
throw new Error('Failed to create test pairing')
}
return { pairing }
}
async function createInvalidHandshakeServer(): Promise<{ pairing: PairingOffer }> {
const serverKeyPair = generateKeyPair()
const wss = new WebSocketServer({ host: '127.0.0.1', port: 0 })
servers.push(wss)
wss.on('connection', (ws) => {
ws.once('message', () => ws.send(JSON.stringify({ type: 'not_orca' })))
})
await new Promise<void>((resolve) => wss.once('listening', resolve))
const address = wss.address() as AddressInfo
const pairing = parsePairingCode(
encodePairingOffer({
v: 2,
endpoint: `ws://127.0.0.1:${address.port}`,
deviceToken: 'device-token',
publicKeyB64: publicKeyToBase64(serverKeyPair.publicKey)
})
)
if (!pairing) {
throw new Error('Failed to create test pairing')
}
return { pairing }
}
async function createOneShotServer(
options: {
response?: (requestId: string) => unknown
onAuth?: (auth: Record<string, unknown>) => void
onRequest?: (request: Record<string, unknown>) => void
sendUndecryptableResponse?: boolean
} = {}
): Promise<{ pairing: PairingOffer }> {
const serverKeyPair = generateKeyPair()
const wss = new WebSocketServer({ host: '127.0.0.1', port: 0 })
servers.push(wss)
wss.on('connection', (ws) => {
let sharedKey: Uint8Array | null = null
let authenticated = false
ws.on('message', (data, isBinary) => {
if (isBinary) {
return
}
const frame = data.toString()
if (!sharedKey) {
const hello = JSON.parse(frame) as { publicKeyB64: string }
sharedKey = deriveSharedKey(
serverKeyPair.secretKey,
publicKeyFromBase64(hello.publicKeyB64)
)
ws.send(JSON.stringify({ type: 'e2ee_ready' }))
return
}
const plaintext = decrypt(frame, sharedKey)
if (!plaintext) {
return
}
if (!authenticated) {
options.onAuth?.(JSON.parse(plaintext) as Record<string, unknown>)
authenticated = true
sendEncrypted(ws, sharedKey, { type: 'e2ee_authenticated' })
return
}
const request = JSON.parse(plaintext) as { id: string } & Record<string, unknown>
options.onRequest?.(request)
if (options.sendUndecryptableResponse) {
ws.send('not-an-encrypted-frame')
return
}
const key = sharedKey
const keepalive = setInterval(() => {
sendEncrypted(ws, key, { _keepalive: true })
}, 100)
ws.once('close', () => clearInterval(keepalive))
setTimeout(() => {
clearInterval(keepalive)
sendEncrypted(
ws,
key,
options.response?.(request.id) ?? {
id: request.id,
ok: true,
result: { satisfied: true },
_meta: { runtimeId: 'runtime-test' }
}
)
}, 550)
})
})
await new Promise<void>((resolve) => wss.once('listening', resolve))
const address = wss.address() as AddressInfo
const pairing = parsePairingCode(
encodePairingOffer({
v: 2,
endpoint: `ws://127.0.0.1:${address.port}`,
deviceToken: 'device-token',
publicKeyB64: publicKeyToBase64(serverKeyPair.publicKey)
})
)
if (!pairing) {
throw new Error('Failed to create test pairing')
}
return { pairing }
}