/** * Yonexus Server - Discord Notification Service * * Sends pairing notifications to the configured admin user via Discord DM. */ import type { PairingRequest } from "../services/pairing.js"; import { redactPairingCode, safeErrorMessage } from "../core/logging.js"; export interface DiscordNotificationService { /** * Send a pairing code notification to the admin user. * @returns Whether the notification was sent successfully */ sendPairingNotification(request: PairingRequest): Promise; } export interface DiscordNotificationConfig { botToken: string; adminUserId: string; } export interface DiscordApiResponse { ok: boolean; status: number; json(): Promise; } export type DiscordFetch = ( input: string, init?: { method?: string; headers?: Record; body?: string; } ) => Promise; interface CreateDmChannelResponse { id?: string; } interface SendDiscordDirectMessageOptions { config: DiscordNotificationConfig; message: string; fetcher: DiscordFetch; } const DISCORD_API_BASE_URL = "https://discord.com/api/v10"; /** * Create a Discord notification service backed by Discord's REST API. * * Flow: * 1. Create or fetch a DM channel for the configured admin user * 2. Post the formatted pairing message into that DM channel */ export function createDiscordNotificationService( config: DiscordNotificationConfig, options: { fetcher?: DiscordFetch } = {} ): DiscordNotificationService { const fetcher = options.fetcher ?? getDefaultFetch(); return { async sendPairingNotification(request: PairingRequest): Promise { if (!config.botToken.trim() || !config.adminUserId.trim()) { console.error("[Yonexus.Server] Discord DM notification misconfigured", { hasBotToken: Boolean(config.botToken.trim()), hasAdminUserId: Boolean(config.adminUserId.trim()) }); return false; } try { await sendDiscordDirectMessage({ config, message: formatPairingMessage(request), fetcher }); console.log("[Yonexus.Server] Pairing notification sent via Discord DM", { identifier: request.identifier, pairingCode: redactPairingCode(request.pairingCode), expiresAt: request.expiresAt, ttlSeconds: request.ttlSeconds, adminUserId: config.adminUserId }); return true; } catch (error) { console.error("[Yonexus.Server] Failed to send Discord DM pairing notification", { identifier: request.identifier, pairingCode: redactPairingCode(request.pairingCode), adminUserId: config.adminUserId, error: safeErrorMessage(error) }); return false; } } }; } async function sendDiscordDirectMessage( options: SendDiscordDirectMessageOptions ): Promise { const { config, message, fetcher } = options; const headers = { Authorization: `Bot ${config.botToken}`, "Content-Type": "application/json" }; const dmResponse = await fetcher(`${DISCORD_API_BASE_URL}/users/@me/channels`, { method: "POST", headers, body: JSON.stringify({ recipient_id: config.adminUserId }) }); if (!dmResponse.ok) { throw new Error(`Discord DM channel creation failed with status ${dmResponse.status}`); } const dmPayload = (await dmResponse.json()) as CreateDmChannelResponse; const channelId = dmPayload.id?.trim(); if (!channelId) { throw new Error("Discord DM channel creation did not return a channel id"); } const messageResponse = await fetcher(`${DISCORD_API_BASE_URL}/channels/${channelId}/messages`, { method: "POST", headers, body: JSON.stringify({ content: message }) }); if (!messageResponse.ok) { throw new Error(`Discord DM send failed with status ${messageResponse.status}`); } await messageResponse.json(); } function getDefaultFetch(): DiscordFetch { if (typeof fetch !== "function") { throw new Error("Global fetch is not available in this runtime"); } return (input, init) => fetch(input, { method: init?.method, headers: init?.headers, body: init?.body }) as Promise; } /** * Format a pairing request as a Discord DM message. */ export function formatPairingMessage(request: PairingRequest): string { const expiresDate = new Date(request.expiresAt * 1000); const expiresStr = expiresDate.toISOString(); return [ "🔐 **Yonexus Pairing Request**", "", `**Identifier:** \`${request.identifier}\``, `**Pairing Code:** \`${request.pairingCode}\``, `**Expires At:** ${expiresStr}`, `**TTL:** ${request.ttlSeconds} seconds`, "", "Please relay this pairing code to the client operator via a trusted out-of-band channel.", "Do not share this code over the Yonexus WebSocket connection." ].join("\n"); } /** * Create a mock notification service for testing. * Returns success/failure based on configuration. */ export function createMockNotificationService( options: { shouldSucceed?: boolean } = {} ): DiscordNotificationService { const shouldSucceed = options.shouldSucceed ?? true; return { async sendPairingNotification(request: PairingRequest): Promise { console.log("[Yonexus.Server] Mock pairing notification:"); console.log(` Identifier: ${request.identifier}`); console.log(` Pairing Code: ${redactPairingCode(request.pairingCode)}`); console.log(` Success: ${shouldSucceed}`); return shouldSucceed; } }; }