From 54830e840185f5761a3d7dc810a27650518cf779 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 18 Dec 2025 02:04:56 +0000 Subject: [PATCH] Bridge: persist advertised invoke commands --- src/infra/bridge/server.test.ts | 11 +++++++++++ src/infra/bridge/server.ts | 24 ++++++++++++++++++++++-- src/infra/node-pairing.ts | 12 +++++++++++- 3 files changed, 44 insertions(+), 3 deletions(-) diff --git a/src/infra/bridge/server.test.ts b/src/infra/bridge/server.test.ts index 232614416..2bfb70451 100644 --- a/src/infra/bridge/server.test.ts +++ b/src/infra/bridge/server.test.ts @@ -431,6 +431,7 @@ describe("node bridge server", () => { deviceFamily: "iPad", modelIdentifier: "iPad14,5", caps: ["canvas", "camera"], + commands: ["canvas.eval", "canvas.snapshot", "camera.snap"], }); // Approve the pending request from the gateway side. @@ -458,10 +459,20 @@ describe("node bridge server", () => { expect(node?.deviceFamily).toBe("iPad"); expect(node?.modelIdentifier).toBe("iPad14,5"); expect(node?.caps).toEqual(["canvas", "camera"]); + expect(node?.commands).toEqual([ + "canvas.eval", + "canvas.snapshot", + "camera.snap", + ]); const after = await listNodePairing(baseDir); const paired = after.paired.find((p) => p.nodeId === "n-caps"); expect(paired?.caps).toEqual(["canvas", "camera"]); + expect(paired?.commands).toEqual([ + "canvas.eval", + "canvas.snapshot", + "camera.snap", + ]); socket.destroy(); await server.close(); diff --git a/src/infra/bridge/server.ts b/src/infra/bridge/server.ts index 6db362105..304b8f621 100644 --- a/src/infra/bridge/server.ts +++ b/src/infra/bridge/server.ts @@ -21,6 +21,7 @@ type BridgeHelloFrame = { deviceFamily?: string; modelIdentifier?: string; caps?: string[]; + commands?: string[]; }; type BridgePairRequestFrame = { @@ -32,6 +33,7 @@ type BridgePairRequestFrame = { deviceFamily?: string; modelIdentifier?: string; caps?: string[]; + commands?: string[]; remoteAddress?: string; }; @@ -119,6 +121,7 @@ export type NodeBridgeClientInfo = { modelIdentifier?: string; remoteIp?: string; caps?: string[]; + commands?: string[]; }; export type NodeBridgeServerOpts = { @@ -263,8 +266,12 @@ export async function startNodeBridgeServer( platform?: string; deviceFamily?: string; }): string[] | undefined => { - const platform = String(frame.platform ?? "").trim().toLowerCase(); - const family = String(frame.deviceFamily ?? "").trim().toLowerCase(); + const platform = String(frame.platform ?? "") + .trim() + .toLowerCase(); + const family = String(frame.deviceFamily ?? "") + .trim() + .toLowerCase(); if (platform.includes("ios") || platform.includes("ipados")) { return ["canvas", "camera"]; } @@ -287,6 +294,11 @@ export async function startNodeBridgeServer( verified.node.caps ?? inferCaps(hello); + const commands = + Array.isArray(hello.commands) && hello.commands.length > 0 + ? hello.commands.map((c) => String(c)).filter(Boolean) + : verified.node.commands; + isAuthenticated = true; const existing = connections.get(nodeId); if (existing?.socket && existing.socket !== socket) { @@ -304,6 +316,7 @@ export async function startNodeBridgeServer( deviceFamily: verified.node.deviceFamily ?? hello.deviceFamily, modelIdentifier: verified.node.modelIdentifier ?? hello.modelIdentifier, caps, + commands, remoteIp: remoteAddress, }; await updatePairedNodeMetadata( @@ -316,6 +329,7 @@ export async function startNodeBridgeServer( modelIdentifier: nodeInfo.modelIdentifier, remoteIp: nodeInfo.remoteIp, caps: nodeInfo.caps, + commands: nodeInfo.commands, }, opts.pairingBaseDir, ); @@ -378,6 +392,9 @@ export async function startNodeBridgeServer( caps: Array.isArray(req.caps) ? req.caps.map((c) => String(c)).filter(Boolean) : undefined, + commands: Array.isArray(req.commands) + ? req.commands.map((c) => String(c)).filter(Boolean) + : undefined, remoteIp: remoteAddress, }, opts.pairingBaseDir, @@ -411,6 +428,9 @@ export async function startNodeBridgeServer( caps: Array.isArray(req.caps) ? req.caps.map((c) => String(c)).filter(Boolean) : undefined, + commands: Array.isArray(req.commands) + ? req.commands.map((c) => String(c)).filter(Boolean) + : undefined, remoteIp: remoteAddress, }; connections.set(nodeId, { socket, nodeInfo, invokeWaiters }); diff --git a/src/infra/node-pairing.ts b/src/infra/node-pairing.ts index 00596888a..1b16c710a 100644 --- a/src/infra/node-pairing.ts +++ b/src/infra/node-pairing.ts @@ -12,6 +12,7 @@ export type NodePairingPendingRequest = { deviceFamily?: string; modelIdentifier?: string; caps?: string[]; + commands?: string[]; remoteIp?: string; isRepair?: boolean; ts: number; @@ -26,6 +27,7 @@ export type NodePairingPairedNode = { deviceFamily?: string; modelIdentifier?: string; caps?: string[]; + commands?: string[]; remoteIp?: string; createdAtMs: number; approvedAtMs: number; @@ -181,6 +183,7 @@ export async function requestNodePairing( deviceFamily: req.deviceFamily, modelIdentifier: req.modelIdentifier, caps: req.caps, + commands: req.commands, remoteIp: req.remoteIp, isRepair, ts: Date.now(), @@ -211,6 +214,7 @@ export async function approveNodePairing( deviceFamily: pending.deviceFamily, modelIdentifier: pending.modelIdentifier, caps: pending.caps, + commands: pending.commands, remoteIp: pending.remoteIp, createdAtMs: existing?.createdAtMs ?? now, approvedAtMs: now, @@ -251,7 +255,12 @@ export async function verifyNodeToken( export async function updatePairedNodeMetadata( nodeId: string, - patch: Partial>, + patch: Partial< + Omit< + NodePairingPairedNode, + "nodeId" | "token" | "createdAtMs" | "approvedAtMs" + > + >, baseDir?: string, ) { await withLock(async () => { @@ -269,6 +278,7 @@ export async function updatePairedNodeMetadata( modelIdentifier: patch.modelIdentifier ?? existing.modelIdentifier, remoteIp: patch.remoteIp ?? existing.remoteIp, caps: patch.caps ?? existing.caps, + commands: patch.commands ?? existing.commands, }; state.pairedByNodeId[normalized] = next;