# @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`. ## 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`).… - [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`.… ## 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