Loading files
710fbeaaeThis file is pinned to the selected release. Download the original to open it in your design tool.
import { isWrapped } from '../e2ee/core.js'
import type { E2eeManager } from '../e2ee/manager.js'
import { deviceDump } from './dump.js'
import { type AutonomousDeviceService, type AutonomousDeviceFrame } from './service.js'
type Frame = Record<string, unknown>
/** Application RPC over the existing relay E2EE session. No socket, identity, PAKE or trust of its own. */
export class AutonomousDeviceRelay {
private readonly clients = new Map<string, { identity: string; tokens: number; at: number; active: number }>()
constructor(private readonly crypto: Pick<E2eeManager, 'sessionIdentity' | 'sessionRole' | 'unwrapDown' | 'wrapTarget'>,
private readonly send: (connId: string, frame: Frame) => void,
private readonly service: AutonomousDeviceService, private readonly machineId: string, private readonly onReady?: () => void,
private readonly onRemoteRevoke?: (identity: string) => void) {}
count(include: (connId: string) => boolean = () => true): number { return [...this.clients].filter(([id, client]) => include(id) && this.crypto.sessionIdentity(id) === client.identity && this.crypto.sessionRole(id) === 'device').length }
connected(): boolean { return this.count() > 0 }
private sendTo(connId: string, identity: string, type: string, payload: Frame): void {
if (this.crypto.sessionIdentity(connId) !== identity || this.crypto.sessionRole(connId) !== 'device') return
deviceDump.record('out', 'rpc', connId, { type, payload })
const wrapped = this.crypto.wrapTarget(connId, type, payload)
if (wrapped) this.send(connId, wrapped)
}
async handle(connId: string, frame: Frame): Promise<void> {
for (const [id, client] of this.clients) if (this.crypto.sessionIdentity(id) !== client.identity) this.drop(id)
const identity = this.crypto.sessionIdentity(connId)
if (!identity || this.crypto.sessionRole(connId) !== 'device' || !isWrapped(frame.payload) || frame.dbSessionId !== undefined) return
const opened = this.crypto.unwrapDown(connId, frame)
const req = opened?.payload as Frame | undefined
if (!req || typeof req !== 'object' || Array.isArray(req)) return
deviceDump.record('in', 'rpc', connId, { type: frame.type, payload: req })
const reply = (payload: Frame) => this.sendTo(connId, identity, 'autonomous_device_result', payload)
if (req.type === 'hello') {
if (req.proto !== 1 || typeof req.requestId !== 'string') { reply({ type: 'hello_result', requestId: req.requestId, error: { code: 'PROTO_UNSUPPORTED', message: 'Protocol 1 required' } }); return }
const current = this.clients.get(connId)
this.clients.set(connId, current?.identity === identity ? current : { identity, tokens: 20, at: Date.now(), active: 0 })
const resume = req.resume as { serverInstanceId?: unknown; cursor?: unknown } | undefined
reply({ type: 'hello_result', requestId: req.requestId, proto: 1, machineId: this.machineId, serverInstanceId: this.service.serverInstanceId, capabilities: this.service.capabilities, ...this.service.resume(resume) })
this.service.replay(resume, event => this.sendEvent(connId, identity, event))
this.onReady?.()
return
}
const client = this.clients.get(connId)
if (!client || client.identity !== identity) { reply({ type: `${req.type}_result`, requestId: req.requestId, error: { code: 'HELLO_REQUIRED', message: 'Send application hello first' } }); return }
// A device deleting its local trust must say so while the authenticated channel still exists.
// A socket close alone is deliberately only an offline signal: treating it as revocation would
// unpair users whenever their LAN briefly drops.
if (req.type === 'pair.revoke') {
if (typeof req.requestId !== 'string' || Object.keys(req).some(key => key !== 'type' && key !== 'requestId')) {
reply({ type: 'pair.revoke_result', requestId: req.requestId, error: { code: 'INVALID_REQUEST', message: 'Invalid device revoke request' } })
return
}
reply({ type: 'pair.revoke_result', requestId: req.requestId, revoked: true })
this.onRemoteRevoke?.(identity)
return
}
const now = Date.now(); client.tokens = Math.min(20, client.tokens + Math.max(0, now - client.at) / 1000); client.at = now
const error = client.tokens < 1 ? 'RATE_LIMITED' : client.active >= 4 ? 'BACKPRESSURE' : null
if (error) { reply({ type: `${req.type}_result`, requestId: req.requestId, error: { code: error, message: error } }); return }
client.tokens--; client.active++
try { reply(await this.service.request(identity, req)) } finally { client.active-- }
}
private sendEvent(connId: string, identity: string, event: AutonomousDeviceFrame): void {
const p = event.payload as Frame | undefined
if (event.kind === 'turn.summary' && p?.resultId !== undefined) {
if (this.service.canSendResult(identity, event)) this.sendTo(connId, identity, 'autonomous_device_event', event)
return
}
if (typeof p?.idempotencyKey === 'string') {
const own = this.service.receipt(identity, p.idempotencyKey)
const receipt = p.receipt as Frame | undefined
if (!own || (receipt && receipt.deliveryId !== own.deliveryId) || (p.turnId && p.turnId !== own.turnId)) return
}
this.sendTo(connId, identity, 'autonomous_device_event', event)
}
/** `deviceId` set: an agent stream frame, sent to that identity's links and no other. */
emit(event: AutonomousDeviceFrame, deviceId?: string): void {
for (const [connId, client] of this.clients) if (!deviceId || client.identity === deviceId) this.sendEvent(connId, client.identity, event)
}
drop(connId: string): void {
const client = this.clients.get(connId)
this.clients.delete(connId)
if (client && ![...this.clients.values()].some(c => c.identity === client.identity)) this.service.deviceOffline(client.identity)
}
/** App-side revoke: tell the device while its authenticated session still exists (the E2eeManager drops
* the session right after), so it clears its own pin instead of showing "paired / disconnected" forever.
* Best-effort: a device that is offline learns it on reconnect, when its e2e_hello gets e2e_denied. */
revoke(identity: string): void {
for (const [connId, client] of this.clients) if (client.identity === identity) {
try { this.sendTo(connId, identity, 'autonomous_device_event', { type: 'pair.revoke', machineId: this.machineId }) } catch { /* Local removal must proceed regardless. */ }
this.clients.delete(connId)
}
this.service.revoke(identity)
}
}