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

134 lines
9.9 KiB
Markdown

# @suaveplan/sms - Universal SMS core: provider-agnostic send pipeline with E.164 validation, Zod boundaries, template rendering, opt-out registry, token-bucket rate limiter, exponential-backoff retry, message scheduler, bounded delivery-tracker LRU cache, typed EventBus, legacy on/off mirror, in-memory test provider, four OTel instruments, thirteen SMS.* error codes via defineCodes, and back-compat for upstream SmsError + SMS_ERROR_METADATA.
Universal SMS core: provider-agnostic send pipeline with strict
E.164 validation, Zod boundaries, template rendering, opt-out
registry, token-bucket rate limiter, exponential-backoff retry with
abort propagation, message scheduling, bounded LRU delivery tracker,
typed `EventBus` + legacy `on/off/emit` mirror, an in-memory test
provider, four OpenTelemetry instruments, thirteen `SMS.*` error
codes via `defineCodes`, and full back-compat for the upstream
`SmsError` class and `SMS_ERROR_METADATA` table.
This package is the SMS core. Concrete provider integrations
(Twilio, Vonage, AWS SNS, etc.) ship in `@suaveplan/sms-providers`.
<!-- begin:auto-docs -->
## Documentation tree
- [delivery-tracker](./src/delivery-tracker/delivery-tracker.md) — `delivery-tracker` is the bounded LRU cache of `SmsDeliveryReport` records that `SmsService` consults for delivery stat…
- [delivery-tracker](./src/delivery-tracker/delivery-tracker.md) — `delivery-tracker` is the bounded LRU cache of `SmsDeliveryReport` records that `SmsService` consults for delivery stat…
- [error-codes](./src/error-codes/error-codes.md) — The error-codes module owns the canonical `SMS.*` registry for `@suaveplan/sms`. Every error surfaced by the package —…
- [error-codes](./src/error-codes/error-codes.md) — The error-codes module owns the canonical `SMS.*` registry for `@suaveplan/sms`. Every error surfaced by the package —…
- [in-memory-provider](./src/in-memory-provider/in-memory-provider.md) — `InMemorySmsProvider` is the production-quality in-process `SmsProvider` implementation that ships with `@suaveplan/sms…
- [in-memory-provider](./src/in-memory-provider/in-memory-provider.md) — `InMemorySmsProvider` is the production-quality in-process `SmsProvider` implementation that ships with `@suaveplan/sms…
- [legacy-errors](./src/legacy-errors/legacy-errors.md) — `legacy-errors` preserves the upstream `@suaveplan/sms` v1 public surface — the `SmsError` class, the `SmsErrorCode` st…
- [legacy-errors](./src/legacy-errors/legacy-errors.md) — `legacy-errors` preserves the upstream `@suaveplan/sms` v1 public surface — the `SmsError` class, the `SmsErrorCode` st…
- [legacy-metadata](./src/legacy-metadata/legacy-metadata.md) — `legacy-metadata` preserves the upstream `SMS_ERROR_METADATA` table — the per-code mapping of `httpStatus`, `severity`,…
- [legacy-metadata](./src/legacy-metadata/legacy-metadata.md) — `legacy-metadata` preserves the upstream `SMS_ERROR_METADATA` table — the per-code mapping of `httpStatus`, `severity`,…
- [opt-out](./src/opt-out/opt-out.md) — `opt-out` is the in-memory opt-out registry that `SmsService` consults before every outbound message. Recipients on the…
- [opt-out](./src/opt-out/opt-out.md) — `opt-out` is the in-memory opt-out registry that `SmsService` consults before every outbound message. Recipients on the…
- [otel](./src/otel/metrics.md) — `otel/metrics` is the OpenTelemetry instrumentation layer for `@suaveplan/sms`. It owns four meter instruments — three…
- [metrics](./src/otel/metrics.md) — `otel/metrics` is the OpenTelemetry instrumentation layer for `@suaveplan/sms`. It owns four meter instruments — three…
- [rate-limiter](./src/rate-limiter/rate-limiter.md) — `rate-limiter` is the token-bucket throttle that `SmsService` consults before every outbound send. The limiter is keyed…
- [rate-limiter](./src/rate-limiter/rate-limiter.md) — `rate-limiter` is the token-bucket throttle that `SmsService` consults before every outbound send. The limiter is keyed…
- [retry](./src/retry/retry.md) — `retry` is the exponential-backoff retry policy used by `SmsService.send` to absorb transient provider failures. The po…
- [retry](./src/retry/retry.md) — `retry` is the exponential-backoff retry policy used by `SmsService.send` to absorb transient provider failures. The po…
- [routes](./src/routes/problem-json.md) — `routes/problem-json` is the RFC 7807 `application/problem+json` response helper for hosts wiring `@suaveplan/sms` erro…
- [problem-json](./src/routes/problem-json.md) — `routes/problem-json` is the RFC 7807 `application/problem+json` response helper for hosts wiring `@suaveplan/sms` erro…
- [scheduler](./src/scheduler/scheduler.md) — `scheduler` provides `MessageScheduler` — the minimal timer seam that `SmsService.scheduleAt` uses to fire a delayed de…
- [scheduler](./src/scheduler/scheduler.md) — `scheduler` provides `MessageScheduler` — the minimal timer seam that `SmsService.scheduleAt` uses to fire a delayed de…
- [schemas](./src/schemas/schemas.md) — `schemas` is the Zod boundary for every public input that `@suaveplan/sms` exposes. The schemas mirror the public type…
- [schemas](./src/schemas/schemas.md) — `schemas` is the Zod boundary for every public input that `@suaveplan/sms` exposes. The schemas mirror the public type…
- [bus-map](./src/service/bus-map.md) — `bus-map` declares `SmsBusMap` — the single-payload topic contract used by `SmsService.bus` (an `EventBus<SmsBusMap>`).…
- [legacy-emitter](./src/service/legacy-emitter.md) — `LegacyEmitter` is the back-compat shim that preserves the upstream `@suaveplan/sms` v1 `on/off/emit` listener API on t…
- [sms-service](./src/service/sms-service.md) — `SmsService` is the provider-agnostic orchestrator that ties together every primitive in `@suaveplan/sms`. A single `se…
- [templates](./src/templates/templates.md) — `templates` is the in-process template registry and renderer used by `SmsService` to expand `{{variable}}` placeholders…
- [templates](./src/templates/templates.md) — `templates` is the in-process template registry and renderer used by `SmsService` to expand `{{variable}}` placeholders…
- [validation](./src/validation/phone.md) — `validation/phone` is the strict-E.164 phone number validator used by every public input boundary in `@suaveplan/sms`.…
- [phone](./src/validation/phone.md) — `validation/phone` is the strict-E.164 phone number validator used by every public input boundary in `@suaveplan/sms`.…
<!-- end:auto-docs -->
## Highlights
- **`SmsService`** — orchestrates the full send pipeline: abort
check → provider check → E.164 validation → opt-out check →
rate-limit → template render → retry → provider send → delivery
tracker ingest → OTel + bus emit.
- **`InMemorySmsProvider`** — production-quality in-process
`SmsProvider` for tests and demos; records every sent message,
supports configurable failure predicates and status transitions.
- **`TemplateRegistry`** + `renderTemplate` — `{{var}}` substitution
with template-level and call-level `defaults` maps.
- **`OptOutRegistry`** — channel-scoped opt-out / opt-in with bulk
import/export.
- **`RateLimiter`** — token bucket per `(channel, recipient)` key
with injectable `Clock`.
- **`RetryPolicy`** — exponential backoff with full jitter,
AbortSignal-aware, injectable `Timers` seam.
- **`MessageScheduler`** — `scheduleAt(date, fn)` with cancel
handle, injectable timers + clock.
- **`DeliveryTracker`** — bounded LRU receipt cache with provider
fall-back via `lookupOrFetch`.
- **Typed `EventBus`** on `service.bus` plus the upstream
`on/off/emit` API mirroring against it.
- **Four OTel instruments**: `sms.sent_total`, `sms.failed_total`,
`sms.retry_total`, `sms.send_duration_ms`.
- **Thirteen `SMS.*` error codes** via `defineCodes("SMS", [...])`
plus the upstream `SmsError` legacy class preserved verbatim for
back-compat.
## Install
```bash
bun add @suaveplan/sms
```
## Quick start
```ts
import {
InMemorySmsProvider,
SmsService,
} from "@suaveplan/sms";
const provider = new InMemorySmsProvider();
const service = new SmsService({ provider });
service.registerTemplate({
id: "welcome",
body: "Hi {{name}}!",
variables: ["name"],
});
service.bus.on("sms.sent", (report) => {
console.log("delivered", report.messageId, "→", report.to);
});
await service.send({
to: "+15551234567",
body: "ignored when templateId is set",
templateId: "welcome",
templateVars: { name: "Alice" },
});
```
## Modules
- [`error-codes/error-codes.md`](./src/error-codes/error-codes.md) — `defineCodes("SMS", [...])`, leaf classes
- [`legacy-errors/legacy-errors.md`](./src/legacy-errors/legacy-errors.md) — upstream `SmsError` back-compat
- [`legacy-metadata/legacy-metadata.md`](./src/legacy-metadata/legacy-metadata.md) — `SMS_ERROR_METADATA` table
- [`schemas/schemas.md`](./src/schemas/schemas.md) — Zod boundary parsers
- [`validation/phone.md`](./src/validation/phone.md) — strict E.164 validator
- [`templates/templates.md`](./src/templates/templates.md) — `TemplateRegistry`, `renderTemplate`
- [`opt-out/opt-out.md`](./src/opt-out/opt-out.md) — `OptOutRegistry`
- [`rate-limiter/rate-limiter.md`](./src/rate-limiter/rate-limiter.md) — token-bucket limiter
- [`retry/retry.md`](./src/retry/retry.md) — `RetryPolicy`
- [`scheduler/scheduler.md`](./src/scheduler/scheduler.md) — `MessageScheduler`
- [`delivery-tracker/delivery-tracker.md`](./src/delivery-tracker/delivery-tracker.md) — bounded LRU receipt cache
- [`in-memory-provider/in-memory-provider.md`](./src/in-memory-provider/in-memory-provider.md) — `InMemorySmsProvider`
- [`service/sms-service.md`](./src/service/sms-service.md) — orchestrator
- [`service/legacy-emitter.md`](./src/service/legacy-emitter.md) — legacy `on/off/emit` mirror
- [`routes/problem-json.md`](./src/routes/problem-json.md) — RFC 7807 Hono helper
- [`otel/metrics.md`](./src/otel/metrics.md) — OpenTelemetry instruments