← all parts

Part · sms.transactional

What's actually behind sms.transactional

The part exactly as partkit add sms.transactional vendors it into your repo — verified, locked, every byte readable. Nothing here is mocked.

sms.transactionalv1.1.0

✓ attested🔒 read-only↑ ctrlai upgradesms.transactional@1

Lives at parts/sms.transactional/ in your repo — open, owned, readable. Not buried in node_modules. 370 lines of source you can audit.

15 conformance tests passedverified 2026-06-15adapter twilio↗ CI run
content hash 825ad5a325…d55d8fpinned in parts.lockctrlai guard fails CI if a single byte changes
tested against node 25.3.0

Public API — what your seam calls

  • send(message: SmsMessage): Promise<SendResult>
  • class SmsError extends Error { code: SmsErrorCode; retryable: boolean; status: number | null }
  • types: SmsMessage, SendResult, SmsErrorCode

Invariants — guarantees the contract pins

  1. Importing the part performs no I/O and never throws; configuration is validated at call time with typed errors
  2. An invalid message fails fast with a typed error and zero network calls — the recipient must be E.164 and the body non-empty
  3. Disallowed control characters in the body or sender are rejected before send (injection/garbage defense)
  4. Transient vendor failures (429, 5xx, network) are retried up to 3 attempts with exponential backoff and jitter; permanent failures are never retried
  5. All failures surface as typed SmsError values; raw vendor responses never escape the part
  6. Secret values never appear in error messages

Dependencies

zero-dep — runs on your Postgres

Swappable adapters

twilioamazon-sns
SOURCEparts/sms.transactional/17 files · click to read
parts/sms.transactional/src/index.tstypescript · 2,140 bytes
/**
 * sms.transactional — public interface. The ONLY legal import surface.
 * Contract: ../contract.json · What your app must provide: ../seams.md
 *
 * Send one transactional SMS through a vendored adapter (twilio / amazon-sns).
 * Importing this module performs no I/O; configuration is validated at call time
 * with typed errors (serverless-safe). Transient vendor failures are retried;
 * every failure surfaces as an SmsError with secrets redacted.
 */
import { requireEnv } from "./internal/config";
import { SmsError } from "./internal/errors";
import { redactSecrets } from "./internal/redact";
import { withRetry } from "./internal/retry";
import type { SmsMessage } from "./internal/types";
import { normalizeMessage } from "./internal/validate";
import { adapter } from "../adapters/selected/adapter";

export { SmsError } from "./internal/errors";
export type { SmsErrorCode } from "./internal/errors";
export type { SmsMessage } from "./internal/types";

export interface SendResult {
  /** Vendor-assigned message id. */
  id: string;
  /** Adapter that performed the send. */
  adapter: string;
}

/** Send one transactional SMS through the vendored adapter. */
export async function send(message: SmsMessage): Promise<SendResult> {
  try {
    const normalized = normalizeMessage(message);

    const configured = requireEnv("SMS_ADAPTER");
    if (configured !== adapter.name) {
      throw new SmsError(
        "config",
        `SMS_ADAPTER is "${configured}" but the vendored adapter is "${adapter.name}" — ` +
          `re-vendor with: partkit upgrade sms.transactional --adapter=${configured}`,
        { retryable: false },
      );
    }

    const result = await withRetry(() => adapter.send(normalized));
    return { id: result.id, adapter: adapter.name };
  } catch (e) {
    if (e instanceof SmsError) {
      throw new SmsError(e.code, redactSecrets(e.message), {
        retryable: e.retryable,
        ...(e.status !== null && { status: e.status }),
      });
    }
    throw new SmsError("unknown", redactSecrets(e instanceof Error ? e.message : String(e)), {
      retryable: false,
    });
  }
}