Move docs/ package mirror into document-navigator plugin
Relocate the ~6k-file docs tree from the repo root into plugins/document-navigator/docs/ so the plugin is self-contained and portable. docs/superpowers/ (specs and plans) stays at the repo root. Moved via cp+rm rather than git mv because directory rename() fails with EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the identical-content files at commit time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user