Files
suaveplan/packages/communication/messaging/sms-providers/src/twilio/twilio.md
2026-05-27 23:29:53 +01:00

86 lines
4.2 KiB
Markdown

# Twilio — `@suaveplan/sms-providers`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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<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.