From c581183839cf4b12de89c574a32ee35aefb640cd Mon Sep 17 00:00:00 2001 From: yushen Date: Wed, 4 Feb 2026 13:43:48 +0800 Subject: [PATCH] feat(hub): store device metadata in whitelist and pass to confirm handler Extend DeviceEntry with optional DeviceMeta field. Verify handler extracts meta from params and passes it through to onConfirmDevice callback and deviceStore.allowDevice for persistence. Co-Authored-By: Claude Opus 4.5 --- src/hub/device-store.ts | 11 +++++++++-- src/hub/hub.ts | 10 +++++----- src/hub/rpc/handlers/verify.ts | 14 ++++++++------ 3 files changed, 22 insertions(+), 13 deletions(-) diff --git a/src/hub/device-store.ts b/src/hub/device-store.ts index e015b15c..9711808d 100644 --- a/src/hub/device-store.ts +++ b/src/hub/device-store.ts @@ -10,10 +10,17 @@ interface TokenEntry { expiresAt: number; } +export interface DeviceMeta { + userAgent?: string; + platform?: string; + language?: string; +} + export interface DeviceEntry { deviceId: string; agentId: string; addedAt: number; + meta?: DeviceMeta; } // ============ Persistence ============ @@ -76,8 +83,8 @@ export class DeviceStore { // ---- Device whitelist ---- /** Add a device to the whitelist (called after token verification + user confirmation) */ - allowDevice(deviceId: string, agentId: string): void { - const entry: DeviceEntry = { deviceId, agentId, addedAt: Date.now() }; + allowDevice(deviceId: string, agentId: string, meta?: DeviceMeta): void { + const entry: DeviceEntry = { deviceId, agentId, addedAt: Date.now(), meta }; this.allowedDevices.set(deviceId, entry); this.persist(); } diff --git a/src/hub/hub.ts b/src/hub/hub.ts index f18dbc48..42da14c4 100644 --- a/src/hub/hub.ts +++ b/src/hub/hub.ts @@ -21,7 +21,7 @@ import { createListAgentsHandler } from "./rpc/handlers/list-agents.js"; import { createCreateAgentHandler } from "./rpc/handlers/create-agent.js"; import { createDeleteAgentHandler } from "./rpc/handlers/delete-agent.js"; import { createUpdateGatewayHandler } from "./rpc/handlers/update-gateway.js"; -import { DeviceStore } from "./device-store.js"; +import { DeviceStore, type DeviceMeta } from "./device-store.js"; import { createVerifyHandler } from "./rpc/handlers/verify.js"; export class Hub { @@ -32,7 +32,7 @@ export class Hub { private readonly rpc: RpcDispatcher; private client: GatewayClient; readonly deviceStore: DeviceStore; - private _onConfirmDevice: ((deviceId: string, agentId: string) => Promise) | null = null; + private _onConfirmDevice: ((deviceId: string, agentId: string, meta?: DeviceMeta) => Promise) | null = null; url: string; readonly path: string; readonly hubId: string; @@ -52,12 +52,12 @@ export class Hub { this.rpc.register("verify", createVerifyHandler({ hubId: this.hubId, deviceStore: this.deviceStore, - onConfirmDevice: (deviceId, agentId) => { + onConfirmDevice: (deviceId, agentId, meta) => { if (!this._onConfirmDevice) { // No UI confirm handler registered (CLI mode etc.) — auto-approve return Promise.resolve(true); } - return this._onConfirmDevice(deviceId, agentId); + return this._onConfirmDevice(deviceId, agentId, meta); }, })); this.rpc.register("getAgentMessages", createGetAgentMessagesHandler()); @@ -166,7 +166,7 @@ export class Hub { } /** Register a confirmation handler for new device connections (called by Desktop UI) */ - setConfirmHandler(handler: ((deviceId: string, agentId: string) => Promise) | null): void { + setConfirmHandler(handler: ((deviceId: string, agentId: string, meta?: DeviceMeta) => Promise) | null): void { this._onConfirmDevice = handler; } diff --git a/src/hub/rpc/handlers/verify.ts b/src/hub/rpc/handlers/verify.ts index bef35ca8..53b98735 100644 --- a/src/hub/rpc/handlers/verify.ts +++ b/src/hub/rpc/handlers/verify.ts @@ -1,20 +1,23 @@ import type { RpcHandler } from "../dispatcher.js"; import { RpcError } from "../dispatcher.js"; -import type { DeviceStore } from "../../device-store.js"; +import type { DeviceStore, DeviceMeta } from "../../device-store.js"; interface VerifyContext { hubId: string; deviceStore: DeviceStore; /** Called for first-time connections. Returns true if user approves, false if rejected. */ - onConfirmDevice: (deviceId: string, agentId: string) => Promise; + onConfirmDevice: (deviceId: string, agentId: string, meta?: DeviceMeta) => Promise; } interface VerifyParams { token?: string; + meta?: DeviceMeta; } export function createVerifyHandler(ctx: VerifyContext): RpcHandler { return async (params: unknown, from: string) => { + const { token, meta } = (params ?? {}) as VerifyParams; + // 1. Already in whitelist → pass through (reconnection, no confirmation needed) const allowed = ctx.deviceStore.isAllowed(from); if (allowed) { @@ -22,7 +25,6 @@ export function createVerifyHandler(ctx: VerifyContext): RpcHandler { } // 2. Validate token - const { token } = (params ?? {}) as VerifyParams; if (!token) { throw new RpcError("UNAUTHORIZED", "Device not authorized"); } @@ -33,13 +35,13 @@ export function createVerifyHandler(ctx: VerifyContext): RpcHandler { } // 3. Token valid → await Desktop user confirmation - const confirmed = await ctx.onConfirmDevice(from, result.agentId); + const confirmed = await ctx.onConfirmDevice(from, result.agentId, meta); if (!confirmed) { throw new RpcError("REJECTED", "Connection rejected by user"); } - // 4. User confirmed → add to whitelist - ctx.deviceStore.allowDevice(from, result.agentId); + // 4. User confirmed → add to whitelist (with device metadata) + ctx.deviceStore.allowDevice(from, result.agentId, meta); return { hubId: ctx.hubId, agentId: result.agentId }; }; }