Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,87 @@
# `opt-out`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`opt-out` is the in-memory opt-out registry that `SmsService`
consults before every outbound message. Recipients on the
registry receive no further messages — `service.send` throws
`SmsRecipientOptedOutError` (code `SMS.RECIPIENT_OPTED_OUT`).
Entries can be scoped to a channel so transactional messages
remain deliverable when only marketing has been opted out by the
host application.
The registry is intentionally pluggable but ships as an in-process
`Map` so unit tests and small deployments need no external state;
production hosts that share opt-out across processes can wrap
`OptOutRegistry` in their own persistence layer.
## Features
- `add(phoneNumber, channel?)` — records the opt-out with a
timestamp from an injectable `Clock`.
- `remove(phoneNumber)` — opt-in helper.
- `has(phoneNumber)` and `get(phoneNumber)` — predicate +
detail accessors.
- `list()` snapshot, `size()`, and `clear()` for bulk operations.
- `importEntries(entries)` — bulk import with overwrite-by-id
semantics for syncing from an external source.
## Usage: Basic
```ts
import { OptOutRegistry } from "@suaveplan/sms/opt-out";
const reg = new OptOutRegistry();
reg.add("+15551234567", "marketing");
console.log(reg.has("+15551234567")); // true
reg.remove("+15551234567");
```
## Usage: Advanced
```ts
import { OptOutRegistry } from "@suaveplan/sms/opt-out";
// Inject a deterministic clock for tests; importEntries
// reseeds the registry from a downstream cache.
const reg = new OptOutRegistry({ clock: () => 1_700_000_000_000 });
reg.importEntries([
{
phoneNumber: "+15550000001",
optedOutAt: new Date(1_700_000_000_000),
channel: "marketing",
},
{
phoneNumber: "+15550000002",
optedOutAt: new Date(1_700_000_000_000),
},
]);
console.log(reg.size()); // 2
```
## API Reference
- `class OptOutRegistry`
- `constructor(options?: { clock?: Clock })`
- `add(phoneNumber: string, channel?: string): OptOutEntry`
- `remove(phoneNumber: string): boolean`
- `has(phoneNumber: string): boolean`
- `get(phoneNumber: string): OptOutEntry | undefined`
- `list(): ReadonlyArray<OptOutEntry>`
- `importEntries(entries: ReadonlyArray<OptOutEntry>): void`
- `clear(): void`
- `size(): number`
## Implementation Notes
The registry stores `OptOutEntry` records keyed by the verbatim
phone number — no normalisation. Callers SHOULD pass E.164
strings; mismatched formats will fail to match. `add` overwrites
existing entries by `phoneNumber`, so a caller switching the
`channel` on an existing opt-out simply re-adds. The `clock`
defaults to `Date.now`, deterministic only when explicitly
injected.