97 lines
3.0 KiB
TypeScript
97 lines
3.0 KiB
TypeScript
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<void> {
|
|
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');
|
|
}
|
|
}
|