Add TWILIO_SENDER_SID override and better funnel/setup error messages

This commit is contained in:
Peter Steinberger
2025-11-24 12:36:03 +01:00
parent 6c6e217f83
commit e52e943317
2 changed files with 52 additions and 30 deletions

View File

@@ -7,6 +7,7 @@ Small TypeScript CLI to send, monitor, and webhook WhatsApp messages via Twilio.
1. `pnpm install` 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:`). 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`. - 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` 3. Build once for the runnable bin: `pnpm build`
## Commands ## Commands

View File

@@ -46,6 +46,7 @@ type GlobalOptions = {
type EnvConfig = { type EnvConfig = {
accountSid: string; accountSid: string;
whatsappFrom: string; whatsappFrom: string;
whatsappSenderSid?: string;
auth: AuthMode; auth: AuthMode;
}; };
@@ -53,6 +54,7 @@ function readEnv(): EnvConfig {
// Load and validate Twilio auth + sender configuration from env. // Load and validate Twilio auth + sender configuration from env.
const accountSid = process.env.TWILIO_ACCOUNT_SID; const accountSid = process.env.TWILIO_ACCOUNT_SID;
const whatsappFrom = process.env.TWILIO_WHATSAPP_FROM; const whatsappFrom = process.env.TWILIO_WHATSAPP_FROM;
const whatsappSenderSid = process.env.TWILIO_SENDER_SID;
const authToken = process.env.TWILIO_AUTH_TOKEN; const authToken = process.env.TWILIO_AUTH_TOKEN;
const apiKey = process.env.TWILIO_API_KEY; const apiKey = process.env.TWILIO_API_KEY;
const apiSecret = process.env.TWILIO_API_SECRET; const apiSecret = process.env.TWILIO_API_SECRET;
@@ -79,6 +81,7 @@ function readEnv(): EnvConfig {
return { return {
accountSid, accountSid,
whatsappFrom, whatsappFrom,
whatsappSenderSid,
auth auth
}; };
} }
@@ -445,28 +448,39 @@ async function ensureFunnel(port: number) {
async function findWhatsappSenderSid(client: ReturnType<typeof createClient>, from: string) { async function findWhatsappSenderSid(client: ReturnType<typeof createClient>, from: string) {
// Fetch sender SID that matches configured WhatsApp from number. // Fetch sender SID that matches configured WhatsApp from number.
const resp = await (client as unknown as { request: (options: Record<string, unknown>) => Promise<{ data?: unknown }> }).request({ try {
method: 'get', const resp = await (client as unknown as { request: (options: Record<string, unknown>) => Promise<{ data?: unknown }> }).request({
uri: 'https://messaging.twilio.com/v2/Channels/Senders', method: 'get',
qs: { Channel: 'whatsapp', PageSize: 50 } uri: 'https://messaging.twilio.com/v2/Channels/Senders',
}); qs: { Channel: 'whatsapp', PageSize: 50 }
const data = resp?.data as Record<string, unknown> | undefined; });
const senders = Array.isArray((data as Record<string, unknown> | undefined)?.senders) const data = resp?.data as Record<string, unknown> | undefined;
? (data as { senders: unknown[] }).senders const senders = Array.isArray((data as Record<string, unknown> | undefined)?.senders)
: undefined; ? (data as { senders: unknown[] }).senders
if (!senders) { : undefined;
throw new Error('Unable to list WhatsApp senders'); if (!senders) {
throw new Error('List senders response missing "senders" array');
}
const match = senders.find(
(s) =>
typeof s === 'object' &&
s !== null &&
(s as Record<string, unknown>).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<string, unknown>).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( async function updateWebhook(
@@ -476,15 +490,22 @@ async function updateWebhook(
method: 'POST' | 'GET' = 'POST' method: 'POST' | 'GET' = 'POST'
) { ) {
// Point Twilio sender webhook at the provided URL. // Point Twilio sender webhook at the provided URL.
await (client as unknown as { request: (options: Record<string, unknown>) => Promise<unknown> }).request({ await (client as unknown as { request: (options: Record<string, unknown>) => Promise<unknown> })
method: 'post', .request({
uri: `https://messaging.twilio.com/v2/Channels/Senders/${senderSid}`, method: 'post',
form: { uri: `https://messaging.twilio.com/v2/Channels/Senders/${senderSid}`,
CallbackUrl: url, form: {
CallbackMethod: method CallbackUrl: url,
} CallbackMethod: method
}); }
console.log(`✅ Twilio webhook set to ${url}`); })
.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) { function sleep(ms: number) {