feat: messaging-compliance provider abstraction, webhooks, template library, and settings connection-test history - #208
Merged
Conversation
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R6hYGDtjhEfMXwWDz4om21
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R6hYGDtjhEfMXwWDz4om21
…tionConditions Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R6hYGDtjhEfMXwWDz4om21
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R6hYGDtjhEfMXwWDz4om21
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R6hYGDtjhEfMXwWDz4om21
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R6hYGDtjhEfMXwWDz4om21
…valuator Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R6hYGDtjhEfMXwWDz4om21
…eliverAction Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R6hYGDtjhEfMXwWDz4om21
Two incidental gate-fixes surfaced by the full sweep (no automation-core or delivery logic changed): - eslint: ignore `.worktrees/**` (gitignored scratch worktrees were being linted and failing the type-aware parser — same category as the `.wrangler/**` / `.react-router/**` entries already ignored; CI is unaffected, this is local-worktree hygiene). - tenant-scoping baseline: refrozen via `--update`. The Task 8 re-wire relocated the `automationLogs` by-id updates into the delivery logger adapter, shifting their line numbers; every entry is the same provably-safe pattern (PK obtained from the tenant-joined flush query), so the baseline line numbers were re-anchored. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R6hYGDtjhEfMXwWDz4om21
Final-review minor: the prior doc comment claimed "query behavior is byte-identical to the former monolith". Outcomes (verdict + skip-reason) ARE byte-identical, but because the adapter pre-resolves the signed / booked-service facts before the core decides, a rule with multiple gates that skips on an earlier gate may run a later gate's read-only scoped SELECT the old sequential short-circuit would have skipped. Comment-only; no behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R6hYGDtjhEfMXwWDz4om21
…e columns Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R6hYGDtjhEfMXwWDz4om21
…olumns Drizzle-generated DDL: CREATE TABLE message_templates + its tenant+channel index, and ALTER automations ADD email_template_id / sms_template_id. No drops (the frozen subject_template / body_template / sms_body columns stay). db:check green, applied to local D1. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R6hYGDtjhEfMXwWDz4om21
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R6hYGDtjhEfMXwWDz4om21
The plan's verbatim test used 'Café', but é (U+00E9) IS in the GSM-7 basic charset, so the (correct) algorithm classifies it as gsm — making the test's unicode expectation unsatisfiable. Switched to ë (U+00EB), which is absent from GSM-7 and genuinely forces UCS-2. Length (75) and segment count (2) unchanged; the helper itself is untouched. Plan-text typo, not a logic bug. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R6hYGDtjhEfMXwWDz4om21
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R6hYGDtjhEfMXwWDz4om21
…d templates Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R6hYGDtjhEfMXwWDz4om21
…template Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R6hYGDtjhEfMXwWDz4om21
…nt + review_url guards Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R6hYGDtjhEfMXwWDz4om21
…d after decoupling Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R6hYGDtjhEfMXwWDz4om21
…iew, test-send) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R6hYGDtjhEfMXwWDz4om21
Task 7 made deliverSms resolve automation.smsTemplateId via the TemplateStore (replacing the embedded smsBody). sms-metering.spec.ts drives deliverSms with a hand-built ctx + stub db, and its automation had no smsTemplateId, so post-Task-7 deliverSms skipped 'no sms template' before the send/metering path — record was never called. Task 7's targeted test run didn't include this file. Fix: stub createOiTemplateStore to return the sms template body and give the ctx automation an smsTemplateId, so resolution succeeds and the metering contract (records once on success, never on failure) is exercised again. Full test:unit green (2315). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R6hYGDtjhEfMXwWDz4om21
…y fields - Remove subjectTemplate/bodyTemplate/smsBody from CreateAutomationBase, AutomationSchema (response) - Add emailTemplateId/smsTemplateId to CreateAutomationBase (nullish) and AutomationSchema (nullable) - Remove smsBodyRequiredWhenSms superRefine and both .superRefine() call sites (delivery fail-closes) - core.ts create(): accept emailTemplateId/smsTemplateId; write '' tombstones to dead NOT NULL columns - core.ts update(): drop smsBody from patch logic; emailTemplateId/smsTemplateId flow via rest spread - automations.ts: no change confirmed (c.req.valid passes straight through to service) - automation-schema.spec.ts: adapt fixture + 3 tests for removed fields; add 2 SP2 cases - automation-channels.spec.ts: adapt 7 tests (remove subjectTemplate/bodyTemplate/smsBody args; assert smsTemplateId instead of smsBody on create/update round-trips) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R6hYGDtjhEfMXwWDz4om21
…oss-links Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R6hYGDtjhEfMXwWDz4om21
…st-send, compliance SMS) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R6hYGDtjhEfMXwWDz4om21
…+ rebaseline Full SP2 gate sweep surfaced lint:tenant-scope drift. Two changes: - message-template-backfill.ts: the backfill UPDATE now filters by (id AND tenantId) instead of id alone. The id already came from a tenant-scoped SELECT so this is defensive, but it matches the re-pinning pattern of the peer service methods and drops the entry from the scanner. - tenant-scoping-baseline.json: re-anchored the line numbers of the (provably safe, already-accepted) automations/automation_logs by-id reads in core.ts / delivery.ts that shifted when the SP2 template-resolution blocks were added. No new unscoped query is baselined. Full sweep otherwise green: type-check 0, lint 0, db:check no-drift, test:unit 2317, test:web 610, build OK, bundle 1515 KiB gzip (49%). test:workers 68/69 — the lone failure is a pre-existing, unrelated intermittent flake in tests/workers/presence-do.spec.ts (roster-on-close); SP2 touches no presence/DO/workers code. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R6hYGDtjhEfMXwWDz4om21
Final whole-branch review caught a behavior regression the per-task reviews and
characterization tests missed: trigger.ts's "skip agreement-link rules unless the
inspection opted into agreements" filter read the now-DEAD embedded
subject_template/body_template columns. After SP2, user-created/edited automations
carry '' tombstones there (the body lives in the referenced message_template), so
the filter never matched for them — an agreement-template automation would fire on
an inspection with agreementRequired=false and send a sign-agreement link it should
not. Seeded rules were unaffected (ensureSeeds still writes real bodies), which is
why the delivery characterization tests (seeded rules) stayed green; the gap was on
the user-authored path no test exercised.
Fix: resolve the rule's emailTemplateId via createOiTemplateStore and test the
RESOLVED subject/body for {{agreement_sign_url}} (matching what delivery.ts already
does), then apply the same agreementRequired gate. agreement_sign_url is email-only
(the SMS path never resolves it), so only the email template matters. The three
agreement-filter tests in automation.service.spec.ts now seed a message_template +
set emailTemplateId with empty embedded bodies — exercising the exact user-created
path that regressed (regression coverage). 77 automation tests green, type-check:api 0.
Also sweeps three now-stale doc comments flagged by the review (delivery.ts inline
templateStore, sms.ts mixin JSDoc, message-templates.ts CORRECTION dev-note).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R6hYGDtjhEfMXwWDz4om21
crypto.subtle supports {name:'Ed25519'} sign/verify and raw 32-byte public-key
import in the workerd runtime — so the Telnyx inbound verifier (which checks an
Ed25519 signature over `${timestamp}|${rawBody}` against the account public key)
needs no third-party dependency. Decision: use crypto.subtle directly.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R6hYGDtjhEfMXwWDz4om21
Replace the Twilio-shaped 4-arg MessagingProvider.validateInboundSignature
with a single InboundSignatureContext object so each provider reads exactly
the fields its scheme needs. Twilio verifies byte-identically (validateTwilioSignature
unchanged; the wrapper just maps the ctx, reading x-twilio-signature from the
lower-cased header map). Telnyx now verifies Ed25519 over `${ts}|${rawBody}` via
WebCrypto crypto.subtle (no third-party dep), fail-closed on missing sig/timestamp,
+-300s anti-replay, malformed base64, or verify failure. M3: drop the recipient
phone from the Telnyx send-error/fetch-error logs (status/error only).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R6hYGDtjhEfMXwWDz4om21
Complete tenant BYO Telnyx inbound SMS (STOP/HELP/START). The inbound
handler was Twilio-only, so a BYO-Telnyx tenant inbound webhook silently
failed. handleInbound now carries { provider, secret, scopeTenantId },
reads the body once as raw text, and branches:
- twilio: parse URLSearchParams(rawBody) -> params, verify the HMAC
signature over url + sorted params (byte-identical to the prior
formData() path); HELP returns the TwiML auto-reply.
- telnyx: verify the Ed25519 signature over the raw body with the tenant
base64 public key (anti-replay handled by verifyTelnyxSignature), then
parse JSON and extract From = data.payload.from.phone_number,
Body = data.payload.text. Only event_type === message.received is a
user reply; other types and malformed JSON -> 200 no-op. HELP -> empty
200 (Telnyx does not consume TwiML).
Both branches feed from + cmd into one shared STOP/START consent tail.
Fail-closed throughout: missing/invalid signature, missing key,
out-of-tolerance timestamp -> 403; never throws out of verify/parse.
The /sms/inbound/:tenant route reads sms_byo_provider; telnyx tenants load
TELNYX_PUBLIC_KEY from the encrypted secrets envelope. TELNYX_PUBLIC_KEY is
added to INTEGRATION_SECRET_KEYS (un-gated, like TELNYX_API_KEY) and wired
through Settings -> Communication (SmsSecretsPanel field + route state).
Tests: BYO-Telnyx STOP with a valid Ed25519 signature -> revoked; tampered
body -> 403 no change; non-message event -> 200 no-op. Existing Twilio
inbound tests stay green byte-identical.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R6hYGDtjhEfMXwWDz4om21
…cal, #wh1) Completes Telnyx inbound (STOP/HELP) signature verification for OI tenant BYO: ctx-based validateInboundSignature, Telnyx Ed25519 (crypto.subtle, no dep), provider-aware handleInbound (Twilio byte-identical), TELNYX_PUBLIC_KEY tenant secret + Settings field. Whole-branch opus review: READY TO MERGE. LOCAL only — not pushed, no upstream PR, OI superproject pointer not bumped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R6hYGDtjhEfMXwWDz4om21
…test
twilio@6.0.2 + createFetchHttpClient (fetch-backed RequestClient, no
axios/node-http). Workers smoke proves import+construct+resource-call in real
workerd (2/2 green). Bumped vitest.workers.config compat 2024-11-01 -> 2026-04-12
(twilio module-load require('os') is compat-date-gated, not pool-injected);
full workers suite still green (14 files/73 tests).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R6hYGDtjhEfMXwWDz4om21
MessagingComplianceService becomes a thin coordinator. provision / syncManagedStatus / sweepManagedStatuses delegate to an injected ComplianceProvider built via resolveComplianceProvider(env, 'twilio'), persisting through D1ComplianceStateStore. The in-service provision step graph and the private getRow/initRow/persist helpers are removed (now in TwilioComplianceProvider + the store). applyComplianceCallback keeps its webhook-apply state machine, reading/writing via the store. The coordinator owns change-detection: syncManagedStatus recomputes `changed = snapshot.complianceStatus !== priorRow.complianceStatus`; the changed-gated, fail-soft outbox emit and the managed-mode sweep filter are unchanged. Provider build sites hard-code 'twilio' (Plan 2 reads tenant.managedProvider). Send path untouched. File-size baseline: sms.ts +3 (required Plan 2 comments); the service file shrank below 400 and drops off the >400 list. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E88hT7ob9HdXM9BBnmnarq
…r_meta) Rename messaging_compliance.messaging_service_sid -> messaging_resource_sid (provider-neutral) + add nullable provider_meta TEXT. Migration 0013 is a data-preserving RENAME COLUMN + ADD. Updated all compliance-row reads (provider/service/route) to messagingResourceSid; KEPT the compliance-status API response JSON key as messagingServiceSid (portal contract unchanged, Plan 2 handles portal); send-path messagingServiceSid (sendMessage arg / TwilioCreds) left untouched. db:check clean (74=74). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R6hYGDtjhEfMXwWDz4om21
…ider) Generalises POST /twilio/compliance-status/:tenant to POST /:provider/compliance-status/:tenant. Unknown provider -> 404; telnyx (Plan 2, not yet implemented) -> 503; signature verify and event parse delegated to ComplianceProvider interface. complianceWebhookUrl gains a providerId arg (single source of truth for the URL); callers updated to pass 'twilio' with Plan-2 comment. Adds unknown-provider 404 test; all 118 existing compliance tests pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Closes review Minor M1 — the telnyx provider param (known id, Plan 2 receiver not yet implemented) must return 503 fail-closed without a DB write. Mirrors the unknown-provider 404 test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R6hYGDtjhEfMXwWDz4om21
… baseline scheduled.ts cron sweep + managed-send-gate need no change (sweep builds the provider via sweepManagedStatuses' default param from the coordinator refactor; send-gate reads complianceStatus, not the renamed column). Refreshed the tenant-scoping baseline for one PRE-EXISTING query (helpReplyBrand tenants lookup) whose line shifted 81->82 from this plan's sms.ts edits. Full gate: type-check 0, lint 0-new, db:check 74=74, test:unit 2558, test:workers 73, build ok, bundle 65.3%/3MiB. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R6hYGDtjhEfMXwWDz4om21
Final-review hardening (T4a): the 3 generic client.request() posts have no type-check guarding their body, so a param-name/casing regression would silently drop a field at the carrier. Capture + assert the Usa2p campaign (BrandRegistrationSid, indexed MessageSamples[0..1], UsAppToPersonUsecase) and Tollfree Verifications (TollfreePhoneNumberSid, MessagingServiceSid, UseCaseDescription, OptInType) bodies. Mirrors the existing capturedProfile hook. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R6hYGDtjhEfMXwWDz4om21
…an 1) Provider-agnostic ComplianceProvider seam (interface + D1ComplianceStateStore) with the Twilio managed-SMS-compliance path migrated onto the official twilio-node SDK via a fetch httpClient (workerd-verified), behaviour-equivalent. Schema generalized (messaging_resource_sid + provider_meta); webhook route generalized to /:provider/. SaaS-gated, standalone unaffected, dormant until ISV env set. Whole-branch opus review: READY TO MERGE. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R6hYGDtjhEfMXwWDz4om21
Review Important: client.messagingProfiles.create returns
MessagingProfileCreateResponse { data?: MessagingProfile } — read mp.data?.id,
not mp.id (the sibling numberOrders/availablePhoneNumbers calls already unwrap
.data). Would throw 'no id' on the first LIVE call; tests passed only because
the fake mirrored the wrong flat shape. Fixed provider read + type + fake.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R6hYGDtjhEfMXwWDz4om21
… tfvSid Review Important: tfvSid must be VerificationRequestEgress.id, not verificationRequestId. requests.retrieve(id) keys on id, and the retrieve/status response (VerificationRequestStatus) carries only id — verificationRequestId is absent from it, so it cannot be the sync/webhook match key. Fixed provider read + type doc + test assertion; refreshed file-size baseline (438->442, the added explanatory comments). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R6hYGDtjhEfMXwWDz4om21
…selector Adds tests/web/unit/managed-compliance-wizard.spec.ts — 4 render assertions that the Twilio/Telnyx toggle buttons appear and reflect the managedProvider prop via aria-pressed. Uses createMemoryRouter + RouterProvider to satisfy <Form>'s data-router context; createRoot + act harness mirrors existing version-history-panel / media-viewer precedents. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E88hT7ob9HdXM9BBnmnarq
Final whole-branch review found applyComplianceCallback's campaign/tfv else branches reset to pending UNCONDITIONALLY (unlike the brand branch's no-regress guard). A non-terminal callback arriving after approval would downgrade an approved row to pending, disabling SMS at the send gate until the next sweep. Guarded both branches (only set pending when not already 'approved') — universal fix for both providers; also neutralizes the Telnyx-webhook flapping landmine (Telnyx raw statuses are non-terminal under the current Twilio-only vocabulary). NOTE: making applyComplianceCallback provider-aware (normalize Telnyx raw statuses so Telnyx webhooks actively advance, not just cron) remains a LIVE prerequisite, co-requisite of confirming the Telnyx webhook event_type strings. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R6hYGDtjhEfMXwWDz4om21
TelnyxComplianceProvider (10DLC + toll-free) on the official telnyx SDK behind the Plan 1 ComplianceProvider seam: per-tenant managedProvider selection, per-row cron sweep, webhook reception (Ed25519, provider-keyed secret), Settings UI selector. SaaS-gated + dormant until TELNYX_API_KEY set; standalone unaffected. Whole-branch opus review: READY TO MERGE (campaign/tfv no-regress guard added). LIVE-activation prerequisites tracked in the SDD ledger. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R6hYGDtjhEfMXwWDz4om21
ComplianceProvider abstraction + Twilio & Telnyx SMS-compliance providers
…email/Stripe/Gemini Every on-demand provider probe (SMS test send, Resend/email-provider validate, Stripe, Gemini) now records its outcome so the settings panels show the LAST tested time + check/cross + reason after a reload, plus a short recent history. - New `integration_test_results` table (migration 0015) — bounded to the newest 5 rows per (tenant, target); no FK, tenant-scoped, epoch-ms ts per schema rules. - Single shared write point `recordIntegrationTest` (helper) — no endpoint duplicates the insert+prune; `detail` carries a non-sensitive summary only. - `GET /api/integrations/test-results` returns the retained history; the communication / integrations / advanced loaders read it via `parseTestResults`. - Shared `<ConnectionTestStatus>` component reused by all four panels (pairs with the existing shared `<TestConnectionButton>`) — relative time + collapsible recent list, "Not tested yet" when empty. - Unit tests for the prune logic + the component; existing loader specs updated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(settings): persist last Test-connection time + result (SMS/email/Stripe/Gemini)
# Conflicts: # scripts/file-size-baseline.json
…upstream Rename the API error code managed_requires_paid_plan -> managed_not_enabled and reword the managed-eligibility gate comments/test names to describe the managed_eligible flag itself rather than any plan/tier framing. No behaviour change: same fail-closed 403 when managed_eligible is unset. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CodeQL js/incomplete-url-substring-sanitization (high) flagged the
String(url).includes('api.telnyx.com') check in the dispatch test. Parse the
URL and compare hostname exactly; functionally identical assertion, no alert.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Large integration bringing the messaging/compliance and settings surface up to date. SaaS-only paths are gated by construction via the DI seam, so the standalone OSS engine is unaffected.
What's included
message_templatestable, CRUD service + API (duplicate/delete-guard/preview/test-send), automations reference template ids instead of embedding bodies, Settings → Communication Templates hub.ComplianceProviderinterface + normalized types,D1ComplianceStateStore(persist-before-advance + resume), provider-param webhook route. Twilio provider ontwilio-node; Telnyx provider (telnyxSDK) as a second implementation. Both verified in workerd; total bundle well under the Workers Free 3 MiB limit.Verification