Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,34 @@
# Design — enterprise-sms-providers
## Tier choice — universal
`src/` ships zero `node:*` / `bun:*` / `fs` / `path` / `stream` / `net` / `http` / `crypto` imports. Every HTTP call routes through an injectable `FetchLike` (defaulting to `globalThis.fetch.bind(globalThis)`); every HMAC / RSA signing routes through an injectable `HmacSign`/`Rsa1Verify` seam (defaulting to `globalThis.crypto.subtle` when running on Bun/browsers, or to a caller-supplied adapter when running on Node 18 ≤ environments without subtle exposure). Test files (`*.test.ts`) under `src/` and `tests/` are allowed to use `node:http` + `node:crypto` since they are excluded from `src/` shipment by the build's `*.test.ts` exclusion and the dep-graph test's filter.
## Why injectable seams rather than provider SDKs
The Stripe / Twilio / AWS SDKs each pull in tens of MB of generated code plus a bespoke fetch runtime. The genesis monorepo standardises on a single `FetchLike` seam (mirrored from `@suaveplan/payment-providers`) so that:
1. The same provider class works in Bun, Node, browsers, Workers, and Deno.
2. Tests inject a recording `FetchLike` (e.g. one that proxies to a `node:http` server on a random port) without monkey-patching `globalThis.fetch`.
3. Bundle size stays bounded — every provider weighs in around 250–300 LOC including its signature verifier and status map.
4. Signature schemes (HMAC-SHA1 / HMAC-SHA256 / JWT HS256 / RSA-SHA1) are exposed as a per-call seam so consumers may plug in `@suaveplan/crypto-hash` / `@suaveplan/crypto-rsa` when they wish, or rely on `globalThis.crypto.subtle` (universal-tier default).
## SigV4 inline
Rather than depend on `@aws-sdk/signature-v4` (15+ MB of dep tree), the SNS provider implements AWS SigV4 inline: canonical request → string-to-sign → signing key (`HMAC(HMAC(HMAC(HMAC("AWS4"+secret, date), region), service), "aws4_request")`) → final signature. This is ~50 LOC and is covered by a real round-trip test that captures the `Authorization` header and asserts the shape (`AWS4-HMAC-SHA256 Credential=K/<YYYYMMDD>/<region>/sns/aws4_request, SignedHeaders=…, Signature=<64 hex>`). HMAC is delegated to the injectable `HmacSign` seam.
## Error code namespace
The upstream package registered codes (`SMS_SEND_FAILED`, `SMS_INVALID_NUMBER`, …) on the shared `@suaveplan/error` `ErrorCodes` registry, which made the codes globally addressable but coupled the package's namespace to `@suaveplan/sms`'s own `SMS.*` codes (defined via `defineCodes("SMS", […])`). Genesis settles the conflict by giving this package its own namespace `SMS_PROVIDERS.*` via `defineCodes` — there is now no collision with `@suaveplan/sms`'s `SMS.*` codes, and consumers can branch on `error.code.startsWith("SMS_PROVIDERS.")` versus `"SMS."` cleanly.
## Webhook router pattern
Each provider class exposes `verifyWebhookSignature(...)` (boolean-returning unless invalid, in which case it throws `SIGNATURE_INVALID`) and `parseInbound(body, headers): InboundSmsEvent`. The discriminated `InboundSmsEvent` union covers `message_received`, `delivery_receipt`, `opt_out`, `opt_in`, and `help`. Consumers wire this into the Hono `@suaveplan/api-plugin-*` of their choice; the provider package does not own the HTTP framing layer.
## Status mapping
Each provider exposes `mapStatus(raw): SmsDeliveryStatus`. The mapping is intentionally per-provider (not a single shared table) because Twilio's `undelivered`, Vonage's `expired`, MessageBird's `delivery_failed`, and SNS's `optout` all carry different semantics that consumers may want to inspect via the original raw status surfaced on `errorCode`.
## Why no `executeWithRetry` here
Retry is `@suaveplan/sms`'s `RetryPolicy` (already shipped). Providers stay stateless and synchronous w.r.t. their HTTP layer; the service-level retry/rate-limit/scheduler wraps the provider in `@suaveplan/sms`. Adding retry inside the provider class would double-retry and conflict with `RetryPolicy`'s budget accounting.

View File

@@ -0,0 +1,50 @@
# Enterprise migration — `@suaveplan/sms-providers`
## Why
`@suaveplan/sms` shipped a clean send/receive/template/scheduler/opt-out core with `InMemorySmsProvider` as the canonical test double. Production deployments need real provider clients — Twilio, Vonage, MessageBird, Plivo, AWS SNS — each speaking its own REST dialect, authenticating with its own scheme, returning its own status vocabulary, and signing its own webhooks. Upstream `packages/universal/sms-providers` shipped *simulated* providers (random IDs, no network IO, no signature verification) — not enterprise grade.
This change ports the package fresh under `packages/communication/messaging/sms-providers/` (per `assignment.yml`), replacing every simulated implementation with real HTTP clients over an injectable `FetchLike` seam, real HMAC/signature verifiers for each provider's inbound webhook contract, and a typed `SMS_PROVIDERS.*` error registry. Tests bind a real `node:http` server on a random port for end-to-end flows; no mocks.
## What changes
- **ADDED** Package `@suaveplan/sms-providers` at `packages/communication/messaging/sms-providers/`, version `0.1.0`, universal-tier `src/` (zero hard browser/node deps; injectable `fetch` + injectable HMAC seam).
- **ADDED** `SMS_PROVIDERS.*` namespaced error code registry via `defineCodes("SMS_PROVIDERS", […])` with eight codes: `HTTP_ERROR`, `TIMEOUT`, `PARSE_ERROR`, `CONFIG_MISSING`, `SIGNATURE_INVALID`, `INVALID_NUMBER`, `PROVIDER_NOT_REGISTERED`, `PROVIDER_ALREADY_REGISTERED`.
- **ADDED** `SmsProvidersError` discriminated subclass of `SuaveplanError` carrying `provider`, `httpStatus`, `url`, `bodyExcerpt` discriminants; factory helpers per code.
- **ADDED** Slim foundation: `httpRequest` over `FetchLike` with composed `AbortSignal` + per-call timeout + JSON parse + typed error translation; `composeAbortSignal` helper.
- **ADDED** `ProviderRegistry` indexing `SmsProvider` instances by name (insertion-ordered, strict register/lookup, frozen `list()`).
- **ADDED** Five real provider clients implementing `SmsProvider` from `@suaveplan/sms`:
1. `TwilioProvider` — `POST /2010-04-01/Accounts/{AccountSid}/Messages.json`, HTTP Basic auth (`AccountSid:AuthToken`), form-encoded body, MMS `MediaUrl` pass-through, status URL → polled, X-Twilio-Signature HMAC-SHA1 webhook verifier.
2. `VonageProvider` — `POST /sms/json`, `api_key`+`api_secret` body params, alphanumeric / short-code / long-code `from`, HMAC-SHA256 webhook signature verifier honouring `sig`/`timestamp`.
3. `MessageBirdProvider` — `POST /messages`, `AccessKey <key>` auth header, JSON body, `Messagebird-Signature-Jwt` JWT (HS256) webhook verifier with timestamp freshness window.
4. `PlivoProvider` — `POST /v1/Account/{AuthId}/Message/`, HTTP Basic auth (`AuthId:AuthToken`), JSON body, `X-Plivo-Signature-V3` HMAC-SHA256 webhook verifier (canonical-string of URL+nonce+body).
5. `AwsSnsProvider` — `POST /` to `sns.{region}.amazonaws.com` with AWS SigV4 signing (computed inline; no aws-sdk dep), `Action=Publish` form body, message-attribute mapped sender + `SMSType` (Transactional/Promotional); SNS webhook verifier accepts subscription confirmation, notification, and unsubscribe-confirmation payloads with PEM-cert injectable resolver.
- **ADDED** Per-provider raw-status → `SmsDeliveryStatus` map; injectable `Clock` + `IdGenerator` per provider for deterministic message-id fallbacks (when the provider does not return one synchronously).
- **ADDED** Inbound webhook router pipeline per provider: signature verify → header timestamp freshness check → discriminated `InboundSmsEvent` (`message_received` | `delivery_receipt` | `opt_out` | `opt_in`) — opt-in/opt-out keywords (STOP/UNSTOP/HELP family) recognised from inbound body.
- **ADDED** OTel instruments (5):
- counter `sms_providers.send_total{provider,outcome}`
- counter `sms_providers.webhook_total{provider,event,outcome}`
- counter `sms_providers.signature_failures_total{provider}`
- histogram `sms_providers.send_duration_ms{provider}`
- histogram `sms_providers.webhook_verify_duration_ms{provider}`
- **ADDED** Real `node:http` round-trip integration tests per provider (test binds an HTTP server on a random port, captures the request, returns a canned response, asserts URL/method/headers/body) and real-`node:crypto`-driven webhook signature suites (accept + reject paths).
- **ADDED** Co-located `.md` per `.ts` (six required sections, ≥200 words excl. code) and a root `README.md`.
- **MODIFIED** STATUS.md row for `@suaveplan/sms-providers` flipped `–` → `✅`.
## Parity Checklist
| Capability | Upstream | Migrated | Notes |
|---|---|---|---|
| `TwilioProvider` simulated send | ✅ | ⬆️ | Replaced: real HTTP Basic + form-encoded POST + MMS `MediaUrl` + status poll + X-Twilio-Signature HMAC-SHA1 verifier. |
| `VonageProvider` simulated send | ✅ | ⬆️ | Replaced: real POST + alpha/short/long sender parsing + HMAC-SHA256 webhook verifier. |
| `MessageBirdProvider` simulated send | ✅ | ⬆️ | Replaced: real `AccessKey` POST + JWT (HS256) webhook verifier with timestamp freshness. |
| `AwsSnsProvider` simulated send | ✅ | ⬆️ | Replaced: real SigV4-signed POST to `sns.<region>.amazonaws.com` + PEM-cert injectable SNS verifier. |
| `PlivoProvider` | 🚫 | ✅ | ADDED — orchestrator scope explicitly names Plivo. |
| `httpRequest` helper | ✅ | ⬆️ | Re-implemented over injectable `FetchLike` + composed `AbortSignal` + `SMS_PROVIDERS.*` codes. |
| `SMS_PROVIDER_*` codes on shared `ErrorCodes` | ✅ | ⬆️ | Migrated to genesis-native `defineCodes("SMS_PROVIDERS", […])`. |
| `mapStatus(raw)` per provider | ✅ | ✅ | Preserved on every provider class. |
| Webhook signature verification | 🚫 | ✅ | ADDED — production requirement. |
| Provider registry | 🚫 | ✅ | ADDED — multiplexing by name (mirrors `@suaveplan/payment-providers`). |
| OTel instrumentation | 🚫 | ✅ | ADDED — production requirement. |
| `node:http` round-trip tests | 🚫 | ✅ | ADDED — orchestrator requirement; real in-process server, no mocks. |
| Inbound opt-in/opt-out keyword detection | 🚫 | ✅ | ADDED — required for compliance integration with `@suaveplan/sms`'s `OptOutRegistry`. |

View File

@@ -0,0 +1,110 @@
# sms-providers — spec delta
## ADDED Requirements
### Requirement: Package layout & versioning
The package SHALL live at `packages/communication/messaging/sms-providers/`, ship at `0.1.0`, be universal-tier in `src/` (no hard browser/node imports outside the test tree), and expose a public ES-module barrel + per-feature subpath exports.
#### Scenario: Package metadata is correct
- **WHEN** `package.json` is loaded
- **THEN** `name === "@suaveplan/sms-providers"`, `version === "0.1.0"`, `type === "module"`, `sideEffects === false`, and `exports["."]` resolves to `./dist/index.js`
**Verified by:** structural inspection via `tests/dep-graph.test.ts` + the `package.json` JSON itself; CI gate `bunx turbo lint typecheck test build --filter=@suaveplan/sms-providers`.
#### Scenario: Source tree is universal-tier
- **WHEN** every `.ts` under `src/` (excluding `*.test.ts`) is scanned
- **THEN** none import from `node:*`, `bun:*`, `fs`, `path`, `stream`, `net`, `http`, or `crypto`
**Verified by:** `tests/dep-graph.test.ts` (regex scan failing the suite on any banned import).
### Requirement: Namespaced error code registry
The package SHALL define an `SMS_PROVIDERS_CODES` registry via `defineCodes("SMS_PROVIDERS", […])` containing exactly eight codes — `HTTP_ERROR`, `TIMEOUT`, `PARSE_ERROR`, `CONFIG_MISSING`, `SIGNATURE_INVALID`, `INVALID_NUMBER`, `PROVIDER_NOT_REGISTERED`, `PROVIDER_ALREADY_REGISTERED` — and SHALL export a `SmsProvidersError` discriminated subclass of `SuaveplanError` carrying optional `provider`, `httpStatus`, `url`, and `bodyExcerpt` discriminants alongside six factory helpers (`httpError`, `timeoutError`, `parseError`, `configMissing`, `signatureInvalid`, `providerNotRegistered`/`providerAlreadyRegistered`).
#### Scenario: Codes are namespaced
- **WHEN** `SMS_PROVIDERS_CODES.HTTP_ERROR` is read
- **THEN** its value equals `"SMS_PROVIDERS.HTTP_ERROR"`
#### Scenario: Factories propagate discriminants
- **WHEN** `httpError("twilio", 422, "https://api.twilio.com/x", "body", "msg")` is called
- **THEN** the returned error has `code === "SMS_PROVIDERS.HTTP_ERROR"`, `provider === "twilio"`, `httpStatus === 422`, `url === "https://api.twilio.com/x"`, and `bodyExcerpt.startsWith("body")`
**Verified by:** `src/error-codes/error-codes.test.ts` (every code + every factory exercised; body-excerpt truncation at 256 chars asserted).
### Requirement: `httpRequest` over injectable `FetchLike`
The package SHALL ship a universal `httpRequest<T>` helper that wraps an injectable `FetchLike` (defaulting to `globalThis.fetch.bind(globalThis)`), serialises JSON or `string` bodies, composes a caller-supplied `AbortSignal` with an internal timeout-driven signal via `composeAbortSignal`, raises `SmsProvidersError(SMS_PROVIDERS.TIMEOUT)` on the internal abort, `SmsProvidersError(SMS_PROVIDERS.HTTP_ERROR)` on non-2xx responses (with the request body excerpted to 256 chars), and `SmsProvidersError(SMS_PROVIDERS.PARSE_ERROR)` on JSON parse failure.
#### Scenario: Timeout fires the typed error
- **WHEN** `httpRequest` is invoked with `timeoutMs: 5` against a fetch that never resolves
- **THEN** it rejects with `SmsProvidersError` carrying `code === "SMS_PROVIDERS.TIMEOUT"`
**Verified by:** `src/http/http.test.ts` (timeout, abort propagation, JSON+form body shapes, non-2xx → typed error, parse failure → typed error, header forwarding).
### Requirement: Provider registry
The package SHALL ship a `ProviderRegistry` indexing `SmsProvider` instances by `name`, with `register` throwing `PROVIDER_ALREADY_REGISTERED` on duplicate names, `get` throwing `PROVIDER_NOT_REGISTERED` on absent names, `has`, `unregister` (idempotent), `list()` returning a frozen insertion-ordered array, and `size`.
#### Scenario: Registry rejects duplicate names
- **WHEN** `register(provider1)` and `register(provider2)` are called with `provider1.name === provider2.name`
- **THEN** the second call throws `SmsProvidersError(SMS_PROVIDERS.PROVIDER_ALREADY_REGISTERED)` with `provider` discriminant set to the colliding name
**Verified by:** `src/registry/registry.test.ts`.
### Requirement: Five real provider clients
The package SHALL ship `TwilioProvider`, `VonageProvider`, `MessageBirdProvider`, `PlivoProvider`, and `AwsSnsProvider`, each implementing the `SmsProvider` interface from `@suaveplan/sms` (literal `name`, `send(message, options?)`, `getStatus(messageId)`). Every implementation SHALL issue real HTTP requests through `httpRequest`, honour the caller-supplied `AbortSignal`, fail fast with `SmsProvidersError(SMS_PROVIDERS.CONFIG_MISSING)` on missing required credentials at construction time, and expose a `verifyWebhookSignature(...)` method using the provider's documented signing scheme.
#### Scenario: Twilio issues the documented Messages.json POST
- **WHEN** `new TwilioProvider({accountSid:"AC1", authToken:"t", fetch}).send({to:"+1…", from:"+1…", body:"hi"})` is called
- **THEN** the captured request method is `"POST"`, URL ends with `/2010-04-01/Accounts/AC1/Messages.json`, `Authorization` header equals `Basic ${base64(AC1:t)}`, `Content-Type` is `application/x-www-form-urlencoded`, and the body contains `To=%2B1%E2%80%A6`, `From=…`, `Body=hi`
#### Scenario: Vonage issues the documented /sms/json POST
- **WHEN** `new VonageProvider({apiKey:"k", apiSecret:"s", fetch}).send({to:"447…", from:"Sender", body:"hi"})` is called
- **THEN** the captured request URL ends with `/sms/json`, the form body contains `api_key=k`, `api_secret=s`, `from=Sender`, `to=447…`, `text=hi`, and `type` is `unicode` whenever the body contains a non-ASCII codepoint
#### Scenario: MessageBird issues the documented /messages POST
- **WHEN** `new MessageBirdProvider({accessKey:"K", fetch}).send({to:"+1…", from:"Sender", body:"hi"})` is called
- **THEN** the captured request `Authorization` header equals `"AccessKey K"`, `Content-Type` is `application/json`, and the JSON body has `recipients: ["+1…"]`, `originator: "Sender"`, `body: "hi"`
#### Scenario: Plivo issues the documented Message POST
- **WHEN** `new PlivoProvider({authId:"A", authToken:"T", fetch}).send({to:"+1…", from:"+1…", body:"hi"})` is called
- **THEN** the captured URL ends with `/v1/Account/A/Message/`, `Authorization` equals `Basic ${base64("A:T")}`, `Content-Type` is `application/json`, and the JSON body has `src`, `dst`, `text`
#### Scenario: AWS SNS issues a SigV4-signed Publish POST
- **WHEN** `new AwsSnsProvider({region:"us-east-1", accessKeyId:"K", secretAccessKey:"S", fetch}).send({to:"+1…", body:"hi"})` is called
- **THEN** the captured URL hostname matches `sns.us-east-1.amazonaws.com`, `Authorization` starts with `AWS4-HMAC-SHA256 Credential=K/`, contains `SignedHeaders=`, contains `Signature=` (64 hex chars), and the form body contains `Action=Publish&Message=hi&PhoneNumber=%2B1%E2%80%A6`
**Verified by:** one `<provider>.test.ts` per provider that binds a real `node:http` server on a random port, configures the provider with a `FetchLike` pointing at that server, dispatches the operation, captures the raw request, and asserts URL/method/headers/body/SigV4-shape; ditto for `getStatus`; all under `bunx turbo test --filter=@suaveplan/sms-providers` (verified by tests).
### Requirement: Webhook signature verification
Each provider class SHALL expose `verifyWebhookSignature(...)` returning `true` on a valid signature and throwing `SmsProvidersError(SMS_PROVIDERS.SIGNATURE_INVALID)` (also recorded on the `signature_failures_total` counter) on an invalid one. The signing algorithms are: Twilio HMAC-SHA1 over `url + sorted-form-fields`, Vonage HMAC-SHA256 over `sorted-params-without-sig`, MessageBird JWT HS256 with `iat`/`exp`/`nbf` window enforcement, Plivo HMAC-SHA256 over `url + nonce + sorted-form-fields`, AWS SNS PEM-cert RSA-SHA1 over the canonical string-to-sign assembled from the message fields.
#### Scenario: A tampered Twilio webhook is rejected
- **WHEN** the body is mutated after signing and `verifyWebhookSignature(url, params, signature)` is called
- **THEN** the call throws `SmsProvidersError(SMS_PROVIDERS.SIGNATURE_INVALID)` with `provider === "twilio"` and increments `sms_providers.signature_failures_total{provider="twilio"}`
**Verified by:** one signature suite per provider in `<provider>.test.ts` using real `node:crypto` HMAC + RSA to compute expected signatures, exercising accept + reject paths (verified by tests).
### Requirement: Status mapping
Each provider class SHALL expose a `mapStatus(raw: string): SmsDeliveryStatus` method covering at minimum the provider's documented terminal and in-flight states (Twilio: queued/sending/sent/delivered/undelivered/failed; Vonage: submitted/buffered/delivered/expired/failed/rejected/accepted; MessageBird: scheduled/sent/buffered/delivered/expired/delivery_failed; Plivo: queued/sent/delivered/undelivered/failed/rejected; AWS SNS: pending/success/failure/optout), and SHALL default unknown values to `"failed"`.
#### Scenario: Case-insensitive status mapping
- **WHEN** `TwilioProvider#mapStatus("DELIVERED")` is invoked
- **THEN** it returns `"delivered"`
**Verified by:** dedicated `mapStatus` describe block per provider (verified by tests).
### Requirement: OTel instrumentation
The package SHALL emit exactly five OTel instruments — `sms_providers.send_total{provider,outcome}` (counter), `sms_providers.webhook_total{provider,event,outcome}` (counter), `sms_providers.signature_failures_total{provider}` (counter), `sms_providers.send_duration_ms{provider}` (histogram), `sms_providers.webhook_verify_duration_ms{provider}` (histogram) — via helpers `recordSend`, `recordWebhook`, `recordSignatureFailure`, `recordSendDuration`, `recordWebhookVerifyDuration`, all routed through `@suaveplan/opentelemetry`.
#### Scenario: A successful send increments `send_total{outcome="ok"}`
- **WHEN** `recordSend("twilio", "ok")` is called
- **THEN** the recorder receives an increment of 1 on the `send_total` counter with attributes `{provider:"twilio", outcome:"ok"}`
**Verified by:** `src/otel/otel.test.ts` using an in-memory test reader (verified by tests).
### Requirement: Inbound opt-in/opt-out keyword detection
The package SHALL ship a universal `parseOptOutKeyword(body: string): OptOutAction` recognising the canonical compliance keywords STOP / STOPALL / UNSUBSCRIBE / CANCEL / END / QUIT (→ `"opt_out"`), START / YES / UNSTOP (→ `"opt_in"`), HELP / INFO (→ `"help"`), and any other body → `"none"`. Recognition SHALL be case-insensitive and ignore surrounding whitespace.
#### Scenario: STOP is recognised regardless of case and whitespace
- **WHEN** `parseOptOutKeyword(" Stop ")` is called
- **THEN** it returns `"opt_out"`
**Verified by:** `src/keywords/keywords.test.ts` (every keyword + non-match path) (verified by tests).

View File

@@ -0,0 +1,44 @@
# Tasks: enterprise-sms-providers
- [ ] 1.1 Scaffold package at `packages/communication/messaging/sms-providers/` with `package.json` v0.1.0, vite + dts build, `vitest.config.ts`, `tsconfig.json` + `tsconfig.lib.json`, `tests/setup/polyfills.ts`.
- [ ] 1.2 Add `dependencies`: `@suaveplan/error`, `@suaveplan/sms`, `@suaveplan/opentelemetry`, `tslib`, `zod`; `devDependencies`: testing infrastructure + node types.
- [ ] 2.1 `src/error-codes/error-codes.ts` — `SMS_PROVIDERS_CODES` via `defineCodes("SMS_PROVIDERS", [8 codes])`, `SmsProvidersError` discriminated class, six factory helpers, eager metadata.
- [ ] 2.2 `src/error-codes/error-codes.md` — six required sections.
- [ ] 2.3 `src/error-codes/error-codes.test.ts` — every factory covered (verified by tests).
- [ ] 3.1 `src/types/types.ts` — `FetchLike`, `HttpOptions`, `ProviderResponse`, per-provider config interfaces (`TwilioConfig`, `VonageConfig`, `MessageBirdConfig`, `PlivoConfig`, `AwsSnsConfig`), `InboundSmsEvent` discriminated union, `WebhookVerifyOptions`.
- [ ] 3.2 `src/types/types.md` — six required sections.
- [ ] 4.1 `src/http/http.ts` — `composeAbortSignal` + `httpRequest<T>` over `FetchLike` with timeout + typed-error translation.
- [ ] 4.2 `src/http/http.md` — six required sections.
- [ ] 4.3 `src/http/http.test.ts` — happy path, JSON body, form body, timeout, non-2xx → `HTTP_ERROR`, parse failure → `PARSE_ERROR`, abort propagation, header forwarding (verified by tests).
- [ ] 5.1 `src/registry/registry.ts` — `ProviderRegistry` (register/get/has/unregister/list/size) throwing typed errors on duplicate/missing.
- [ ] 5.2 `src/registry/registry.md`.
- [ ] 5.3 `src/registry/registry.test.ts` (verified by tests).
- [ ] 6.1 `src/otel/otel.ts` — meter + 5 instruments + `recordSend`, `recordWebhook`, `recordSignatureFailure`, `recordSendDuration`, `recordWebhookVerifyDuration` helpers.
- [ ] 6.2 `src/otel/otel.md`.
- [ ] 6.3 `src/otel/otel.test.ts` — verifies every helper records on the corresponding instrument (verified by tests).
- [ ] 7.1 `src/keywords/keywords.ts` — STOP/UNSTOP/HELP/START keyword detector + canonical `OptOutAction` enum.
- [ ] 7.2 `src/keywords/keywords.md`.
- [ ] 7.3 `src/keywords/keywords.test.ts` (verified by tests).
- [ ] 8.1 `src/twilio/twilio.ts` — `TwilioProvider implements SmsProvider`: HTTP Basic auth, form-encoded POST, MMS `MediaUrl[]`, `mapStatus`, status polling, `verifyWebhookSignature(rawBody, url, headers)` (HMAC-SHA1 via injectable `HmacSign`), `parseInbound(body)` → `InboundSmsEvent`.
- [ ] 8.2 `src/twilio/twilio.md`.
- [ ] 8.3 `src/twilio/twilio.test.ts` — `node:http` round-trip on random port (send), status fetch round-trip, MMS, status mapping, signature accept + reject, inbound parse (verified by tests).
- [ ] 9.1 `src/vonage/vonage.ts` — `VonageProvider` (api_key/api_secret body params, alpha/short/long sender parsing, HMAC-SHA256 webhook verifier).
- [ ] 9.2 `src/vonage/vonage.md`.
- [ ] 9.3 `src/vonage/vonage.test.ts` — round-trip + status + signature accept/reject + inbound (verified by tests).
- [ ] 10.1 `src/messagebird/messagebird.ts` — `MessageBirdProvider` (`AccessKey` header, JWT HS256 webhook verify with timestamp freshness window).
- [ ] 10.2 `src/messagebird/messagebird.md`.
- [ ] 10.3 `src/messagebird/messagebird.test.ts` (verified by tests).
- [ ] 11.1 `src/plivo/plivo.ts` — `PlivoProvider` (Basic auth, JSON body, X-Plivo-Signature-V3 canonical-string HMAC-SHA256 verify).
- [ ] 11.2 `src/plivo/plivo.md`.
- [ ] 11.3 `src/plivo/plivo.test.ts` (verified by tests).
- [ ] 12.1 `src/aws-sns/aws-sns.ts` — `AwsSnsProvider`: AWS SigV4 signer (computed inline), `Action=Publish` form body, `SMSType`/sender attributes; SNS-notification verifier with injectable PEM-cert resolver.
- [ ] 12.2 `src/aws-sns/aws-sns.md`.
- [ ] 12.3 `src/aws-sns/aws-sns.test.ts` (verified by tests).
- [ ] 13.1 `src/index.ts` — explicit named re-exports of all public symbols.
- [ ] 13.2 `README.md` describing public surface, dependency rationale, quick-start examples for each provider.
- [ ] 14.1 Pre-publish gate: `bunx turbo lint typecheck test build --filter=@suaveplan/sms-providers` green.
- [ ] 14.2 Coverage gate: `bunx turbo test:coverage --filter=@suaveplan/sms-providers` ≥ 100/100/100/100.
- [ ] 15.1 Refresh MANIFEST via `bun scripts/folder-reorg/generate-manifest.ts`.
- [ ] 15.2 Fold spec delta into `openspec/specs/sms-providers/spec.md` and archive change to `openspec/archive/2026-05-25-enterprise-sms-providers/`.
- [ ] 15.3 Flip STATUS.md row `–` → `✅`.
- [ ] 16.1 Commit on `feat/migrate-sms-providers` with Conventional message + Claude co-author.