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

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 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

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.