3.9 KiB
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:
- The same provider class works in Bun, Node, browsers, Workers, and Deno.
- Tests inject a recording
FetchLike(e.g. one that proxies to anode:httpserver on a random port) without monkey-patchingglobalThis.fetch. - Bundle size stays bounded — every provider weighs in around 250–300 LOC including its signature verifier and status map.
- 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-rsawhen they wish, or rely onglobalThis.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.