import { Vonage } from '@vonage/server-sdk'; import { SMS } from '@vonage/messages'; import { Auth } from '@vonage/auth'; import { logger, maskPhone } from '../logger.js'; const VONAGE_API_KEY = process.env.VONAGE_API_KEY || ''; const VONAGE_API_SECRET = process.env.VONAGE_API_SECRET || ''; const VONAGE_APPLICATION_ID = process.env.VONAGE_APPLICATION_ID || ''; const VONAGE_PRIVATE_KEY_PATH = process.env.VONAGE_PRIVATE_KEY_PATH || ''; const VONAGE_PRIVATE_KEY = process.env.VONAGE_PRIVATE_KEY || ''; let vonageClient: Vonage | null = null; /** * Resolve the private key for the Vonage SDK. * * Supports three modes (checked in order): * 1. VONAGE_PRIVATE_KEY — raw PEM string (starts with "-----BEGIN") * 2. VONAGE_PRIVATE_KEY — base64-encoded PEM (decode first) * 3. VONAGE_PRIVATE_KEY_PATH — path to a .key file on disk * * The Vonage Auth constructor accepts either a file path (string) or * the key content directly (string / Buffer). */ function resolvePrivateKey(): string | undefined { if (VONAGE_PRIVATE_KEY) { // Raw PEM passed directly if (VONAGE_PRIVATE_KEY.startsWith('-----BEGIN')) { return VONAGE_PRIVATE_KEY; } // Base64-encoded PEM (common for PaaS env vars) try { const decoded = Buffer.from(VONAGE_PRIVATE_KEY, 'base64').toString('utf-8'); if (decoded.startsWith('-----BEGIN')) return decoded; } catch { /* fall through */ } // Treat as raw content anyway return VONAGE_PRIVATE_KEY; } if (VONAGE_PRIVATE_KEY_PATH) { return VONAGE_PRIVATE_KEY_PATH; } return undefined; } function getVonageClient(): Vonage | null { if (vonageClient) return vonageClient; const privateKey = resolvePrivateKey(); if (!VONAGE_APPLICATION_ID || !privateKey) { return null; } vonageClient = new Vonage(new Auth({ apiKey: VONAGE_API_KEY, apiSecret: VONAGE_API_SECRET, applicationId: VONAGE_APPLICATION_ID, privateKey, })); return vonageClient; } /** * Send an SMS reply via the Vonage Messages API. * Phone numbers should be in E.164 format (e.g. +14155550100). * Vonage expects numbers without the leading '+'. */ export async function sendSms(to: string, from: string, body: string): Promise { const client = getVonageClient(); if (!client) { logger.warn({ event: 'sms.send_skipped', to: maskPhone(to) }, 'No Vonage credentials configured, skipping SMS send'); logger.info({ event: 'sms.would_send', body }, 'SMS body (dev mode)'); return; } // Strip leading '+' — Vonage expects bare numbers const toNumber = to.replace(/^\+/, ''); const fromNumber = from.replace(/^\+/, ''); try { await client.messages.send( new SMS({ to: toNumber, from: fromNumber, text: body }) ); logger.info({ event: 'sms.sent', to: maskPhone(to) }, 'SMS sent'); } catch (err) { const error = err as Error & { response?: { data?: unknown; status?: number } }; logger.error({ event: 'sms.send_error', error: error.message, status: error.response?.status, responseData: error.response?.data, }, 'SMS send error'); } }