35 lines
3.9 KiB
Markdown
35 lines
3.9 KiB
Markdown
# 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.
|