From e52e9433173acaf7b0e053ace940bcd7723c6b8b Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 24 Nov 2025 12:36:03 +0100 Subject: [PATCH] Add TWILIO_SENDER_SID override and better funnel/setup error messages --- README.md | 1 + src/index.ts | 81 +++++++++++++++++++++++++++++++++------------------- 2 files changed, 52 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index cd12c0990c9..6095ad27d16 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ Small TypeScript CLI to send, monitor, and webhook WhatsApp messages via Twilio. 1. `pnpm install` 2. Copy `.env.example` to `.env` and fill in `TWILIO_ACCOUNT_SID`, `TWILIO_AUTH_TOKEN`, and `TWILIO_WHATSAPP_FROM` (use your approved WhatsApp-enabled Twilio number, prefixed with `whatsapp:`). - Alternatively, use API keys: `TWILIO_API_KEY` + `TWILIO_API_SECRET` instead of `TWILIO_AUTH_TOKEN`. + - Optional: `TWILIO_SENDER_SID` to skip auto-discovery of the WhatsApp sender in Twilio. 3. Build once for the runnable bin: `pnpm build` ## Commands diff --git a/src/index.ts b/src/index.ts index 7b033d3803c..99e720807e5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -46,6 +46,7 @@ type GlobalOptions = { type EnvConfig = { accountSid: string; whatsappFrom: string; + whatsappSenderSid?: string; auth: AuthMode; }; @@ -53,6 +54,7 @@ function readEnv(): EnvConfig { // Load and validate Twilio auth + sender configuration from env. const accountSid = process.env.TWILIO_ACCOUNT_SID; const whatsappFrom = process.env.TWILIO_WHATSAPP_FROM; + const whatsappSenderSid = process.env.TWILIO_SENDER_SID; const authToken = process.env.TWILIO_AUTH_TOKEN; const apiKey = process.env.TWILIO_API_KEY; const apiSecret = process.env.TWILIO_API_SECRET; @@ -79,6 +81,7 @@ function readEnv(): EnvConfig { return { accountSid, whatsappFrom, + whatsappSenderSid, auth }; } @@ -445,28 +448,39 @@ async function ensureFunnel(port: number) { async function findWhatsappSenderSid(client: ReturnType, from: string) { // Fetch sender SID that matches configured WhatsApp from number. - const resp = await (client as unknown as { request: (options: Record) => Promise<{ data?: unknown }> }).request({ - method: 'get', - uri: 'https://messaging.twilio.com/v2/Channels/Senders', - qs: { Channel: 'whatsapp', PageSize: 50 } - }); - const data = resp?.data as Record | undefined; - const senders = Array.isArray((data as Record | undefined)?.senders) - ? (data as { senders: unknown[] }).senders - : undefined; - if (!senders) { - throw new Error('Unable to list WhatsApp senders'); + try { + const resp = await (client as unknown as { request: (options: Record) => Promise<{ data?: unknown }> }).request({ + method: 'get', + uri: 'https://messaging.twilio.com/v2/Channels/Senders', + qs: { Channel: 'whatsapp', PageSize: 50 } + }); + const data = resp?.data as Record | undefined; + const senders = Array.isArray((data as Record | undefined)?.senders) + ? (data as { senders: unknown[] }).senders + : undefined; + if (!senders) { + throw new Error('List senders response missing "senders" array'); + } + const match = senders.find( + (s) => + typeof s === 'object' && + s !== null && + (s as Record).sender_id === withWhatsAppPrefix(from) + ) as { sid?: string } | undefined; + if (!match || typeof match.sid !== 'string') { + throw new Error(`Could not find sender ${withWhatsAppPrefix(from)} in Twilio account`); + } + return match.sid; + } catch (err) { + console.error(danger('Unable to list WhatsApp senders via Twilio API.')); + if (globalVerbose) console.error(err); + console.error( + info( + 'Provide TWILIO_SENDER_SID in .env to skip lookup (find it in Twilio Console → Messaging → Senders → WhatsApp).' + ) + ); + process.exit(1); } - const match = senders.find( - (s) => - typeof s === 'object' && - s !== null && - (s as Record).sender_id === withWhatsAppPrefix(from) - ) as { sid?: string } | undefined; - if (!match || typeof match.sid !== 'string') { - throw new Error(`Could not find sender ${withWhatsAppPrefix(from)} in Twilio account`); - } - return match.sid; } async function updateWebhook( @@ -476,15 +490,22 @@ async function updateWebhook( method: 'POST' | 'GET' = 'POST' ) { // Point Twilio sender webhook at the provided URL. - await (client as unknown as { request: (options: Record) => Promise }).request({ - method: 'post', - uri: `https://messaging.twilio.com/v2/Channels/Senders/${senderSid}`, - form: { - CallbackUrl: url, - CallbackMethod: method - } - }); - console.log(`✅ Twilio webhook set to ${url}`); + await (client as unknown as { request: (options: Record) => Promise }) + .request({ + method: 'post', + uri: `https://messaging.twilio.com/v2/Channels/Senders/${senderSid}`, + form: { + CallbackUrl: url, + CallbackMethod: method + } + }) + .catch((err) => { + console.error(danger('Failed to set Twilio webhook.')); + if (globalVerbose) console.error(err); + console.error(info('Double-check your sender SID and credentials; you can set TWILIO_SENDER_SID to force a specific sender.')); + process.exit(1); + }); + console.log(success(`✅ Twilio webhook set to ${url}`)); } function sleep(ms: number) {