# 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 repeated `MediaUrl` params. - 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 throw `SmsProvidersError(SIGNATURE_INVALID)` and increment `sms_providers.signature_failures_total{provider="twilio"}`. - `parseInbound(params)` discriminates between message bodies, delivery receipts, and opt-in/opt-out/help keywords using the canonical `parseOptOutKeyword` classifier. - OTel — every `send` is counted (`send_total`) and timed (`send_duration_ms`); every signature verify is timed (`webhook_verify_duration_ms`). ## Usage: Basic ```ts 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 ```ts 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((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[0]); await new Promise((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.