@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.
Files changed (116) hide show
  1. package/dist/bootstrap.d.ts.map +1 -1
  2. package/dist/bootstrap.js +8 -9
  3. package/dist/bootstrap.js.map +1 -1
  4. package/dist/docs/templates/bootstrap/frontend/app/_layout.template.tsx +1 -1
  5. package/dist/docs/upgrades/56.0.0-beta.2.md +59 -0
  6. package/dist/docs/versioned/next/README.md +2 -0
  7. package/dist/docs/versioned/next/explanation/roadmap-seed-issues.md +21 -0
  8. package/dist/docs/versioned/next/how-to/README.md +2 -0
  9. package/dist/docs/versioned/next/how-to/migrate-rtk-to-syncdb.md +445 -0
  10. package/dist/docs/versioned/next/implementationPlans/PLAN_INDEX.md +28 -50
  11. package/dist/docs/versioned/next/implementationPlans/b2b-platform-program.md +3 -2
  12. package/dist/docs/versioned/next/implementationPlans/comms-abstraction.md +98 -15
  13. package/dist/docs/versioned/next/implementationPlans/comms-adapter-sendgrid.md +63 -13
  14. package/dist/docs/versioned/next/implementationPlans/comms-adapter-twilio-sms.md +65 -12
  15. package/dist/docs/versioned/next/implementationPlans/comms-adapter-twilio-verify.md +9 -3
  16. package/dist/docs/versioned/next/implementationPlans/comms-admin-dashboard.md +180 -0
  17. package/dist/docs/versioned/next/implementationPlans/migrate-cicd-to-circleci-research.md +126 -0
  18. package/dist/docs/versioned/next/implementationPlans/migrate-cicd-to-circleci.md +269 -0
  19. package/dist/docs/versioned/next/implementationPlans/rtk-to-syncdb-migration-docs.md +52 -47
  20. package/dist/docs/versioned/next/implementationPlans/syncdb-api-inventory.md +303 -0
  21. package/dist/docs/versioned/next/implementationPlans/syncdb-codegen.md +252 -0
  22. package/dist/docs/versioned/next/implementationPlans/syncdb-local-first.md +468 -420
  23. package/dist/docs/versioned/next/implementationPlans/syncdb-phase-c-design.md +425 -0
  24. package/dist/docs/versioned/next/implementationPlans/terreno-syncdb-2.md +800 -0
  25. package/dist/docs/versioned/next/reference/README.md +2 -0
  26. package/dist/docs/versioned/next/reference/comms.md +132 -0
  27. package/dist/docs/versioned/next/reference/environment-variables.md +9 -0
  28. package/dist/docs/versioned/next/reference/syncdb.md +456 -0
  29. package/dist/docs/versioned/next/superpowers/plans/2026-07-28-admin-mobile-navigation.md +97 -0
  30. package/dist/docs/versioned/next/superpowers/specs/2026-07-28-admin-mobile-navigation-design.md +36 -0
  31. package/dist/docs/versioned/next/tasks/comms-abstraction.md +29 -16
  32. package/dist/docs/versioned/next/tasks/comms-adapter-sendgrid.md +17 -7
  33. package/dist/docs/versioned/next/tasks/comms-adapter-twilio-sms.md +17 -7
  34. package/dist/docs/versioned/next/tasks/comms-adapter-twilio-verify.md +3 -3
  35. package/dist/docs/versioned/next/tasks/comms-admin-dashboard.md +52 -0
  36. package/dist/docs/versioned/next/tasks/migrate-cicd-to-circleci.md +215 -0
  37. package/dist/docs/versioned/next/tasks/rtk-to-syncdb-migration-docs.md +10 -10
  38. package/dist/docs/versioned/next/tasks/syncdb-codegen.md +65 -0
  39. package/dist/docs/versioned/next/tasks/syncdb-local-first.md +434 -117
  40. package/package.json +3 -3
  41. package/src/docs/templates/bootstrap/frontend/app/_layout.template.tsx +1 -1
  42. package/src/docs/upgrades/56.0.0-beta.2.md +59 -0
  43. package/src/docs/versioned/next/README.md +2 -0
  44. package/src/docs/versioned/next/explanation/roadmap-seed-issues.md +21 -0
  45. package/src/docs/versioned/next/how-to/README.md +2 -0
  46. package/src/docs/versioned/next/how-to/migrate-rtk-to-syncdb.md +445 -0
  47. package/src/docs/versioned/next/implementationPlans/PLAN_INDEX.md +28 -50
  48. package/src/docs/versioned/next/implementationPlans/b2b-platform-program.md +3 -2
  49. package/src/docs/versioned/next/implementationPlans/comms-abstraction.md +98 -15
  50. package/src/docs/versioned/next/implementationPlans/comms-adapter-sendgrid.md +63 -13
  51. package/src/docs/versioned/next/implementationPlans/comms-adapter-twilio-sms.md +65 -12
  52. package/src/docs/versioned/next/implementationPlans/comms-adapter-twilio-verify.md +9 -3
  53. package/src/docs/versioned/next/implementationPlans/comms-admin-dashboard.md +180 -0
  54. package/src/docs/versioned/next/implementationPlans/migrate-cicd-to-circleci-research.md +126 -0
  55. package/src/docs/versioned/next/implementationPlans/migrate-cicd-to-circleci.md +269 -0
  56. package/src/docs/versioned/next/implementationPlans/rtk-to-syncdb-migration-docs.md +52 -47
  57. package/src/docs/versioned/next/implementationPlans/syncdb-api-inventory.md +303 -0
  58. package/src/docs/versioned/next/implementationPlans/syncdb-codegen.md +252 -0
  59. package/src/docs/versioned/next/implementationPlans/syncdb-local-first.md +468 -420
  60. package/src/docs/versioned/next/implementationPlans/syncdb-phase-c-design.md +425 -0
  61. package/src/docs/versioned/next/implementationPlans/terreno-syncdb-2.md +800 -0
  62. package/src/docs/versioned/next/reference/README.md +2 -0
  63. package/src/docs/versioned/next/reference/comms.md +132 -0
  64. package/src/docs/versioned/next/reference/environment-variables.md +9 -0
  65. package/src/docs/versioned/next/reference/syncdb.md +456 -0
  66. package/src/docs/versioned/next/superpowers/plans/2026-07-28-admin-mobile-navigation.md +97 -0
  67. package/src/docs/versioned/next/superpowers/specs/2026-07-28-admin-mobile-navigation-design.md +36 -0
  68. package/src/docs/versioned/next/tasks/comms-abstraction.md +29 -16
  69. package/src/docs/versioned/next/tasks/comms-adapter-sendgrid.md +17 -7
  70. package/src/docs/versioned/next/tasks/comms-adapter-twilio-sms.md +17 -7
  71. package/src/docs/versioned/next/tasks/comms-adapter-twilio-verify.md +3 -3
  72. package/src/docs/versioned/next/tasks/comms-admin-dashboard.md +52 -0
  73. package/src/docs/versioned/next/tasks/migrate-cicd-to-circleci.md +215 -0
  74. package/src/docs/versioned/next/tasks/rtk-to-syncdb-migration-docs.md +10 -10
  75. package/src/docs/versioned/next/tasks/syncdb-codegen.md +65 -0
  76. package/src/docs/versioned/next/tasks/syncdb-local-first.md +434 -117
  77. package/dist/__tests__/bootstrap.test.d.ts +0 -2
  78. package/dist/__tests__/bootstrap.test.d.ts.map +0 -1
  79. package/dist/__tests__/bootstrap.test.js +0 -299
  80. package/dist/__tests__/bootstrap.test.js.map +0 -1
  81. package/dist/__tests__/databaseQuery.test.d.ts +0 -2
  82. package/dist/__tests__/databaseQuery.test.d.ts.map +0 -1
  83. package/dist/__tests__/databaseQuery.test.js +0 -83
  84. package/dist/__tests__/databaseQuery.test.js.map +0 -1
  85. package/dist/__tests__/docsRoot.test.d.ts +0 -2
  86. package/dist/__tests__/docsRoot.test.d.ts.map +0 -1
  87. package/dist/__tests__/docsRoot.test.js +0 -59
  88. package/dist/__tests__/docsRoot.test.js.map +0 -1
  89. package/dist/__tests__/inferPackages.test.d.ts +0 -2
  90. package/dist/__tests__/inferPackages.test.d.ts.map +0 -1
  91. package/dist/__tests__/inferPackages.test.js +0 -35
  92. package/dist/__tests__/inferPackages.test.js.map +0 -1
  93. package/dist/__tests__/preload.d.ts +0 -2
  94. package/dist/__tests__/preload.d.ts.map +0 -1
  95. package/dist/__tests__/preload.js +0 -12
  96. package/dist/__tests__/preload.js.map +0 -1
  97. package/dist/__tests__/prompts.test.d.ts +0 -2
  98. package/dist/__tests__/prompts.test.d.ts.map +0 -1
  99. package/dist/__tests__/prompts.test.js +0 -295
  100. package/dist/__tests__/prompts.test.js.map +0 -1
  101. package/dist/__tests__/resources.test.d.ts +0 -2
  102. package/dist/__tests__/resources.test.d.ts.map +0 -1
  103. package/dist/__tests__/resources.test.js +0 -352
  104. package/dist/__tests__/resources.test.js.map +0 -1
  105. package/dist/__tests__/search.test.d.ts +0 -2
  106. package/dist/__tests__/search.test.d.ts.map +0 -1
  107. package/dist/__tests__/search.test.js +0 -163
  108. package/dist/__tests__/search.test.js.map +0 -1
  109. package/dist/__tests__/tools.test.d.ts +0 -2
  110. package/dist/__tests__/tools.test.d.ts.map +0 -1
  111. package/dist/__tests__/tools.test.js +0 -540
  112. package/dist/__tests__/tools.test.js.map +0 -1
  113. package/dist/__tests__/upgradeGuide.test.d.ts +0 -2
  114. package/dist/__tests__/upgradeGuide.test.d.ts.map +0 -1
  115. package/dist/__tests__/upgradeGuide.test.js +0 -22
  116. 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
- | [RBAC Permissions](rbac-permissions.md) | Draft (API design) | 2026-07-08 | TBD |
20
- | [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) |
21
- | [MCP Boost parity](mcp-boost-parity.md) | In progress ([PR #802](https://github.com/flourishhealth/terreno/pull/802)) | 2026-06-21 | TBD |
22
- | [Infrastructure MCP server](infra-mcp.md) | Draft — blocked on [RBAC](rbac-permissions.md) | 2026-08-09 | [tasks](../tasks/infra-mcp.md) |
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
- ## Open source launch IPs
30
+ ## Deferred / Closed
25
31
 
26
- All part of the [launch program](oss-launch-program.md). The **RTK flag** column marks dependence on the `@terreno/rtk` → `@terreno/syncdb` migration in PR #869: `None` is safe to implement now, `Partial` has some blocked tasks, `Blocked` cannot start until #869 merges.
32
+ *(Plans parked or superseded.)*
27
33
 
28
- ### Wave 0 — unblocked
34
+ ## Backlog
29
35
 
30
- | Plan | Status | RTK flag | Tasks |
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 nine IPs open
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<{valid: boolean}>;
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, optional), `metadata`
166
- (Mixed). Static `logSend()` mirrors `AIRequest.logRequest`.
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 routes via `modelRouter` with `preCreate` owner injection and
178
- `OwnerQueryFilter`; explorer via `createOpenApiBuilder`, modeled on
179
- `addAiRequestsExplorerRoutes`.
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
- - Notification center, user notification preferences.
213
- - Queue-backed sending with retries — arrives with `job-queues`; until then sends are
214
- inline with one retry.
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
- - [ ] A Terreno app with only console adapters can call `sendMail`, `sendSms`,
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
- - [ ] `POST /comms/pushTokens` registers a device token for the authenticated user;
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
- - [ ] `GET /comms/messages` is admin-only, paginated, and filterable by channel and status.
238
- - [ ] Production apps with an unconfigured channel get a 501 `APIError`, not a silent no-op.
239
- - [ ] `@terreno/comms` core has zero provider SDKs in `dependencies`.
240
- - [ ] All routes appear in `/openapi.json` and the generated SDK compiles.
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 facade. The provider never
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 mapping, mocked-client
65
- tests; example-backend env-gated registration; docs (including the sender-verification
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 with ECDSA
68
- signature verification; delivered/bounce/dropped/open → `DeliveryEvent` →
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, unsubscribe groups.
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 4xx errors surface as `accepted: false` with the SendGrid error message;
103
- nothing throws through the facade.
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 for
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` with the Twilio error code kept in
54
- `CommsMessage.metadata` for triage. Invalid destination numbers fail fast with a
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 mapping, tests with
64
- mocked Twilio client; example-backend env-gated registration; docs.
65
- 2. **Delivery callbacks** *(gated on inbound-webhooks IP)*: status callback route with
66
- Twilio signature verification, statuses → `DeliveryEvent` → `CommsMessage.status`
67
- updates.
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 the Twilio error code in
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, destination redacted to
68
- last-4).
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.