gateway: persist and surface node capabilities
This commit is contained in:
@@ -164,6 +164,9 @@ final class BridgeConnectionController {
|
|||||||
: UserDefaults.standard.bool(forKey: "camera.enabled")
|
: UserDefaults.standard.bool(forKey: "camera.enabled")
|
||||||
if cameraEnabled { caps.append("camera") }
|
if cameraEnabled { caps.append("camera") }
|
||||||
|
|
||||||
|
let voiceWakeEnabled = UserDefaults.standard.bool(forKey: VoiceWakePreferences.enabledKey)
|
||||||
|
if voiceWakeEnabled { caps.append("voiceWake") }
|
||||||
|
|
||||||
return caps
|
return caps
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2975,7 +2975,7 @@ export async function startGatewayServer(port = 18789): Promise<GatewayServer> {
|
|||||||
deviceFamily: live?.deviceFamily ?? n.deviceFamily,
|
deviceFamily: live?.deviceFamily ?? n.deviceFamily,
|
||||||
modelIdentifier: live?.modelIdentifier ?? n.modelIdentifier,
|
modelIdentifier: live?.modelIdentifier ?? n.modelIdentifier,
|
||||||
remoteIp: live?.remoteIp ?? n.remoteIp,
|
remoteIp: live?.remoteIp ?? n.remoteIp,
|
||||||
caps: live?.caps,
|
caps: live?.caps ?? n.caps,
|
||||||
connected: Boolean(live),
|
connected: Boolean(live),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -459,6 +459,10 @@ describe("node bridge server", () => {
|
|||||||
expect(node?.modelIdentifier).toBe("iPad14,5");
|
expect(node?.modelIdentifier).toBe("iPad14,5");
|
||||||
expect(node?.caps).toEqual(["canvas", "camera"]);
|
expect(node?.caps).toEqual(["canvas", "camera"]);
|
||||||
|
|
||||||
|
const after = await listNodePairing(baseDir);
|
||||||
|
const paired = after.paired.find((p) => p.nodeId === "n-caps");
|
||||||
|
expect(paired?.caps).toEqual(["canvas", "camera"]);
|
||||||
|
|
||||||
socket.destroy();
|
socket.destroy();
|
||||||
await server.close();
|
await server.close();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
listNodePairing,
|
listNodePairing,
|
||||||
type NodePairingPendingRequest,
|
type NodePairingPendingRequest,
|
||||||
requestNodePairing,
|
requestNodePairing,
|
||||||
|
updatePairedNodeMetadata,
|
||||||
verifyNodeToken,
|
verifyNodeToken,
|
||||||
} from "../node-pairing.js";
|
} from "../node-pairing.js";
|
||||||
|
|
||||||
@@ -258,6 +259,34 @@ export async function startNodeBridgeServer(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const inferCaps = (frame: {
|
||||||
|
platform?: string;
|
||||||
|
deviceFamily?: string;
|
||||||
|
}): string[] | undefined => {
|
||||||
|
const platform = String(frame.platform ?? "").trim().toLowerCase();
|
||||||
|
const family = String(frame.deviceFamily ?? "").trim().toLowerCase();
|
||||||
|
if (platform.includes("ios") || platform.includes("ipados")) {
|
||||||
|
return ["canvas", "camera"];
|
||||||
|
}
|
||||||
|
if (platform.includes("android")) {
|
||||||
|
return ["canvas", "camera"];
|
||||||
|
}
|
||||||
|
if (family === "ipad" || family === "iphone" || family === "ios") {
|
||||||
|
return ["canvas", "camera"];
|
||||||
|
}
|
||||||
|
if (family === "android") {
|
||||||
|
return ["canvas", "camera"];
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const caps =
|
||||||
|
(Array.isArray(hello.caps)
|
||||||
|
? hello.caps.map((c) => String(c)).filter(Boolean)
|
||||||
|
: undefined) ??
|
||||||
|
verified.node.caps ??
|
||||||
|
inferCaps(hello);
|
||||||
|
|
||||||
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) {
|
||||||
@@ -274,11 +303,22 @@ export async function startNodeBridgeServer(
|
|||||||
version: verified.node.version ?? hello.version,
|
version: verified.node.version ?? hello.version,
|
||||||
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: Array.isArray(hello.caps)
|
caps,
|
||||||
? hello.caps.map((c) => String(c)).filter(Boolean)
|
|
||||||
: undefined,
|
|
||||||
remoteIp: remoteAddress,
|
remoteIp: remoteAddress,
|
||||||
};
|
};
|
||||||
|
await updatePairedNodeMetadata(
|
||||||
|
nodeId,
|
||||||
|
{
|
||||||
|
displayName: nodeInfo.displayName,
|
||||||
|
platform: nodeInfo.platform,
|
||||||
|
version: nodeInfo.version,
|
||||||
|
deviceFamily: nodeInfo.deviceFamily,
|
||||||
|
modelIdentifier: nodeInfo.modelIdentifier,
|
||||||
|
remoteIp: nodeInfo.remoteIp,
|
||||||
|
caps: nodeInfo.caps,
|
||||||
|
},
|
||||||
|
opts.pairingBaseDir,
|
||||||
|
);
|
||||||
connections.set(nodeId, { socket, nodeInfo, invokeWaiters });
|
connections.set(nodeId, { socket, nodeInfo, invokeWaiters });
|
||||||
send({ type: "hello-ok", serverName } satisfies BridgeHelloOkFrame);
|
send({ type: "hello-ok", serverName } satisfies BridgeHelloOkFrame);
|
||||||
await opts.onAuthenticated?.(nodeInfo);
|
await opts.onAuthenticated?.(nodeInfo);
|
||||||
@@ -335,6 +375,9 @@ export async function startNodeBridgeServer(
|
|||||||
version: req.version,
|
version: req.version,
|
||||||
deviceFamily: req.deviceFamily,
|
deviceFamily: req.deviceFamily,
|
||||||
modelIdentifier: req.modelIdentifier,
|
modelIdentifier: req.modelIdentifier,
|
||||||
|
caps: Array.isArray(req.caps)
|
||||||
|
? req.caps.map((c) => String(c)).filter(Boolean)
|
||||||
|
: undefined,
|
||||||
remoteIp: remoteAddress,
|
remoteIp: remoteAddress,
|
||||||
},
|
},
|
||||||
opts.pairingBaseDir,
|
opts.pairingBaseDir,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export type NodePairingPendingRequest = {
|
|||||||
version?: string;
|
version?: string;
|
||||||
deviceFamily?: string;
|
deviceFamily?: string;
|
||||||
modelIdentifier?: string;
|
modelIdentifier?: string;
|
||||||
|
caps?: string[];
|
||||||
remoteIp?: string;
|
remoteIp?: string;
|
||||||
isRepair?: boolean;
|
isRepair?: boolean;
|
||||||
ts: number;
|
ts: number;
|
||||||
@@ -24,6 +25,7 @@ export type NodePairingPairedNode = {
|
|||||||
version?: string;
|
version?: string;
|
||||||
deviceFamily?: string;
|
deviceFamily?: string;
|
||||||
modelIdentifier?: string;
|
modelIdentifier?: string;
|
||||||
|
caps?: string[];
|
||||||
remoteIp?: string;
|
remoteIp?: string;
|
||||||
createdAtMs: number;
|
createdAtMs: number;
|
||||||
approvedAtMs: number;
|
approvedAtMs: number;
|
||||||
@@ -178,6 +180,7 @@ export async function requestNodePairing(
|
|||||||
version: req.version,
|
version: req.version,
|
||||||
deviceFamily: req.deviceFamily,
|
deviceFamily: req.deviceFamily,
|
||||||
modelIdentifier: req.modelIdentifier,
|
modelIdentifier: req.modelIdentifier,
|
||||||
|
caps: req.caps,
|
||||||
remoteIp: req.remoteIp,
|
remoteIp: req.remoteIp,
|
||||||
isRepair,
|
isRepair,
|
||||||
ts: Date.now(),
|
ts: Date.now(),
|
||||||
@@ -207,6 +210,7 @@ export async function approveNodePairing(
|
|||||||
version: pending.version,
|
version: pending.version,
|
||||||
deviceFamily: pending.deviceFamily,
|
deviceFamily: pending.deviceFamily,
|
||||||
modelIdentifier: pending.modelIdentifier,
|
modelIdentifier: pending.modelIdentifier,
|
||||||
|
caps: pending.caps,
|
||||||
remoteIp: pending.remoteIp,
|
remoteIp: pending.remoteIp,
|
||||||
createdAtMs: existing?.createdAtMs ?? now,
|
createdAtMs: existing?.createdAtMs ?? now,
|
||||||
approvedAtMs: now,
|
approvedAtMs: now,
|
||||||
@@ -244,3 +248,30 @@ export async function verifyNodeToken(
|
|||||||
if (!node) return { ok: false };
|
if (!node) return { ok: false };
|
||||||
return node.token === token ? { ok: true, node } : { ok: false };
|
return node.token === token ? { ok: true, node } : { ok: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function updatePairedNodeMetadata(
|
||||||
|
nodeId: string,
|
||||||
|
patch: Partial<Omit<NodePairingPairedNode, "nodeId" | "token" | "createdAtMs" | "approvedAtMs">>,
|
||||||
|
baseDir?: string,
|
||||||
|
) {
|
||||||
|
await withLock(async () => {
|
||||||
|
const state = await loadState(baseDir);
|
||||||
|
const normalized = normalizeNodeId(nodeId);
|
||||||
|
const existing = state.pairedByNodeId[normalized];
|
||||||
|
if (!existing) return;
|
||||||
|
|
||||||
|
const next: NodePairingPairedNode = {
|
||||||
|
...existing,
|
||||||
|
displayName: patch.displayName ?? existing.displayName,
|
||||||
|
platform: patch.platform ?? existing.platform,
|
||||||
|
version: patch.version ?? existing.version,
|
||||||
|
deviceFamily: patch.deviceFamily ?? existing.deviceFamily,
|
||||||
|
modelIdentifier: patch.modelIdentifier ?? existing.modelIdentifier,
|
||||||
|
remoteIp: patch.remoteIp ?? existing.remoteIp,
|
||||||
|
caps: patch.caps ?? existing.caps,
|
||||||
|
};
|
||||||
|
|
||||||
|
state.pairedByNodeId[normalized] = next;
|
||||||
|
await persistState(state, baseDir);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user