spine-framework 0.4.3 → 0.5.0-rc.4

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 (320) 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/migrate.ts +36 -30
  8. package/.framework/cli/commands/migrations.ts +16 -56
  9. package/.framework/cli/commands/upgrade.ts +35 -0
  10. package/.framework/cli/commands/validate-schema.ts +147 -0
  11. package/.framework/cli/context.ts +223 -110
  12. package/.framework/cli/index.ts +6 -0
  13. package/.framework/cli/lib/project-files.ts +241 -1
  14. package/.framework/functions/_shared/agent-runner.ts +1000 -81
  15. package/.framework/functions/_shared/cil.ts +2 -1
  16. package/.framework/functions/_shared/db.ts +3 -6
  17. package/.framework/functions/_shared/hash-api-key.ts +183 -79
  18. package/.framework/functions/_shared/kb.ts +61 -7
  19. package/.framework/functions/_shared/logging.ts +562 -224
  20. package/.framework/functions/_shared/message-sequence.ts +53 -0
  21. package/.framework/functions/_shared/middleware.ts +47 -44
  22. package/.framework/functions/_shared/pipeline-runner.ts +150 -37
  23. package/.framework/functions/_shared/principal.ts +9 -6
  24. package/.framework/functions/_shared/trigger-engine.ts +149 -40
  25. package/.framework/functions/admin-data.ts +48 -31
  26. package/.framework/functions/ai-agents.ts +103 -33
  27. package/.framework/functions/api-keys.ts +301 -122
  28. package/.framework/functions/apps.ts +43 -26
  29. package/.framework/functions/cil-ingest.ts +48 -15
  30. package/.framework/functions/debug-auth.ts +14 -8
  31. package/.framework/functions/deliveries.ts +39 -12
  32. package/.framework/functions/embeddings.ts +145 -76
  33. package/.framework/functions/integration-outbound.ts +67 -27
  34. package/.framework/functions/integrations.ts +254 -127
  35. package/.framework/functions/logs.ts +273 -133
  36. package/.framework/functions/observability.ts +277 -147
  37. package/.framework/functions/pipeline-executions.ts +1 -1
  38. package/.framework/functions/pipeline-runner-background.ts +120 -0
  39. package/.framework/functions/pipelines.ts +14 -9
  40. package/.framework/functions/prompt-configs.ts +7 -4
  41. package/.framework/functions/roles.ts +8 -5
  42. package/.framework/functions/security.ts +30 -17
  43. package/.framework/functions/system-cron.ts +250 -128
  44. package/.framework/functions/tests.ts +7 -6
  45. package/.framework/functions/timers.ts +310 -107
  46. package/.framework/functions/trigger-scheduler.ts +65 -5
  47. package/.framework/functions/triggers.ts +28 -13
  48. package/.framework/functions/types.ts +13 -10
  49. package/.framework/migrations/064_episode_leases.sql +196 -0
  50. package/.framework/migrations/065_execution_tables_updated_at.sql +20 -0
  51. package/.framework/migrations/066_episode_steal_thread_merge.sql +148 -0
  52. package/.framework/migrations/067_episode_lease_stale_steal.sql +93 -0
  53. package/.framework/migrations/068_cron_schedule_timer_rpcs.sql +251 -0
  54. package/.framework/migrations/069_log_stats_window.sql +54 -0
  55. package/.framework/migrations/070_app_type_schema_rpcs.sql +166 -0
  56. package/.framework/migrations/071_timer_interval_keys.sql +96 -0
  57. package/.framework/migrations/072_slug_autofill.sql +77 -0
  58. package/.framework/migrations/073_seed_member_roles.sql +26 -0
  59. package/.framework/schemas/spine.config.schema.json +62 -0
  60. package/.framework/src/App.tsx +3 -1
  61. package/.framework/src/apps/admin/index.tsx +1141 -775
  62. package/.framework/src/components/ErrorBoundary.tsx +136 -0
  63. package/.framework/src/components/admin/AdminListPage.tsx +81 -73
  64. package/.framework/src/components/admin/AdminSidebar.tsx +92 -81
  65. package/.framework/src/components/admin/EntitySelect.tsx +181 -0
  66. package/.framework/src/components/admin/PageInstructions.tsx +86 -127
  67. package/.framework/src/components/app-shell/GenericAppShell.tsx +3 -1
  68. package/.framework/src/components/app-shell/GenericDetailPage.tsx +37 -13
  69. package/.framework/src/components/app-shell/GenericListPage.tsx +28 -32
  70. package/.framework/src/components/auth/TurnstileWidget.tsx +9 -6
  71. package/.framework/src/components/kb/KBArticleEditor.tsx +49 -6
  72. package/.framework/src/components/layout/Layout.tsx +4 -2
  73. package/.framework/src/components/layout/Sidebar.tsx +304 -179
  74. package/.framework/src/components/runtime/DataDetailPage.tsx +67 -35
  75. package/.framework/src/components/runtime/DataHeader.tsx +96 -48
  76. package/.framework/src/components/runtime/DataListPage.tsx +3 -26
  77. package/.framework/src/components/runtime/DataStats.tsx +10 -10
  78. package/.framework/src/components/runtime/DataTable.tsx +12 -10
  79. package/.framework/src/components/ui/sheet.tsx +24 -26
  80. package/.framework/src/components/ui/textarea.tsx +74 -48
  81. package/.framework/src/hooks/useBranding.ts +47 -0
  82. package/.framework/src/hooks/useEntityList.ts +12 -9
  83. package/.framework/src/hooks/useListSchema.ts +108 -58
  84. package/.framework/src/instructions/README.md +57 -0
  85. package/.framework/src/instructions/ai-agents.md +69 -0
  86. package/.framework/src/instructions/alerts.md +80 -0
  87. package/.framework/src/instructions/api-keys.md +87 -0
  88. package/.framework/src/instructions/apps.md +77 -0
  89. package/.framework/src/instructions/concepts/ai-and-prompts.md +150 -0
  90. package/.framework/src/instructions/concepts/apps-and-renderers.md +162 -0
  91. package/.framework/src/instructions/concepts/auth-and-api-keys.md +133 -0
  92. package/.framework/src/instructions/concepts/automation.md +186 -0
  93. package/.framework/src/instructions/concepts/concurrency-and-idempotency.md +125 -0
  94. package/.framework/src/instructions/concepts/core-vs-custom.md +124 -0
  95. package/.framework/src/instructions/concepts/design-schema.md +152 -0
  96. package/.framework/src/instructions/concepts/integrations-and-webhooks.md +127 -0
  97. package/.framework/src/instructions/concepts/observability.md +116 -0
  98. package/.framework/src/instructions/concepts/permissions.md +150 -0
  99. package/.framework/src/instructions/concepts/runtime-records.md +111 -0
  100. package/.framework/src/instructions/concepts/scope-and-accounts.md +112 -0
  101. package/.framework/src/instructions/concepts/spine-way.md +172 -0
  102. package/.framework/src/instructions/deliveries.md +59 -0
  103. package/.framework/src/instructions/guide/00-orientation.md +106 -0
  104. package/.framework/src/instructions/guide/01-install-and-first-run.md +253 -0
  105. package/.framework/src/instructions/guide/02-project-map.md +147 -0
  106. package/.framework/src/instructions/guide/03-domain-model.md +223 -0
  107. package/.framework/src/instructions/guide/04-first-app.md +227 -0
  108. package/.framework/src/instructions/guide/05-glossary.md +183 -0
  109. package/.framework/src/instructions/integrations.md +96 -0
  110. package/.framework/src/instructions/internal/cil-markers.md +83 -0
  111. package/.framework/src/instructions/invites.md +60 -0
  112. package/.framework/src/instructions/kb.md +80 -0
  113. package/.framework/src/instructions/logs.md +77 -0
  114. package/.framework/src/instructions/observability.md +61 -0
  115. package/.framework/src/instructions/operations/deployment.md +115 -0
  116. package/.framework/src/instructions/operations/migrations.md +125 -0
  117. package/.framework/src/instructions/operations/security-checklist.md +148 -0
  118. package/.framework/src/instructions/operations/troubleshooting.md +170 -0
  119. package/.framework/src/instructions/operations/upgrade.md +116 -0
  120. package/.framework/src/instructions/pipeline-executions.md +62 -0
  121. package/.framework/src/instructions/pipelines.md +78 -0
  122. package/.framework/src/instructions/prompts.md +95 -0
  123. package/.framework/src/instructions/reference/api/admin-data.md +162 -0
  124. package/.framework/src/instructions/reference/api/auth.md +113 -0
  125. package/.framework/src/instructions/reference/api/errors-and-envelopes.md +113 -0
  126. package/.framework/src/instructions/reference/app-manifest.md +150 -0
  127. package/.framework/src/instructions/reference/cli.md +124 -0
  128. package/.framework/src/instructions/reference/database.md +132 -0
  129. package/.framework/src/instructions/reference/design-schema.md +280 -0
  130. package/.framework/src/instructions/reference/env-vars.md +133 -0
  131. package/.framework/src/instructions/reference/functions/account-nodes.md +75 -0
  132. package/.framework/src/instructions/reference/functions/admin-data.md +195 -0
  133. package/.framework/src/instructions/reference/functions/admin-funnel.md +90 -0
  134. package/.framework/src/instructions/reference/functions/admin-install-funnel.md +107 -0
  135. package/.framework/src/instructions/reference/functions/admin-signals.md +102 -0
  136. package/.framework/src/instructions/reference/functions/ai-agents.md +148 -0
  137. package/.framework/src/instructions/reference/functions/ai-interactions.md +156 -0
  138. package/.framework/src/instructions/reference/functions/api-keys.md +130 -0
  139. package/.framework/src/instructions/reference/functions/apps.md +140 -0
  140. package/.framework/src/instructions/reference/functions/auth.md +99 -0
  141. package/.framework/src/instructions/reference/functions/debug-auth.md +91 -0
  142. package/.framework/src/instructions/reference/functions/deliveries.md +110 -0
  143. package/.framework/src/instructions/reference/functions/embeddings.md +166 -0
  144. package/.framework/src/instructions/reference/functions/eval-harness.md +149 -0
  145. package/.framework/src/instructions/reference/functions/index.md +103 -0
  146. package/.framework/src/instructions/reference/functions/integration-outbound.md +200 -0
  147. package/.framework/src/instructions/reference/functions/integration-routes.md +151 -0
  148. package/.framework/src/instructions/reference/functions/integrations.md +130 -0
  149. package/.framework/src/instructions/reference/functions/invites.md +182 -0
  150. package/.framework/src/instructions/reference/functions/item-progress.md +107 -0
  151. package/.framework/src/instructions/reference/functions/kb.md +166 -0
  152. package/.framework/src/instructions/reference/functions/logs.md +141 -0
  153. package/.framework/src/instructions/reference/functions/observability.md +105 -0
  154. package/.framework/src/instructions/reference/functions/pipeline-executions.md +132 -0
  155. package/.framework/src/instructions/reference/functions/pipeline-runner-background.md +121 -0
  156. package/.framework/src/instructions/reference/functions/pipelines.md +136 -0
  157. package/.framework/src/instructions/reference/functions/prompt-configs.md +121 -0
  158. package/.framework/src/instructions/reference/functions/roles.md +121 -0
  159. package/.framework/src/instructions/reference/functions/security.md +140 -0
  160. package/.framework/src/instructions/reference/functions/system-cron.md +136 -0
  161. package/.framework/src/instructions/reference/functions/system.md +106 -0
  162. package/.framework/src/instructions/reference/functions/tests.md +97 -0
  163. package/.framework/src/instructions/reference/functions/timers.md +117 -0
  164. package/.framework/src/instructions/reference/functions/trigger-scheduler.md +102 -0
  165. package/.framework/src/instructions/reference/functions/triggers.md +140 -0
  166. package/.framework/src/instructions/reference/functions/types.md +146 -0
  167. package/.framework/src/instructions/reference/seeds.md +126 -0
  168. package/.framework/src/instructions/reference/shared-modules.md +127 -0
  169. package/.framework/src/instructions/reference/spine-config.md +114 -0
  170. package/.framework/src/instructions/roles.md +80 -0
  171. package/.framework/src/instructions/runtime-detail.md +70 -0
  172. package/.framework/src/instructions/runtime-list.md +77 -0
  173. package/.framework/src/instructions/security.md +67 -0
  174. package/.framework/src/instructions/tasks/add-a-pipeline.md +110 -0
  175. package/.framework/src/instructions/tasks/add-a-role.md +96 -0
  176. package/.framework/src/instructions/tasks/add-a-timer.md +114 -0
  177. package/.framework/src/instructions/tasks/add-a-trigger.md +106 -0
  178. package/.framework/src/instructions/tasks/add-a-type.md +151 -0
  179. package/.framework/src/instructions/tasks/add-a-webhook.md +108 -0
  180. package/.framework/src/instructions/tasks/add-an-ai-agent.md +108 -0
  181. package/.framework/src/instructions/tasks/add-an-integration.md +124 -0
  182. package/.framework/src/instructions/tasks/build-a-custom-page.md +118 -0
  183. package/.framework/src/instructions/tasks/package-and-install-an-app.md +127 -0
  184. package/.framework/src/instructions/tasks/test-your-code.md +108 -0
  185. package/.framework/src/instructions/tasks/write-a-function.md +132 -0
  186. package/.framework/src/instructions/tasks/write-a-migration.md +110 -0
  187. package/.framework/src/instructions/testing.md +59 -0
  188. package/.framework/src/instructions/timers.md +82 -0
  189. package/.framework/src/instructions/triggers.md +69 -0
  190. package/.framework/src/instructions/types.md +93 -0
  191. package/.framework/src/lib/branding.ts +128 -0
  192. package/.framework/src/pages/NoAppPage.tsx +3 -1
  193. package/.framework/src/pages/NotFoundPage.tsx +62 -31
  194. package/.framework/src/pages/SetupPage.tsx +5 -2
  195. package/.framework/src/pages/admin/AIAgentDetailPage.tsx +117 -106
  196. package/.framework/src/pages/admin/AIAgentsPage.tsx +86 -48
  197. package/.framework/src/pages/admin/APIKeyDetailPage.tsx +153 -152
  198. package/.framework/src/pages/admin/APIKeysPage.tsx +91 -90
  199. package/.framework/src/pages/admin/AlertsConfigPage.tsx +29 -18
  200. package/.framework/src/pages/admin/AppDetailPage.tsx +6 -60
  201. package/.framework/src/pages/admin/AppsPage.tsx +104 -66
  202. package/.framework/src/pages/admin/DeliveriesPage.tsx +5 -5
  203. package/.framework/src/pages/admin/EmbeddingDetailPage.tsx +5 -23
  204. package/.framework/src/pages/admin/EmbeddingsPage.tsx +5 -26
  205. package/.framework/src/pages/admin/IntegrationDetailPage.tsx +7 -7
  206. package/.framework/src/pages/admin/IntegrationsPage.tsx +85 -43
  207. package/.framework/src/pages/admin/LogsPage.tsx +144 -76
  208. package/.framework/src/pages/admin/ObservabilityDashboard.tsx +223 -140
  209. package/.framework/src/pages/admin/PipelineDetailPage.tsx +134 -113
  210. package/.framework/src/pages/admin/PipelineExecutionsPage.tsx +108 -45
  211. package/.framework/src/pages/admin/PipelinesPage.tsx +160 -115
  212. package/.framework/src/pages/admin/PromptConfigDetailPage.tsx +96 -98
  213. package/.framework/src/pages/admin/PromptConfigsPage.tsx +101 -59
  214. package/.framework/src/pages/admin/RoleDetailPage.tsx +90 -79
  215. package/.framework/src/pages/admin/RolesPage.tsx +86 -43
  216. package/.framework/src/pages/admin/SecurityPage.tsx +1 -1
  217. package/.framework/src/pages/admin/TestingDashboard.tsx +16 -28
  218. package/.framework/src/pages/admin/TimerDetailPage.tsx +117 -100
  219. package/.framework/src/pages/admin/TimersPage.tsx +90 -62
  220. package/.framework/src/pages/admin/TriggerDetailPage.tsx +90 -83
  221. package/.framework/src/pages/admin/TriggersPage.tsx +124 -99
  222. package/.framework/src/pages/admin/TypeDetailPage.tsx +152 -138
  223. package/.framework/src/pages/admin/TypesPage.tsx +90 -48
  224. package/.framework/src/pages/admin/cil/CILAgentPage.tsx +17 -25
  225. package/.framework/src/pages/admin/cil/CILChunkDetailPage.tsx +17 -27
  226. package/.framework/src/pages/admin/cil/CILDashboardPage.tsx +72 -40
  227. package/.framework/src/pages/admin/cil/CILFileListPage.tsx +321 -129
  228. package/.framework/src/pages/admin/cil/CILFileSelectPage.tsx +18 -29
  229. package/.framework/src/pages/admin/cil/CILGraphPage.tsx +26 -38
  230. package/.framework/src/pages/admin/cil/CILIngestionPage.tsx +34 -46
  231. package/.framework/src/pages/admin/cil/CILProjectsPage.tsx +189 -103
  232. package/.framework/src/pages/admin/cil/CILSearchPage.tsx +246 -136
  233. package/.framework/src/pages/admin/cil/CILSuggestionsPage.tsx +11 -22
  234. package/.framework/src/pages/admin/cil/CILVersionsPage.tsx +9 -20
  235. package/.framework/src/pages/admin/cil/FlexiblePipelinePage.tsx +26 -38
  236. package/.framework/src/pages/admin/kb/KBAdminPage.tsx +7 -26
  237. package/.framework/src/pages/auth/AuthConfirmPage.tsx +3 -1
  238. package/.framework/src/pages/auth/ForgotPasswordPage.tsx +3 -1
  239. package/.framework/src/pages/auth/LoginPage.tsx +4 -2
  240. package/.framework/src/pages/auth/RegisterPage.tsx +3 -1
  241. package/.framework/templates/agent/AGENTS.md +150 -0
  242. package/.framework/templates/agent/skills/spine-add-ai-agent/SKILL.md +58 -0
  243. package/.framework/templates/agent/skills/spine-add-automation/SKILL.md +62 -0
  244. package/.framework/templates/agent/skills/spine-add-integration/SKILL.md +63 -0
  245. package/.framework/templates/agent/skills/spine-architect/SKILL.md +86 -0
  246. package/.framework/templates/agent/skills/spine-install-app/SKILL.md +72 -0
  247. package/.framework/templates/agent/skills/spine-migration/SKILL.md +75 -0
  248. package/.framework/templates/agent/skills/spine-new-app/SKILL.md +67 -0
  249. package/.framework/templates/agent/skills/spine-new-function/SKILL.md +65 -0
  250. package/.framework/templates/agent/skills/spine-new-type/SKILL.md +65 -0
  251. package/.framework/templates/agent/skills/spine-schema/SKILL.md +77 -0
  252. package/.framework/templates/agent/skills/spine-test/SKILL.md +67 -0
  253. package/.framework/templates/agent/skills/spine-troubleshoot/SKILL.md +74 -0
  254. package/.framework/templates/agent/skills/spine-upgrade/SKILL.md +69 -0
  255. package/.framework/templates/project.json +5 -1
  256. package/.framework/tests/unit/account-scoping-gates.test.ts +392 -0
  257. package/.framework/tests/unit/agent-files.test.ts +355 -0
  258. package/.framework/tests/unit/agent-runner.test.ts +496 -47
  259. package/.framework/tests/unit/api-keys-create-scope-gate.test.ts +160 -0
  260. package/.framework/tests/unit/api-keys-omit-secret-fields.test.ts +38 -0
  261. package/.framework/tests/unit/bootstrap.test.ts +242 -0
  262. package/.framework/tests/unit/cli-context-api-key.test.ts +114 -0
  263. package/.framework/tests/unit/debug-auth.test.ts +38 -0
  264. package/.framework/tests/unit/deliveries-account-scoping.test.ts +185 -0
  265. package/.framework/tests/unit/episode-lease.test.ts +695 -0
  266. package/.framework/tests/unit/integration-outbound-credentials.test.ts +167 -0
  267. package/.framework/tests/unit/integrations-audit-redaction.test.ts +130 -0
  268. package/.framework/tests/unit/kb-ingest-lease.test.ts +19 -0
  269. package/.framework/tests/unit/logging-app-id.test.ts +75 -0
  270. package/.framework/tests/unit/pipeline-runner-background.test.ts +120 -0
  271. package/.framework/tests/unit/pipeline-runner.test.ts +153 -1
  272. package/.framework/tests/unit/principal-cache.test.ts +15 -0
  273. package/.framework/tests/unit/system-context-denial.test.ts +145 -0
  274. package/.framework/tests/unit/system-cron.test.ts +240 -0
  275. package/.framework/tests/unit/timers.test.ts +159 -0
  276. package/.framework/tests/unit/trigger-engine.test.ts +278 -91
  277. package/.framework/tests/unit/trigger-scheduler.test.ts +78 -0
  278. package/README.md +125 -166
  279. package/config/tsconfig.cli.json +2 -2
  280. package/netlify.toml +5 -0
  281. package/package.json +15 -4
  282. package/templates/project/package-lock.json +2 -2
  283. package/.framework/docs/admin-routes.md +0 -76
  284. package/.framework/docs/api-docs-progress.md +0 -38
  285. package/.framework/docs/api-governance.md +0 -146
  286. package/.framework/docs/api-testing-results.md +0 -212
  287. package/.framework/docs/assembly-launch-guide.md +0 -138
  288. package/.framework/docs/audit-results.md +0 -590
  289. package/.framework/docs/db-api-inventory.md +0 -95
  290. package/.framework/docs/gap-remediation-backlog.md +0 -103
  291. package/.framework/docs/ui-adoption-verification.md +0 -111
  292. package/.framework/docs/ui-api-coverage.md +0 -84
  293. package/.framework/docs/v2-compatibility-audit.md +0 -228
  294. package/.framework/mcp/index.ts +0 -133
  295. package/.framework/mcp/load-env.ts +0 -60
  296. package/.framework/mcp/shared/app-manifest.ts +0 -275
  297. package/.framework/mcp/shared/db.ts +0 -477
  298. package/.framework/mcp/shared/kb.ts +0 -118
  299. package/.framework/mcp/tools/architect.ts +0 -431
  300. package/.framework/mcp/tools/cil/context.ts +0 -345
  301. package/.framework/mcp/tools/cil/graph.ts +0 -242
  302. package/.framework/mcp/tools/cil/ingest.ts +0 -1217
  303. package/.framework/mcp/tools/cil/pipeline-orchestrator.ts +0 -693
  304. package/.framework/mcp/tools/cil/pipeline.ts +0 -767
  305. package/.framework/mcp/tools/cil/project.ts +0 -791
  306. package/.framework/mcp/tools/cil/retrieval.ts +0 -336
  307. package/.framework/mcp/tools/cil/search-core.ts +0 -454
  308. package/.framework/mcp/tools/cil/search.ts +0 -97
  309. package/.framework/mcp/tools/cil/suggest.ts +0 -186
  310. package/.framework/mcp/tools/cil/validate.ts +0 -1369
  311. package/.framework/mcp/tools/kb/delete.ts +0 -139
  312. package/.framework/mcp/tools/kb/index.ts +0 -52
  313. package/.framework/mcp/tools/kb/ingest.ts +0 -84
  314. package/.framework/mcp/tools/kb/list-types.ts +0 -102
  315. package/.framework/mcp/tools/kb/publish.ts +0 -100
  316. package/.framework/mcp/tools/kb/reindex.ts +0 -86
  317. package/.framework/mcp/tools/kb/revoke.ts +0 -85
  318. package/.framework/mcp/tools/kb/search.ts +0 -101
  319. package/.framework/mcp/tools/kb/upsert.ts +0 -125
  320. 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.