feat: complete DM pairing codes with channel adapters, gateway handlers, and TUI command (Tier 4 feature 4)
This commit is contained in:
@@ -18,6 +18,7 @@ import type {
|
||||
ChannelStatus,
|
||||
} from '../types.js';
|
||||
import { splitMessage } from '../utils.js';
|
||||
import type { PairingManager } from '../pairing.js';
|
||||
|
||||
/** Configuration for the Discord channel adapter. */
|
||||
export interface DiscordAdapterConfig {
|
||||
@@ -28,6 +29,8 @@ export interface DiscordAdapterConfig {
|
||||
allowedChannelIds?: string[];
|
||||
/** Whether to require mention to respond in guild channels (default: true). DMs always respond. */
|
||||
requireMention?: boolean;
|
||||
/** Optional pairing manager for DM pairing codes. */
|
||||
pairingManager?: PairingManager;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -194,6 +197,20 @@ export class DiscordAdapter implements ChannelAdapter {
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// DM pairing check — if pairing is enabled, require approval
|
||||
const pm = this.config.pairingManager;
|
||||
if (pm?.enabled && !pm.isApproved('discord', message.channelId)) {
|
||||
const text = message.content.trim();
|
||||
if (text && pm.validateCode('discord', message.channelId, text)) {
|
||||
try {
|
||||
if ('send' in message.channel) {
|
||||
(message.channel as any).send('Pairing successful! You can now chat with Flynn.');
|
||||
}
|
||||
} catch { /* ignore send errors */ }
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Send typing indicator (lasts 10 seconds, no need for interval)
|
||||
|
||||
@@ -16,6 +16,7 @@ import type {
|
||||
ChannelStatus,
|
||||
} from '../types.js';
|
||||
import { splitMessage } from '../utils.js';
|
||||
import type { PairingManager } from '../pairing.js';
|
||||
|
||||
/** Configuration for the Slack channel adapter. */
|
||||
export interface SlackAdapterConfig {
|
||||
@@ -26,6 +27,8 @@ export interface SlackAdapterConfig {
|
||||
allowedChannelIds?: string[];
|
||||
/** Require bot mention to respond (default: false). */
|
||||
requireMention?: boolean;
|
||||
/** Optional pairing manager for DM pairing codes. */
|
||||
pairingManager?: PairingManager;
|
||||
}
|
||||
|
||||
/** Minimal shape of a Slack message event from Bolt. */
|
||||
@@ -275,7 +278,32 @@ export class SlackAdapter implements ChannelAdapter {
|
||||
this.config.allowedChannelIds.length > 0 &&
|
||||
!this.config.allowedChannelIds.includes(channelId)
|
||||
) {
|
||||
return;
|
||||
// Pairing fallback — check if the Slack user is approved or sending a valid code
|
||||
const pm = this.config.pairingManager;
|
||||
const userId = message.user;
|
||||
if (pm?.enabled && userId) {
|
||||
if (pm.isApproved('slack', userId)) {
|
||||
// Approved — fall through to normal message handling
|
||||
} else {
|
||||
const text = (message.text ?? '').trim();
|
||||
if (text && pm.validateCode('slack', userId, text)) {
|
||||
// Code validated — send confirmation via Slack
|
||||
if (this.app) {
|
||||
const threadTs = message.thread_ts ?? message.ts ?? '';
|
||||
try {
|
||||
await this.app.client.chat.postMessage({
|
||||
channel: channelId,
|
||||
text: 'Pairing successful! You can now chat with Flynn.',
|
||||
thread_ts: threadTs || undefined,
|
||||
});
|
||||
} catch { /* ignore send errors */ }
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Mention requirement
|
||||
|
||||
@@ -12,6 +12,7 @@ import type {
|
||||
import { isAllowedChat } from '../../frontends/telegram/handlers.js';
|
||||
import { parseConfirmationCallback } from '../../frontends/telegram/confirmations.js';
|
||||
import { splitMessage } from '../utils.js';
|
||||
import type { PairingManager } from '../pairing.js';
|
||||
|
||||
/** Configuration for the Telegram channel adapter. */
|
||||
export interface TelegramAdapterConfig {
|
||||
@@ -20,6 +21,8 @@ export interface TelegramAdapterConfig {
|
||||
/** Require bot mention or reply-to-bot to respond in group chats (default: true). */
|
||||
requireMention?: boolean;
|
||||
hookEngine?: HookEngine;
|
||||
/** Optional pairing manager for DM pairing codes. */
|
||||
pairingManager?: PairingManager;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -76,14 +79,34 @@ export class TelegramAdapter implements ChannelAdapter {
|
||||
this.bot = new Bot(this.config.botToken);
|
||||
this._status = 'connecting';
|
||||
|
||||
// ── Auth middleware — reject messages from unknown chats ──
|
||||
// ── Auth middleware — reject messages from unknown chats (with pairing fallback) ──
|
||||
this.bot.use(async (ctx, next) => {
|
||||
const chatId = ctx.chat?.id;
|
||||
if (chatId === undefined || !isAllowedChat(chatId, this.config.allowedChatIds)) {
|
||||
console.log(`Rejected message from unauthorized chat: ${chatId}`);
|
||||
if (chatId === undefined) return;
|
||||
|
||||
// Allowlist check
|
||||
if (isAllowedChat(chatId, this.config.allowedChatIds)) {
|
||||
await next();
|
||||
return;
|
||||
}
|
||||
await next();
|
||||
|
||||
// Pairing fallback — check if sender is already approved or sending a valid code
|
||||
const pm = this.config.pairingManager;
|
||||
if (pm?.enabled) {
|
||||
const senderId = String(chatId);
|
||||
if (pm.isApproved('telegram', senderId)) {
|
||||
await next();
|
||||
return;
|
||||
}
|
||||
// Check if the message text is a valid pairing code
|
||||
const text = ctx.message?.text?.trim();
|
||||
if (text && pm.validateCode('telegram', senderId, text)) {
|
||||
await ctx.reply('Pairing successful! You can now chat with Flynn.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Rejected message from unauthorized chat: ${chatId}`);
|
||||
});
|
||||
|
||||
// ── Confirmation callback handler (requires hookEngine) ──
|
||||
|
||||
@@ -18,6 +18,7 @@ import type {
|
||||
ChannelStatus,
|
||||
} from '../types.js';
|
||||
import { splitMessage } from '../utils.js';
|
||||
import type { PairingManager } from '../pairing.js';
|
||||
|
||||
/** Configuration for the WhatsApp channel adapter. */
|
||||
export interface WhatsAppAdapterConfig {
|
||||
@@ -29,6 +30,8 @@ export interface WhatsAppAdapterConfig {
|
||||
requireMention?: boolean;
|
||||
/** Directory for session persistence (LocalAuth data path). */
|
||||
dataDir?: string;
|
||||
/** Optional pairing manager for DM pairing codes. */
|
||||
pairingManager?: PairingManager;
|
||||
}
|
||||
|
||||
/** Minimal shape of a whatsapp-web.js message. */
|
||||
@@ -232,7 +235,26 @@ export class WhatsAppAdapter implements ChannelAdapter {
|
||||
this.config.allowedNumbers.length > 0 &&
|
||||
!this.config.allowedNumbers.includes(phoneNumber)
|
||||
) {
|
||||
return;
|
||||
// Pairing fallback — check if the sender is approved or sending a valid code
|
||||
const pm = this.config.pairingManager;
|
||||
if (pm?.enabled) {
|
||||
if (pm.isApproved('whatsapp', phoneNumber)) {
|
||||
// Approved — fall through to normal message handling
|
||||
} else {
|
||||
const text = (message.body ?? '').trim();
|
||||
if (text && pm.validateCode('whatsapp', phoneNumber, text)) {
|
||||
// Code validated — send confirmation via WhatsApp
|
||||
if (this.client) {
|
||||
try {
|
||||
await this.client.sendMessage(from, 'Pairing successful! You can now chat with Flynn.');
|
||||
} catch { /* ignore send errors */ }
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -371,6 +371,7 @@ export const configSchema = z.object({
|
||||
agent_configs: agentConfigsSchema,
|
||||
routing: routingSchema,
|
||||
sessions: sessionsSchema,
|
||||
pairing: pairingSchema,
|
||||
});
|
||||
|
||||
export type Config = z.infer<typeof configSchema>;
|
||||
@@ -404,3 +405,4 @@ export type HeartbeatConfig = z.infer<typeof heartbeatSchema>;
|
||||
export type HeartbeatCheck = z.infer<typeof heartbeatCheckSchema>;
|
||||
export type EmbeddingConfig = z.infer<typeof embeddingSchema>;
|
||||
export type EmbeddingProvider = z.infer<typeof embeddingProviderSchema>;
|
||||
export type PairingCodeConfig = z.infer<typeof pairingSchema>;
|
||||
|
||||
+23
-1
@@ -16,7 +16,7 @@ import { VectorStore, HybridSearch, createEmbeddingProvider, chunkText, contentH
|
||||
import type { EmbeddingProvider as EmbeddingProviderInterface } from '../memory/index.js';
|
||||
import { createMemoryTools } from '../tools/builtin/index.js';
|
||||
import { GatewayServer } from '../gateway/index.js';
|
||||
import { ChannelRegistry, TelegramAdapter, WebChatAdapter, DiscordAdapter, SlackAdapter, WhatsAppAdapter } from '../channels/index.js';
|
||||
import { ChannelRegistry, TelegramAdapter, WebChatAdapter, DiscordAdapter, SlackAdapter, WhatsAppAdapter, PairingManager } from '../channels/index.js';
|
||||
import { CronScheduler, WebhookHandler, HeartbeatMonitor, GmailWatcher } from '../automation/index.js';
|
||||
import type { InboundMessage, OutboundMessage } from '../channels/index.js';
|
||||
import { McpManager } from '../mcp/index.js';
|
||||
@@ -793,6 +793,23 @@ export async function startDaemon(config: Config): Promise<DaemonContext> {
|
||||
// Initialize channel registry (created early so the gateway can reference it)
|
||||
const channelRegistry = new ChannelRegistry();
|
||||
|
||||
// Create PairingManager if pairing is enabled
|
||||
let pairingManager: PairingManager | undefined;
|
||||
if (config.pairing.enabled) {
|
||||
// Parse code_ttl: supports '5m', '1h', '30s' → milliseconds
|
||||
const ttlMatch = config.pairing.code_ttl.match(/^(\d+)(s|m|h)$/);
|
||||
const codeTtlMs = ttlMatch
|
||||
? Number(ttlMatch[1]) * ({ s: 1000, m: 60_000, h: 3_600_000 }[ttlMatch[2] as 's' | 'm' | 'h'])
|
||||
: 5 * 60_000; // default 5 minutes
|
||||
|
||||
pairingManager = new PairingManager({
|
||||
enabled: true,
|
||||
codeTtl: codeTtlMs,
|
||||
codeLength: config.pairing.code_length,
|
||||
});
|
||||
console.log(`Pairing codes enabled (TTL: ${config.pairing.code_ttl}, length: ${config.pairing.code_length})`);
|
||||
}
|
||||
|
||||
// Mutable reference to channel agents map — set after createMessageRouter() below.
|
||||
// This allows the gateway's getTokenUsage callback to access channel agent usage data.
|
||||
let channelAgents: Map<string, { orchestrator: AgentOrchestrator; collector: OutboundAttachmentCollector }> | null = null;
|
||||
@@ -815,6 +832,7 @@ export async function startDaemon(config: Config): Promise<DaemonContext> {
|
||||
uiDir: resolve(import.meta.dirname, '../gateway/ui'),
|
||||
config,
|
||||
channelRegistry,
|
||||
pairingManager,
|
||||
restart: async () => {
|
||||
console.log('Restart requested via gateway');
|
||||
await lifecycle.shutdown();
|
||||
@@ -883,6 +901,7 @@ export async function startDaemon(config: Config): Promise<DaemonContext> {
|
||||
allowedChatIds: config.telegram.allowed_chat_ids,
|
||||
requireMention: config.telegram.require_mention,
|
||||
hookEngine,
|
||||
pairingManager,
|
||||
});
|
||||
channelRegistry.register(telegramAdapter);
|
||||
|
||||
@@ -893,6 +912,7 @@ export async function startDaemon(config: Config): Promise<DaemonContext> {
|
||||
allowedGuildIds: config.discord.allowed_guild_ids.length > 0 ? config.discord.allowed_guild_ids : undefined,
|
||||
allowedChannelIds: config.discord.allowed_channel_ids.length > 0 ? config.discord.allowed_channel_ids : undefined,
|
||||
requireMention: config.discord.require_mention,
|
||||
pairingManager,
|
||||
});
|
||||
channelRegistry.register(discordAdapter);
|
||||
}
|
||||
@@ -905,6 +925,7 @@ export async function startDaemon(config: Config): Promise<DaemonContext> {
|
||||
signingSecret: config.slack.signing_secret,
|
||||
allowedChannelIds: config.slack.allowed_channel_ids.length > 0 ? config.slack.allowed_channel_ids : undefined,
|
||||
requireMention: config.slack.require_mention,
|
||||
pairingManager,
|
||||
});
|
||||
channelRegistry.register(slackAdapter);
|
||||
}
|
||||
@@ -916,6 +937,7 @@ export async function startDaemon(config: Config): Promise<DaemonContext> {
|
||||
allowedGroupIds: config.whatsapp.allowed_group_ids.length > 0 ? config.whatsapp.allowed_group_ids : undefined,
|
||||
requireMention: config.whatsapp.require_mention,
|
||||
dataDir: config.whatsapp.data_dir,
|
||||
pairingManager,
|
||||
});
|
||||
channelRegistry.register(whatsappAdapter);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ export type Command =
|
||||
| { type: 'backend'; provider?: string }
|
||||
| { type: 'login'; provider?: string }
|
||||
| { type: 'transfer'; target: string }
|
||||
| { type: 'pair'; action?: 'generate' | 'list' | 'revoke'; args?: string }
|
||||
| { type: 'message'; content: string };
|
||||
|
||||
export function parseCommand(input: string): Command | null {
|
||||
@@ -99,6 +100,19 @@ export function parseCommand(input: string): Command | null {
|
||||
return { type: 'login', provider: provider || undefined };
|
||||
}
|
||||
|
||||
// Pair
|
||||
if (trimmed === '/pair' || trimmed === '/pair list') {
|
||||
return { type: 'pair', action: 'list' };
|
||||
}
|
||||
if (trimmed === '/pair generate' || trimmed.startsWith('/pair generate ')) {
|
||||
const label = trimmed.slice('/pair generate'.length).trim() || undefined;
|
||||
return { type: 'pair', action: 'generate', args: label };
|
||||
}
|
||||
if (trimmed.startsWith('/pair revoke ')) {
|
||||
const args = trimmed.slice('/pair revoke '.length).trim();
|
||||
return { type: 'pair', action: 'revoke', args };
|
||||
}
|
||||
|
||||
// Regular message
|
||||
return { type: 'message', content: trimmed };
|
||||
}
|
||||
@@ -111,6 +125,9 @@ Commands:
|
||||
/model <tier> <p/m> Change tier's provider/model (e.g. /model default anthropic/claude-sonnet-4)
|
||||
/backend [provider] Show or switch local backend (ollama, llamacpp)
|
||||
/login [provider] Authenticate with GitHub
|
||||
/pair List pending pairing codes and approved senders
|
||||
/pair generate [label] Generate a new DM pairing code
|
||||
/pair revoke <ch> <id> Revoke an approved sender
|
||||
/reset, /clear, /new Clear conversation history
|
||||
/compact Compact conversation history
|
||||
/usage Show token usage and estimated cost
|
||||
@@ -139,6 +156,7 @@ export const SLASH_COMMANDS = [
|
||||
'/fullscreen',
|
||||
'/fs',
|
||||
'/login',
|
||||
'/pair',
|
||||
'/transfer',
|
||||
'/quit',
|
||||
'/exit',
|
||||
@@ -159,6 +177,7 @@ export const COMMAND_TOOLTIPS: Record<string, string> = {
|
||||
'/fullscreen': 'Switch to fullscreen mode',
|
||||
'/fs': 'Switch to fullscreen mode',
|
||||
'/login': 'Authenticate with GitHub (OAuth device flow)',
|
||||
'/pair': 'Generate/list/revoke DM pairing codes',
|
||||
'/transfer': 'Transfer session to another frontend',
|
||||
'/quit': 'Exit TUI',
|
||||
'/exit': 'Exit TUI',
|
||||
|
||||
@@ -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