Bridge: persist advertised invoke commands

main
Peter Steinberger 2025-12-18 02:04:56 +00:00
parent ce1a8d70d9
commit 54830e8401
3 changed files with 44 additions and 3 deletions

View File

@ -431,6 +431,7 @@ describe("node bridge server", () => {
deviceFamily: "iPad", deviceFamily: "iPad",
modelIdentifier: "iPad14,5", modelIdentifier: "iPad14,5",
caps: ["canvas", "camera"], caps: ["canvas", "camera"],
commands: ["canvas.eval", "canvas.snapshot", "camera.snap"],
}); });
// Approve the pending request from the gateway side. // Approve the pending request from the gateway side.
@ -458,10 +459,20 @@ describe("node bridge server", () => {
expect(node?.deviceFamily).toBe("iPad"); expect(node?.deviceFamily).toBe("iPad");
expect(node?.modelIdentifier).toBe("iPad14,5"); expect(node?.modelIdentifier).toBe("iPad14,5");
expect(node?.caps).toEqual(["canvas", "camera"]); expect(node?.caps).toEqual(["canvas", "camera"]);
expect(node?.commands).toEqual([
"canvas.eval",
"canvas.snapshot",
"camera.snap",
]);
const after = await listNodePairing(baseDir); const after = await listNodePairing(baseDir);
const paired = after.paired.find((p) => p.nodeId === "n-caps"); const paired = after.paired.find((p) => p.nodeId === "n-caps");
expect(paired?.caps).toEqual(["canvas", "camera"]); expect(paired?.caps).toEqual(["canvas", "camera"]);
expect(paired?.commands).toEqual([
"canvas.eval",
"canvas.snapshot",
"camera.snap",
]);
socket.destroy(); socket.destroy();
await server.close(); await server.close();

View File

@ -21,6 +21,7 @@ type BridgeHelloFrame = {
deviceFamily?: string; deviceFamily?: string;
modelIdentifier?: string; modelIdentifier?: string;
caps?: string[]; caps?: string[];
commands?: string[];
}; };
type BridgePairRequestFrame = { type BridgePairRequestFrame = {
@ -32,6 +33,7 @@ type BridgePairRequestFrame = {
deviceFamily?: string; deviceFamily?: string;
modelIdentifier?: string; modelIdentifier?: string;
caps?: string[]; caps?: string[];
commands?: string[];
remoteAddress?: string; remoteAddress?: string;
}; };
@ -119,6 +121,7 @@ export type NodeBridgeClientInfo = {
modelIdentifier?: string; modelIdentifier?: string;
remoteIp?: string; remoteIp?: string;
caps?: string[]; caps?: string[];
commands?: string[];
}; };
export type NodeBridgeServerOpts = { export type NodeBridgeServerOpts = {
@ -263,8 +266,12 @@ export async function startNodeBridgeServer(
platform?: string; platform?: string;
deviceFamily?: string; deviceFamily?: string;
}): string[] | undefined => { }): string[] | undefined => {
const platform = String(frame.platform ?? "").trim().toLowerCase(); const platform = String(frame.platform ?? "")
const family = String(frame.deviceFamily ?? "").trim().toLowerCase(); .trim()
.toLowerCase();
const family = String(frame.deviceFamily ?? "")
.trim()
.toLowerCase();
if (platform.includes("ios") || platform.includes("ipados")) { if (platform.includes("ios") || platform.includes("ipados")) {
return ["canvas", "camera"]; return ["canvas", "camera"];
} }
@ -287,6 +294,11 @@ export async function startNodeBridgeServer(
verified.node.caps ?? verified.node.caps ??
inferCaps(hello); 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; isAuthenticated = true;
const existing = connections.get(nodeId); const existing = connections.get(nodeId);
if (existing?.socket && existing.socket !== socket) { if (existing?.socket && existing.socket !== socket) {
@ -304,6 +316,7 @@ export async function startNodeBridgeServer(
deviceFamily: verified.node.deviceFamily ?? hello.deviceFamily, deviceFamily: verified.node.deviceFamily ?? hello.deviceFamily,
modelIdentifier: verified.node.modelIdentifier ?? hello.modelIdentifier, modelIdentifier: verified.node.modelIdentifier ?? hello.modelIdentifier,
caps, caps,
commands,
remoteIp: remoteAddress, remoteIp: remoteAddress,
}; };
await updatePairedNodeMetadata( await updatePairedNodeMetadata(
@ -316,6 +329,7 @@ export async function startNodeBridgeServer(
modelIdentifier: nodeInfo.modelIdentifier, modelIdentifier: nodeInfo.modelIdentifier,
remoteIp: nodeInfo.remoteIp, remoteIp: nodeInfo.remoteIp,
caps: nodeInfo.caps, caps: nodeInfo.caps,
commands: nodeInfo.commands,
}, },
opts.pairingBaseDir, opts.pairingBaseDir,
); );
@ -378,6 +392,9 @@ export async function startNodeBridgeServer(
caps: Array.isArray(req.caps) caps: Array.isArray(req.caps)
? req.caps.map((c) => String(c)).filter(Boolean) ? req.caps.map((c) => String(c)).filter(Boolean)
: undefined, : undefined,
commands: Array.isArray(req.commands)
? req.commands.map((c) => String(c)).filter(Boolean)
: undefined,
remoteIp: remoteAddress, remoteIp: remoteAddress,
}, },
opts.pairingBaseDir, opts.pairingBaseDir,
@ -411,6 +428,9 @@ export async function startNodeBridgeServer(
caps: Array.isArray(req.caps) caps: Array.isArray(req.caps)
? req.caps.map((c) => String(c)).filter(Boolean) ? req.caps.map((c) => String(c)).filter(Boolean)
: undefined, : undefined,
commands: Array.isArray(req.commands)
? req.commands.map((c) => String(c)).filter(Boolean)
: undefined,
remoteIp: remoteAddress, remoteIp: remoteAddress,
}; };
connections.set(nodeId, { socket, nodeInfo, invokeWaiters }); connections.set(nodeId, { socket, nodeInfo, invokeWaiters });

View File

@ -12,6 +12,7 @@ export type NodePairingPendingRequest = {
deviceFamily?: string; deviceFamily?: string;
modelIdentifier?: string; modelIdentifier?: string;
caps?: string[]; caps?: string[];
commands?: string[];
remoteIp?: string; remoteIp?: string;
isRepair?: boolean; isRepair?: boolean;
ts: number; ts: number;
@ -26,6 +27,7 @@ export type NodePairingPairedNode = {
deviceFamily?: string; deviceFamily?: string;
modelIdentifier?: string; modelIdentifier?: string;
caps?: string[]; caps?: string[];
commands?: string[];
remoteIp?: string; remoteIp?: string;
createdAtMs: number; createdAtMs: number;
approvedAtMs: number; approvedAtMs: number;
@ -181,6 +183,7 @@ export async function requestNodePairing(
deviceFamily: req.deviceFamily, deviceFamily: req.deviceFamily,
modelIdentifier: req.modelIdentifier, modelIdentifier: req.modelIdentifier,
caps: req.caps, caps: req.caps,
commands: req.commands,
remoteIp: req.remoteIp, remoteIp: req.remoteIp,
isRepair, isRepair,
ts: Date.now(), ts: Date.now(),
@ -211,6 +214,7 @@ export async function approveNodePairing(
deviceFamily: pending.deviceFamily, deviceFamily: pending.deviceFamily,
modelIdentifier: pending.modelIdentifier, modelIdentifier: pending.modelIdentifier,
caps: pending.caps, caps: pending.caps,
commands: pending.commands,
remoteIp: pending.remoteIp, remoteIp: pending.remoteIp,
createdAtMs: existing?.createdAtMs ?? now, createdAtMs: existing?.createdAtMs ?? now,
approvedAtMs: now, approvedAtMs: now,
@ -251,7 +255,12 @@ export async function verifyNodeToken(
export async function updatePairedNodeMetadata( export async function updatePairedNodeMetadata(
nodeId: string, nodeId: string,
patch: Partial<Omit<NodePairingPairedNode, "nodeId" | "token" | "createdAtMs" | "approvedAtMs">>, patch: Partial<
Omit<
NodePairingPairedNode,
"nodeId" | "token" | "createdAtMs" | "approvedAtMs"
>
>,
baseDir?: string, baseDir?: string,
) { ) {
await withLock(async () => { await withLock(async () => {
@ -269,6 +278,7 @@ export async function updatePairedNodeMetadata(
modelIdentifier: patch.modelIdentifier ?? existing.modelIdentifier, modelIdentifier: patch.modelIdentifier ?? existing.modelIdentifier,
remoteIp: patch.remoteIp ?? existing.remoteIp, remoteIp: patch.remoteIp ?? existing.remoteIp,
caps: patch.caps ?? existing.caps, caps: patch.caps ?? existing.caps,
commands: patch.commands ?? existing.commands,
}; };
state.pairedByNodeId[normalized] = next; state.pairedByNodeId[normalized] = next;