Files
2026-05-27 23:29:53 +01:00

35 lines
3.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.