Fix issues and add linting
This commit is contained in:
@@ -1,30 +1,95 @@
|
||||
import { Vonage } from '@vonage/server-sdk';
|
||||
import { SMS } from '@vonage/messages';
|
||||
import { Auth } from '@vonage/auth';
|
||||
import { logger, maskPhone } from '../logger.js';
|
||||
|
||||
const TELNYX_API_KEY = process.env.TELNYX_API_KEY || '';
|
||||
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> {
|
||||
if (!TELNYX_API_KEY) {
|
||||
logger.warn({ event: 'sms.send_skipped', to: maskPhone(to) }, 'No TELNYX_API_KEY, skipping SMS send');
|
||||
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;
|
||||
}
|
||||
|
||||
try {
|
||||
const resp = await fetch('https://api.telnyx.com/v2/messages', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${TELNYX_API_KEY}`,
|
||||
},
|
||||
body: JSON.stringify({ from, to, text: body }),
|
||||
});
|
||||
// Strip leading '+' — Vonage expects bare numbers
|
||||
const toNumber = to.replace(/^\+/, '');
|
||||
const fromNumber = from.replace(/^\+/, '');
|
||||
|
||||
if (!resp.ok) {
|
||||
logger.error({ event: 'sms.send_failed', status: resp.status }, 'Failed to send SMS');
|
||||
} else {
|
||||
logger.info({ event: 'sms.sent', to: maskPhone(to) }, 'SMS sent');
|
||||
}
|
||||
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) {
|
||||
logger.error({ event: 'sms.send_error', error: (err as Error).message }, 'SMS send error');
|
||||
const error = err as Error & { response?: { data?: unknown; status?: number } };
|
||||
logger.error({
|
||||
event: 'sms.send_error',
|
||||
error: error.message,
|
||||
status: error.response?.status,
|
||||
}, 'SMS send error');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user