diff --git a/server/src/routes/webhook-sms.ts b/server/src/routes/webhook-sms.ts index 2161cee..f961a72 100644 --- a/server/src/routes/webhook-sms.ts +++ b/server/src/routes/webhook-sms.ts @@ -85,9 +85,44 @@ export function createWebhookSmsRouter(deps: WebhookSmsRouterDeps): Router { return; } - const status = (req.body as Record)?.status; - const messageUuid = (req.body as Record)?.message_uuid; - logger.info({ event: 'sms.status_update', messageUuid, status }, 'Message status update'); + const data = (req.body ?? {}) as Record; + const status = data.status; + const messageUuid = data.message_uuid; + + // Vonage Messages API status callbacks include fields like: + // message_uuid, to, from, channel, timestamp, status (plus optional fields like workflow). + // For rejected statuses, additional reason/code fields may also be present depending on channel/route. + const to = typeof data.to === 'string' ? data.to : undefined; + const from = typeof data.from === 'string' ? data.from : undefined; + const channel = typeof data.channel === 'string' ? data.channel : undefined; + const timestamp = typeof data.timestamp === 'string' ? data.timestamp : undefined; + + const rejectionCode = + (typeof data.error_code === 'string' || typeof data.error_code === 'number' ? data.error_code : undefined) ?? + (typeof data.err_code === 'string' || typeof data.err_code === 'number' ? data.err_code : undefined) ?? + (typeof data.code === 'string' || typeof data.code === 'number' ? data.code : undefined); + const rejectionReason = + (typeof data.error_title === 'string' ? data.error_title : undefined) ?? + (typeof data.error === 'string' ? data.error : undefined) ?? + (typeof data.reason === 'string' ? data.reason : undefined) ?? + (typeof data.detail === 'string' ? data.detail : undefined) ?? + (typeof data.description === 'string' ? data.description : undefined); + + logger.info( + { + event: 'sms.status_update', + messageUuid, + status, + channel, + timestamp, + to: to ? maskPhone(to) : undefined, + from: from ? maskPhone(from) : undefined, + rejectionCode, + rejectionReason, + payload: sanitizeVonageStatusPayload(data), + }, + 'Message status update' + ); res.status(200).json({ status: 'received' }); }); @@ -95,6 +130,19 @@ export function createWebhookSmsRouter(deps: WebhookSmsRouterDeps): Router { return router; } +function sanitizeVonageStatusPayload(data: Record): Record { + // Avoid leaking full phone numbers or message contents into logs. + const out: Record = { ...data }; + if (typeof out.to === 'string') out.to = maskPhone(out.to); + if (typeof out.from === 'string') out.from = maskPhone(out.from); + + // Best-effort removal of any accidental content fields if present. + for (const key of ['text', 'message', 'content', 'image', 'video', 'file', 'audio']) { + if (key in out) delete out[key]; + } + return out; +} + async function handleInbound(body: unknown, deps: WebhookSmsRouterDeps) { const parsed = parseVonageInboundMessage(body); if (!parsed) {