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 }; }; }