// Gateway protocol types — JSON-RPC-like messages over WebSocket. // ── Client → Server ──────────────────────────────────────────── export interface GatewayRequest { id: number; method: string; params?: Record; } export const GATEWAY_PROTOCOL_VERSION = 1; export interface NodeRegisterParams { connectionId: string; nodeId: string; role: string; protocolVersion: number; capabilities: string[]; } export interface NodeLocationSetParams { connectionId: string; latitude: number; longitude: number; accuracyMeters?: number; altitudeMeters?: number; headingDegrees?: number; speedMps?: number; source?: 'gps' | 'network' | 'manual' | 'unknown'; capturedAt?: number; } export interface NodeLocationGetParams { connectionId: string; } export interface NodeStatusSetParams { connectionId: string; platform: 'macos' | 'ios' | 'android' | 'linux' | 'windows' | 'unknown'; appVersion?: string; deviceName?: string; statusText?: string; batteryPct?: number; powerSource?: 'ac' | 'battery' | 'unknown'; } export interface NodePushTokenSetParams { connectionId: string; provider: 'apns'; token: string; topic?: string; environment?: 'sandbox' | 'production'; } // ── Server → Client ──────────────────────────────────────────── export interface GatewayResponse { id: number; result: unknown; } export interface GatewayError { id: number; error: { code: ErrorCode; message: string; }; } export interface GatewayEvent { id: number; event: EventType; data: unknown; } // ── Attachment data for gateway protocol messages ─────────────── /** Attachment data sent in agent.send params or emitted as events. */ export interface GatewayAttachment { /** MIME type (e.g. "image/jpeg", "audio/ogg") */ mimeType: string; /** Base64-encoded data */ data?: string; /** URL to the resource */ url?: string; /** Filename hint */ filename?: string; } // ── Event types emitted during agent.send ────────────────────── export type EventType = | 'content' | 'tool_start' | 'tool_end' | 'attachment' | 'done' | 'error'; export interface ContentEventData { text: string; } export interface ToolStartEventData { tool: string; args: unknown; } export interface ToolEndEventData { tool: string; result: { success: boolean; output: string; error?: string; }; } export interface AttachmentEventData { mimeType: string; data?: string; url?: string; filename?: string; } export interface DoneEventData { content: string; } export interface ErrorEventData { code: ErrorCode; message: string; queue?: Record; } // ── Error codes ──────────────────────────────────────────────── export enum ErrorCode { ParseError = -1, InvalidRequest = -2, MethodNotFound = -3, AuthRequired = -4, AuthFailed = -5, SessionNotFound = 1, ToolNotFound = 2, AgentBusy = 3, RequestCancelled = 4, InternalError = 5, } // ── Outbound message (union of all server → client types) ────── export type OutboundMessage = GatewayResponse | GatewayError | GatewayEvent; // ── Validation helpers ───────────────────────────────────────── export function isValidRequest(msg: unknown): msg is GatewayRequest { if (typeof msg !== 'object' || msg === null) {return false;} const obj = msg as Record; return ( typeof obj.id === 'number' && typeof obj.method === 'string' && (obj.params === undefined || (typeof obj.params === 'object' && obj.params !== null)) ); } export function parseMessage(raw: string): GatewayRequest | null { try { const parsed = JSON.parse(raw); if (isValidRequest(parsed)) {return parsed;} return null; } catch { return null; } } export function parseNodeRegisterParams(params: unknown): NodeRegisterParams | null { if (!params || typeof params !== 'object') { return null; } const p = params as Record; if (typeof p.connectionId !== 'string' || !p.connectionId.trim()) { return null; } if (typeof p.nodeId !== 'string' || !p.nodeId.trim()) { return null; } if (typeof p.role !== 'string' || !p.role.trim()) { return null; } if (typeof p.protocolVersion !== 'number' || !Number.isFinite(p.protocolVersion) || p.protocolVersion < 1) { return null; } const capabilitiesRaw = p.capabilities; if (!Array.isArray(capabilitiesRaw) || !capabilitiesRaw.every((entry) => typeof entry === 'string')) { return null; } return { connectionId: p.connectionId, nodeId: p.nodeId, role: p.role, protocolVersion: Math.floor(p.protocolVersion), capabilities: capabilitiesRaw, }; } export function parseNodeLocationSetParams(params: unknown): NodeLocationSetParams | null { if (!params || typeof params !== 'object') { return null; } const p = params as Record; if (typeof p.connectionId !== 'string' || !p.connectionId.trim()) { return null; } if (typeof p.latitude !== 'number' || !Number.isFinite(p.latitude) || p.latitude < -90 || p.latitude > 90) { return null; } if (typeof p.longitude !== 'number' || !Number.isFinite(p.longitude) || p.longitude < -180 || p.longitude > 180) { return null; } if (p.accuracyMeters !== undefined && (typeof p.accuracyMeters !== 'number' || !Number.isFinite(p.accuracyMeters) || p.accuracyMeters < 0)) { return null; } if (p.altitudeMeters !== undefined && (typeof p.altitudeMeters !== 'number' || !Number.isFinite(p.altitudeMeters))) { return null; } if (p.headingDegrees !== undefined && (typeof p.headingDegrees !== 'number' || !Number.isFinite(p.headingDegrees) || p.headingDegrees < 0 || p.headingDegrees > 360)) { return null; } if (p.speedMps !== undefined && (typeof p.speedMps !== 'number' || !Number.isFinite(p.speedMps) || p.speedMps < 0)) { return null; } if (p.capturedAt !== undefined && (typeof p.capturedAt !== 'number' || !Number.isFinite(p.capturedAt) || p.capturedAt <= 0)) { return null; } if (p.source !== undefined && !['gps', 'network', 'manual', 'unknown'].includes(String(p.source))) { return null; } return { connectionId: p.connectionId, latitude: p.latitude, longitude: p.longitude, accuracyMeters: p.accuracyMeters as number | undefined, altitudeMeters: p.altitudeMeters as number | undefined, headingDegrees: p.headingDegrees as number | undefined, speedMps: p.speedMps as number | undefined, source: p.source as NodeLocationSetParams['source'] | undefined, capturedAt: p.capturedAt as number | undefined, }; } export function parseNodeLocationGetParams(params: unknown): NodeLocationGetParams | null { if (!params || typeof params !== 'object') { return null; } const p = params as Record; if (typeof p.connectionId !== 'string' || !p.connectionId.trim()) { return null; } return { connectionId: p.connectionId, }; } export function parseNodeStatusSetParams(params: unknown): NodeStatusSetParams | null { if (!params || typeof params !== 'object') { return null; } const p = params as Record; if (typeof p.connectionId !== 'string' || !p.connectionId.trim()) { return null; } if (typeof p.platform !== 'string' || !['macos', 'ios', 'android', 'linux', 'windows', 'unknown'].includes(p.platform)) { return null; } if (p.appVersion !== undefined && typeof p.appVersion !== 'string') { return null; } if (p.deviceName !== undefined && typeof p.deviceName !== 'string') { return null; } if (p.statusText !== undefined && typeof p.statusText !== 'string') { return null; } if (p.batteryPct !== undefined && (typeof p.batteryPct !== 'number' || !Number.isFinite(p.batteryPct) || p.batteryPct < 0 || p.batteryPct > 100)) { return null; } if (p.powerSource !== undefined && !['ac', 'battery', 'unknown'].includes(String(p.powerSource))) { return null; } return { connectionId: p.connectionId, platform: p.platform as NodeStatusSetParams['platform'], appVersion: typeof p.appVersion === 'string' ? p.appVersion : undefined, deviceName: typeof p.deviceName === 'string' ? p.deviceName : undefined, statusText: typeof p.statusText === 'string' ? p.statusText : undefined, batteryPct: typeof p.batteryPct === 'number' ? p.batteryPct : undefined, powerSource: p.powerSource as NodeStatusSetParams['powerSource'] | undefined, }; } export function parseNodePushTokenSetParams(params: unknown): NodePushTokenSetParams | null { if (!params || typeof params !== 'object') { return null; } const p = params as Record; if (typeof p.connectionId !== 'string' || !p.connectionId.trim()) { return null; } if (p.provider !== 'apns') { return null; } if (typeof p.token !== 'string' || p.token.trim().length < 16) { return null; } if (p.topic !== undefined && typeof p.topic !== 'string') { return null; } if (p.environment !== undefined && p.environment !== 'sandbox' && p.environment !== 'production') { return null; } return { connectionId: p.connectionId, provider: 'apns', token: p.token.trim(), topic: typeof p.topic === 'string' ? p.topic.trim() : undefined, environment: p.environment as NodePushTokenSetParams['environment'] | undefined, }; } export function makeResponse(id: number, result: unknown): GatewayResponse { return { id, result }; } export function makeError(id: number, code: ErrorCode, message: string): GatewayError { return { id, error: { code, message } }; } export function makeEvent(id: number, event: EventType, data: unknown): GatewayEvent { return { id, event, data }; }