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 <noreply@anthropic.com>
This commit is contained in:
parent
dd701a2472
commit
c581183839
3 changed files with 22 additions and 13 deletions
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<boolean>) | null = null;
|
||||
private _onConfirmDevice: ((deviceId: string, agentId: string, meta?: DeviceMeta) => Promise<boolean>) | 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<boolean>) | null): void {
|
||||
setConfirmHandler(handler: ((deviceId: string, agentId: string, meta?: DeviceMeta) => Promise<boolean>) | null): void {
|
||||
this._onConfirmDevice = handler;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<boolean>;
|
||||
onConfirmDevice: (deviceId: string, agentId: string, meta?: DeviceMeta) => Promise<boolean>;
|
||||
}
|
||||
|
||||
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 };
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue