Add TWILIO_SENDER_SID override and better funnel/setup error messages
This commit is contained in:
@@ -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
|
||||||
|
|||||||
81
src/index.ts
81
src/index.ts
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user