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

3.9 KiB
Raw Permalink Blame History

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.