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

3.9 KiB

OpenSpec Change: enterprise-server-sms

  • Change ID: enterprise-server-sms
  • Package: @suaveplan/sms
  • Category: server/services
  • Target path: server/services/sms
  • Status: PROPOSED
  • Date: 2026-04-24
  • Reference spec: /mnt/s/web/suaveplan/enterprise/sms.md

Summary

Deliver @suaveplan/sms on the genesis branch from scratch at enterprise quality. The package provides SMS send/receive and delivery status tracking with a pluggable provider interface. It ships a SmsService facade, a SmsProvider interface, an InMemoryProvider for testing, and webhook handler utilities for inbound SMS and delivery status callbacks. Genesis ships with full Zod schemas, @suaveplan/error codes, @suaveplan/retry on provider calls, @suaveplan/suppression integration (skip suppressed numbers), OTel spans, and idempotency via @suaveplan/idempotency.


Current-state audit (genesis)

Does not exist on genesis — fresh build.


Available workspace deps on genesis

Package Role
@suaveplan/error (workspace:^) Typed SMS.* error codes
@suaveplan/types (workspace:^) LoggerLike
@suaveplan/retry (workspace:^) Retry provider calls
@suaveplan/suppression (workspace:^) Skip suppressed phone numbers
@suaveplan/idempotency (workspace:^) Dedup retried sends
@suaveplan/templates (workspace:^) SMS body templating
@suaveplan/logger (workspace:^) Structured logs
zod (catalog:forms) Options + message schemas
@opentelemetry/api (catalog:observability) Spans + counters
tslib (catalog:build) Runtime helpers

Public API surface

class SmsService {
  constructor(options: SmsServiceOptions)
  send(message: SmsMessage): Promise<SmsSendResult>
  sendBatch(messages: SmsMessage[]): Promise<SmsSendResult[]>
  getStatus(messageId: string): Promise<SmsDeliveryStatus | undefined>
  handleInbound(payload: unknown): InboundSms
  handleStatusCallback(payload: unknown): SmsDeliveryStatus
}

function createSmsService(options: SmsServiceOptions): SmsService

interface SmsProvider {
  name: string
  send(message: SmsMessage): Promise<SmsSendResult>
  getStatus(messageId: string): Promise<SmsDeliveryStatus | undefined>
}

interface SmsMessage {
  to: string               // E.164 format
  from?: string            // sender ID or number
  body: string
  idempotencyKey?: string
}

interface SmsSendResult {
  messageId: string
  status: 'queued' | 'sent' | 'failed'
  provider: string
  error?: string
}

interface SmsDeliveryStatus {
  messageId: string
  status: 'queued' | 'sent' | 'delivered' | 'failed' | 'undelivered'
  timestamp: number
  errorCode?: string
}

interface InboundSms {
  from: string
  to: string
  body: string
  timestamp: number
  messageId: string
}

interface SmsServiceOptions {
  provider: SmsProvider
  suppressionStore?: SuppressionStore
  maxRetries?: number
  logger?: LoggerLike
}

type SmsErrorCode =
  | 'SMS_SEND_FAILED'
  | 'SMS_INVALID_NUMBER'
  | 'SMS_SUPPRESSED'
  | 'SMS_PROVIDER_UNAVAILABLE'
  | 'SMS_INBOUND_PARSE_FAILED'

function registerSmsErrorCodes(): void

File layout

server/services/sms/
  package.json
  tsconfig.json
  vitest.config.ts
  src/
    index.ts
    error-codes.ts
    constants/index.ts
    types/
      sms.types.ts
      sms.types.md
    service/
      sms-service/
        sms-service.ts
        sms-service.md
        sms-service.types.ts
        sms-service.test.ts
        index.ts
    providers/
      in-memory-provider/
        in-memory-provider.ts
        in-memory-provider.md
        in-memory-provider.test.ts
        index.ts
  tests/
    integration.test.ts

Impact

Downstream: auth OTP, notification service, order confirmations.

Prerequisites on Verdaccio: @suaveplan/error, @suaveplan/types, @suaveplan/retry, @suaveplan/suppression, @suaveplan/idempotency, @suaveplan/templates, @suaveplan/logger.