Skip to content

feat: messaging-compliance provider abstraction, webhooks, template library, and settings connection-test history - #208

Merged
important-new merged 93 commits into
InspectorHub:mainfrom
important-new:main
Jun 28, 2026
Merged

feat: messaging-compliance provider abstraction, webhooks, template library, and settings connection-test history#208
important-new merged 93 commits into
InspectorHub:mainfrom
important-new:main

Conversation

@important-new

Copy link
Copy Markdown
Contributor

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

  • Automation-core extraction (SP-ENG): pure, OSS-safe condition evaluator + delivery orchestration with injected Clock; existing automation behaviour characterized and preserved.
  • Message-template library (SP2): message_templates table, CRUD service + API (duplicate/delete-guard/preview/test-send), automations reference template ids instead of embedding bodies, Settings → Communication Templates hub.
  • Inbound/outbound webhooks (WH-1/2/3): ctx-based inbound signature verification (Ed25519, 4 providers), SMS delivery-status receiver + store, email bounce/complaint receiver with per-tenant suppression list; all tenant-scoped + fail-closed.
  • Messaging-compliance provider abstraction: ComplianceProvider interface + normalized types, D1ComplianceStateStore (persist-before-advance + resume), provider-param webhook route. Twilio provider on twilio-node; Telnyx provider (telnyx SDK) as a second implementation. Both verified in workerd; total bundle well under the Workers Free 3 MiB limit.
  • Settings connection-test history: Settings "Test connection" buttons (SMS / Email / Stripe / Gemini) now persist last tested time + result with a short recent history, surfaced via a shared component.

Verification

important-new and others added 30 commits June 26, 2026 23:20
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
important-new and others added 26 commits June 27, 2026 20:44
…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>
Comment thread tests/unit/sms-api.spec.ts Fixed
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>
@important-new
important-new merged commit a12e7af into InspectorHub:main Jun 28, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants