@volter/twin-sendblue
v0.1.37
Published
Local SendBlue twin for the iMessage/SMS messaging surface (send-message + media_url, HMAC-SHA256-signed sb-signature status callbacks with a deterministic QUEUED->SENT->DELIVERED/ERROR lifecycle, evaluate-service lookup, message history) built on @volter
Readme
@volter/twin-sendblue
Legacy connector helpers: this package still has callable helpers using the retired v1
syncPullAPI. Those paths require migration before use on the current kernel; older helper descriptions below do not establish current compatibility. Check the generated index for protocol standing and use the shared model for current state semantics.
Local SendBlue twin for the iMessage/SMS messaging surface: send-message (text +
media_url + send_style) behind sb-api-key-id/sb-api-secret-key header auth, a
deterministic QUEUED→SENT→DELIVERED status lifecycle (fail-pattern numbers → terminal
ERROR) advanced as a poll-fold — one step per read of the message history — whose
transitions deliver sb-signature-signed (HMAC-SHA256 hex over the raw JSON body) status
callbacks through an injected deliverer, an evaluate-service iMessage/SMS lookup (pure
deterministic derivation), a message-history read surface, and the
GET /twin/store/messages store door — built on the shared @volter/world-core kernel. API-first vendor (no product UI to mirror — the real SendBlue
surface an integration touches is the REST API; the SendBlue dashboard is a config/analytics
console, not an agent navigation target).
bun packages/twin/sendblue/src/cli.tsCoverage
This is a v1 slice of the SendBlue API (single host, api.sendblue.co; JSON request bodies —
the inverse of twilio's form-urlencoded surface). Grounding: the PeakHealth backend's
production SendBlue consumer (SendblueClient / SendblueWebhookController — the
authoritative consumer contract: auth header names, request/response field read set,
HMAC-SHA256-hex sb-signature verification) plus SendBlue's public docs
(docs.sendblue.com, fetched read-only during this build: endpoints, callback payload field
names, the status enum, error-code list, per-line rate limits). SendBlue publishes no
machine-readable first-party spec, so specifics neither source pins down — exact HTTP status
codes, the error envelope beyond error_message — are annotated ⚠ in the manifest and never
claimed beyond what the verify() proves. Errors carry {status:'ERROR', error_message}
(error_message is the consumer-grounded field).
Modeled done (31): header auth (accept the world's established pair / 401-reject others,
credentials-are-state), send-message
(happy path + missing-number/missing-content/invalid-number 400s + JSON-only body parsing +
media_url roundtrip + send_style echo + minted uuid-shaped message_handle), the exact
QUEUED→SENT→DELIVERED lifecycle as a poll-fold (kernel-folded, deterministic, no wall clock,
no timer) with per-transition sb-signature-signed callback delivery over an injected deliverer, the
failure-simulation path (a recipient number matching the configurable fail pattern — default
ending 0000 — goes QUEUED→ERROR, terminal, with error_code/error_message), signature
sign/verify/tamper (consumer parity), evaluate-service (deterministic derivation + 400),
message history (GET /accounts/messages), the {error_message} error envelope,
readOnly/unmodeled-route safety, kernel-persisted state, the self-referential conformance
snapshot, and a list-driven connector pull (idempotent).
Left as todo (honest gaps, not yet modeled — 23): group messages
(send-group-message, modify-group), expressive-style catalog validation, history
pagination/filters, RCS as a service class, media-object upload + voice notes, typing
indicators, inbound message webhooks (+ inbound media), the ACCEPTED intermediate status,
was_downgraded/service callback enrichment, callback retry policy, the documented
error-code catalog, lines/seats/rate-limits/number-registration lifecycle, lookup rate
limits, connector push + lines pull, and a fixtures seed.
The delivery lifecycle is a POLL-FOLD, not a timer
A send answers QUEUED and schedules nothing. Every read of the message history
(GET /accounts/messages — the vendor's own read, and the poll) folds each stored message one
step from (stored row, the request's world instant) and delivers that transition's
sb-signature-signed status_callback as part of the fold, stamped from the same instant.
The fold is the pure function foldSendblueStatus(row, at): a step lands only when the world
clock has moved past the row's date_updated, so
- re-serving one read at one instant is byte-identical (serve-path determinism, R9), and
- a consumer polling a live world walks
QUEUED → SENT → DELIVEREDone status per read, and - a frozen world holds its messages exactly where they are until the clock advances — which is this twin's stated time model (clock-set, seed, clock-advance).
The vendor's async ordering is preserved where a consumer depends on it: the send response
carries the message_handle and no callback has fired yet, so the handle is stored before
the first callback arrives. There is no setTimeout and no wall-clock read anywhere on the
serve path.
The store door: GET /twin/store/messages
GET /twin/store/messages is the twin's own named, read-only, deterministic projection
over stored state (the twin programming model's store door; GET /twin names it under
stores). The vendor's only read of a message IS the poll, which advances the lifecycle by
design — so this is the read that observes a sent message without moving it. It is a twin
door, not vendor surface: keyless, never folding, and deliberately absent from the capability
manifest (counting scaffolding SendBlue does not have would pad the denominator).
The failure-simulation convention (D2 honesty)
No real send can fail locally, so the twin simulates the vendor's ERROR path
deterministically: a recipient number matching the fail pattern (default 0000$,
configurable via config.failNumberPattern / SENDBLUE_TWIN_FAIL_PATTERN) folds
QUEUED → ERROR (terminal) on the next read, with error_code/error_message set, and its
signed callback delivers like any other transition. Real Apple/carrier delivery physics are out
of scope — lifecycle semantics and callback signing are the faithful part.
sb-signature
hex(HMAC-SHA256(webhookSigningSecret, rawJsonBody)), sent in the sb-signature header —
written FRESH in src/sendblue-signing.ts (never ports twilio's HMAC-SHA1-base64 or the
svix v1, scheme). Byte-for-byte what the PeakHealth consumer's verifyWebhookSignature
recomputes (case-insensitive hex compare, length-checked timingSafeEqual); ⚠ the public
docs don't document the signing algorithm, so the scheme is grounded in the real consumer's
verification code rather than a vendor spec.
Credentials are STATE, not a function of the world's path
The accepted sb-api-key-id/sb-api-secret-key pair and the sb-signature signing secret are
the root's own persisted credential resource, established exactly once and folded through
the kernel action log (so they ride snapshots/hydrate/flush and survive a process restart). A
root's pair is established in one of two ways:
- minted with real entropy —
sendblueCredentials(root)/sendblueApiKeyId(root)/sendblueApiSecretKey(root)/sendblueSigningSecret(root)on a virgin world generate one fromrandomBytes, so two fresh worlds are distinct by construction; or - seeded via the wire — the first request to reach a virgin world carries a pair and that
pair becomes the world's credentials (the runtime contract's "fake vendor creds an app
presents are twin state, seeded via the wire"). Every later request must match it or gets a
401.
They were previously sha256('sendblue-twin-…:' + root) — the world's directory was the
credential, so identical worlds in different directories disagreed and moving a world dir
rotated its keys. The path now never enters a credential, a signature, or a served byte, and
nothing this twin serves ever carries one (the pair is compared, never echoed).
Config (sane local defaults)
apiKeyId/apiSecretKey/signingSecret come from that credential state and can be overridden
per request (config) or on the server via the SAME env names the PeakHealth consumer uses
(SENDBLUE_API_KEY_ID, SENDBLUE_API_SECRET_KEY, SENDBLUE_WEBHOOK_SIGNING_SECRET), plus
twin-only SENDBLUE_TWIN_FROM_NUMBER / SENDBLUE_TWIN_FAIL_PATTERN.
Inspection
GET /twin/store/messages (the store door — observes without folding), GET /accounts/messages
(vendor-shaped — the poll, which folds), or the in-process listSendblueMessages(root)
accessor behind the door, which adds status_history (every status the lifecycle emitted) and
the registered status_callback URL per message — the "message received by SendBlue" assertion
surface for tests and pilots.
