feat: complete DM pairing codes with channel adapters, gateway handlers, and TUI command (Tier 4 feature 4)
This commit is contained in:
@@ -5,6 +5,8 @@ import { createSessionHandlers } from './sessions.js';
|
||||
import { createToolHandlers } from './tools.js';
|
||||
import { createAgentHandlers } from './agent.js';
|
||||
import { createConfigHandlers, redactConfig } from './config.js';
|
||||
import { createPairingHandlers } from './pairing.js';
|
||||
import { PairingManager } from '../../channels/pairing.js';
|
||||
import { LaneQueue } from '../lane-queue.js';
|
||||
import { ErrorCode } from '../protocol.js';
|
||||
import type { GatewayRequest, GatewayResponse, GatewayError, GatewayEvent, OutboundMessage } from '../protocol.js';
|
||||
@@ -747,3 +749,75 @@ describe('redactConfig – comprehensive credential redaction', () => {
|
||||
expect(config.server.token).toBe('bearer-secret');
|
||||
});
|
||||
});
|
||||
|
||||
describe('pairing handlers', () => {
|
||||
let pm: PairingManager;
|
||||
let handlers: ReturnType<typeof createPairingHandlers>;
|
||||
|
||||
beforeEach(() => {
|
||||
pm = new PairingManager({ enabled: true, codeTtl: 300_000, codeLength: 6 });
|
||||
handlers = createPairingHandlers({ pairingManager: pm });
|
||||
});
|
||||
|
||||
it('pairing.generate returns a code and expiry', async () => {
|
||||
const req: GatewayRequest = { id: 1, method: 'pairing.generate', params: { label: 'for alice' } };
|
||||
const result = await handlers['pairing.generate'](req) as GatewayResponse;
|
||||
|
||||
const r = result.result as { code: string; expiresAt: number };
|
||||
expect(r.code).toHaveLength(6);
|
||||
expect(r.expiresAt).toBeGreaterThan(Date.now());
|
||||
});
|
||||
|
||||
it('pairing.generate works without label', async () => {
|
||||
const req: GatewayRequest = { id: 2, method: 'pairing.generate' };
|
||||
const result = await handlers['pairing.generate'](req) as GatewayResponse;
|
||||
|
||||
const r = result.result as { code: string; expiresAt: number };
|
||||
expect(r.code).toHaveLength(6);
|
||||
});
|
||||
|
||||
it('pairing.list returns pending codes and approved senders', async () => {
|
||||
// Generate a code first
|
||||
pm.generateCode('test');
|
||||
// Approve a sender
|
||||
const code = pm.generateCode('for bob');
|
||||
pm.validateCode('telegram', '12345', code);
|
||||
|
||||
const req: GatewayRequest = { id: 3, method: 'pairing.list' };
|
||||
const result = await handlers['pairing.list'](req) as GatewayResponse;
|
||||
|
||||
const r = result.result as { pending: unknown[]; approved: unknown[] };
|
||||
expect(r.pending).toHaveLength(1); // one code remaining (the other was consumed)
|
||||
expect(r.approved).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('pairing.revoke removes an approved sender', async () => {
|
||||
// Approve a sender
|
||||
const code = pm.generateCode();
|
||||
pm.validateCode('discord', 'chan-1', code);
|
||||
expect(pm.isApproved('discord', 'chan-1')).toBe(true);
|
||||
|
||||
const req: GatewayRequest = { id: 4, method: 'pairing.revoke', params: { channel: 'discord', senderId: 'chan-1' } };
|
||||
const result = await handlers['pairing.revoke'](req) as GatewayResponse;
|
||||
|
||||
const r = result.result as { revoked: boolean };
|
||||
expect(r.revoked).toBe(true);
|
||||
expect(pm.isApproved('discord', 'chan-1')).toBe(false);
|
||||
});
|
||||
|
||||
it('pairing.revoke returns false for unknown sender', async () => {
|
||||
const req: GatewayRequest = { id: 5, method: 'pairing.revoke', params: { channel: 'telegram', senderId: 'unknown' } };
|
||||
const result = await handlers['pairing.revoke'](req) as GatewayResponse;
|
||||
|
||||
const r = result.result as { revoked: boolean };
|
||||
expect(r.revoked).toBe(false);
|
||||
});
|
||||
|
||||
it('pairing.revoke requires channel and senderId', async () => {
|
||||
const req: GatewayRequest = { id: 6, method: 'pairing.revoke', params: {} };
|
||||
const result = await handlers['pairing.revoke'](req) as GatewayError;
|
||||
|
||||
expect(result.error.code).toBe(ErrorCode.InvalidRequest);
|
||||
expect(result.error.message).toContain('channel');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,3 +8,5 @@ export { createAgentHandlers } from './agent.js';
|
||||
export type { AgentHandlerDeps } from './agent.js';
|
||||
export { createConfigHandlers } from './config.js';
|
||||
export type { ConfigHandlerDeps } from './config.js';
|
||||
export { createPairingHandlers } from './pairing.js';
|
||||
export type { PairingHandlerDeps } from './pairing.js';
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import type { GatewayRequest, OutboundMessage } from '../protocol.js';
|
||||
import { makeResponse, makeError, ErrorCode } from '../protocol.js';
|
||||
import type { PairingManager } from '../../channels/pairing.js';
|
||||
|
||||
export interface PairingHandlerDeps {
|
||||
pairingManager: PairingManager;
|
||||
}
|
||||
|
||||
export function createPairingHandlers(deps: PairingHandlerDeps) {
|
||||
return {
|
||||
'pairing.generate': async (request: GatewayRequest): Promise<OutboundMessage> => {
|
||||
const label = request.params?.label as string | undefined;
|
||||
const code = deps.pairingManager.generateCode(label);
|
||||
const pending = deps.pairingManager.listPendingCodes();
|
||||
const entry = pending.find(p => p.code === code);
|
||||
|
||||
return makeResponse(request.id, {
|
||||
code,
|
||||
expiresAt: entry?.expiresAt ?? null,
|
||||
});
|
||||
},
|
||||
|
||||
'pairing.list': async (request: GatewayRequest): Promise<OutboundMessage> => {
|
||||
return makeResponse(request.id, {
|
||||
pending: deps.pairingManager.listPendingCodes(),
|
||||
approved: deps.pairingManager.listApproved(),
|
||||
});
|
||||
},
|
||||
|
||||
'pairing.revoke': async (request: GatewayRequest): Promise<OutboundMessage> => {
|
||||
const channel = request.params?.channel as string | undefined;
|
||||
const senderId = request.params?.senderId as string | undefined;
|
||||
|
||||
if (!channel || !senderId) {
|
||||
return makeError(request.id, ErrorCode.InvalidRequest, 'Missing required params: channel, senderId');
|
||||
}
|
||||
|
||||
const revoked = deps.pairingManager.revokeApproval(channel, senderId);
|
||||
return makeResponse(request.id, { revoked });
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
createToolHandlers,
|
||||
createAgentHandlers,
|
||||
createConfigHandlers,
|
||||
createPairingHandlers,
|
||||
} from './handlers/index.js';
|
||||
import type { TokenUsageEntry } from './handlers/system.js';
|
||||
import type { SessionManager } from '../session/manager.js';
|
||||
@@ -29,6 +30,7 @@ import type { ToolRegistry } from '../tools/registry.js';
|
||||
import type { ToolExecutor } from '../tools/executor.js';
|
||||
import type { WebhookHandler } from '../automation/webhooks.js';
|
||||
import type { GmailWatcher } from '../automation/gmail.js';
|
||||
import type { PairingManager } from '../channels/pairing.js';
|
||||
|
||||
export interface GatewayServerConfig {
|
||||
port: number;
|
||||
@@ -55,6 +57,8 @@ export interface GatewayServerConfig {
|
||||
gmailHandler?: GmailWatcher;
|
||||
/** Optional callback to retrieve per-session token usage data for the dashboard. */
|
||||
getTokenUsage?: () => TokenUsageEntry[];
|
||||
/** Optional pairing manager for DM pairing code management via gateway. */
|
||||
pairingManager?: PairingManager;
|
||||
}
|
||||
|
||||
export class GatewayServer {
|
||||
@@ -124,6 +128,14 @@ export class GatewayServer {
|
||||
}
|
||||
}
|
||||
|
||||
// Pairing handlers (only if pairing manager is provided)
|
||||
if (this.config.pairingManager) {
|
||||
const pairingHandlers = createPairingHandlers({ pairingManager: this.config.pairingManager });
|
||||
for (const [method, handler] of Object.entries(pairingHandlers)) {
|
||||
this.router.register(method, handler);
|
||||
}
|
||||
}
|
||||
|
||||
// Register all methods
|
||||
for (const [method, handler] of Object.entries(systemHandlers)) {
|
||||
this.router.register(method, handler);
|
||||
|
||||
Reference in New Issue
Block a user