spine-framework 0.4.4 → 0.5.0-rc.5

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 (305) hide show
  1. package/.env.example +13 -3
  2. package/.framework/cli/commands/bootstrap.ts +392 -0
  3. package/.framework/cli/commands/dev.ts +91 -218
  4. package/.framework/cli/commands/doctor.ts +132 -26
  5. package/.framework/cli/commands/generate.ts +38 -298
  6. package/.framework/cli/commands/init.ts +20 -1
  7. package/.framework/cli/commands/install-app.ts +183 -13
  8. package/.framework/cli/commands/migrate.ts +36 -30
  9. package/.framework/cli/commands/migrations.ts +16 -56
  10. package/.framework/cli/commands/upgrade.ts +35 -0
  11. package/.framework/cli/commands/validate-app.ts +127 -1
  12. package/.framework/cli/commands/validate-schema.ts +147 -0
  13. package/.framework/cli/context.ts +223 -110
  14. package/.framework/cli/index.ts +6 -0
  15. package/.framework/cli/lib/project-files.ts +241 -1
  16. package/.framework/functions/_shared/app-manifest.ts +2 -0
  17. package/.framework/functions/_shared/hash-api-key.ts +183 -79
  18. package/.framework/functions/_shared/logging.ts +562 -224
  19. package/.framework/functions/_shared/middleware.ts +47 -44
  20. package/.framework/functions/_shared/permissions.ts +4 -0
  21. package/.framework/functions/admin-data.ts +51 -28
  22. package/.framework/functions/ai-agents.ts +10 -6
  23. package/.framework/functions/api-keys.ts +301 -122
  24. package/.framework/functions/apps.ts +63 -29
  25. package/.framework/functions/cil-ingest.ts +48 -15
  26. package/.framework/functions/debug-auth.ts +14 -8
  27. package/.framework/functions/deliveries.ts +39 -12
  28. package/.framework/functions/embeddings.ts +149 -78
  29. package/.framework/functions/integration-outbound.ts +67 -27
  30. package/.framework/functions/integrations.ts +254 -127
  31. package/.framework/functions/logs.ts +273 -133
  32. package/.framework/functions/observability.ts +278 -148
  33. package/.framework/functions/pipeline-executions.ts +1 -1
  34. package/.framework/functions/pipelines.ts +14 -9
  35. package/.framework/functions/prompt-configs.ts +7 -4
  36. package/.framework/functions/roles.ts +8 -5
  37. package/.framework/functions/security.ts +30 -17
  38. package/.framework/functions/system-cron.ts +240 -127
  39. package/.framework/functions/tests.ts +7 -6
  40. package/.framework/functions/timers.ts +310 -107
  41. package/.framework/functions/triggers.ts +28 -13
  42. package/.framework/functions/types.ts +13 -10
  43. package/.framework/migrations/069_log_stats_window.sql +54 -0
  44. package/.framework/migrations/070_app_type_schema_rpcs.sql +166 -0
  45. package/.framework/migrations/071_timer_interval_keys.sql +96 -0
  46. package/.framework/migrations/072_slug_autofill.sql +77 -0
  47. package/.framework/migrations/073_seed_member_roles.sql +26 -0
  48. package/.framework/migrations/074_app_route_prefix_domains.sql +168 -0
  49. package/.framework/migrations/075_security_definer_grants.sql +157 -0
  50. package/.framework/src/apps/admin/index.tsx +1141 -775
  51. package/.framework/src/components/ErrorBoundary.tsx +136 -0
  52. package/.framework/src/components/admin/AdminListPage.tsx +81 -73
  53. package/.framework/src/components/admin/AdminSidebar.tsx +88 -79
  54. package/.framework/src/components/admin/EntitySelect.tsx +181 -0
  55. package/.framework/src/components/admin/PageInstructions.tsx +86 -127
  56. package/.framework/src/components/app-shell/GenericDetailPage.tsx +37 -13
  57. package/.framework/src/components/app-shell/GenericListPage.tsx +28 -32
  58. package/.framework/src/components/layout/Sidebar.tsx +300 -177
  59. package/.framework/src/components/runtime/DataDetailPage.tsx +67 -35
  60. package/.framework/src/components/runtime/DataHeader.tsx +96 -48
  61. package/.framework/src/components/runtime/DataListPage.tsx +3 -26
  62. package/.framework/src/components/runtime/DataStats.tsx +10 -10
  63. package/.framework/src/components/runtime/DataTable.tsx +12 -10
  64. package/.framework/src/components/ui/sheet.tsx +24 -26
  65. package/.framework/src/components/ui/textarea.tsx +74 -48
  66. package/.framework/src/contexts/AppContext.tsx +5 -12
  67. package/.framework/src/contexts/AuthContext.tsx +17 -9
  68. package/.framework/src/hooks/useApps.ts +18 -22
  69. package/.framework/src/hooks/useEntityList.ts +12 -9
  70. package/.framework/src/hooks/useListSchema.ts +108 -58
  71. package/.framework/src/instructions/README.md +74 -0
  72. package/.framework/src/instructions/accounts.md +97 -0
  73. package/.framework/src/instructions/ai-agents.md +69 -0
  74. package/.framework/src/instructions/alerts.md +80 -0
  75. package/.framework/src/instructions/api-keys.md +87 -0
  76. package/.framework/src/instructions/apps.md +77 -0
  77. package/.framework/src/instructions/concepts/ai-and-prompts.md +150 -0
  78. package/.framework/src/instructions/concepts/apps-and-renderers.md +228 -0
  79. package/.framework/src/instructions/concepts/auth-and-api-keys.md +133 -0
  80. package/.framework/src/instructions/concepts/automation.md +186 -0
  81. package/.framework/src/instructions/concepts/concurrency-and-idempotency.md +125 -0
  82. package/.framework/src/instructions/concepts/core-vs-custom.md +124 -0
  83. package/.framework/src/instructions/concepts/design-schema.md +152 -0
  84. package/.framework/src/instructions/concepts/integrations-and-webhooks.md +127 -0
  85. package/.framework/src/instructions/concepts/observability.md +116 -0
  86. package/.framework/src/instructions/concepts/permissions.md +150 -0
  87. package/.framework/src/instructions/concepts/runtime-records.md +111 -0
  88. package/.framework/src/instructions/concepts/scope-and-accounts.md +112 -0
  89. package/.framework/src/instructions/concepts/spine-way.md +172 -0
  90. package/.framework/src/instructions/deliveries.md +59 -0
  91. package/.framework/src/instructions/guide/00-orientation.md +106 -0
  92. package/.framework/src/instructions/guide/01-install-and-first-run.md +253 -0
  93. package/.framework/src/instructions/guide/02-project-map.md +147 -0
  94. package/.framework/src/instructions/guide/03-domain-model.md +223 -0
  95. package/.framework/src/instructions/guide/04-first-app.md +227 -0
  96. package/.framework/src/instructions/guide/05-glossary.md +183 -0
  97. package/.framework/src/instructions/integrations.md +96 -0
  98. package/.framework/src/instructions/internal/cil-markers.md +83 -0
  99. package/.framework/src/instructions/internal/docs-contract.md +158 -0
  100. package/.framework/src/instructions/invites.md +60 -0
  101. package/.framework/src/instructions/kb.md +80 -0
  102. package/.framework/src/instructions/logs.md +77 -0
  103. package/.framework/src/instructions/notifications.md +77 -0
  104. package/.framework/src/instructions/observability.md +61 -0
  105. package/.framework/src/instructions/operations/deployment.md +115 -0
  106. package/.framework/src/instructions/operations/migrations.md +125 -0
  107. package/.framework/src/instructions/operations/security-checklist.md +170 -0
  108. package/.framework/src/instructions/operations/troubleshooting.md +170 -0
  109. package/.framework/src/instructions/operations/upgrade.md +116 -0
  110. package/.framework/src/instructions/people.md +91 -0
  111. package/.framework/src/instructions/pipeline-executions.md +62 -0
  112. package/.framework/src/instructions/pipelines.md +78 -0
  113. package/.framework/src/instructions/prompts.md +95 -0
  114. package/.framework/src/instructions/reference/api/admin-data.md +162 -0
  115. package/.framework/src/instructions/reference/api/auth.md +113 -0
  116. package/.framework/src/instructions/reference/api/errors-and-envelopes.md +113 -0
  117. package/.framework/src/instructions/reference/app-manifest.md +184 -0
  118. package/.framework/src/instructions/reference/cli.md +124 -0
  119. package/.framework/src/instructions/reference/database.md +132 -0
  120. package/.framework/src/instructions/reference/design-schema.md +280 -0
  121. package/.framework/src/instructions/reference/env-vars.md +133 -0
  122. package/.framework/src/instructions/reference/frontend-sdk.md +207 -0
  123. package/.framework/src/instructions/reference/functions/account-nodes.md +75 -0
  124. package/.framework/src/instructions/reference/functions/admin-data.md +197 -0
  125. package/.framework/src/instructions/reference/functions/admin-funnel.md +90 -0
  126. package/.framework/src/instructions/reference/functions/admin-install-funnel.md +107 -0
  127. package/.framework/src/instructions/reference/functions/admin-signals.md +102 -0
  128. package/.framework/src/instructions/reference/functions/ai-agents.md +148 -0
  129. package/.framework/src/instructions/reference/functions/ai-interactions.md +156 -0
  130. package/.framework/src/instructions/reference/functions/api-keys.md +130 -0
  131. package/.framework/src/instructions/reference/functions/apps.md +140 -0
  132. package/.framework/src/instructions/reference/functions/auth.md +99 -0
  133. package/.framework/src/instructions/reference/functions/debug-auth.md +91 -0
  134. package/.framework/src/instructions/reference/functions/deliveries.md +110 -0
  135. package/.framework/src/instructions/reference/functions/embeddings.md +166 -0
  136. package/.framework/src/instructions/reference/functions/eval-harness.md +149 -0
  137. package/.framework/src/instructions/reference/functions/index.md +103 -0
  138. package/.framework/src/instructions/reference/functions/integration-outbound.md +200 -0
  139. package/.framework/src/instructions/reference/functions/integration-routes.md +151 -0
  140. package/.framework/src/instructions/reference/functions/integrations.md +130 -0
  141. package/.framework/src/instructions/reference/functions/invites.md +182 -0
  142. package/.framework/src/instructions/reference/functions/item-progress.md +107 -0
  143. package/.framework/src/instructions/reference/functions/kb.md +166 -0
  144. package/.framework/src/instructions/reference/functions/logs.md +141 -0
  145. package/.framework/src/instructions/reference/functions/observability.md +105 -0
  146. package/.framework/src/instructions/reference/functions/pipeline-executions.md +132 -0
  147. package/.framework/src/instructions/reference/functions/pipeline-runner-background.md +121 -0
  148. package/.framework/src/instructions/reference/functions/pipelines.md +136 -0
  149. package/.framework/src/instructions/reference/functions/prompt-configs.md +121 -0
  150. package/.framework/src/instructions/reference/functions/roles.md +121 -0
  151. package/.framework/src/instructions/reference/functions/security.md +140 -0
  152. package/.framework/src/instructions/reference/functions/system-cron.md +136 -0
  153. package/.framework/src/instructions/reference/functions/system.md +106 -0
  154. package/.framework/src/instructions/reference/functions/tests.md +97 -0
  155. package/.framework/src/instructions/reference/functions/timers.md +117 -0
  156. package/.framework/src/instructions/reference/functions/trigger-scheduler.md +102 -0
  157. package/.framework/src/instructions/reference/functions/triggers.md +140 -0
  158. package/.framework/src/instructions/reference/functions/types.md +146 -0
  159. package/.framework/src/instructions/reference/seeds.md +126 -0
  160. package/.framework/src/instructions/reference/shared-modules.md +127 -0
  161. package/.framework/src/instructions/reference/spine-config.md +114 -0
  162. package/.framework/src/instructions/roles.md +80 -0
  163. package/.framework/src/instructions/runtime-collaboration.md +106 -0
  164. package/.framework/src/instructions/runtime-detail.md +70 -0
  165. package/.framework/src/instructions/runtime-list.md +77 -0
  166. package/.framework/src/instructions/security.md +67 -0
  167. package/.framework/src/instructions/tasks/add-a-pipeline.md +110 -0
  168. package/.framework/src/instructions/tasks/add-a-role.md +96 -0
  169. package/.framework/src/instructions/tasks/add-a-timer.md +114 -0
  170. package/.framework/src/instructions/tasks/add-a-trigger.md +106 -0
  171. package/.framework/src/instructions/tasks/add-a-type.md +151 -0
  172. package/.framework/src/instructions/tasks/add-a-webhook.md +108 -0
  173. package/.framework/src/instructions/tasks/add-an-ai-agent.md +108 -0
  174. package/.framework/src/instructions/tasks/add-an-integration.md +124 -0
  175. package/.framework/src/instructions/tasks/build-a-custom-page.md +118 -0
  176. package/.framework/src/instructions/tasks/document-your-app.md +117 -0
  177. package/.framework/src/instructions/tasks/package-and-install-an-app.md +136 -0
  178. package/.framework/src/instructions/tasks/register-an-app.md +137 -0
  179. package/.framework/src/instructions/tasks/test-your-code.md +108 -0
  180. package/.framework/src/instructions/tasks/write-a-function.md +132 -0
  181. package/.framework/src/instructions/tasks/write-a-migration.md +110 -0
  182. package/.framework/src/instructions/testing.md +59 -0
  183. package/.framework/src/instructions/timers.md +84 -0
  184. package/.framework/src/instructions/triggers.md +69 -0
  185. package/.framework/src/instructions/types.md +93 -0
  186. package/.framework/src/lib/app-mount.ts +88 -0
  187. package/.framework/src/pages/NotFoundPage.tsx +62 -31
  188. package/.framework/src/pages/admin/AIAgentDetailPage.tsx +117 -106
  189. package/.framework/src/pages/admin/AIAgentsPage.tsx +86 -48
  190. package/.framework/src/pages/admin/APIKeyDetailPage.tsx +153 -152
  191. package/.framework/src/pages/admin/APIKeysPage.tsx +91 -90
  192. package/.framework/src/pages/admin/AlertsConfigPage.tsx +29 -18
  193. package/.framework/src/pages/admin/AppDetailPage.tsx +6 -60
  194. package/.framework/src/pages/admin/AppsPage.tsx +104 -66
  195. package/.framework/src/pages/admin/DeliveriesPage.tsx +5 -5
  196. package/.framework/src/pages/admin/EmbeddingDetailPage.tsx +5 -23
  197. package/.framework/src/pages/admin/EmbeddingsPage.tsx +5 -26
  198. package/.framework/src/pages/admin/IntegrationDetailPage.tsx +7 -7
  199. package/.framework/src/pages/admin/IntegrationsPage.tsx +85 -43
  200. package/.framework/src/pages/admin/LogsPage.tsx +144 -76
  201. package/.framework/src/pages/admin/ObservabilityDashboard.tsx +223 -140
  202. package/.framework/src/pages/admin/PipelineDetailPage.tsx +134 -113
  203. package/.framework/src/pages/admin/PipelineExecutionsPage.tsx +108 -45
  204. package/.framework/src/pages/admin/PipelinesPage.tsx +160 -115
  205. package/.framework/src/pages/admin/PromptConfigDetailPage.tsx +96 -98
  206. package/.framework/src/pages/admin/PromptConfigsPage.tsx +101 -59
  207. package/.framework/src/pages/admin/RoleDetailPage.tsx +90 -79
  208. package/.framework/src/pages/admin/RolesPage.tsx +86 -43
  209. package/.framework/src/pages/admin/SecurityPage.tsx +1 -1
  210. package/.framework/src/pages/admin/TestingDashboard.tsx +16 -28
  211. package/.framework/src/pages/admin/TimerDetailPage.tsx +117 -100
  212. package/.framework/src/pages/admin/TimersPage.tsx +90 -62
  213. package/.framework/src/pages/admin/TriggerDetailPage.tsx +90 -83
  214. package/.framework/src/pages/admin/TriggersPage.tsx +124 -99
  215. package/.framework/src/pages/admin/TypeDetailPage.tsx +152 -138
  216. package/.framework/src/pages/admin/TypesPage.tsx +90 -48
  217. package/.framework/src/pages/admin/cil/CILAgentPage.tsx +17 -25
  218. package/.framework/src/pages/admin/cil/CILChunkDetailPage.tsx +17 -27
  219. package/.framework/src/pages/admin/cil/CILDashboardPage.tsx +72 -40
  220. package/.framework/src/pages/admin/cil/CILFileListPage.tsx +321 -129
  221. package/.framework/src/pages/admin/cil/CILFileSelectPage.tsx +18 -29
  222. package/.framework/src/pages/admin/cil/CILGraphPage.tsx +26 -38
  223. package/.framework/src/pages/admin/cil/CILIngestionPage.tsx +34 -46
  224. package/.framework/src/pages/admin/cil/CILProjectsPage.tsx +189 -103
  225. package/.framework/src/pages/admin/cil/CILSearchPage.tsx +246 -136
  226. package/.framework/src/pages/admin/cil/CILSuggestionsPage.tsx +11 -22
  227. package/.framework/src/pages/admin/cil/CILVersionsPage.tsx +9 -20
  228. package/.framework/src/pages/admin/cil/FlexiblePipelinePage.tsx +26 -38
  229. package/.framework/src/pages/admin/kb/KBAdminPage.tsx +7 -26
  230. package/.framework/src/pages/auth/LoginPage.tsx +3 -2
  231. package/.framework/src/pages/auth/RegisterPage.tsx +16 -8
  232. package/.framework/templates/agent/AGENTS.md +167 -0
  233. package/.framework/templates/agent/skills/spine-add-ai-agent/SKILL.md +58 -0
  234. package/.framework/templates/agent/skills/spine-add-automation/SKILL.md +62 -0
  235. package/.framework/templates/agent/skills/spine-add-integration/SKILL.md +63 -0
  236. package/.framework/templates/agent/skills/spine-architect/SKILL.md +86 -0
  237. package/.framework/templates/agent/skills/spine-install-app/SKILL.md +79 -0
  238. package/.framework/templates/agent/skills/spine-migration/SKILL.md +75 -0
  239. package/.framework/templates/agent/skills/spine-new-app/SKILL.md +79 -0
  240. package/.framework/templates/agent/skills/spine-new-function/SKILL.md +71 -0
  241. package/.framework/templates/agent/skills/spine-new-type/SKILL.md +72 -0
  242. package/.framework/templates/agent/skills/spine-schema/SKILL.md +77 -0
  243. package/.framework/templates/agent/skills/spine-test/SKILL.md +67 -0
  244. package/.framework/templates/agent/skills/spine-troubleshoot/SKILL.md +74 -0
  245. package/.framework/templates/agent/skills/spine-upgrade/SKILL.md +69 -0
  246. package/.framework/templates/project.json +4 -1
  247. package/.framework/tests/unit/account-scoping-gates.test.ts +394 -0
  248. package/.framework/tests/unit/agent-files.test.ts +355 -0
  249. package/.framework/tests/unit/agent-runner.test.ts +479 -44
  250. package/.framework/tests/unit/api-keys-create-scope-gate.test.ts +160 -0
  251. package/.framework/tests/unit/api-keys-omit-secret-fields.test.ts +38 -0
  252. package/.framework/tests/unit/app-mount.test.ts +80 -0
  253. package/.framework/tests/unit/bootstrap.test.ts +242 -0
  254. package/.framework/tests/unit/cli-context-api-key.test.ts +114 -0
  255. package/.framework/tests/unit/debug-auth.test.ts +38 -0
  256. package/.framework/tests/unit/deliveries-account-scoping.test.ts +185 -0
  257. package/.framework/tests/unit/integration-outbound-credentials.test.ts +167 -0
  258. package/.framework/tests/unit/integrations-audit-redaction.test.ts +130 -0
  259. package/.framework/tests/unit/logging-app-id.test.ts +75 -0
  260. package/.framework/tests/unit/security-definer-grants.test.ts +154 -0
  261. package/.framework/tests/unit/system-context-denial.test.ts +145 -0
  262. package/.framework/tests/unit/system-cron.test.ts +240 -0
  263. package/.framework/tests/unit/timers.test.ts +159 -0
  264. package/README.md +125 -166
  265. package/config/tsconfig.cli.json +2 -2
  266. package/package.json +15 -4
  267. package/templates/project/package-lock.json +2 -2
  268. package/.framework/docs/admin-routes.md +0 -76
  269. package/.framework/docs/api-docs-progress.md +0 -38
  270. package/.framework/docs/api-governance.md +0 -146
  271. package/.framework/docs/api-testing-results.md +0 -212
  272. package/.framework/docs/assembly-launch-guide.md +0 -138
  273. package/.framework/docs/audit-results.md +0 -590
  274. package/.framework/docs/db-api-inventory.md +0 -95
  275. package/.framework/docs/gap-remediation-backlog.md +0 -103
  276. package/.framework/docs/ui-adoption-verification.md +0 -111
  277. package/.framework/docs/ui-api-coverage.md +0 -84
  278. package/.framework/docs/v2-compatibility-audit.md +0 -228
  279. package/.framework/mcp/index.ts +0 -133
  280. package/.framework/mcp/load-env.ts +0 -60
  281. package/.framework/mcp/shared/app-manifest.ts +0 -275
  282. package/.framework/mcp/shared/db.ts +0 -477
  283. package/.framework/mcp/shared/kb.ts +0 -118
  284. package/.framework/mcp/tools/architect.ts +0 -431
  285. package/.framework/mcp/tools/cil/context.ts +0 -345
  286. package/.framework/mcp/tools/cil/graph.ts +0 -242
  287. package/.framework/mcp/tools/cil/ingest.ts +0 -1217
  288. package/.framework/mcp/tools/cil/pipeline-orchestrator.ts +0 -693
  289. package/.framework/mcp/tools/cil/pipeline.ts +0 -767
  290. package/.framework/mcp/tools/cil/project.ts +0 -791
  291. package/.framework/mcp/tools/cil/retrieval.ts +0 -336
  292. package/.framework/mcp/tools/cil/search-core.ts +0 -454
  293. package/.framework/mcp/tools/cil/search.ts +0 -97
  294. package/.framework/mcp/tools/cil/suggest.ts +0 -186
  295. package/.framework/mcp/tools/cil/validate.ts +0 -1369
  296. package/.framework/mcp/tools/kb/delete.ts +0 -139
  297. package/.framework/mcp/tools/kb/index.ts +0 -52
  298. package/.framework/mcp/tools/kb/ingest.ts +0 -84
  299. package/.framework/mcp/tools/kb/list-types.ts +0 -102
  300. package/.framework/mcp/tools/kb/publish.ts +0 -100
  301. package/.framework/mcp/tools/kb/reindex.ts +0 -86
  302. package/.framework/mcp/tools/kb/revoke.ts +0 -85
  303. package/.framework/mcp/tools/kb/search.ts +0 -101
  304. package/.framework/mcp/tools/kb/upsert.ts +0 -125
  305. package/.framework/mcp/tools/observability.ts +0 -510
@@ -0,0 +1,200 @@
1
+ ---
2
+ title: integration-outbound
3
+ audience: both
4
+ kind: reference
5
+ verified_against: f69c37d
6
+ verified_on: 2026-10-30
7
+ source_files:
8
+ - .framework/functions/integration-outbound.ts
9
+ - .framework/functions/_shared/resolve-credentials.ts
10
+ - .framework/functions/_shared/retry-deliveries.ts
11
+ - .framework/functions/_shared/llm.ts
12
+ - .framework/functions/_shared/middleware.ts
13
+ - .framework/functions/adapters/generic-webhook-outbound.ts
14
+ - .framework/functions/adapters/openai-outbound.ts
15
+ - .framework/functions/adapters/resend-outbound.ts
16
+ - .framework/functions/deliveries.ts
17
+ - .framework/migrations/004_core_rpc_and_broker.sql
18
+ - public/_redirects
19
+ ---
20
+
21
+ # integration-outbound
22
+
23
+ ## What it does
24
+
25
+ `integration-outbound` (`.framework/functions/integration-outbound.ts`) is the
26
+ single egress path for outbound calls. Every attempt is recorded as a
27
+ `webhook_deliveries` row (`direction: 'outbound'`), so there is no
28
+ fire-and-forget — the delivery row exists before, during, and after the wire
29
+ call.
30
+
31
+ `execute` runs this sequence:
32
+
33
+ 1. Resolve the integration row by `integration_slug`: the caller-account row
34
+ first, then a system row (`account_id IS NULL`); both must be `is_active`.
35
+ 2. Open (or reuse, on `delivery_id`) the `webhook_deliveries` row — `status:
36
+ 'pending'`, `attempt_count: 0`, `max_attempts` from
37
+ `action_config.max_attempts` → `integration.config.retry_policy.max_attempts`
38
+ → `3`.
39
+ 3. Resolve the credential field (`action_config.credential_field`, default
40
+ `api_token`) via `resolveCredential` (`.framework/functions/_shared/
41
+ resolve-credentials.ts`) — server-side only: `source: 'env_var'` reads
42
+ `process.env[value]`, `source: 'value'` returns the literal. An absent or
43
+ explicitly-empty (`source: 'value'`, `value: ''`) entry means "call
44
+ without auth": `execute` skips resolution, passes no credential to the
45
+ adapter, and audits `integration.credential_missing` /
46
+ `integration.credential_empty` with `skipped: true` — the adapter decides
47
+ whether a key is required. A named-but-unset `env_var` still throws. Every
48
+ resolution or failure emits an `integration.credential_*` audit event.
49
+ 4. Pick the adapter: `action_config.adapter ||
50
+ integration.config.outbound_adapter`. Built-ins live in
51
+ `builtInOutboundAdapters` (`adapters/openai-outbound`,
52
+ `adapters/generic-webhook-outbound`, `adapters/resend-outbound`); any other
53
+ name is dynamically imported as `./<name>` and must export `buildRequest`.
54
+ 5. `fetch` the request the adapter built, then update the delivery to
55
+ `delivered` (with `response_status`, `response_body`, `duration_ms`) or back
56
+ to `pending`/`failed` with `next_attempt_at`.
57
+
58
+ Known callers: `_shared/llm.ts` (`callOpenAI`, `callOpenAIEmbeddings`,
59
+ `callOpenAIChat` — all `integration_slug: 'openai'`), the pipeline stage
60
+ handlers `http_request`, `agent_inference`, `send_email`, and
61
+ `handler_module: 'webhook'` (all `integration_slug: 'generic-http'`), the
62
+ `retryPendingDeliveries` sweep (`.framework/functions/_shared/
63
+ retry-deliveries.ts`), and manual retry in `deliveries.ts`.
64
+
65
+ ## Route and methods
66
+
67
+ - `POST /api/integration-outbound` — `public/_redirects` maps `/api/*` to
68
+ `/.netlify/functions/:splat`; the default export is `createHandler(execute)`.
69
+ - There is no `action` dispatch — the request body *is* the contract.
70
+
71
+ ## Authentication
72
+
73
+ `createHandler` resolves a principal and rejects anonymous requests with 401,
74
+ so any authenticated caller works (human JWT or `x-api-key` machine principal).
75
+ Internal callers import `execute` directly and pass their own `CoreContext`.
76
+
77
+ The `delivery_id` (retry) path additionally requires
78
+ `ctx.principal.isInternal` — true for the internal-invoke secret,
79
+ `is_internal` API keys, and `SYSTEM_PRINCIPAL`. A non-internal caller passing
80
+ `delivery_id` is *not* rejected: the stored-payload retry branch is skipped,
81
+ but the row id is still reused for status bookkeeping instead of inserting a
82
+ new delivery. See Agent notes.
83
+
84
+ ## Request contract
85
+
86
+ Top-level body fields:
87
+
88
+ | Field | Type | Required | Notes |
89
+ |---|---|---|---|
90
+ | `integration_slug` | string | yes | `integrations.slug`; account-scoped row wins over the system row. |
91
+ | `record_id` | uuid | no | Fetched via admin-data `get` and passed to the adapter as `record`. |
92
+ | `action_config` | object | no | See below. |
93
+ | `delivery_id` | uuid | no | Reuse an existing delivery row (retry path). |
94
+
95
+ `action_config` keys consumed by `execute` itself:
96
+
97
+ | Key | Notes |
98
+ |---|---|
99
+ | `adapter` | Adapter name; falls back to `integration.config.outbound_adapter`. |
100
+ | `credential_field` | `integrations.credentials` field to resolve; default `api_token`. |
101
+ | `max_attempts` | Overrides `config.retry_policy.max_attempts` and the default 3. |
102
+ | `action_slug` | Stored on the delivery row for traceability. |
103
+ | `update_record` / `update_fields` | On success, stamps `last_outbound_status`, `last_outbound_at`, plus `update_fields` onto `record_id` via admin-data `update`. |
104
+
105
+ Adapter-specific `action_config` keys:
106
+
107
+ - `adapters/generic-webhook-outbound` — `url` (required), `method` (default
108
+ `POST`), `headers`, `body`. Adds `Authorization: Bearer <credential>` when a
109
+ credential resolved.
110
+ - `adapters/openai-outbound` — `endpoint` (required, e.g. `/chat/completions`,
111
+ `/embeddings`), `body`. Base URL from `integration.config.base_url` (default
112
+ `https://api.openai.com/v1`).
113
+ - `adapters/resend-outbound` — `to` and `subject` required; `html` or `text`;
114
+ `from` resolves `actionConfig.from` → `config.from` → `EMAIL_FROM` env →
115
+ `onboarding@resend.dev`; optional `reply_to`; `config.api_url` overrides the
116
+ Resend endpoint.
117
+ - Custom adapters — dynamically imported `./<name>` module exporting
118
+ `buildRequest(record, ctx)` → `{ url, method, headers?, body?,
119
+ external_id? }`.
120
+
121
+ ## Response contract
122
+
123
+ Success returns, inside the standard `{ data, error, meta }` envelope:
124
+
125
+ ```json
126
+ { "success": true, "delivery_id": "<uuid>", "response_status": 200,
127
+ "response_body": { }, "is_retry": false }
128
+ ```
129
+
130
+ `response_body` is the parsed JSON response, or the raw text when it isn't
131
+ JSON. A non-2xx target response throws — the delivery row is still updated
132
+ first (see Side effects), so the attempt is durable even though the HTTP
133
+ response is an error envelope.
134
+
135
+ ## Side effects
136
+
137
+ - `webhook_deliveries` insert (`pending`) or update (`processing` →
138
+ `delivered`/`pending`/`failed` with `attempt_count`, `next_attempt_at`,
139
+ `response_status`, `response_body`, `duration_ms`, `last_error`).
140
+ - Audit events: `integration.outbound.started`, `.succeeded`,
141
+ `.failed_http`, `.error`, plus `integration.credential_*` events from
142
+ `resolveCredential`.
143
+ - One external `fetch` per attempt. The tracking INSERT is intentionally
144
+ overlapped with the wire call (`insertPromise`), so logging does not
145
+ serialize ahead of latency.
146
+ - Optional admin-data `update` on `record_id` when
147
+ `action_config.update_record` is set — runs the full permission/audit
148
+ pipeline, not a raw write.
149
+
150
+ ## Errors
151
+
152
+ | Condition | Error |
153
+ |---|---|
154
+ | No slug | `integration_slug is required` |
155
+ | Slug matches no active account or system integration | `Integration not found: <slug>` |
156
+ | Retry of unknown delivery | `Delivery not found: <id>` |
157
+ | `env_var` credential names an unset variable | `Environment variable <var> … is not set` (audits `integration.credential_env_missing`). Absent/empty credential entries do **not** throw — the call proceeds unauthenticated and the adapter enforces its own requirement (e.g. `OpenAI adapter requires api_token credential`) |
158
+ | No adapter name resolved | `No outbound adapter configured for integration <slug>` |
159
+ | Adapter import failed or lacks `buildRequest` | `Outbound adapter not found: <name>` |
160
+ | Adapter returned no url/method | `Outbound adapter must return url and method` |
161
+ | Delivery insert failed | `Failed to create outbound delivery record` |
162
+ | Non-2xx target | `Outbound HTTP <method> to <url> failed with status <status>` |
163
+ | Delivery-row insert races and fails | `Failed to create outbound delivery record` (surfaced where `insertPromise` is awaited) |
164
+
165
+ Thrown errors map to HTTP 500 unless they carry `statusCode`; the delivery
166
+ row keeps the attempt either way.
167
+
168
+ ## Agent notes
169
+
170
+ - **Retry semantics.** `computeNextAttempt` is `30s * 2^attempt` capped at 1
171
+ hour plus up to 20% jitter. A failed attempt stays `pending` with
172
+ `next_attempt_at` until `attempt_count >= max_attempts`, then `failed`.
173
+ `retryPendingDeliveries` (run by `system-cron` each tick) sweeps `pending`
174
+ rows with `attempt_count < 10` whose `next_attempt_at` is due or null — up
175
+ to 100 per pass — and re-invokes `execute({ delivery_id })` under
176
+ `SYSTEM_PRINCIPAL`. Manual retry: `POST /api/deliveries?action=retry { id }`
177
+ — outbound only. Inbound retry is unimplemented.
178
+ - **Non-internal `delivery_id`.** The `isInternal` check gates only the
179
+ *payload-replay* branch; the row id itself is still honored. An ordinary
180
+ caller can pass `delivery_id` and have their request's outcome stamped onto
181
+ that row (`status: 'processing'` → result). There is no per-account check on
182
+ the row — treat `delivery_id` as an internal field.
183
+ - **`generic-http` is unauthenticated by default — and that works.**
184
+ Migration `004` seeds it with `api_token: { source: 'value', value: '' }`;
185
+ `execute` treats an absent or empty-literal credential as "no auth", so
186
+ `http_request`/`webhook`/`agent_inference` stages send the request with no
187
+ `Authorization` header on a stock install (the generic adapter only adds
188
+ Bearer when a credential resolved). To authenticate, set a real token or
189
+ point the field at an `env_var`; to authenticate differently, pass
190
+ `action_config.headers`. The skip is still audited as
191
+ `integration.credential_empty`/`credential_missing` with `skipped: true`.
192
+ - **`openai` seed** uses `env_var: OPENAI_API_KEY`; `callOpenAIChat` defaults
193
+ to `gpt-4o-mini`/0.7/4000, overridable via `config.models.chat` /
194
+ `.embedding` on the integration row.
195
+ - **Mock responses live in callers, not here.** `callInference` in
196
+ `.framework/functions/_shared/agent-runner.ts` catches "not found"/
197
+ "credential" errors and returns a marked mock; this function always throws.
198
+ - Do not call outbound adapters or `fetch` external URLs directly — delivery
199
+ tracking, credential audit, and retry all depend on going through
200
+ `execute`. See [integrations and webhooks](../../concepts/integrations-and-webhooks.md).
@@ -0,0 +1,151 @@
1
+ ---
2
+ title: integration-routes
3
+ audience: both
4
+ kind: reference
5
+ verified_against: f69c37d
6
+ verified_on: 2026-10-30
7
+ source_files:
8
+ - .framework/functions/integration-routes.ts
9
+ - .framework/functions/_shared/webhook-registry.ts
10
+ - .framework/functions/_shared/webhook-registration.ts
11
+ - .framework/functions/_shared/middleware.ts
12
+ - .framework/functions/_shared/retry-deliveries.ts
13
+ - .framework/functions/deliveries.ts
14
+ - .framework/migrations/004_core_rpc_and_broker.sql
15
+ - public/_redirects
16
+ ---
17
+
18
+ # integration-routes
19
+
20
+ ## What it does
21
+
22
+ `integration-routes` (`.framework/functions/integration-routes.ts`) is the
23
+ inbound webhook surface. An external caller POSTs a provider payload to a
24
+ named integration; the function authenticates the caller's API key, resolves
25
+ the integration's registered handler through the `webhook_handlers` registry,
26
+ lets the adapter normalize the payload into an item shape, creates the record
27
+ through admin-data (full permission/type/audit pipeline), and records the
28
+ whole attempt as a `webhook_deliveries` row (`direction: 'inbound'`).
29
+
30
+ The `execute` sequence:
31
+
32
+ 1. Read `slug` from `?slug=` or `body.slug`; require a machine principal —
33
+ `ctx.principal.id` must match an `api_keys` row.
34
+ 2. Load the `integrations` row matching `slug` **and** `apiKey.account_id`
35
+ (inbound integrations are always account-scoped here), `is_active`.
36
+ 3. Enforce key pinning: `apiKey.integration_id` must equal the integration id
37
+ when set; `apiKey.app_id` must equal `integration.app_id` when both set.
38
+ 4. Extract the event name — `x-event-type` header first, else
39
+ `integration.config.event_path` (a dot-path into the payload).
40
+ 5. Resolve the handler name `config.handler_name || provider ||
41
+ 'generic-webhook'` through `resolveHandler` (`.framework/functions/_shared/
42
+ webhook-registry.ts`): a `webhook_handlers` row maps `name → function_name`,
43
+ scoped by `(app_id, account_id)` with a global `NULL/NULL` fallback; the
44
+ module is dynamically imported (`custom/…` paths resolve into the custom
45
+ tree) and must export `adapt`, `handler`, or a default function.
46
+ 6. Insert the inbound `webhook_deliveries` row (`payload`, `api_key_id`,
47
+ `handler_name`, `external_id: body.id`), emit
48
+ `integration.inbound.started`, invoke the adapter, create the `items`
49
+ record via admin-data `create`, and mark the delivery `delivered`.
50
+
51
+ ## Route and methods
52
+
53
+ - `/api/integration-routes?slug=<integration-slug>` — `public/_redirects`
54
+ maps `/api/*` to `/.netlify/functions/:splat`; the default export is
55
+ `createPublicHandler(execute)`.
56
+ - `execute` performs no method check — any HTTP method with a JSON body
57
+ works; in practice providers POST.
58
+
59
+ ## Authentication
60
+
61
+ `createPublicHandler` resolves a principal but does not reject anonymous
62
+ requests — `execute` itself enforces:
63
+
64
+ - `ctx.principal.id` must exist — `401 Machine principal required` otherwise
65
+ (JWT-only human callers have a person id, not an api-key id, so they fail
66
+ at the next check).
67
+ - The id must match an `api_keys` row — `401 Invalid API key` otherwise.
68
+ - The key's `account_id` scopes the integration lookup; a key pinned to
69
+ `integration_id` or `app_id` gets `403` for anything else.
70
+
71
+ There is **no HMAC/signature verification** — the API key sent as
72
+ `x-api-key` is the entire credential. Prefer keys pinned to
73
+ `integration_id`/`app_id` and rotate them like any secret. See
74
+ [auth and API keys](../../concepts/auth-and-api-keys.md).
75
+
76
+ ## Request contract
77
+
78
+ | Input | Where | Notes |
79
+ |---|---|---|
80
+ | `slug` | query `?slug=` or `body.slug` | Integration slug, required. |
81
+ | `x-api-key` | header | Machine credential; resolved by `resolvePrincipal`. |
82
+ | `x-event-type` | header | Optional event name; overrides `config.event_path` extraction. |
83
+ | payload | JSON body | Passed verbatim to the adapter; `body.id` is stored as the delivery's `external_id`. |
84
+
85
+ The adapter returns:
86
+
87
+ ```ts
88
+ { type_slug: string, data: Record<string, any>, title?: string,
89
+ status?: string, thread?: any, links?: any[], external_id?: string,
90
+ error?: string, retryable?: boolean }
91
+ ```
92
+
93
+ `type_slug` is required for record creation. `thread` and `links` are
94
+ declared in the contract but currently no-ops (TODO in source).
95
+
96
+ ## Response contract
97
+
98
+ Success returns the admin-data `create` result — the created `items` record —
99
+ inside the standard `{ data, error, meta }` envelope, and the delivery is
100
+ marked `delivered` with `response_status: 200`.
101
+
102
+ Adapter-reported errors (`adapterResult.error`) throw with the adapter's
103
+ message; `retryable: true` marks the delivery `pending` (with a backoff
104
+ `next_attempt_at`) before the throw — though see Agent notes: the outer catch
105
+ re-marks *every* post-insert error `pending`, and nothing re-delivers inbound
106
+ rows anyway.
107
+
108
+ ## Side effects
109
+
110
+ - `webhook_deliveries` insert (`direction: 'inbound'`, `api_key_id`,
111
+ `handler_name`, `payload`, `external_id`) then status updates.
112
+ - One `items` row created via `admin-data` `create` under a synthesized
113
+ context (`adminDb`, `accountId: integration.account_id`,
114
+ `principal.id: null`) — type resolution, validation, triggers, and audit
115
+ all run.
116
+ - Audit events: `integration.inbound.started`, `.delivered`,
117
+ `.adapter_retryable`, `.adapter_failed`, `.error`.
118
+
119
+ ## Errors
120
+
121
+ | Status | Condition |
122
+ |---|---|
123
+ | 400 | `Integration slug is required` |
124
+ | 401 | `Machine principal required` · `Invalid API key` |
125
+ | 403 | `API key is not authorized for this integration` · `… for this app` |
126
+ | 404 | `Integration not found: <slug>` · `Handler not found: <name>` |
127
+ | 500 | `Failed to create inbound delivery record` · `Adapter result is missing type_slug` · adapter/admin-data errors propagate |
128
+
129
+ ## Agent notes
130
+
131
+ - **Inbound retry is not implemented.** `retryPendingDeliveries`
132
+ (`.framework/functions/_shared/retry-deliveries.ts`) counts inbound rows as
133
+ `inbound_failed` and `deliveries?action=retry` rejects non-outbound
134
+ deliveries outright. Worse, the `catch` in `execute` re-marks *any* error
135
+ after the delivery insert as `pending` with `next_attempt_at` — including
136
+ non-retryable adapter errors and admin-data create failures — so failed
137
+ inbound deliveries sit `pending` forever. When debugging "stuck" inbound
138
+ rows, check `last_error`, not just `status`.
139
+ - `registerWebhookHandler`/`deregisterWebhookHandler`
140
+ (`.framework/functions/_shared/webhook-registration.ts`) manage the
141
+ `webhook_handlers` table — upsert on `(app_id, account_id, name)`;
142
+ deregister soft-deletes. Migration `004` seeds the `generic-webhook`
143
+ handler (`adapters/generic-webhook-inbound`), which stores the raw payload
144
+ as a `webhook_event` item.
145
+ - The lookup is cached per `(appId, accountId, handlerName)` in-process —
146
+ newly registered handlers may not resolve until a cold start.
147
+ - Adapters run inside the function with a console `logger` and receive the
148
+ raw headers — they can do their own signature checks if the provider
149
+ requires it, but nothing enforces that.
150
+ - See [integrations and webhooks](../../concepts/integrations-and-webhooks.md)
151
+ for the full mental model.
@@ -0,0 +1,130 @@
1
+ ---
2
+ title: integrations
3
+ audience: both
4
+ kind: reference
5
+ verified_against: f69c37d
6
+ verified_on: 2026-10-30
7
+ source_files:
8
+ - .framework/functions/integrations.ts
9
+ - .framework/functions/_shared/middleware.ts
10
+ - .framework/functions/_shared/permissions.ts
11
+ - .framework/migrations/004_core_rpc_and_broker.sql
12
+ - .framework/public/_redirects
13
+ ---
14
+
15
+ # integrations
16
+
17
+ ## What it does
18
+
19
+ CRUD over `public.integrations` — the registry of external providers
20
+ (`integration_type`: webhook/oauth/api/scheduled_sync, `provider`,
21
+ `version`, `config`) including their `credentials` secrets map.
22
+ Runtime delivery happens elsewhere — `integration-outbound` executes
23
+ deliveries and `deliveries` lists them (see
24
+ [Integrations and webhooks](../../concepts/integrations-and-webhooks.md)).
25
+
26
+ ## Route and methods
27
+
28
+ `/.netlify/functions/integrations`, reachable as `/api/integrations`
29
+ (`/api/*` → `/.netlify/functions/:splat`,
30
+ `.framework/public/_redirects`). Dispatch is by HTTP method only:
31
+
32
+ | Call | Handler |
33
+ |---|---|
34
+ | `GET /api/integrations` | `list` |
35
+ | `GET /api/integrations?id=<uuid>` | `get` |
36
+ | `POST /api/integrations` | `create` |
37
+ | `PATCH /api/integrations` (`id` in body or query) | `update` |
38
+ | `DELETE /api/integrations?id=<uuid>` | `remove` |
39
+
40
+ ## Authentication
41
+
42
+ Standard `createHandler` auth: `x-api-key` → machine principal,
43
+ `x-internal-secret` → internal principal, `Bearer <jwt>` → human;
44
+ anonymous → 401.
45
+
46
+ - `list`/`create` require `ctx.accountId` (`Account context required` /
47
+ `User context (person and account) required`).
48
+ - There is **no role check** anywhere in this file. System admins use
49
+ `adminDb`; others use `ctx.db` (JWT-scoped for humans, service role
50
+ for machines).
51
+ - Credential exposure is controlled on the **read** path by
52
+ `sanitizeRecordData`: system admins get the full row including
53
+ `credentials`; every other principal gets `{ id }` — the integration
54
+ collapses to an opaque handle
55
+ (`.framework/functions/_shared/permissions.ts`,
56
+ `sanitizeFirstSurfaceRecordData`: no `design_schema` on the row →
57
+ minimal response). `credentials` never leave the function for
58
+ non-admins.
59
+
60
+ ## Request contract
61
+
62
+ **list** (query): `integration_type`, `provider`, `is_active`,
63
+ `is_configured` (`'true'`/`'false'`), `limit` (default 100), `offset`
64
+ (default 0). Ordered by `name`; joins `app` and `created_by` person.
65
+
66
+ **get** (query): `id`.
67
+
68
+ **create** (body): `name`, `integration_type`, `provider` required.
69
+ Optional: `slug` (defaults to a slugified `name`), `app_id`,
70
+ `description`, `version` (default `'1.0.0'`), `config` (default `{}`),
71
+ `credentials`, `metadata`. `account_id` and `created_by` are stamped
72
+ from context. `is_configured` is **computed**, not accepted: every
73
+ `credentials` entry must be `{ source, value }` with a non-empty string
74
+ `value` for the flag to be `true`.
75
+
76
+ **update** (`id` in body or query + body): allowlist — `name`, `slug`,
77
+ `description`, `integration_type`, `provider`, `version`, `config`,
78
+ `credentials`, `metadata`, `is_active`. Passing `credentials`
79
+ recomputes `is_configured`.
80
+
81
+ **remove** (query): `id` — soft delete (`is_active = false`).
82
+
83
+ ## Response contract
84
+
85
+ Standard `{ data, error, meta }` envelope. `list`/`get`/`create`/
86
+ `update`/`remove` return sanitized row(s): full row for system admins,
87
+ `{ id }` for non-admin humans. `remove` returns the deactivated row.
88
+
89
+ ## Side effects
90
+
91
+ - `create`/`update` store `credentials` verbatim — the column is a
92
+ secrets map `{ field: { source: 'value'|'env_var', value } }`
93
+ (`.framework/migrations/004_core_rpc_and_broker.sql`), not encrypted at
94
+ rest by this function.
95
+ - `update` recomputes `is_configured` whenever `credentials` is in the
96
+ body; it is never recomputed for `config`-only edits.
97
+ - `emitLog` writes `integration.created` / `.updated` / `.deleted` audit
98
+ rows to `public.logs`. The update audit payload includes the full
99
+ `updateData` — **including credentials** — in `after`.
100
+
101
+ ## Errors
102
+
103
+ | Condition | HTTP | Message |
104
+ |---|---|---|
105
+ | No account context | 500 | `Account context required` |
106
+ | Missing create fields | 500 | `name, integration_type, and provider are required` |
107
+ | No person+account context (create) | 500 | `User context (person and account) required` |
108
+ | Missing `id` | 500 | `Integration ID is required` |
109
+ | Row missing on delete | 500 | `Integration not found` |
110
+ | Unknown method | 500 | `Unsupported method: <METHOD>` |
111
+
112
+ Validation failures surface as HTTP 500 — thrown `Error`s without a
113
+ `statusCode` map to 500 in the shared error path.
114
+
115
+ ## Agent notes
116
+
117
+ - Writes are guarded only by authentication + RLS (for humans).
118
+ `create`/`update`/`remove` do not call any `canCreate`/`canUpdate`
119
+ permission check — a machine principal's service-role `ctx.db` writes
120
+ unconditionally.
121
+ - `update`'s audit log records `credentials` in the `after` payload —
122
+ secrets land in `public.logs`. Rotate rather than patch in place if
123
+ log exposure matters.
124
+ - Slug uniqueness is `UNIQUE(app_id, account_id, slug)`
125
+ (`.framework/migrations/004_core_rpc_and_broker.sql`); there is no
126
+ pre-check, so duplicates surface as PostgREST constraint errors.
127
+ - Inbound webhook execution and credential resolution live in
128
+ `integration-routes`/`integration-outbound` and
129
+ `.framework/functions/_shared/resolve-credentials.ts` — this function
130
+ only manages the registry rows.
@@ -0,0 +1,182 @@
1
+ ---
2
+ title: invites
3
+ audience: both
4
+ kind: reference
5
+ verified_against: f69c37d
6
+ verified_on: 2026-10-30
7
+ source_files:
8
+ - .framework/functions/invites.ts
9
+ - .framework/functions/_shared/middleware.ts
10
+ - .framework/functions/_shared/rate-limit.ts
11
+ - .framework/functions/_shared/security.ts
12
+ - .framework/functions/admin-data.ts
13
+ - .framework/functions/integration-outbound.ts
14
+ - .framework/public/_redirects
15
+ ---
16
+
17
+ # invites
18
+
19
+ ## What it does
20
+
21
+ The registration and membership endpoint. Public actions probe the system
22
+ (first user, domain match, invite-token validation); authenticated
23
+ actions complete registration, create/revoke/re-send invites, approve or
24
+ deny join requests, and manage account members. Invitees are `people`
25
+ rows in `invited`/`pending` status carrying `invite_*` fields in `data`.
26
+
27
+ ## Route and methods
28
+
29
+ `/.netlify/functions/invites`, reachable as `/api/invites` (`/api/*` →
30
+ `/.netlify/functions/:splat`). The outer handler is a
31
+ `createPublicHandler` dispatching **only** on `?action=` — there is no
32
+ HTTP-method check, so `GET` and `POST` both reach every action (write
33
+ actions still need a JSON body):
34
+
35
+ | `?action=` | Auth | Purpose |
36
+ |---|---|---|
37
+ | `check-first-user` | public | `{ is_first_user }` — true when `people` is empty |
38
+ | `check-domain` | public | domain→account match for registration routing |
39
+ | `validate-token` | public | inspect an invite token (`?token=`) |
40
+ | `complete-registration` | authenticated | place the caller via `complete_registration_placement` RPC |
41
+ | `create-invite` | account admin | create an invited `people` stub + invite URL |
42
+ | `list-invites` | authenticated | list pending invites, account-scoped |
43
+ | `revoke-invite` | account admin | deactivate an invited person |
44
+ | `reinvite` | account admin | rotate token + expiry on invited/inactive person |
45
+ | `block-invite` | account admin | stamp `invite_blocked_at` on a person |
46
+ | `approve` | account admin | activate a `pending` join request |
47
+ | `deny` | account admin | deactivate a `pending` join request |
48
+ | `list-members` | `*_admin` | list account members (via `admin-data`) |
49
+ | `update-member` | `*_admin` | change `full_name` / `role_slug` |
50
+ | `deactivate-member` | `*_admin` | set status `inactive` |
51
+
52
+ Unknown action → `Unknown action: <action>`.
53
+
54
+ ## Authentication
55
+
56
+ Mixed. The dispatcher is public (`createPublicHandler`), so anonymous
57
+ reaches the three probe actions. Every other action requires an
58
+ authenticated principal — each inner handler re-checks
59
+ `principal.id !== 'anonymous'` and throws `Authentication required` (a
60
+ plain `Error`, so it surfaces as HTTP 500, not 401).
61
+
62
+ "Account admin" (`requireAccountAdmin`) means: principal permissions
63
+ include `system`, **or** roles include `system_admin`, **or** any
64
+ `*_admin` role **and** `principal.accountId` equals the target account.
65
+ Member actions (`list-members`, `update-member`, `deactivate-member`)
66
+ accept `system` permission or any `*_admin` role (`canManageMembers`)
67
+ plus the same account check.
68
+
69
+ ## Request contract
70
+
71
+ **`check-domain`** (query): `email` — must contain `@`. Personal-mailbox
72
+ domains (gmail, yahoo, hotmail, outlook, icloud, protonmail, etc.) always
73
+ return `{ match: false }`; otherwise matches when an active `people` row
74
+ exists with `@<domain>`.
75
+
76
+ **`validate-token`** (query): `token`. Looks up a `people` row in
77
+ `invited`/`pending` status with `data.invite_token = <token>`.
78
+
79
+ **`complete-registration`** (body): `app` and `path` required;
80
+ `account_name`, `email` optional. `path` values are resolved by the RPC
81
+ (e.g. `new_account` triggers extra `account_create` rate limits).
82
+
83
+ **`create-invite`** (body): `email`, `account_id`, `role_slug` required;
84
+ `full_name`, `base_url`, `app` optional. `role_slug: 'system_admin'` is
85
+ refused, as is any role whose `is_system` flag is true or that is absent
86
+ from the app's `invitation.allowed_roles`/`registration.roles` allowlist
87
+ (when the app declares one).
88
+
89
+ **`revoke-invite` / `reinvite` / `block-invite` / `approve` / `deny`**
90
+ (body): `person_id` (+ `base_url` for `reinvite`).
91
+
92
+ **`list-invites`** (query): `account_id` (system users may pass any;
93
+ others are scoped to their own), `role_slugs` (comma-separated).
94
+
95
+ **`list-members`** (query/body): `account_id` (defaults to the caller's).
96
+ **`update-member`** (body): `person_id`, `full_name`, `role_slug`.
97
+ **`deactivate-member`** (body): `person_id`.
98
+
99
+ ## Response contract
100
+
101
+ Standard `{ data, error, meta }` envelope.
102
+
103
+ - `check-first-user` → `{ is_first_user: boolean }`.
104
+ - `check-domain` → `{ match, account_id? }`.
105
+ - `validate-token` → `{ email, full_name, account_name, role_name,
106
+ kind }`.
107
+ - `complete-registration` → `{ status, account_slug, role_slug?, next? }`
108
+ from the RPC placement.
109
+ - `create-invite` → `{ id, token, invite_url, expires_at, email_sent }`.
110
+ - `reinvite` → `{ token, invite_url, expires_at }`.
111
+ - `list-invites` → array of `{ id, email, full_name, status, kind,
112
+ expires_at, blocked_at, created_at, account, role }`.
113
+ - `revoke-invite`/`block-invite`/`approve`/`deny` → `{ success: true }`.
114
+ - `list-members` → `{ data: [...] }`; `update-member`/`deactivate-member`
115
+ → `{ success: true, data }`.
116
+
117
+ ## Side effects
118
+
119
+ - `create-invite`/`reinvite` write `people` rows (via `admin-data` create
120
+ for the stub): `data.invite_token` (16-char alnum),
121
+ `invite_kind: 'invite'`, `invite_expires_at` (now + 7 days),
122
+ `invite_attempts: 0`, `invite_blocked_at`, `invite_invited_by`.
123
+ - `invite_url` = `<base>/register?t=<token>`; `base_url` is honored only
124
+ for localhost or origins in `APP_BASE_URL`/`ALLOWED_ORIGINS`, otherwise
125
+ falls back to `APP_BASE_URL`.
126
+ - `create-invite` sends email through `integration-outbound` when an
127
+ active `invite-email` (preferred) or `transactional-email` integration
128
+ exists; `config.templates.invite` subject/html support
129
+ `{{to_name}}`, `{{invite_url}}`, `{{account_name}}`, `{{expires_at}}`
130
+ placeholders, with a built-in HTML fallback.
131
+ - `complete-registration` calls the service-role
132
+ `complete_registration_placement` RPC (the only writer of
133
+ `account_id`/`role_id` placement), then `recordRegistrationGeo` +
134
+ `registration.completed` log.
135
+ - `checkRegistrationBlocklist` (email, email domain, IP) runs before
136
+ placement; `checkRateLimit` enforces `registration` and
137
+ `account_create` (person + IP keyed), `invite_create`, and
138
+ `invite_email` (account keyed).
139
+ - `emitLog` writes `invite.created`, `invite.revoked`,
140
+ `invite.reinvited`, `invite.approved`, `invite.blocked`,
141
+ `invite.denied`, `member.updated`, `member.deactivated`.
142
+ - Member actions delegate to `admin-data` list/update internally, so
143
+ admin-data logging and trigger firing apply there too.
144
+
145
+ ## Errors
146
+
147
+ | Condition | HTTP | Message |
148
+ |---|---|---|
149
+ | Anonymous on a gated action | 500 | `Authentication required` |
150
+ | Not an account admin | 500 | `Insufficient permissions — *_admin role required for this account` (and member variants) |
151
+ | Missing required fields | 500 | `email, account_id, and role_slug are required` / `app and path are required` / `person_id is required` / `Token is required` / `Valid email is required` |
152
+ | Unknown action | 500 | `Unknown action: <action>` |
153
+ | `system_admin` or system role requested | 500 | `Role '<slug>' is not permitted in this context` / `Role '<slug>' not found` |
154
+ | Duplicate email+account | 500 | `A person with this email already exists or has a pending invite for this account` |
155
+ | Bad/used token | 500 | `Invite not found or already used` |
156
+ | Blocked invite | 500 | `This invite link has been blocked. Please contact the administrator.` |
157
+ | Expired invite | 500 | `This invite link has expired. Please request a new one.` |
158
+ | ≥5 failed attempts | 500 | `Too many failed attempts. This invite has been blocked.` |
159
+ | Registration blocklist hit | 403 | `This registration is not eligible. Contact support if you believe this is an error.` |
160
+ | Rate limit | 429 | `RateLimitError` envelope fields (`action`, `limit`, `retry_after`) |
161
+ | RPC placement failure | 400 | RPC message or `Registration placement failed` |
162
+ | Wrong state | 500 | `Invite not found or already processed` / `Person is not in a reinvitable state` / `Access request not found or already processed` |
163
+
164
+ ## Agent notes
165
+
166
+ - First-user bootstrap is **not** here — the Postgres
167
+ `handle_new_auth_user` trigger grants `system_admin` on the very first
168
+ signup. `check-first-user` only reports whether `people` is empty.
169
+ - Invite tokens live on `people.data.invite_token`; revoke clears the
170
+ token and deactivates; reinvite rotates it (`invited` or `inactive`
171
+ rows only); `validate-token` enforces expiry, `invite_blocked_at`, and
172
+ the 5-attempt ceiling.
173
+ - `approve`/`deny` target `status = 'pending'` rows — the join-request
174
+ path — while `revoke-invite` targets `invited`/`pending`.
175
+ - `update-member` can only assign roles allowed by the target account's
176
+ owned apps (`invitation.allowed_roles`/`registration.roles` in
177
+ `apps.config`), and never `system_admin` or `is_system` roles.
178
+ - `list-invites` does **not** verify membership in the queried account —
179
+ it scopes by `account_id` for non-system callers but accepts any
180
+ `account_id` they pass. Don't treat it as an authorization boundary.
181
+ - Registration never trusts client claims: the placement RPC derives
182
+ account and role server-side from the app's registration config.