4.2 KiB
Twilio — @suaveplan/sms-providers
(no submodules)
Purpose
Real Twilio SMS provider implementing the SmsProvider contract from @suaveplan/sms. Issues authenticated, form-encoded HTTP requests against Twilio's documented Messages API (POST /2010-04-01/Accounts/{AccountSid}/Messages.json), normalises Twilio's status vocabulary into the canonical SmsDeliveryStatus, fetches delivery status via the corresponding GET, and verifies inbound webhooks per Twilio's X-Twilio-Signature HMAC-SHA1 scheme. MMS MediaUrl[] is forwarded as repeated MediaUrl form parameters per Twilio's wire convention.
The class holds no in-flight state: every send issues exactly one HTTP request through the injectable FetchLike seam. Tests bind a node:http server on a random port and inject a fetch adapter pointing at it; production resolves globalThis.fetch.bind(globalThis).
Features
- HTTP Basic auth (
Authorization: Basic base64(AccountSid:AuthToken)). - Form-encoded body via
URLSearchParams— Twilio rejects JSON bodies. - MMS
MediaUrl[]forwarded as repeatedMediaUrlparams. - Canonical status mapping (
queued,sending,sent,delivered,undelivered,failed,canceled,receiving,received,accepted,scheduled). getStatus(sid)round-trips the same auth + URL convention.verifyWebhookSignature(url, params, signature)implements the documented HMAC-SHA1-over-sorted-form scheme; mismatched signatures throwSmsProvidersError(SIGNATURE_INVALID)and incrementsms_providers.signature_failures_total{provider="twilio"}.parseInbound(params)discriminates between message bodies, delivery receipts, and opt-in/opt-out/help keywords using the canonicalparseOptOutKeywordclassifier.- OTel — every
sendis counted (send_total) and timed (send_duration_ms); every signature verify is timed (webhook_verify_duration_ms).
Usage: Basic
import { TwilioProvider } from "@suaveplan/sms-providers";
const twilio = new TwilioProvider({
accountSid: "AC…",
authToken: "…",
});
const report = await twilio.send({
to: "+15551234567",
from: "+15559999999",
body: "Hello",
});
console.log(report.messageId, report.status);
Usage: Advanced
import { TwilioProvider, type FetchLike } from "@suaveplan/sms-providers";
import { createServer } from "node:http";
// Stand up a real HTTP server, point Twilio's FetchLike at it.
const server = createServer((req, res) => {
res.writeHead(201, { "content-type": "application/json" });
res.end(JSON.stringify({ sid: "SMxxx", status: "queued", to: "+1" }));
});
await new Promise<void>((r) => server.listen(0, r));
const { port } = server.address() as { port: number };
const fetchImpl: FetchLike = async (url, init) =>
globalThis.fetch(url.replace("https://api.twilio.com", `http://127.0.0.1:${port}`), init);
const twilio = new TwilioProvider({
accountSid: "AC1",
authToken: "tok",
fetch: fetchImpl,
});
const r = await twilio.send({
to: "+15551234567",
from: "+15550000000",
body: "Hi",
mediaUrls: ["https://cdn/example.png"],
} as Parameters<typeof twilio.send>[0]);
await new Promise<void>((r) => server.close(() => r()));
API Reference
| Symbol | Kind | Notes |
|---|---|---|
TwilioProvider |
class | Implements SmsProvider from @suaveplan/sms. |
send(message, options?) |
method | Issues the POST and returns an SmsDeliveryReport. |
getStatus(sid) |
method | Round-trips a GET for the message resource. |
mapStatus(raw) |
method | Maps a Twilio status string to SmsDeliveryStatus. |
verifyWebhookSignature(url, params, sig) |
method | HMAC-SHA1 over url + sortedKey1 + value1 + …; throws SIGNATURE_INVALID on mismatch. |
parseInbound(params) |
method | Discriminates into the InboundSmsEvent union. |
Implementation Notes
The baseUrl strips any trailing / so callers may pass either form. accountSid and authToken are validated at construction time so credential omissions surface immediately rather than on first call. MMS mediaUrls are read off the canonical OutboundMessage shape — SmsMessage & OutboundExtras — so the call site can pass mediaUrls without breaking the SmsProvider#send signature.