@terreno/mcp 0.31.0 → 56.0.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +8 -9
- package/dist/bootstrap.js.map +1 -1
- package/dist/docs/templates/bootstrap/frontend/app/_layout.template.tsx +1 -1
- package/dist/docs/upgrades/56.0.0-beta.2.md +59 -0
- package/dist/docs/versioned/next/README.md +2 -0
- package/dist/docs/versioned/next/explanation/roadmap-seed-issues.md +21 -0
- package/dist/docs/versioned/next/how-to/README.md +2 -0
- package/dist/docs/versioned/next/how-to/migrate-rtk-to-syncdb.md +445 -0
- package/dist/docs/versioned/next/implementationPlans/PLAN_INDEX.md +28 -50
- package/dist/docs/versioned/next/implementationPlans/b2b-platform-program.md +3 -2
- package/dist/docs/versioned/next/implementationPlans/comms-abstraction.md +98 -15
- package/dist/docs/versioned/next/implementationPlans/comms-adapter-sendgrid.md +63 -13
- package/dist/docs/versioned/next/implementationPlans/comms-adapter-twilio-sms.md +65 -12
- package/dist/docs/versioned/next/implementationPlans/comms-adapter-twilio-verify.md +9 -3
- package/dist/docs/versioned/next/implementationPlans/comms-admin-dashboard.md +180 -0
- package/dist/docs/versioned/next/implementationPlans/migrate-cicd-to-circleci-research.md +126 -0
- package/dist/docs/versioned/next/implementationPlans/migrate-cicd-to-circleci.md +269 -0
- package/dist/docs/versioned/next/implementationPlans/rtk-to-syncdb-migration-docs.md +52 -47
- package/dist/docs/versioned/next/implementationPlans/syncdb-api-inventory.md +303 -0
- package/dist/docs/versioned/next/implementationPlans/syncdb-codegen.md +252 -0
- package/dist/docs/versioned/next/implementationPlans/syncdb-local-first.md +468 -420
- package/dist/docs/versioned/next/implementationPlans/syncdb-phase-c-design.md +425 -0
- package/dist/docs/versioned/next/implementationPlans/terreno-syncdb-2.md +800 -0
- package/dist/docs/versioned/next/reference/README.md +2 -0
- package/dist/docs/versioned/next/reference/comms.md +132 -0
- package/dist/docs/versioned/next/reference/environment-variables.md +9 -0
- package/dist/docs/versioned/next/reference/syncdb.md +456 -0
- package/dist/docs/versioned/next/superpowers/plans/2026-07-28-admin-mobile-navigation.md +97 -0
- package/dist/docs/versioned/next/superpowers/specs/2026-07-28-admin-mobile-navigation-design.md +36 -0
- package/dist/docs/versioned/next/tasks/comms-abstraction.md +29 -16
- package/dist/docs/versioned/next/tasks/comms-adapter-sendgrid.md +17 -7
- package/dist/docs/versioned/next/tasks/comms-adapter-twilio-sms.md +17 -7
- package/dist/docs/versioned/next/tasks/comms-adapter-twilio-verify.md +3 -3
- package/dist/docs/versioned/next/tasks/comms-admin-dashboard.md +52 -0
- package/dist/docs/versioned/next/tasks/migrate-cicd-to-circleci.md +215 -0
- package/dist/docs/versioned/next/tasks/rtk-to-syncdb-migration-docs.md +10 -10
- package/dist/docs/versioned/next/tasks/syncdb-codegen.md +65 -0
- package/dist/docs/versioned/next/tasks/syncdb-local-first.md +434 -117
- package/package.json +3 -3
- package/src/docs/templates/bootstrap/frontend/app/_layout.template.tsx +1 -1
- package/src/docs/upgrades/56.0.0-beta.2.md +59 -0
- package/src/docs/versioned/next/README.md +2 -0
- package/src/docs/versioned/next/explanation/roadmap-seed-issues.md +21 -0
- package/src/docs/versioned/next/how-to/README.md +2 -0
- package/src/docs/versioned/next/how-to/migrate-rtk-to-syncdb.md +445 -0
- package/src/docs/versioned/next/implementationPlans/PLAN_INDEX.md +28 -50
- package/src/docs/versioned/next/implementationPlans/b2b-platform-program.md +3 -2
- package/src/docs/versioned/next/implementationPlans/comms-abstraction.md +98 -15
- package/src/docs/versioned/next/implementationPlans/comms-adapter-sendgrid.md +63 -13
- package/src/docs/versioned/next/implementationPlans/comms-adapter-twilio-sms.md +65 -12
- package/src/docs/versioned/next/implementationPlans/comms-adapter-twilio-verify.md +9 -3
- package/src/docs/versioned/next/implementationPlans/comms-admin-dashboard.md +180 -0
- package/src/docs/versioned/next/implementationPlans/migrate-cicd-to-circleci-research.md +126 -0
- package/src/docs/versioned/next/implementationPlans/migrate-cicd-to-circleci.md +269 -0
- package/src/docs/versioned/next/implementationPlans/rtk-to-syncdb-migration-docs.md +52 -47
- package/src/docs/versioned/next/implementationPlans/syncdb-api-inventory.md +303 -0
- package/src/docs/versioned/next/implementationPlans/syncdb-codegen.md +252 -0
- package/src/docs/versioned/next/implementationPlans/syncdb-local-first.md +468 -420
- package/src/docs/versioned/next/implementationPlans/syncdb-phase-c-design.md +425 -0
- package/src/docs/versioned/next/implementationPlans/terreno-syncdb-2.md +800 -0
- package/src/docs/versioned/next/reference/README.md +2 -0
- package/src/docs/versioned/next/reference/comms.md +132 -0
- package/src/docs/versioned/next/reference/environment-variables.md +9 -0
- package/src/docs/versioned/next/reference/syncdb.md +456 -0
- package/src/docs/versioned/next/superpowers/plans/2026-07-28-admin-mobile-navigation.md +97 -0
- package/src/docs/versioned/next/superpowers/specs/2026-07-28-admin-mobile-navigation-design.md +36 -0
- package/src/docs/versioned/next/tasks/comms-abstraction.md +29 -16
- package/src/docs/versioned/next/tasks/comms-adapter-sendgrid.md +17 -7
- package/src/docs/versioned/next/tasks/comms-adapter-twilio-sms.md +17 -7
- package/src/docs/versioned/next/tasks/comms-adapter-twilio-verify.md +3 -3
- package/src/docs/versioned/next/tasks/comms-admin-dashboard.md +52 -0
- package/src/docs/versioned/next/tasks/migrate-cicd-to-circleci.md +215 -0
- package/src/docs/versioned/next/tasks/rtk-to-syncdb-migration-docs.md +10 -10
- package/src/docs/versioned/next/tasks/syncdb-codegen.md +65 -0
- package/src/docs/versioned/next/tasks/syncdb-local-first.md +434 -117
- package/dist/__tests__/bootstrap.test.d.ts +0 -2
- package/dist/__tests__/bootstrap.test.d.ts.map +0 -1
- package/dist/__tests__/bootstrap.test.js +0 -299
- package/dist/__tests__/bootstrap.test.js.map +0 -1
- package/dist/__tests__/databaseQuery.test.d.ts +0 -2
- package/dist/__tests__/databaseQuery.test.d.ts.map +0 -1
- package/dist/__tests__/databaseQuery.test.js +0 -83
- package/dist/__tests__/databaseQuery.test.js.map +0 -1
- package/dist/__tests__/docsRoot.test.d.ts +0 -2
- package/dist/__tests__/docsRoot.test.d.ts.map +0 -1
- package/dist/__tests__/docsRoot.test.js +0 -59
- package/dist/__tests__/docsRoot.test.js.map +0 -1
- package/dist/__tests__/inferPackages.test.d.ts +0 -2
- package/dist/__tests__/inferPackages.test.d.ts.map +0 -1
- package/dist/__tests__/inferPackages.test.js +0 -35
- package/dist/__tests__/inferPackages.test.js.map +0 -1
- package/dist/__tests__/preload.d.ts +0 -2
- package/dist/__tests__/preload.d.ts.map +0 -1
- package/dist/__tests__/preload.js +0 -12
- package/dist/__tests__/preload.js.map +0 -1
- package/dist/__tests__/prompts.test.d.ts +0 -2
- package/dist/__tests__/prompts.test.d.ts.map +0 -1
- package/dist/__tests__/prompts.test.js +0 -295
- package/dist/__tests__/prompts.test.js.map +0 -1
- package/dist/__tests__/resources.test.d.ts +0 -2
- package/dist/__tests__/resources.test.d.ts.map +0 -1
- package/dist/__tests__/resources.test.js +0 -352
- package/dist/__tests__/resources.test.js.map +0 -1
- package/dist/__tests__/search.test.d.ts +0 -2
- package/dist/__tests__/search.test.d.ts.map +0 -1
- package/dist/__tests__/search.test.js +0 -163
- package/dist/__tests__/search.test.js.map +0 -1
- package/dist/__tests__/tools.test.d.ts +0 -2
- package/dist/__tests__/tools.test.d.ts.map +0 -1
- package/dist/__tests__/tools.test.js +0 -540
- package/dist/__tests__/tools.test.js.map +0 -1
- package/dist/__tests__/upgradeGuide.test.d.ts +0 -2
- package/dist/__tests__/upgradeGuide.test.d.ts.map +0 -1
- package/dist/__tests__/upgradeGuide.test.js +0 -22
- package/dist/__tests__/upgradeGuide.test.js.map +0 -1
|
@@ -9,35 +9,47 @@ Umbrella plans that coordinate several IPs.
|
|
|
9
9
|
| Program | Status | Created | IPs |
|
|
10
10
|
|---------|--------|---------|-----|
|
|
11
11
|
| [Open source launch](oss-launch-program.md) | Draft — decisions recorded; Vercel spike open | 2026-07-27 | 15 |
|
|
12
|
-
| [B2B platform](b2b-platform-program.md) | Draft — decisions D1–D7 resolved | 2026-08-09 | 9 written (+ [rbac-permissions](rbac-permissions.md)); 29 roadmap items seeded |
|
|
13
12
|
|
|
14
13
|
## Active
|
|
15
14
|
|
|
16
15
|
| Plan | Status | Created | Tasks |
|
|
17
16
|
|------|--------|---------|-------|
|
|
17
|
+
| [Migrate CI/CD to CircleCI](migrate-cicd-to-circleci.md) | Draft — decisions recorded | 2026-08-17 | [tasks](../tasks/migrate-cicd-to-circleci.md) |
|
|
18
18
|
| [Admin UI v2 — Django-parity admin](admin-ui-v2-django-parity.md) | Approved | 2026-06-15 | [tasks](../tasks/admin-ui-v2-django-parity.md) |
|
|
19
|
-
| [
|
|
20
|
-
| [SyncDB
|
|
21
|
-
| [
|
|
22
|
-
| [
|
|
19
|
+
| [@terreno/syncdb-codegen](syncdb-codegen.md) | Draft | 2026-07-08 | [tasks](../tasks/syncdb-codegen.md) |
|
|
20
|
+
| [SyncDB local-first data layer (v2)](syncdb-local-first.md) | In progress — Phases 1–8 landed, Phase 9 follow-ups open | 2026-06-23 | [tasks](../tasks/syncdb-local-first.md) |
|
|
21
|
+
| [SyncDB hardening (syncdb-2)](terreno-syncdb-2.md) | Implemented — Phases A–F landed; deviations carried into Phase 9 | 2026-07-11 | [tasks](../tasks/syncdb-local-first.md) (Phase 8/9) |
|
|
22
|
+
| [SyncDB Phase C design](syncdb-phase-c-design.md) | Implemented — design authority for C1–C8 | 2026-07-11 | [tasks](../tasks/syncdb-local-first.md) (Phase 8) |
|
|
23
|
+
|
|
24
|
+
## Completed
|
|
25
|
+
|
|
26
|
+
| Plan | Status | Created | Tasks |
|
|
27
|
+
|------|--------|---------|-------|
|
|
28
|
+
| [APIError redesign — standards-first extension of Error](apierror-standard-error-redesign.md) | Complete | 2026-07-28 | n/a |
|
|
23
29
|
|
|
24
|
-
##
|
|
30
|
+
## Deferred / Closed
|
|
25
31
|
|
|
26
|
-
|
|
32
|
+
*(Plans parked or superseded.)*
|
|
27
33
|
|
|
28
|
-
|
|
34
|
+
## Backlog
|
|
29
35
|
|
|
30
|
-
|
|
31
|
-
|
|
36
|
+
*(Links to ideas not yet promoted to a full IP.)*
|
|
37
|
+
|
|
38
|
+
| Idea | Notes |
|
|
39
|
+
|------|-------|
|
|
40
|
+
| [Langfuse integration](terreno-langfuse-integration.md) | AI observability plugin |
|
|
41
|
+
| [modelRouter MCP tools](model-router-mcp.md) | MCP surface for consumer app APIs |
|
|
42
|
+
| [Offline mode](offline-mode.md) | Superseded in large part by [syncdb](syncdb-local-first.md) — confirm and close |
|
|
43
|
+
| [modelRouter actions](model-router-actions.md) | — |
|
|
44
|
+
| [B2B platform](b2b-platform-program.md) | Draft — decisions D1–D7 resolved | 2026-08-09 | 10 written (+ [rbac-permissions](rbac-permissions.md)); 30 roadmap items seeded |
|
|
45
|
+
| [RBAC Permissions](rbac-permissions.md) | Draft (API design) | 2026-07-08 | TBD |
|
|
46
|
+
| [SyncDB Local-First Data Layer](syncdb-local-first.md) | In progress ([PR #869](https://github.com/flourishhealth/terreno/pull/869)) | 2026-07-13 | [tasks](../tasks/syncdb-local-first.md) |
|
|
47
|
+
| [MCP Boost parity](mcp-boost-parity.md) | In progress ([PR #802](https://github.com/flourishhealth/terreno/pull/802)) | 2026-06-21 | TBD |
|
|
48
|
+
| [Infrastructure MCP server](infra-mcp.md) | Draft — blocked on [RBAC](rbac-permissions.md) | 2026-08-09 | [tasks](../tasks/infra-mcp.md) |
|
|
32
49
|
| [OSS governance baseline](oss-governance-baseline.md) | Draft | None | [tasks](../tasks/oss-governance-baseline.md) |
|
|
33
50
|
| [Public roadmap on GitHub](public-roadmap-github.md) | Draft | None | [tasks](../tasks/public-roadmap-github.md) |
|
|
34
51
|
| [Deploy to GCP (generalized)](deploy-to-gcp.md) | Draft | None | [tasks](../tasks/deploy-to-gcp.md) |
|
|
35
52
|
| [Agentic SDLC plugin (`/terreno-*`)](agentic-sdlc-plugin.md) | Draft | Partial | [tasks](../tasks/agentic-sdlc-plugin.md) |
|
|
36
|
-
|
|
37
|
-
### Wave 1 — gated on PR #869
|
|
38
|
-
|
|
39
|
-
| Plan | Status | RTK flag | Tasks |
|
|
40
|
-
|------|--------|----------|-------|
|
|
41
53
|
| [RTK deprecation and syncdb migration docs](rtk-to-syncdb-migration-docs.md) | Draft | **Blocked** (gating IP) | [tasks](../tasks/rtk-to-syncdb-migration-docs.md) |
|
|
42
54
|
| [Positioning — Django/Rails for TypeScript](positioning-django-rails-universal.md) | Draft | Partial | [tasks](../tasks/positioning-django-rails-universal.md) |
|
|
43
55
|
| [Reference documentation coverage](docs-reference-coverage.md) | Draft | Blocked | [tasks](../tasks/docs-reference-coverage.md) |
|
|
@@ -48,48 +60,14 @@ All part of the [launch program](oss-launch-program.md). The **RTK flag** column
|
|
|
48
60
|
| [The AI development loop (Boost)](ai-dev-loop-boost.md) | Draft | Partial | [tasks](../tasks/ai-dev-loop-boost.md) |
|
|
49
61
|
| [Dogfooding run and launch blog post](build-terreno-app-validation.md) | Draft | Blocked | [tasks](../tasks/build-terreno-app-validation.md) |
|
|
50
62
|
| [Examples, demo, and test coverage](examples-demo-coverage.md) | Draft | Partial | [tasks](../tasks/examples-demo-coverage.md) |
|
|
51
|
-
|
|
52
|
-
### Wave 2 — independent
|
|
53
|
-
|
|
54
|
-
| Plan | Status | RTK flag | Tasks |
|
|
55
|
-
|------|--------|----------|-------|
|
|
56
63
|
| [Web SSR and admin SPA](web-ssr-and-admin-spa.md) | Draft | Partial | [tasks](../tasks/web-ssr-and-admin-spa.md) |
|
|
57
|
-
|
|
58
|
-
## B2B platform IPs
|
|
59
|
-
|
|
60
|
-
All part of the [B2B platform program](b2b-platform-program.md). Roadmap items for the
|
|
61
|
-
full program (including items whose IPs are pending decisions D1–D4) are seeded in
|
|
62
|
-
[roadmap-seed-issues.md](../explanation/roadmap-seed-issues.md).
|
|
63
|
-
|
|
64
|
-
| Plan | Status | RTK flag | Tasks |
|
|
65
|
-
|------|--------|----------|-------|
|
|
66
64
|
| [Pluggable communications layer (@terreno/comms)](comms-abstraction.md) | Draft | None | [tasks](../tasks/comms-abstraction.md) |
|
|
67
65
|
| [Comms adapter — Expo push](comms-adapter-expo-push.md) | Draft | None | [tasks](../tasks/comms-adapter-expo-push.md) |
|
|
68
66
|
| [Comms adapter — Twilio SMS](comms-adapter-twilio-sms.md) | Draft | None | [tasks](../tasks/comms-adapter-twilio-sms.md) |
|
|
69
67
|
| [Comms adapter — Twilio Verify (OTP)](comms-adapter-twilio-verify.md) | Draft | None | [tasks](../tasks/comms-adapter-twilio-verify.md) |
|
|
70
68
|
| [Comms adapter — SendGrid email](comms-adapter-sendgrid.md) | Draft | None | [tasks](../tasks/comms-adapter-sendgrid.md) |
|
|
69
|
+
| [Comms admin dashboard (errors, retries, log digging)](comms-admin-dashboard.md) | Draft | Partial | [tasks](../tasks/comms-admin-dashboard.md) |
|
|
71
70
|
| [Password reset and email verification](password-reset-and-email-verification.md) | Draft | None | [tasks](../tasks/password-reset-and-email-verification.md) |
|
|
72
71
|
| [Organizations, teams, and multi-tenant scoping](orgs-and-teams.md) | Draft | None | [tasks](../tasks/orgs-and-teams.md) |
|
|
73
72
|
| [Stripe billing and subscriptions](billing-stripe.md) | Draft | None | [tasks](../tasks/billing-stripe.md) |
|
|
74
73
|
| [Native module baseline (next major)](native-module-baseline.md) | Draft | None | [tasks](../tasks/native-module-baseline.md) |
|
|
75
|
-
|
|
76
|
-
## Completed
|
|
77
|
-
|
|
78
|
-
| Plan | Status | Created | Tasks |
|
|
79
|
-
|------|--------|---------|-------|
|
|
80
|
-
| [APIError redesign — standards-first extension of Error](apierror-standard-error-redesign.md) | Complete | 2026-07-28 | n/a |
|
|
81
|
-
|
|
82
|
-
## Deferred / Closed
|
|
83
|
-
|
|
84
|
-
*(Plans parked or superseded.)*
|
|
85
|
-
|
|
86
|
-
## Backlog
|
|
87
|
-
|
|
88
|
-
*(Links to ideas not yet promoted to a full IP.)*
|
|
89
|
-
|
|
90
|
-
| Idea | Notes |
|
|
91
|
-
|------|-------|
|
|
92
|
-
| [Langfuse integration](terreno-langfuse-integration.md) | AI observability plugin |
|
|
93
|
-
| [modelRouter MCP tools](model-router-mcp.md) | MCP surface for consumer app APIs |
|
|
94
|
-
| [Offline mode](offline-mode.md) | Superseded in large part by [syncdb](syncdb-local-first.md) — confirm and close |
|
|
95
|
-
| [modelRouter actions](model-router-actions.md) | — |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Program: B2B platform
|
|
2
2
|
|
|
3
|
-
**Status:** Draft — roadmap items drafted; decisions D1–D7 resolved 2026-08-09; first
|
|
3
|
+
**Status:** Draft — roadmap items drafted; decisions D1–D7 resolved 2026-08-09; first ten IPs open
|
|
4
4
|
**Owner:** unassigned
|
|
5
5
|
**Created:** 2026-08-09
|
|
6
6
|
|
|
@@ -32,7 +32,7 @@ org switcher, dark mode).
|
|
|
32
32
|
| Track | Theme | IPs / roadmap items |
|
|
33
33
|
|---|---|---|
|
|
34
34
|
| A | Tenancy & access | `orgs-and-teams`, `rbac-permissions` (existing draft), `invitations-and-seats`, `org-management-ui` |
|
|
35
|
-
| C | Communications (pluggable) | `comms-abstraction`, one item **per adapter**: `comms-adapter-expo-push` (first push adapter, per D4), `comms-adapter-twilio-sms`, `comms-adapter-twilio-verify`, `comms-adapter-sendgrid` (first mail adapter, per D2); consumers: `password-reset-and-email-verification`, `notification-center` |
|
|
35
|
+
| C | Communications (pluggable) | `comms-abstraction`, one item **per adapter**: `comms-adapter-expo-push` (first push adapter, per D4), `comms-adapter-twilio-sms`, `comms-adapter-twilio-verify`, `comms-adapter-sendgrid` (first mail adapter, per D2); operations: `comms-admin-dashboard`; consumers: `password-reset-and-email-verification`, `notification-center` |
|
|
36
36
|
| B | Billing | `inbound-webhooks`, `billing-stripe` (web-first, per D1), `mobile-iap-revenuecat` (Future) |
|
|
37
37
|
| D | Zero-to-deployed DX | `create-terreno-app`, `mongo-migrations` (deploy items already exist in the OSS launch program) |
|
|
38
38
|
| E | B2B UI surfaces | `data-grid-server-filters`, `charts-and-dashboards`, `dark-mode-theme`, `command-palette`, `wizard-stepper`, `wysiwyg-editor` (markdown stays for now, per D3), `global-search` |
|
|
@@ -97,6 +97,7 @@ All decisions resolved by the maintainer on 2026-08-09:
|
|
|
97
97
|
| [comms-adapter-twilio-sms](comms-adapter-twilio-sms.md) | Draft | [tasks](../tasks/comms-adapter-twilio-sms.md) |
|
|
98
98
|
| [comms-adapter-twilio-verify](comms-adapter-twilio-verify.md) | Draft | [tasks](../tasks/comms-adapter-twilio-verify.md) |
|
|
99
99
|
| [comms-adapter-sendgrid](comms-adapter-sendgrid.md) | Draft | [tasks](../tasks/comms-adapter-sendgrid.md) |
|
|
100
|
+
| [comms-admin-dashboard](comms-admin-dashboard.md) | Draft | [tasks](../tasks/comms-admin-dashboard.md) |
|
|
100
101
|
| [password-reset-and-email-verification](password-reset-and-email-verification.md) | Draft | [tasks](../tasks/password-reset-and-email-verification.md) |
|
|
101
102
|
| [orgs-and-teams](orgs-and-teams.md) | Draft | [tasks](../tasks/orgs-and-teams.md) |
|
|
102
103
|
| [billing-stripe](billing-stripe.md) | Draft | [tasks](../tasks/billing-stripe.md) |
|
|
@@ -45,6 +45,9 @@ consumer apps install only the SDKs they use.
|
|
|
45
45
|
| Templates | Minimal: `{subject, text, html}` render helper with variable interpolation; no template builder |
|
|
46
46
|
| Where do send calls log? | `CommsMessage` Mongoose model, mirroring the `AIRequest.logRequest` pattern — logging failures never break the send |
|
|
47
47
|
| Dev experience | `ConsoleMailProvider` / `ConsoleSmsProvider` / `ConsolePushProvider` / `ConsoleVerificationProvider` print to logger and record to `CommsMessage`; default when no provider configured in non-production |
|
|
48
|
+
| Consumer lifecycle hooks | `CommsApp` options expose `beforeSend` / `onSend` / `onError` / `onRetry` / `onOptOut` / `onDeliveryEvent`. Hooks are awaited but wrapped: a throwing hook is logged (`logger.error`) and never breaks the send. `beforeSend` may mutate or cancel |
|
|
49
|
+
| Error taxonomy | Every `SendResult` failure carries `errorCode` (provider-native code as a string) and `errorClass` (`"permanent"` \| `"transient"` \| `"config"`). Adapters own the mapping; the facade uses `errorClass` to decide its single inline retry (transient only) and the admin dashboard uses it to gate manual retries |
|
|
50
|
+
| Payload retention | `retainPayloadDays` option (default 30) keeps the rendered message on the `CommsMessage` row (post `redactPayload` hook) so the admin dashboard can retry; a TTL-style cleanup clears expired payloads |
|
|
48
51
|
|
|
49
52
|
## Architecture
|
|
50
53
|
|
|
@@ -72,6 +75,8 @@ export interface SendResult {
|
|
|
72
75
|
providerMessageId?: string;
|
|
73
76
|
accepted: boolean;
|
|
74
77
|
error?: string;
|
|
78
|
+
errorCode?: string; // provider-native code, e.g. "21610", "sendgrid-400"
|
|
79
|
+
errorClass?: "permanent" | "transient" | "config"; // drives retry eligibility
|
|
75
80
|
}
|
|
76
81
|
|
|
77
82
|
export interface MailMessage {
|
|
@@ -111,17 +116,41 @@ export interface PushProvider {
|
|
|
111
116
|
export interface VerificationProvider {
|
|
112
117
|
readonly id: string;
|
|
113
118
|
startVerification(options: {to: string; channel: "sms" | "email"}): Promise<SendResult>;
|
|
114
|
-
checkVerification(options: {to: string; code: string}): Promise<{
|
|
119
|
+
checkVerification(options: {to: string; code: string}): Promise<{
|
|
120
|
+
valid: boolean;
|
|
121
|
+
error?: string;
|
|
122
|
+
}>;
|
|
115
123
|
}
|
|
116
124
|
|
|
117
125
|
export interface DeliveryEvent {
|
|
118
126
|
channel: "mail" | "sms" | "push";
|
|
119
127
|
providerMessageId: string;
|
|
120
128
|
status: "delivered" | "bounced" | "failed" | "opened";
|
|
129
|
+
errorCode?: string; // provider failure code from the callback, when present
|
|
130
|
+
raw?: unknown;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface CommsHookContext {
|
|
134
|
+
channel: "mail" | "sms" | "push" | "verification";
|
|
135
|
+
provider: string;
|
|
136
|
+
message: MailMessage | {to: string; body: string} | PushMessage;
|
|
137
|
+
userId?: string;
|
|
138
|
+
isRetry: boolean; // true for facade inline retries and admin-dashboard retries
|
|
139
|
+
messageId?: string; // CommsMessage._id once the log row exists
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export interface OptOutEvent {
|
|
143
|
+
channel: "mail" | "sms";
|
|
144
|
+
to: string;
|
|
145
|
+
provider: string;
|
|
146
|
+
reason: string; // e.g. "sms-stop", "unsubscribe", "spam-report"
|
|
121
147
|
raw?: unknown;
|
|
122
148
|
}
|
|
123
149
|
```
|
|
124
150
|
|
|
151
|
+
Verification start attempts store `metadata.verificationChannel` while recipient values remain
|
|
152
|
+
redacted. Verification codes are never persisted.
|
|
153
|
+
|
|
125
154
|
### CommsApp (TerrenoPlugin)
|
|
126
155
|
|
|
127
156
|
```typescript
|
|
@@ -132,6 +161,17 @@ export interface CommsOptions {
|
|
|
132
161
|
verification?: VerificationProvider;
|
|
133
162
|
defaultFrom?: string; // mail from fallback
|
|
134
163
|
logMessages?: boolean; // default true → CommsMessage
|
|
164
|
+
retainPayloadDays?: number; // default 30; 0 disables retention (and admin retry)
|
|
165
|
+
redactPayload?: (context: CommsHookContext, payload: unknown) => unknown; // before storage
|
|
166
|
+
|
|
167
|
+
// Lifecycle hooks — all optional, all awaited, all wrapped so a throwing
|
|
168
|
+
// hook is logged and never breaks (or blocks) the send itself.
|
|
169
|
+
beforeSend?: (context: CommsHookContext) =>
|
|
170
|
+
Promise<{cancel?: boolean; message?: CommsHookContext["message"]} | void>;
|
|
171
|
+
onSend?: (context: CommsHookContext, result: SendResult) => Promise<void>;
|
|
172
|
+
onError?: (context: CommsHookContext, result: SendResult) => Promise<void>;
|
|
173
|
+
onRetry?: (context: CommsHookContext, attempt: number) => Promise<void>;
|
|
174
|
+
onOptOut?: (event: OptOutEvent) => Promise<void>;
|
|
135
175
|
onDeliveryEvent?: (event: DeliveryEvent) => Promise<void>;
|
|
136
176
|
}
|
|
137
177
|
|
|
@@ -150,6 +190,25 @@ Unconfigured channel behavior: in production, throw `APIError({status: 501, titl
|
|
|
150
190
|
"Comms channel not configured"})`; in development, fall back to the console adapter with a
|
|
151
191
|
`logger.warn`.
|
|
152
192
|
|
|
193
|
+
### Hook order and semantics
|
|
194
|
+
|
|
195
|
+
For every send (including retries) the facade runs:
|
|
196
|
+
|
|
197
|
+
1. `beforeSend` — may replace the message (`{message}`) or cancel (`{cancel: true}`,
|
|
198
|
+
logged as a `CommsMessage` with status `cancelled`). The consuming app uses this for
|
|
199
|
+
quiet hours, per-user notification preferences, and suppression lists.
|
|
200
|
+
2. Provider `send*` → on transient failure (`errorClass: "transient"`), `onRetry` fires
|
|
201
|
+
and the facade retries once inline.
|
|
202
|
+
3. `onSend` (accepted) or `onError` (final failure) — `onError` receives the full
|
|
203
|
+
`SendResult` (`errorCode`, `errorClass`) so apps can alert, escalate, or mark domain
|
|
204
|
+
objects without polling the delivery log.
|
|
205
|
+
4. Asynchronously, adapters feed `onDeliveryEvent` (delivery callbacks) and `onOptOut`
|
|
206
|
+
(STOP replies, unsubscribes, spam reports) once their webhook phases land.
|
|
207
|
+
|
|
208
|
+
Hooks are observation and shaping points, not error channels: exceptions thrown inside a
|
|
209
|
+
hook are caught, logged with `logger.error`, and recorded in `CommsMessage.metadata` —
|
|
210
|
+
they never change the send outcome.
|
|
211
|
+
|
|
153
212
|
## Models
|
|
154
213
|
|
|
155
214
|
All fields carry `description`; both models use `createdUpdatedPlugin` + `isDeletedPlugin`
|
|
@@ -162,8 +221,18 @@ default true), `lastSeenAt` (date).
|
|
|
162
221
|
**CommsMessage** — `channel` ("mail" | "sms" | "push" | "verification"), `provider`
|
|
163
222
|
(string), `to` (string, redacted-at-rest for sms/mail per option), `subject` (string,
|
|
164
223
|
optional), `userId` (ref User, optional), `status` ("sent" | "failed" | "delivered" |
|
|
165
|
-
"bounced"), `providerMessageId` (string, indexed), `error` (string,
|
|
166
|
-
|
|
224
|
+
"bounced" | "cancelled"), `providerMessageId` (string, indexed), `error` (string,
|
|
225
|
+
optional), `errorCode` (string, indexed), `errorClass` ("permanent" | "transient" |
|
|
226
|
+
"config"), `templateId` (string, optional), `attempts` (array of `{at, provider,
|
|
227
|
+
providerMessageId, errorCode, error}` — one entry per facade attempt including the inline
|
|
228
|
+
retry), `attemptCount` (number), `lastAttemptAt` (date), `retriedFromId` / `retriedById`
|
|
229
|
+
(ref CommsMessage — links between an original and its admin-dashboard retries), `payload`
|
|
230
|
+
(Mixed, retained per `retainPayloadDays` after `redactPayload`, cleared on expiry),
|
|
231
|
+
`metadata` (Mixed). Static `logSend()` mirrors `AIRequest.logRequest`.
|
|
232
|
+
|
|
233
|
+
These fields exist so the [comms-admin-dashboard](comms-admin-dashboard.md) IP can filter
|
|
234
|
+
on error codes/classes, show per-attempt history, and re-send from the retained payload
|
|
235
|
+
without any further schema changes.
|
|
167
236
|
|
|
168
237
|
## APIs
|
|
169
238
|
|
|
@@ -174,9 +243,12 @@ optional), `userId` (ref User, optional), `status` ("sent" | "failed" | "deliver
|
|
|
174
243
|
| DELETE | `/comms/pushTokens/:id` | IsOwner | Deactivate on logout/uninstall |
|
|
175
244
|
| GET | `/comms/messages` | IsAdmin | Paginated delivery log explorer; filters: `channel`, `status`, `userId`, date range |
|
|
176
245
|
|
|
177
|
-
Push token
|
|
178
|
-
`
|
|
179
|
-
`
|
|
246
|
+
Push token registration, owner-scoped list, and deactivation use dedicated handlers because
|
|
247
|
+
`modelRouter` create cannot provide idempotent upsert semantics and `OwnerQueryFilter` targets a
|
|
248
|
+
persisted `ownerId` while `PushToken` persists `userId`. Owner read remains on `modelRouter`.
|
|
249
|
+
The explorer uses `createOpenApiBuilder`, modeled on `addAiRequestsExplorerRoutes`, and is
|
|
250
|
+
deliberately minimal here — the [comms-admin-dashboard](comms-admin-dashboard.md) IP upgrades it
|
|
251
|
+
with error-code/class filtering, free-text search, a detail route, stats, and retry endpoints.
|
|
180
252
|
|
|
181
253
|
## Notifications
|
|
182
254
|
|
|
@@ -209,9 +281,13 @@ updates.
|
|
|
209
281
|
|
|
210
282
|
- Adapter IPs: `comms-adapter-expo-push`, `comms-adapter-twilio-sms`,
|
|
211
283
|
`comms-adapter-twilio-verify`, `comms-adapter-sendgrid`.
|
|
212
|
-
-
|
|
213
|
-
-
|
|
214
|
-
|
|
284
|
+
- Admin operations dashboard (rich filtering, log digging, manual retries) —
|
|
285
|
+
[comms-admin-dashboard](comms-admin-dashboard.md); this IP only ships the fields and
|
|
286
|
+
hooks it needs.
|
|
287
|
+
- Notification center, user notification preferences (`beforeSend` is the extension
|
|
288
|
+
point they will plug into).
|
|
289
|
+
- Queue-backed sending with automatic scheduled retries — arrives with `job-queues`;
|
|
290
|
+
until then sends are inline with one transient-error retry plus manual admin retries.
|
|
215
291
|
|
|
216
292
|
## Files to Create / Modify
|
|
217
293
|
|
|
@@ -228,13 +304,20 @@ See [docs/tasks/comms-abstraction.md](../tasks/comms-abstraction.md).
|
|
|
228
304
|
|
|
229
305
|
## Acceptance Criteria
|
|
230
306
|
|
|
231
|
-
- [
|
|
307
|
+
- [x] A Terreno app with only console adapters can call `sendMail`, `sendSms`,
|
|
232
308
|
`sendPushToUser`, and `startVerification`/`checkVerification` in development, each
|
|
233
309
|
producing a logger line and a `CommsMessage` row.
|
|
234
|
-
- [ ] `
|
|
310
|
+
- [ ] `beforeSend` can cancel and mutate; `onSend`/`onError` fire with the final
|
|
311
|
+
`SendResult`; `onRetry` fires on the inline transient retry — and a hook that throws
|
|
312
|
+
is logged without changing the send outcome (test per hook).
|
|
313
|
+
- [ ] Failed sends record `errorCode` and `errorClass` on both the `SendResult` and the
|
|
314
|
+
`CommsMessage` row; only `transient` failures trigger the inline retry.
|
|
315
|
+
- [ ] `CommsMessage.attempts` holds one entry per attempt with per-attempt error data;
|
|
316
|
+
`payload` is retained post-redaction and cleared after `retainPayloadDays`.
|
|
317
|
+
- [x] `POST /comms/pushTokens` registers a device token for the authenticated user;
|
|
235
318
|
re-posting the same token updates rather than duplicates; other users cannot list or
|
|
236
319
|
delete it.
|
|
237
|
-
- [
|
|
238
|
-
- [
|
|
239
|
-
- [
|
|
240
|
-
- [
|
|
320
|
+
- [x] `GET /comms/messages` is admin-only, paginated, and filterable by channel and status.
|
|
321
|
+
- [x] Production apps with an unconfigured channel get a 501 `APIError`, not a silent no-op.
|
|
322
|
+
- [x] `@terreno/comms` core has zero provider SDKs in `dependencies`.
|
|
323
|
+
- [x] All routes appear in `/openapi.json` and the generated SDK compiles.
|
|
@@ -32,7 +32,10 @@ transactional email. This unblocks `password-reset-and-email-verification` and
|
|
|
32
32
|
| From address | Constructor `fromEmail`/`fromName` falling back to `CommsApp.defaultFrom`; sender verification is a documented SendGrid prerequisite, not automated |
|
|
33
33
|
| Templates | Terreno-side rendering (`templates.ts` from the abstraction) sent as `text`/`html`; SendGrid dynamic templates supported pass-through via `templateId` + `dynamicTemplateData` but not required |
|
|
34
34
|
| Sandbox | SendGrid `mail_settings.sandbox_mode` toggle for CI/e2e so tests never send |
|
|
35
|
-
| Delivery events | Phase 2: SendGrid Event Webhook (delivered/bounce/dropped/open) with ECDSA signature verification via the inbound-webhooks framework, mapped to `DeliveryEvent`s |
|
|
35
|
+
| Delivery events | Phase 2: SendGrid Event Webhook (delivered/deferred/bounce/dropped/spamreport/unsubscribe/open) with ECDSA signature verification via the inbound-webhooks framework, mapped to `DeliveryEvent`s / `OptOutEvent`s |
|
|
36
|
+
| Error classification | Adapter owns the HTTP-status + event-type → `errorClass` map (table below); 429/5xx are `transient`, other 4xx `permanent`, 401/403 `config` |
|
|
37
|
+
| Suppression / opt-outs | Bounces, spam reports, and unsubscribes emit `OptOutEvent` → consumer `onOptOut`. SendGrid maintains its own suppression lists; a send to a suppressed address comes back as a `dropped` event with the suppression reason, classified `permanent` |
|
|
38
|
+
| Admin dashboard support | Every `CommsMessage` row stores `errorCode`/`errorClass`, the `x-message-id`, and a `metadata.consoleUrl` deep link to SendGrid Email Activity for log digging |
|
|
36
39
|
|
|
37
40
|
## Architecture
|
|
38
41
|
|
|
@@ -52,8 +55,42 @@ export class SendGridMailProvider implements MailProvider {
|
|
|
52
55
|
|
|
53
56
|
Error mapping: SendGrid 4xx (bad address, unverified sender) → `accepted: false` with the
|
|
54
57
|
response body's first error message in `SendResult.error` and full body in
|
|
55
|
-
`CommsMessage.metadata`; 429/5xx retried once by the core
|
|
56
|
-
throws through `sendMail`.
|
|
58
|
+
`CommsMessage.metadata`; 429/5xx classified `transient` and retried once by the core
|
|
59
|
+
facade. The provider never throws through `sendMail`.
|
|
60
|
+
|
|
61
|
+
### Error classification
|
|
62
|
+
|
|
63
|
+
Send-time failures classify by HTTP status; post-acceptance failures arrive as events
|
|
64
|
+
(Phase 2) and reclassify the `CommsMessage`.
|
|
65
|
+
|
|
66
|
+
| Signal | Meaning | `errorCode` | `errorClass` |
|
|
67
|
+
|---|---|---|---|
|
|
68
|
+
| 400 (bad address, missing field), 413 | Request is wrong | `sendgrid-<status>` + first body error | `permanent` |
|
|
69
|
+
| 401, 403 (bad key, unverified sender) | App misconfiguration | `sendgrid-<status>` | `config` (also `logger.error` at send time) |
|
|
70
|
+
| 429 | Rate limited | `sendgrid-429` | `transient` |
|
|
71
|
+
| 5xx / network errors | SendGrid-side or transport failure | `sendgrid-<status>` | `transient` |
|
|
72
|
+
| Event: `bounce` (hard) | Mailbox permanently rejects | `bounce` + reason | `permanent` |
|
|
73
|
+
| Event: `bounce` (soft) / `deferred` | Temporary mailbox/receiver issue | `deferred` | `transient` |
|
|
74
|
+
| Event: `dropped` | SendGrid suppressed (prior bounce, spam report, unsubscribe, invalid) | `dropped` + reason | `permanent` |
|
|
75
|
+
| Event: `spamreport`, `unsubscribe`, `group_unsubscribe` | Recipient opted out | event type | `permanent` (also emits `OptOutEvent`) |
|
|
76
|
+
|
|
77
|
+
Permanent-class failures are never auto-retried and show as non-retryable in the admin
|
|
78
|
+
dashboard with the reason (retrying a suppressed or unsubscribed address damages sender
|
|
79
|
+
reputation); transient-class failures retry once inline and stay manually retryable.
|
|
80
|
+
|
|
81
|
+
### Event webhook mapping (Phase 2)
|
|
82
|
+
|
|
83
|
+
| SendGrid event | `DeliveryEvent.status` | `CommsMessage.status` |
|
|
84
|
+
|---|---|---|
|
|
85
|
+
| `processed` | — | `sent` (unchanged) |
|
|
86
|
+
| `delivered` | `delivered` | `delivered` |
|
|
87
|
+
| `deferred` | — (attempt noted in metadata) | `sent` (SendGrid keeps retrying) |
|
|
88
|
+
| `bounce`, `dropped` | `bounced` / `failed` (+ reason as `errorCode`) | `bounced` / `failed` |
|
|
89
|
+
| `open` | `opened` | unchanged (recorded in metadata) |
|
|
90
|
+
| `spamreport`, `unsubscribe`, `group_unsubscribe` | — | unchanged; emits `OptOutEvent` → `onOptOut` |
|
|
91
|
+
|
|
92
|
+
Events correlate to rows via the `x-message-id` response header captured at send time
|
|
93
|
+
(SendGrid appends per-recipient suffixes; match on the prefix).
|
|
57
94
|
|
|
58
95
|
## Models / APIs / Notifications / UI
|
|
59
96
|
|
|
@@ -61,12 +98,13 @@ None new. Phase 2 adds one webhook route via the inbound-webhooks framework.
|
|
|
61
98
|
|
|
62
99
|
## Phases
|
|
63
100
|
|
|
64
|
-
1. **Send path:** provider, config resolution, sandbox mode, error
|
|
65
|
-
|
|
101
|
+
1. **Send path:** provider, config resolution, sandbox mode, error classification,
|
|
102
|
+
`x-message-id` capture + Email Activity deep link, mocked-client tests;
|
|
103
|
+
example-backend env-gated registration; docs (including the sender-verification
|
|
66
104
|
prerequisite checklist).
|
|
67
|
-
2. **Delivery events** *(gated on inbound-webhooks IP)*: Event Webhook route
|
|
68
|
-
signature verification;
|
|
69
|
-
`CommsMessage.status
|
|
105
|
+
2. **Delivery events + opt-outs** *(gated on inbound-webhooks IP)*: Event Webhook route
|
|
106
|
+
with ECDSA signature verification; event mapping per the table →
|
|
107
|
+
`DeliveryEvent`/`OptOutEvent` → `CommsMessage.status` + consumer hooks.
|
|
70
108
|
|
|
71
109
|
## Feature Flags & Migrations
|
|
72
110
|
|
|
@@ -78,7 +116,10 @@ All sends logged to `CommsMessage` with provider `sendgrid`.
|
|
|
78
116
|
|
|
79
117
|
## Not Included / Future Work
|
|
80
118
|
|
|
81
|
-
- Suppression-list management
|
|
119
|
+
- Suppression-list *management* (viewing/clearing SendGrid suppressions from admin) — the
|
|
120
|
+
adapter only reports suppression outcomes; a management surface can extend the
|
|
121
|
+
[comms-admin-dashboard](comms-admin-dashboard.md) later.
|
|
122
|
+
- Unsubscribe-group configuration.
|
|
82
123
|
- Batch sending beyond the multiple-recipient `to` array.
|
|
83
124
|
|
|
84
125
|
## Files to Create / Modify
|
|
@@ -99,9 +140,18 @@ See [docs/tasks/comms-adapter-sendgrid.md](../tasks/comms-adapter-sendgrid.md).
|
|
|
99
140
|
given.
|
|
100
141
|
- [ ] Sandbox mode is on by default under test and produces accepted results without real
|
|
101
142
|
sends.
|
|
102
|
-
- [ ] SendGrid
|
|
103
|
-
|
|
143
|
+
- [ ] SendGrid errors surface as `accepted: false` with the SendGrid error message and a
|
|
144
|
+
correct `errorCode`/`errorClass` per the classification table; nothing throws
|
|
145
|
+
through the facade.
|
|
146
|
+
- [ ] 401/403 classify as `config` and `logger.error` at send time; 429/5xx classify as
|
|
147
|
+
`transient` and retry once; other 4xx are `permanent` and never retried.
|
|
148
|
+
- [ ] Each accepted send captures the `x-message-id` and stores a `metadata.consoleUrl`
|
|
149
|
+
Email Activity deep link.
|
|
150
|
+
- [ ] Consumer `onError` fires with the classified `SendResult` on every failed send
|
|
151
|
+
(mocked-client test).
|
|
104
152
|
- [ ] Missing API key fails fast at `CommsApp` registration with a clear error.
|
|
105
153
|
- [ ] Apps not using the adapter do not install `@sendgrid/mail`.
|
|
106
|
-
- [ ] (Phase 2) Event webhook updates `CommsMessage.status` only
|
|
107
|
-
signature-verified payloads
|
|
154
|
+
- [ ] (Phase 2) Event webhook updates `CommsMessage.status` per the mapping table only
|
|
155
|
+
for signature-verified payloads; bounce/dropped reasons land as `errorCode`.
|
|
156
|
+
- [ ] (Phase 2) `spamreport`/`unsubscribe` events fire `onOptOut`; later sends to the
|
|
157
|
+
suppressed address record `dropped` as `permanent`.
|
|
@@ -32,6 +32,10 @@ facade with delivery logging.
|
|
|
32
32
|
| Number validation | Validate/normalize to E.164 with `libphonenumber-js` (already in catalog) before hitting the API |
|
|
33
33
|
| Delivery status | Phase 2: status callback endpoint via the inbound-webhooks framework mapping Twilio statuses to `DeliveryEvent`s |
|
|
34
34
|
| Secrets | `TWILIO_ACCOUNT_SID` + `TWILIO_AUTH_TOKEN` via env or the existing secret-provider layer |
|
|
35
|
+
| Error classification | Adapter owns a Twilio error-code → `errorClass` map (table below) so the facade retries only transient failures and the admin dashboard gates manual retries correctly |
|
|
36
|
+
| SDK failure handling | Twilio SDK calls wrapped with `withApiErrorHandling` from `@terreno/api` (per [call-external-apis](../how-to/call-external-apis.md)) — the SDK owns transport, the wrapper standardizes logging/normalization |
|
|
37
|
+
| Opt-outs | Phase 2: inbound STOP/START (and 21610 send failures) surface as `OptOutEvent` → the consumer's `onOptOut` hook; enforcement stays consumer-side via `beforeSend` (Twilio blocks opted-out sends itself) |
|
|
38
|
+
| Admin dashboard support | Every `CommsMessage` row stores the Twilio error code/class and a `metadata.consoleUrl` deep link to the message in the Twilio console for log digging |
|
|
35
39
|
|
|
36
40
|
## Architecture
|
|
37
41
|
|
|
@@ -50,9 +54,44 @@ export class TwilioSmsProvider implements SmsProvider {
|
|
|
50
54
|
}
|
|
51
55
|
```
|
|
52
56
|
|
|
53
|
-
Errors map Twilio exception codes to `SendResult.error
|
|
54
|
-
`CommsMessage.metadata` for triage. Invalid
|
|
55
|
-
`BadRequestError` before any API call.
|
|
57
|
+
Errors map Twilio exception codes to `SendResult.error`/`errorCode`/`errorClass`, with
|
|
58
|
+
the full Twilio error payload kept in `CommsMessage.metadata` for triage. Invalid
|
|
59
|
+
destination numbers fail fast with a `BadRequestError` before any API call.
|
|
60
|
+
|
|
61
|
+
### Error classification
|
|
62
|
+
|
|
63
|
+
The adapter classifies Twilio error codes into the abstraction's `errorClass` taxonomy.
|
|
64
|
+
Permanent failures must never be auto-retried (retrying an unsubscribed number is a
|
|
65
|
+
compliance problem, not a delivery problem); transient failures are retried once inline
|
|
66
|
+
by the facade and stay manually retryable from the admin dashboard.
|
|
67
|
+
|
|
68
|
+
| Twilio code(s) | Meaning | `errorClass` |
|
|
69
|
+
|---|---|---|
|
|
70
|
+
| 21211, 21214, 21217 | Invalid / unroutable destination number | `permanent` |
|
|
71
|
+
| 21408 | SMS not enabled for destination region | `permanent` |
|
|
72
|
+
| 21610 | Recipient has opted out (STOP) | `permanent` (also emits `OptOutEvent`) |
|
|
73
|
+
| 30003, 30005, 30006 | Unreachable handset / unknown device / landline | `permanent` |
|
|
74
|
+
| 30007 | Carrier content filtering | `permanent` (surface loudly — usually a sender-reputation problem) |
|
|
75
|
+
| 20429, 429 responses | Rate limited / concurrency limit | `transient` |
|
|
76
|
+
| 30001, 30002 | Queue overflow / account suspended-ish delivery interruption | `transient` |
|
|
77
|
+
| 5xx / network errors | Twilio-side or transport failure | `transient` |
|
|
78
|
+
| 20003, 20404 (auth/SID misconfig) | Bad credentials or missing resource | `config` |
|
|
79
|
+
|
|
80
|
+
Unknown codes default to `transient` (safe for one retry) and are logged with the raw
|
|
81
|
+
code so the map can be extended. `config` failures additionally `logger.error` at send
|
|
82
|
+
time — they mean the app is misconfigured, not that the message was undeliverable.
|
|
83
|
+
|
|
84
|
+
### Status callback mapping (Phase 2)
|
|
85
|
+
|
|
86
|
+
| Twilio `MessageStatus` | `DeliveryEvent.status` | `CommsMessage.status` |
|
|
87
|
+
|---|---|---|
|
|
88
|
+
| `queued`, `accepted`, `sending`, `sent` | — (no event; already `sent`) | `sent` |
|
|
89
|
+
| `delivered` | `delivered` | `delivered` |
|
|
90
|
+
| `undelivered`, `failed` | `failed` (+ `errorCode` from `ErrorCode` param) | `failed` |
|
|
91
|
+
|
|
92
|
+
Inbound `STOP`/`START` keyword webhooks (same inbound-webhooks registration) map to
|
|
93
|
+
`OptOutEvent {channel: "sms", reason: "sms-stop"}` / opt-in, feeding the consumer's
|
|
94
|
+
`onOptOut` hook and `CommsMessage.metadata` on subsequent 21610 failures.
|
|
56
95
|
|
|
57
96
|
## Models / APIs / Notifications / UI
|
|
58
97
|
|
|
@@ -60,11 +99,13 @@ None new — Phase 2 adds one webhook route via the inbound-webhooks framework.
|
|
|
60
99
|
|
|
61
100
|
## Phases
|
|
62
101
|
|
|
63
|
-
1. **Send path:** provider, config resolution, E.164 validation, error
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
102
|
+
1. **Send path:** provider, config resolution, E.164 validation, error-code
|
|
103
|
+
classification map, `withApiErrorHandling` wrapping, console deep-link metadata, tests
|
|
104
|
+
with mocked Twilio client; example-backend env-gated registration; docs.
|
|
105
|
+
2. **Delivery callbacks + opt-outs** *(gated on inbound-webhooks IP)*: status callback
|
|
106
|
+
route with Twilio signature verification, statuses → `DeliveryEvent` →
|
|
107
|
+
`CommsMessage.status` updates (with `errorCode`), STOP/START keyword handling →
|
|
108
|
+
`OptOutEvent` → `onOptOut`.
|
|
68
109
|
|
|
69
110
|
## Feature Flags & Migrations
|
|
70
111
|
|
|
@@ -76,8 +117,10 @@ All sends logged to `CommsMessage` with provider `twilio`.
|
|
|
76
117
|
|
|
77
118
|
## Not Included / Future Work
|
|
78
119
|
|
|
79
|
-
- Inbound SMS (replies) handling.
|
|
120
|
+
- Inbound SMS (replies) handling beyond STOP/START keywords.
|
|
80
121
|
- Alphanumeric sender IDs, international compliance tooling.
|
|
122
|
+
- Terreno-managed opt-out suppression list — the adapter emits `OptOutEvent`; storing and
|
|
123
|
+
enforcing preferences is consumer-side (`beforeSend`) until `notification-center` lands.
|
|
81
124
|
|
|
82
125
|
## Files to Create / Modify
|
|
83
126
|
|
|
@@ -95,8 +138,18 @@ See [docs/tasks/comms-adapter-twilio-sms.md](../tasks/comms-adapter-twilio-sms.m
|
|
|
95
138
|
- [ ] `sendSms` delivers via messaging service or from-number config, preferring the
|
|
96
139
|
messaging service when both are present.
|
|
97
140
|
- [ ] A non-E.164 destination fails with a 400 `APIError` before any Twilio call.
|
|
98
|
-
- [ ] Twilio API errors produce `accepted: false` results with
|
|
141
|
+
- [ ] Twilio API errors produce `accepted: false` results with `errorCode` and a correct
|
|
142
|
+
`errorClass` per the classification table; the full Twilio error payload lands in
|
|
99
143
|
`CommsMessage.metadata`; nothing throws through the facade.
|
|
144
|
+
- [ ] Permanent-class failures (21610, 30003, …) are never retried inline and appear as
|
|
145
|
+
non-retryable in the admin dashboard; transient-class failures retry once and stay
|
|
146
|
+
manually retryable.
|
|
147
|
+
- [ ] Each send stores `metadata.consoleUrl` linking the message SID to the Twilio
|
|
148
|
+
console.
|
|
149
|
+
- [ ] Consumer `onError` fires with the classified `SendResult` on every failed send
|
|
150
|
+
(mocked-client test).
|
|
100
151
|
- [ ] Apps not using the adapter do not install the `twilio` SDK.
|
|
101
|
-
- [ ] (Phase 2) Status callbacks flip `CommsMessage.status` to `delivered`/`failed` with
|
|
102
|
-
verified Twilio signatures.
|
|
152
|
+
- [ ] (Phase 2) Status callbacks flip `CommsMessage.status` to `delivered`/`failed` (with
|
|
153
|
+
`errorCode`) under verified Twilio signatures.
|
|
154
|
+
- [ ] (Phase 2) An inbound STOP fires `onOptOut` with `reason: "sms-stop"`; a subsequent
|
|
155
|
+
send to that number records 21610 as `permanent`.
|
|
@@ -32,6 +32,9 @@ it is immediately usable for phone-number verification.
|
|
|
32
32
|
| Channels | `sms` and `email` (Verify's email channel requires a configured Verify email integration; documented, not automated) |
|
|
33
33
|
| Abuse control | Rely on Verify's built-in rate limits; add Terreno-side per-user attempt caps only when `rate-limiting` IP lands |
|
|
34
34
|
| Logging | Start/check attempts logged to `CommsMessage` with channel `verification`, never logging the code |
|
|
35
|
+
| Error classification | Verify errors map to the abstraction taxonomy: 20429/429 and 5xx → `transient`; max-attempts, expired, and invalid-destination → `permanent`; missing/invalid service SID → `config` |
|
|
36
|
+
| Admin dashboard support | Verification rows appear in the [comms-admin-dashboard](comms-admin-dashboard.md) with `errorCode`/`errorClass` and a `metadata.consoleUrl` deep link to the Verify service logs — but the `verification` channel is **never retryable** from admin (codes expire; the user restarts the flow) |
|
|
37
|
+
| Hooks | `onError` fires on failed starts/checks like any other channel; `beforeSend` can gate verification starts (e.g. per-user cool-downs) |
|
|
35
38
|
|
|
36
39
|
## Architecture
|
|
37
40
|
|
|
@@ -41,7 +44,7 @@ export class TwilioVerifyProvider implements VerificationProvider {
|
|
|
41
44
|
readonly id = "twilio-verify";
|
|
42
45
|
constructor(options?: {accountSid?: string; authToken?: string; verifyServiceSid?: string});
|
|
43
46
|
startVerification(options: {to: string; channel: "sms" | "email"}): Promise<SendResult>;
|
|
44
|
-
checkVerification(options: {to: string; code: string}): Promise<{valid: boolean}>;
|
|
47
|
+
checkVerification(options: {to: string; code: string}): Promise<{valid: boolean; error?: string}>;
|
|
45
48
|
}
|
|
46
49
|
```
|
|
47
50
|
|
|
@@ -64,8 +67,8 @@ None.
|
|
|
64
67
|
|
|
65
68
|
## Activity Log & User Updates
|
|
66
69
|
|
|
67
|
-
Verification attempts logged to `CommsMessage` (no codes
|
|
68
|
-
|
|
70
|
+
Verification attempts logged to `CommsMessage` (no codes; destination fully redacted by core,
|
|
71
|
+
with the verification channel stored in metadata).
|
|
69
72
|
|
|
70
73
|
## Not Included / Future Work
|
|
71
74
|
|
|
@@ -89,5 +92,8 @@ See [docs/tasks/comms-adapter-twilio-verify.md](../tasks/comms-adapter-twilio-ve
|
|
|
89
92
|
and email channels.
|
|
90
93
|
- [ ] `checkVerification` returns `valid: true` only for `approved`; expired and
|
|
91
94
|
max-attempt states return `valid: false` with a reason.
|
|
95
|
+
- [ ] Failed starts/checks record `errorCode`/`errorClass` per the classification row and
|
|
96
|
+
fire the consumer `onError` hook; verification rows are marked non-retryable for
|
|
97
|
+
the admin dashboard.
|
|
92
98
|
- [ ] No OTP code ever appears in logs or `CommsMessage` rows; destinations are redacted.
|
|
93
99
|
- [ ] Missing `TWILIO_VERIFY_SERVICE_SID` fails fast at registration with a clear error.
|