spine-framework 0.3.125 → 0.3.126

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 (319) hide show
  1. package/.framework/cli/commands/agents.ts +35 -37
  2. package/.framework/cli/commands/auth.ts +35 -34
  3. package/.framework/cli/commands/create-app.ts +104 -40
  4. package/.framework/cli/commands/dev.ts +104 -69
  5. package/.framework/cli/commands/doctor.ts +169 -97
  6. package/.framework/cli/commands/generate.ts +82 -160
  7. package/.framework/cli/commands/init.ts +72 -34
  8. package/.framework/cli/commands/install-app.ts +260 -125
  9. package/.framework/cli/commands/items.ts +33 -44
  10. package/.framework/cli/commands/migrate.ts +122 -40
  11. package/.framework/cli/commands/migrations.ts +33 -40
  12. package/.framework/cli/commands/pipelines.ts +31 -39
  13. package/.framework/cli/commands/status.ts +47 -23
  14. package/.framework/cli/commands/system.ts +47 -37
  15. package/.framework/cli/commands/test.ts +112 -68
  16. package/.framework/cli/commands/validate-app.ts +215 -121
  17. package/.framework/cli/context.ts +112 -130
  18. package/.framework/cli/env-loader.ts +47 -18
  19. package/.framework/cli/index.ts +81 -42
  20. package/.framework/docs/runtime-types.md +1024 -0
  21. package/.framework/functions/_shared/agent-runner.ts +1296 -961
  22. package/.framework/functions/_shared/app-manifest.ts +82 -68
  23. package/.framework/functions/_shared/audit.ts +135 -83
  24. package/.framework/functions/_shared/{cil-state.ts → cil.ts} +147 -54
  25. package/.framework/functions/_shared/db.ts +80 -126
  26. package/.framework/functions/_shared/hash-api-key.ts +63 -0
  27. package/.framework/functions/_shared/index.ts +222 -347
  28. package/.framework/functions/_shared/kb.ts +319 -0
  29. package/.framework/functions/_shared/llm.ts +92 -0
  30. package/.framework/functions/_shared/logging.ts +288 -0
  31. package/.framework/functions/_shared/middleware.ts +329 -365
  32. package/.framework/functions/_shared/outcomes.ts +36 -0
  33. package/.framework/functions/_shared/permissions.ts +359 -643
  34. package/.framework/functions/_shared/pipeline-runner.ts +243 -316
  35. package/.framework/functions/_shared/principal.ts +376 -468
  36. package/.framework/functions/_shared/resolve-credentials.ts +102 -0
  37. package/.framework/functions/_shared/resolve-ids.ts +162 -14
  38. package/.framework/functions/_shared/retry-deliveries.ts +104 -0
  39. package/.framework/functions/_shared/schema-utils.ts +611 -373
  40. package/.framework/functions/_shared/testing.ts +207 -137
  41. package/.framework/functions/_shared/tracing.ts +276 -0
  42. package/.framework/functions/_shared/trigger-engine.ts +159 -211
  43. package/.framework/functions/_shared/webhook-registration.ts +95 -81
  44. package/.framework/functions/_shared/webhook-registry.ts +153 -75
  45. package/.framework/functions/account-nodes.ts +31 -53
  46. package/.framework/functions/adapters/generic-webhook-inbound.ts +56 -0
  47. package/.framework/functions/adapters/generic-webhook-outbound.ts +43 -0
  48. package/.framework/functions/adapters/openai-outbound.ts +40 -0
  49. package/.framework/functions/admin-data.ts +687 -533
  50. package/.framework/functions/admin-funnel.ts +31 -20
  51. package/.framework/functions/admin-install-funnel.ts +47 -28
  52. package/.framework/functions/admin-signals.ts +47 -29
  53. package/.framework/functions/ai-agents.ts +122 -147
  54. package/.framework/functions/ai-interactions.ts +180 -90
  55. package/.framework/functions/api-keys.ts +250 -192
  56. package/.framework/functions/apps.ts +203 -197
  57. package/.framework/functions/auth.ts +98 -91
  58. package/.framework/functions/cil-agent-ingest.ts +773 -0
  59. package/.framework/functions/cil-agent.ts +592 -0
  60. package/.framework/functions/cil-callmap.ts +1227 -0
  61. package/.framework/functions/cil-config.ts +583 -0
  62. package/.framework/functions/cil-context.ts +445 -0
  63. package/.framework/functions/cil-doc.ts +1752 -0
  64. package/.framework/functions/cil-embeddings.ts +1202 -0
  65. package/.framework/functions/cil-graph.ts +585 -0
  66. package/.framework/functions/cil-ingest.ts +2416 -0
  67. package/.framework/functions/cil-pipeline.ts +318 -0
  68. package/.framework/functions/cil-project.ts +499 -0
  69. package/.framework/functions/cil-validate.ts +469 -0
  70. package/.framework/functions/debug-auth.ts +73 -53
  71. package/.framework/functions/deliveries.ts +117 -0
  72. package/.framework/functions/embeddings.ts +528 -516
  73. package/.framework/functions/eval-harness.ts +158 -106
  74. package/.framework/functions/integration-outbound.ts +311 -0
  75. package/.framework/functions/integration-routes.ts +240 -466
  76. package/.framework/functions/integrations.ts +132 -140
  77. package/.framework/functions/invites.ts +401 -201
  78. package/.framework/functions/item-progress.ts +113 -53
  79. package/.framework/functions/kb.ts +28 -0
  80. package/.framework/functions/logs.ts +239 -222
  81. package/.framework/functions/observability.ts +308 -259
  82. package/.framework/functions/pipeline-executions.ts +179 -235
  83. package/.framework/functions/pipelines.ts +163 -205
  84. package/.framework/functions/prompt-configs.ts +109 -156
  85. package/.framework/functions/roles.ts +97 -147
  86. package/.framework/functions/system-cron.ts +132 -218
  87. package/.framework/functions/system.ts +112 -52
  88. package/.framework/functions/tests.ts +32 -28
  89. package/.framework/functions/timers.ts +162 -210
  90. package/.framework/functions/triggers.ts +282 -346
  91. package/.framework/functions/types.ts +283 -400
  92. package/.framework/migrations/001_core_schema.sql +630 -0
  93. package/.framework/migrations/002_core_rls_and_functions.sql +532 -0
  94. package/.framework/migrations/{003_system_data.sql → 003_core_system_data.sql} +26 -153
  95. package/.framework/migrations/004_core_rpc_and_broker.sql +421 -0
  96. package/.framework/migrations/005_core_observability.sql +598 -0
  97. package/.framework/migrations/006_security_hardening_rls.sql +97 -0
  98. package/.framework/migrations/007_kb_article_list.sql +237 -0
  99. package/.framework/migrations/008_items_target.sql +8 -0
  100. package/.framework/migrations/009_roles_slug_unique.sql +11 -0
  101. package/.framework/public/spine.config.json +91 -0
  102. package/.framework/scripts/app-install-cli.ts +112 -98
  103. package/.framework/src/App.tsx +67 -20
  104. package/.framework/src/apps/admin/index.tsx +97 -27
  105. package/.framework/src/components/AppWrapper.tsx +50 -11
  106. package/.framework/src/components/CustomAppLoader.tsx +131 -22
  107. package/.framework/src/components/admin/AdminListPage.tsx +75 -8
  108. package/.framework/src/components/admin/AdminSidebar.tsx +148 -36
  109. package/.framework/src/components/admin/AdminStatsCard.tsx +48 -29
  110. package/.framework/src/components/admin/PageInstructions.tsx +49 -0
  111. package/.framework/src/components/admin/SortableTableHeader.tsx +49 -2
  112. package/.framework/src/components/app-shell/GenericAppShell.tsx +69 -11
  113. package/.framework/src/components/app-shell/GenericDetailPage.tsx +55 -10
  114. package/.framework/src/components/app-shell/GenericListPage.tsx +76 -9
  115. package/.framework/src/components/app-sidebar.tsx +50 -3
  116. package/.framework/src/components/auth/ProtectedRoute.tsx +53 -45
  117. package/.framework/src/components/kb/KBArticleEditor.tsx +409 -0
  118. package/.framework/src/components/kb/KBArticleList.tsx +557 -0
  119. package/.framework/src/components/layout/AppShell.tsx +54 -16
  120. package/.framework/src/components/layout/Header.tsx +50 -38
  121. package/.framework/src/components/layout/Layout.tsx +48 -32
  122. package/.framework/src/components/layout/Sidebar.tsx +167 -16
  123. package/.framework/src/components/runtime/DataDetailHeader.tsx +33 -1
  124. package/.framework/src/components/runtime/DataDetailPage.tsx +289 -4
  125. package/.framework/src/components/runtime/DataFilters.tsx +78 -1
  126. package/.framework/src/components/runtime/DataHeader.tsx +32 -29
  127. package/.framework/src/components/runtime/DataListPage.tsx +161 -2
  128. package/.framework/src/components/runtime/DataStats.tsx +48 -1
  129. package/.framework/src/components/runtime/DataTable.tsx +153 -1
  130. package/.framework/src/components/runtime/SchemaDetailForm.tsx +77 -46
  131. package/.framework/src/components/runtime/index.ts +16 -9
  132. package/.framework/src/components/search-form.tsx +33 -1
  133. package/.framework/src/components/shared/AgentView.tsx +48 -51
  134. package/.framework/src/components/shared/FieldRenderer.tsx +84 -67
  135. package/.framework/src/components/shared/InviteManager.tsx +113 -52
  136. package/.framework/src/components/shared/MemberList.tsx +182 -0
  137. package/.framework/src/components/shared/SchemaFields.tsx +96 -78
  138. package/.framework/src/components/ui/DataTable.tsx +86 -56
  139. package/.framework/src/components/ui/Form.tsx +188 -109
  140. package/.framework/src/components/ui/ItemCard.tsx +86 -58
  141. package/.framework/src/components/ui/ItemListView.tsx +88 -61
  142. package/.framework/src/components/ui/LoadingSpinner.tsx +52 -27
  143. package/.framework/src/components/ui/Modal.tsx +81 -4
  144. package/.framework/src/components/ui/RichTextEditor.tsx +101 -9
  145. package/.framework/src/components/ui/accordion.tsx +97 -5
  146. package/.framework/src/components/ui/alert-dialog.tsx +225 -13
  147. package/.framework/src/components/ui/alert.tsx +113 -6
  148. package/.framework/src/components/ui/aspect-ratio.tsx +49 -2
  149. package/.framework/src/components/ui/avatar.tsx +129 -7
  150. package/.framework/src/components/ui/badge.tsx +65 -3
  151. package/.framework/src/components/ui/breadcrumb.tsx +145 -8
  152. package/.framework/src/components/ui/button-group.tsx +81 -4
  153. package/.framework/src/components/ui/button.tsx +49 -2
  154. package/.framework/src/components/ui/calendar.tsx +65 -3
  155. package/.framework/src/components/ui/card.tsx +145 -8
  156. package/.framework/src/components/ui/carousel.tsx +129 -7
  157. package/.framework/src/components/ui/chart.tsx +161 -10
  158. package/.framework/src/components/ui/checkbox.tsx +49 -2
  159. package/.framework/src/components/ui/collapsible.tsx +81 -5
  160. package/.framework/src/components/ui/combobox.tsx +305 -20
  161. package/.framework/src/components/ui/command.tsx +177 -11
  162. package/.framework/src/components/ui/context-menu.tsx +273 -17
  163. package/.framework/src/components/ui/dialog.tsx +209 -27
  164. package/.framework/src/components/ui/direction.tsx +64 -4
  165. package/.framework/src/components/ui/drawer.tsx +193 -12
  166. package/.framework/src/components/ui/dropdown-menu.tsx +273 -18
  167. package/.framework/src/components/ui/empty.tsx +145 -9
  168. package/.framework/src/components/ui/field.tsx +209 -14
  169. package/.framework/src/components/ui/hover-card.tsx +81 -4
  170. package/.framework/src/components/ui/input-group.tsx +162 -10
  171. package/.framework/src/components/ui/input-otp.tsx +98 -5
  172. package/.framework/src/components/ui/input.tsx +49 -2
  173. package/.framework/src/components/ui/item.tsx +225 -13
  174. package/.framework/src/components/ui/kbd.tsx +65 -3
  175. package/.framework/src/components/ui/label.tsx +49 -2
  176. package/.framework/src/components/ui/menubar.tsx +289 -17
  177. package/.framework/src/components/ui/native-select.tsx +97 -5
  178. package/.framework/src/components/ui/navigation-menu.tsx +177 -10
  179. package/.framework/src/components/ui/pagination.tsx +161 -9
  180. package/.framework/src/components/ui/popover.tsx +145 -8
  181. package/.framework/src/components/ui/progress.tsx +49 -2
  182. package/.framework/src/components/ui/radio-group.tsx +65 -3
  183. package/.framework/src/components/ui/resizable.tsx +82 -4
  184. package/.framework/src/components/ui/scroll-area.tsx +65 -3
  185. package/.framework/src/components/ui/select.tsx +193 -11
  186. package/.framework/src/components/ui/separator.tsx +49 -2
  187. package/.framework/src/components/ui/sheet.tsx +193 -11
  188. package/.framework/src/components/ui/sidebar.tsx +481 -38
  189. package/.framework/src/components/ui/skeleton.tsx +49 -2
  190. package/.framework/src/components/ui/slider.tsx +49 -2
  191. package/.framework/src/components/ui/sonner.tsx +49 -2
  192. package/.framework/src/components/ui/spinner.tsx +49 -2
  193. package/.framework/src/components/ui/switch.tsx +49 -2
  194. package/.framework/src/components/ui/table-primitives.tsx +160 -33
  195. package/.framework/src/components/ui/table.tsx +161 -9
  196. package/.framework/src/components/ui/tabs.tsx +113 -6
  197. package/.framework/src/components/ui/textarea.tsx +49 -2
  198. package/.framework/src/components/ui/toggle-group.tsx +81 -4
  199. package/.framework/src/components/ui/toggle.tsx +65 -3
  200. package/.framework/src/components/ui/tooltip.tsx +97 -5
  201. package/.framework/src/contexts/AppContext.tsx +129 -45
  202. package/.framework/src/contexts/AuthContext.tsx +173 -145
  203. package/.framework/src/hooks/use-mobile.ts +48 -2
  204. package/.framework/src/hooks/useApi.ts +208 -159
  205. package/.framework/src/hooks/useAppPath.ts +32 -23
  206. package/.framework/src/hooks/useApps.ts +51 -20
  207. package/.framework/src/hooks/useEntityList.ts +32 -78
  208. package/.framework/src/hooks/useEntityRecord.ts +35 -96
  209. package/.framework/src/hooks/useForm.ts +32 -92
  210. package/.framework/src/hooks/useListSchema.ts +49 -78
  211. package/.framework/src/hooks/useSchemaRecord.ts +114 -105
  212. package/.framework/src/lib/api.ts +192 -115
  213. package/.framework/src/lib/supabase.ts +98 -64
  214. package/.framework/src/lib/utils.ts +287 -151
  215. package/.framework/src/main.tsx +49 -3
  216. package/.framework/src/pages/DashboardPage.tsx +52 -29
  217. package/.framework/src/pages/NotFoundPage.tsx +32 -10
  218. package/.framework/src/pages/SetupPage.tsx +32 -12
  219. package/.framework/src/pages/admin/AIAgentDetailPage.tsx +119 -10
  220. package/.framework/src/pages/admin/AIAgentsPage.tsx +48 -19
  221. package/.framework/src/pages/admin/APIKeyDetailPage.tsx +187 -17
  222. package/.framework/src/pages/admin/APIKeysPage.tsx +98 -22
  223. package/.framework/src/pages/admin/AlertsConfigPage.tsx +113 -26
  224. package/.framework/src/pages/admin/AppDetailPage.tsx +125 -8
  225. package/.framework/src/pages/admin/AppsPage.tsx +49 -2
  226. package/.framework/src/pages/admin/DeliveriesPage.tsx +196 -0
  227. package/.framework/src/pages/admin/DesignedPage.tsx +32 -18
  228. package/.framework/src/pages/admin/EmbeddingDetailPage.tsx +215 -484
  229. package/.framework/src/pages/admin/EmbeddingsPage.tsx +116 -319
  230. package/.framework/src/pages/admin/ExtendedShadcnTestPage.tsx +34 -9
  231. package/.framework/src/pages/admin/IncrementalShadcnTestPage.tsx +34 -9
  232. package/.framework/src/pages/admin/IntegratedDashboard.tsx +32 -16
  233. package/.framework/src/pages/admin/IntegrationDetailPage.tsx +183 -26
  234. package/.framework/src/pages/admin/IntegrationsPage.tsx +48 -19
  235. package/.framework/src/pages/admin/InvitesPage.tsx +48 -14
  236. package/.framework/src/pages/admin/LogsPage.tsx +422 -75
  237. package/.framework/src/pages/admin/MinimalShadcnTestPage.tsx +34 -9
  238. package/.framework/src/pages/admin/ObservabilityDashboard.tsx +113 -24
  239. package/.framework/src/pages/admin/PipelineDetailPage.tsx +206 -34
  240. package/.framework/src/pages/admin/PipelineExecutionsPage.tsx +50 -17
  241. package/.framework/src/pages/admin/PipelinesPage.tsx +160 -21
  242. package/.framework/src/pages/admin/PromptConfigDetailPage.tsx +378 -88
  243. package/.framework/src/pages/admin/PromptConfigsPage.tsx +125 -16
  244. package/.framework/src/pages/admin/ProperlyDesignedPage.tsx +49 -10
  245. package/.framework/src/pages/admin/RoleDetailPage.tsx +130 -10
  246. package/.framework/src/pages/admin/RolesPage.tsx +171 -18
  247. package/.framework/src/pages/admin/SelectTestPage.tsx +34 -9
  248. package/.framework/src/pages/admin/ShadcnTestPage.tsx +49 -11
  249. package/.framework/src/pages/admin/SimpleDashboard.tsx +49 -10
  250. package/.framework/src/pages/admin/TestRunDetailPage.tsx +49 -4
  251. package/.framework/src/pages/admin/TestingDashboard.tsx +98 -28
  252. package/.framework/src/pages/admin/TimerDetailPage.tsx +143 -33
  253. package/.framework/src/pages/admin/TimersPage.tsx +48 -20
  254. package/.framework/src/pages/admin/TriggerDetailPage.tsx +118 -30
  255. package/.framework/src/pages/admin/TriggersPage.tsx +64 -19
  256. package/.framework/src/pages/admin/TypeDetailPage.tsx +138 -15
  257. package/.framework/src/pages/admin/TypesPage.tsx +48 -20
  258. package/.framework/src/pages/admin/cil/CILAgentPage.tsx +465 -0
  259. package/.framework/src/pages/admin/cil/CILChunkDetailPage.tsx +787 -0
  260. package/.framework/src/pages/admin/cil/CILDashboardPage.tsx +699 -0
  261. package/.framework/src/pages/admin/cil/CILFileListPage.tsx +647 -0
  262. package/.framework/src/pages/admin/cil/CILGraphPage.tsx +616 -0
  263. package/.framework/src/pages/admin/cil/CILIngestionPage.tsx +1402 -0
  264. package/.framework/src/pages/admin/cil/CILProjectsPage.tsx +614 -0
  265. package/.framework/src/pages/admin/cil/CILSearchPage.tsx +571 -0
  266. package/.framework/src/pages/admin/cil/FlexiblePipelinePage.tsx +731 -0
  267. package/.framework/src/pages/admin/kb/KBAdminPage.tsx +64 -0
  268. package/.framework/src/pages/auth/AuthConfirmPage.tsx +52 -51
  269. package/.framework/src/pages/auth/ForgotPasswordPage.tsx +31 -22
  270. package/.framework/src/pages/auth/LoginPage.tsx +50 -23
  271. package/.framework/src/pages/auth/PendingApprovalPage.tsx +31 -15
  272. package/.framework/src/pages/auth/RegisterPage.tsx +281 -317
  273. package/.framework/src/pages/spine-framework/APIPage.tsx +33 -0
  274. package/.framework/src/pages/spine-framework/CLIPage.tsx +33 -0
  275. package/.framework/src/types/auth.ts +128 -71
  276. package/.framework/src/types/types.ts +373 -157
  277. package/custom/.devin/skills/spine-workbench-discovery/SKILL.md +39 -0
  278. package/custom/.devin/skills/spine-workbench-idea/SKILL.md +36 -0
  279. package/custom/.devin/skills/spine-workbench-implement/SKILL.md +46 -0
  280. package/custom/.devin/skills/spine-workbench-initiative/SKILL.md +28 -0
  281. package/custom/.devin/skills/spine-workbench-plan/SKILL.md +38 -0
  282. package/docs/README.md +9 -130
  283. package/netlify.toml +3 -3
  284. package/package.json +13 -25
  285. package/scripts/kb-ingest-docs.ts +203 -0
  286. package/spine.config.json +65 -4
  287. package/.framework/migrations/001_schema.sql +0 -425
  288. package/.framework/migrations/002_rls_and_functions.sql +0 -763
  289. package/.framework/migrations/004_enhancements.sql +0 -43
  290. package/.framework/migrations/005_auth_user_trigger.sql +0 -105
  291. package/.framework/migrations/006_fix_current_actor_id.sql +0 -21
  292. package/.framework/migrations/016_invites.sql +0 -27
  293. package/.framework/migrations/017_fix_get_account_apps.sql +0 -78
  294. package/.framework/migrations/018_apps_required_roles.sql +0 -17
  295. package/.framework/migrations/019_triggers_unique_constraint.sql +0 -13
  296. package/.framework/migrations/020_get_account_apps_role_filter.sql +0 -117
  297. package/.framework/migrations/021_app_domains.sql +0 -112
  298. package/.framework/migrations/022_route_prefix_domain_unique.sql +0 -21
  299. package/.framework/migrations/023_match_embeddings_rpc.sql +0 -78
  300. package/.framework/migrations/024_messages_account_id.sql +0 -9
  301. package/.framework/migrations/025_retype_threads_messages.sql +0 -240
  302. package/.framework/migrations/026_fix_rls_policies.sql +0 -47
  303. package/.framework/migrations/027_embedding_lifecycle.sql +0 -176
  304. package/.framework/migrations/028_fix_message_data_permissions.sql +0 -49
  305. package/.framework/migrations/029_tighten_support_triage_prompt.sql +0 -41
  306. package/.framework/migrations/030_messages_updated_by.sql +0 -6
  307. package/.framework/migrations/031_seed_upsert_constraints.sql +0 -35
  308. package/docs/APPS.md +0 -232
  309. package/docs/ARCHITECTURE.md +0 -246
  310. package/docs/INSTALL.md +0 -155
  311. package/docs/design-schema-spec.md +0 -1837
  312. package/docs/dev-guides/01-create-an-app.md +0 -183
  313. package/docs/dev-guides/02-create-a-function.md +0 -178
  314. package/docs/dev-guides/03-create-a-component.md +0 -178
  315. package/docs/dev-guides/04-webhook-handlers.md +0 -129
  316. package/docs/dev-guides/05-testing.md +0 -168
  317. package/docs/dev-guides/README.md +0 -40
  318. package/docs/npm-packages-plan.md +0 -288
  319. package/docs/spine-core--api-reference.md +0 -822
@@ -1,523 +1,297 @@
1
- /**
2
- * @module integration-routes
3
- * @audience core-contributor
4
- * @layer api-handler
5
- * @stability stable
6
- *
7
- * Unified integration routing system for all integration types.
8
- * Handles webhook, API, database, file, and custom integrations
9
- * through a single entry point with type-specific handlers.
10
- *
11
- ** Routed by:** `POST /api/integration-routes?slug=:slug`
12
- *
13
- * Route patterns:
14
- * | method | path | handler |
15
- * |--------|-------------------------------------------|------------------------|
16
- * | POST | /api/integration-routes?slug=:slug | handleWebhook |
17
- *
18
- * **Authorization:** API key authentication with integration permissions
19
- * - API key validated via resolvePrincipal
20
- * - Integration permissions checked against principal.permissions.integrations[slug]
21
- *
22
- * **Security Flow:**
23
- * 1. API key validation (resolvePrincipal)
24
- * 2. Integration permission check
25
- * 3. Schema validation (if configured)
26
- * 4. Data sanitization
27
- * 5. Custom script execution
28
- *
29
- * INVARIANT: All validation failures return generic { status: "rejected" }
30
- * INVARIANT: Detailed errors logged server-side only
31
- * INVARIANT: Unknown fields in payload = HARD FAIL (fishing detection)
32
- *
33
- * @seeAlso integrations.ts (integration configuration)
34
- * @seeAlso principal.ts (machine principal resolution)
35
- * @seeAlso api-keys.ts (API key permissions)
36
- */
37
-
1
+ // CIL[BEGIN chunk_id="integration_routes_module_header_1784200006" type="module_header" identity="imports" parent="none"]
2
+ // CIL[DOC-MACRO purpose="Single inbound webhook gateway for all integrations"]
3
+ // CIL[DOC-RESPONSIBILITY responsibility="Authenticate, route, dispatch, and persist inbound webhooks through admin-data"]
4
+ // CIL[DOC-CONTEXT module="integration-routes" layer="backend_functions" related_files="[".framework/functions/_shared/principal.ts",".framework/functions/_shared/webhook-registry.ts",".framework/functions/admin-data.ts",".framework/functions/_shared/hash-api-key.ts",".framework/functions/_shared/resolve-credentials.ts",".framework/migrations/051_integration_broker.sql"]"]
5
+ // CIL[DOC-CONTRACT inputs="x-api-key header and ?slug= query param" outputs="created record" errors="auth, routing, adapter, admin-data errors" side_effects="API key usage log, webhook delivery ledger, admin-data record creation"]
6
+ // CIL[DOC-MICRO logic_summary="Resolves API key by x-api-key, loads integration by slug, finds a registered webhook handler, runs the adapter, and creates the resulting record through admin-data." notable_decisions="["Uses createPublicHandler because the caller is not a human JWT.","Created_by is null for machine writes; account_id is inherited from the integration.","Never passes adminDb or ctx.db to adapters."]"]
7
+ // CIL[DOC-CONFIDENCE pass="first" score="0.9" flags="none"]
8
+ // CIL[DOC-STABILITY stability="experimental"]
9
+ // CIL[DOC-AUDIENCE audience="developer"]
10
+ // CIL[DOC-VECTOR type="context" content="Layer: backend_functions. Direct callers: 0. Direct calls: 0. Related files: .framework/functions/_shared/principal.ts, .framework/functions/_shared/webhook-registry.ts, .framework/functions/admin-data.ts, .framework/functions/_shared/hash-api-key.ts, .framework/functions/_shared/resolve-credentials.ts, .framework/migrations/051_integration_broker.sql."]
11
+ // CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
12
+ // CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
13
+ // CIL[DOC-VECTOR type="constraint" content="Requires x-api-key header and ?slug= query parameter."]
14
+ // CIL[DOC-VECTOR type="implementation_pattern" content="Called by external services as POST /.netlify/functions/integration-routes?slug=<integration>."]
15
+ // CIL[DOC-VECTOR type="user_journey" content="External service pushes data into Spine."]
16
+
17
+ import { createPublicHandler, CoreContext } from './_shared/middleware'
38
18
  import { adminDb } from './_shared/db'
39
- import { resolvePrincipal } from './_shared/principal'
40
- import { resolveHandler, listRegisteredHandlers } from './_shared/webhook-registry'
41
-
42
- // ─── TYPES ────────────────────────────────────────────────────────────────
19
+ import { emitLog } from './_shared/audit'
20
+ import { resolveHandler } from './_shared/webhook-registry'
21
+ import { create as adminDataCreate } from './admin-data'
43
22
 
44
- interface IntegrationContext {
23
+ interface InboundAdapterContext {
24
+ payload: any
45
25
  integration: any
46
- principal: any
47
- requestPath: string
48
- method: string
26
+ apiKey: any
27
+ accountId: string | null
28
+ appId: string | null
29
+ event: string | null
49
30
  headers: Record<string, string>
50
- body: any
51
- query: Record<string, string>
52
- requestId: string
31
+ logger: { info: (msg: string, meta?: any) => void; warn: (msg: string, meta?: any) => void; error: (msg: string, meta?: any) => void }
53
32
  }
54
33
 
55
- interface ValidationResult {
56
- valid: boolean
57
- errors: string[]
58
- unknownFields: string[]
34
+ interface AdapterResult {
35
+ type_slug: string
36
+ data: Record<string, any>
37
+ title?: string
38
+ status?: string
39
+ thread?: any
40
+ links?: any[]
41
+ external_id?: string
42
+ error?: string
43
+ retryable?: boolean
59
44
  }
60
45
 
61
- interface IntegrationHandler {
62
- (ctx: IntegrationContext): Promise<any>
46
+ function getClientIp(event: any): string {
47
+ return event.headers?.['x-forwarded-for'] || event.headers?.['X-Forwarded-For'] || 'unknown'
63
48
  }
64
49
 
65
- // ─── INTEGRATION RESOLUTION ────────────────────────────────────────────────
66
-
67
- /**
68
- * Resolves integration by slug and validates it's active and configured
69
- */
70
- async function resolveIntegration(slug: string, type: string): Promise<any> {
71
- const { data, error } = await adminDb
72
- .from('integrations')
73
- .select('*')
74
- .eq('name', slug)
75
- .eq('integration_type', type)
76
- .eq('is_active', true)
77
- .single()
78
-
79
- if (error || !data) {
80
- throw new Error(`Integration not found: ${type}/${slug}`)
81
- }
82
-
83
- if (!data.is_configured) {
84
- throw new Error(`Integration not configured: ${type}/${slug}`)
85
- }
86
-
87
- return data
50
+ function computeNextAttempt(attemptCount: number, baseMs = 30000, maxDelayMs = 3600000): Date {
51
+ const delay = Math.min(baseMs * Math.pow(2, attemptCount), maxDelayMs)
52
+ const jitter = Math.floor(Math.random() * delay * 0.2)
53
+ return new Date(Date.now() + delay + jitter)
88
54
  }
89
55
 
90
- // ─── VALIDATION & SANITIZATION ───────────────────────────────────────────
91
-
92
- /**
93
- * Validates data against schema definition
94
- * Returns validation result with any errors and unknown fields
95
- */
96
- function validateSchema(data: any, schema: any, requestId: string): ValidationResult {
97
- const errors: string[] = []
98
- const unknownFields: string[] = []
99
-
100
- if (!schema || typeof schema !== 'object') {
101
- return { valid: true, errors: [], unknownFields: [] }
56
+ function extractEvent(payload: any, eventHeader?: string | null, eventPath?: string | null): string | null {
57
+ if (eventHeader) {
58
+ const h = eventHeader.toLowerCase()
59
+ return eventHeader
102
60
  }
103
-
104
- // Check for unknown fields
105
- const allowedFields = Object.keys(schema)
106
- for (const field of Object.keys(data)) {
107
- if (!allowedFields.includes(field)) {
108
- unknownFields.push(field)
61
+ if (eventPath) {
62
+ const parts = eventPath.split('.')
63
+ let current = payload
64
+ for (const p of parts) {
65
+ if (current == null) return null
66
+ current = current[p]
109
67
  }
68
+ return current || null
110
69
  }
111
-
112
- // Validate each field against schema
113
- for (const [fieldName, fieldConfig] of Object.entries(schema)) {
114
- const config = fieldConfig as any
115
- const value = data[fieldName]
116
-
117
- // Check required fields
118
- if (config.required && (value === undefined || value === null || value === '')) {
119
- errors.push(`Field '${fieldName}' is required but missing`)
120
- continue
121
- }
122
-
123
- // Skip further validation if field is not required and not present
124
- if (!config.required && (value === undefined || value === null)) {
125
- continue
126
- }
127
-
128
- // Validate data type
129
- if (config.type) {
130
- const typeValid = validateType(value, config.type, fieldName)
131
- if (!typeValid.valid) {
132
- errors.push(typeValid.error)
133
- }
134
- }
135
-
136
- // Validate constraints
137
- if (config.min !== undefined && typeof value === 'number' && value < config.min) {
138
- errors.push(`Field '${fieldName}' must be at least ${config.min}`)
139
- }
140
-
141
- if (config.max !== undefined && typeof value === 'number' && value > config.max) {
142
- errors.push(`Field '${fieldName}' must be at most ${config.max}`)
143
- }
144
-
145
- if (config.maxLength !== undefined && typeof value === 'string' && value.length > config.maxLength) {
146
- errors.push(`Field '${fieldName}' must be at most ${config.maxLength} characters`)
147
- }
148
-
149
- if (config.pattern && typeof value === 'string') {
150
- const regex = new RegExp(config.pattern)
151
- if (!regex.test(value)) {
152
- errors.push(`Field '${fieldName}' does not match required pattern`)
153
- }
154
- }
155
- }
156
-
157
- return {
158
- valid: errors.length === 0 && unknownFields.length === 0,
159
- errors,
160
- unknownFields
161
- }
70
+ return null
162
71
  }
163
72
 
164
- /**
165
- * Validates a value against a type definition
166
- */
167
- function validateType(value: any, type: string, fieldName: string): { valid: boolean; error?: string } {
168
- switch (type) {
169
- case 'string':
170
- if (typeof value !== 'string') {
171
- return { valid: false, error: `Field '${fieldName}' must be a string` }
172
- }
173
- break
174
- case 'number':
175
- if (typeof value !== 'number' || isNaN(value)) {
176
- return { valid: false, error: `Field '${fieldName}' must be a number` }
177
- }
178
- break
179
- case 'boolean':
180
- if (typeof value !== 'boolean') {
181
- return { valid: false, error: `Field '${fieldName}' must be a boolean` }
182
- }
183
- break
184
- case 'email':
185
- if (typeof value !== 'string' || !value.includes('@')) {
186
- return { valid: false, error: `Field '${fieldName}' must be a valid email` }
187
- }
188
- break
189
- case 'array':
190
- if (!Array.isArray(value)) {
191
- return { valid: false, error: `Field '${fieldName}' must be an array` }
192
- }
193
- break
194
- case 'object':
195
- if (typeof value !== 'object' || value === null || Array.isArray(value)) {
196
- return { valid: false, error: `Field '${fieldName}' must be an object` }
197
- }
198
- break
73
+ export async function execute(ctx: CoreContext, body: any = {}): Promise<any> {
74
+ const slug = ctx.query?.slug || body?.slug
75
+
76
+ if (!slug) {
77
+ const e: any = new Error('Integration slug is required')
78
+ e.statusCode = 400
79
+ throw e
199
80
  }
200
-
201
- return { valid: true }
202
- }
203
81
 
204
- /**
205
- * Sanitizes data to prevent injection attacks
206
- */
207
- function sanitizeData(data: any, rules: string = 'strict'): any {
208
- if (data === null || data === undefined) {
209
- return data
82
+ const apiKeyId = ctx.principal?.id
83
+ if (!apiKeyId) {
84
+ const e: any = new Error('Machine principal required')
85
+ e.statusCode = 401
86
+ throw e
210
87
  }
211
-
212
- if (typeof data === 'string') {
213
- // Remove HTML tags
214
- let sanitized = data.replace(/<[^>]*>/g, '')
215
- // Escape special characters
216
- sanitized = sanitized
217
- .replace(/&/g, '&amp;')
218
- .replace(/</g, '&lt;')
219
- .replace(/>/g, '&gt;')
220
- .replace(/"/g, '&quot;')
221
- .replace(/'/g, '&#x27;')
222
- // Normalize whitespace
223
- sanitized = sanitized.trim().replace(/\s+/g, ' ')
224
- return sanitized
88
+
89
+ const { data: apiKey, error: apiKeyError } = await adminDb
90
+ .from('api_keys')
91
+ .select('*')
92
+ .eq('id', apiKeyId)
93
+ .single()
94
+
95
+ if (apiKeyError || !apiKey) {
96
+ const e: any = new Error('Invalid API key')
97
+ e.statusCode = 401
98
+ throw e
225
99
  }
226
-
227
- if (Array.isArray(data)) {
228
- return data.map(item => sanitizeData(item, rules))
100
+
101
+ const { data: integration, error: integrationError } = await adminDb
102
+ .from('integrations')
103
+ .select('*')
104
+ .eq('slug', slug)
105
+ .eq('account_id', apiKey.account_id)
106
+ .eq('is_active', true)
107
+ .single()
108
+
109
+ if (integrationError || !integration) {
110
+ const e: any = new Error(`Integration not found: ${slug}`)
111
+ e.statusCode = 404
112
+ throw e
229
113
  }
230
-
231
- if (typeof data === 'object') {
232
- const result: any = {}
233
- const BLOCKED_KEYS = ['__proto__', 'constructor', 'prototype']
234
- for (const [key, value] of Object.entries(data)) {
235
- if (BLOCKED_KEYS.includes(key)) continue
236
- // Sanitize keys (prevent prototype pollution, allow hyphens)
237
- const sanitizedKey = key.replace(/[^a-zA-Z0-9_-]/g, '')
238
- if (!sanitizedKey) continue
239
- result[sanitizedKey] = sanitizeData(value, rules)
240
- }
241
- return result
114
+
115
+ if (apiKey.integration_id && apiKey.integration_id !== integration.id) {
116
+ const e: any = new Error('API key is not authorized for this integration')
117
+ e.statusCode = 403
118
+ throw e
242
119
  }
243
-
244
- return data
245
- }
246
120
 
247
- // ─── CUSTOM SCRIPT LOADER ────────────────────────────────────────────────
248
-
249
- /**
250
- * Loads a custom handler from the dynamic webhook registry.
251
- *
252
- * Handlers are registered in the `webhook_handlers` table at runtime,
253
- * allowing custom functions to self-register without core code changes.
254
- *
255
- * @param handlerName - The handler key from integration config (e.g. "webhook-handler")
256
- */
257
- async function loadCustomScript(handlerName: string): Promise<Function | null> {
258
- return resolveHandler(handlerName)
259
- }
121
+ if (apiKey.app_id && integration.app_id && apiKey.app_id !== integration.app_id) {
122
+ const e: any = new Error('API key is not authorized for this app')
123
+ e.statusCode = 403
124
+ throw e
125
+ }
260
126
 
261
- // ─── DIAGNOSTIC HANDLER (temporary — full trace in response) ──────────────
262
-
263
- /**
264
- * Integration router with full diagnostic tracing.
265
- * Every step logs to a trace[] array that is returned in the response body.
266
- * REMOVE THIS DIAGNOSTIC MODE before production.
267
- */
268
- const integrationHandler = async (event: any, _context: any) => {
269
- const requestId = crypto.randomUUID()
270
- const trace: { step: string; status: string; detail?: any }[] = []
271
-
272
- // ── Step 0: Can an external service hit the endpoint? ───────────────
273
- trace.push({
274
- step: '0_endpoint_reached',
275
- status: 'PASS',
276
- detail: {
277
- method: event.httpMethod,
278
- path: event.path,
279
- queryParams: event.queryStringParameters,
280
- hasBody: !!event.body,
281
- timestamp: new Date().toISOString()
282
- }
283
- })
127
+ const event = extractEvent(
128
+ body,
129
+ (ctx as any).headers?.['x-event-type'] || (ctx as any).headers?.['X-Event-Type'],
130
+ integration.config?.event_path
131
+ )
284
132
 
285
- // Method check
286
- if ((event.httpMethod || 'GET') !== 'POST') {
287
- trace.push({ step: '0_method_check', status: 'FAIL', detail: `Expected POST, got ${event.httpMethod}` })
288
- return respond(405, { status: 'rejected', trace })
289
- }
290
- trace.push({ step: '0_method_check', status: 'PASS' })
133
+ const handlerName = integration.config?.handler_name || integration.provider || 'generic-webhook'
134
+ const adapter = await resolveHandler(handlerName, integration.app_id, integration.account_id)
291
135
 
292
- // Slug check
293
- const integrationSlug = event.queryStringParameters?.slug
294
- if (!integrationSlug) {
295
- trace.push({ step: '0_slug_check', status: 'FAIL', detail: 'No ?slug= query param' })
296
- return respond(400, { status: 'rejected', trace })
136
+ if (!adapter) {
137
+ const e: any = new Error(`Handler not found: ${handlerName}`)
138
+ e.statusCode = 404
139
+ throw e
297
140
  }
298
- trace.push({ step: '0_slug_check', status: 'PASS', detail: { slug: integrationSlug } })
299
-
300
- // ── Step 1: Does the endpoint see the API key header? ───────────────
301
- const apiKeyRaw = event.headers?.['x-api-key'] || event.headers?.['X-Api-Key'] || null
302
- trace.push({
303
- step: '1_header_extraction',
304
- status: apiKeyRaw ? 'PASS' : 'FAIL',
305
- detail: {
306
- 'x-api-key_present': !!apiKeyRaw,
307
- 'x-api-key_prefix': apiKeyRaw ? apiKeyRaw.substring(0, 6) + '...' : null,
308
- all_header_keys: Object.keys(event.headers || {})
309
- }
310
- })
311
- if (!apiKeyRaw) {
312
- return respond(401, { status: 'rejected', trace })
141
+
142
+ const deliveryPayload = {
143
+ direction: 'inbound',
144
+ integration_id: integration.id,
145
+ api_key_id: apiKey.id,
146
+ handler_name: handlerName,
147
+ external_id: body?.id || null,
148
+ payload: body,
149
+ status: 'pending',
150
+ attempt_count: 0,
151
+ max_attempts: integration.config?.retry_policy?.max_attempts ?? 3,
152
+ next_attempt_at: null,
153
+ account_id: integration.account_id,
154
+ created_at: new Date().toISOString(),
155
+ updated_at: new Date().toISOString()
313
156
  }
314
157
 
315
- // ── Step 2: Does resolvePrincipal validate the key? ─────────────────
316
- let principal: any
317
- try {
318
- principal = await resolvePrincipal(event)
319
- const isAnon = !principal || principal.id === 'anonymous'
320
- trace.push({
321
- step: '2_api_key_validation',
322
- status: isAnon ? 'FAIL' : 'PASS',
323
- detail: {
324
- principal_id: principal?.id,
325
- principal_type: principal?.type,
326
- account_id: principal?.accountId,
327
- scopes: principal?.scopes,
328
- machine_type: principal?.machineType,
329
- is_internal: principal?.isInternal
330
- }
331
- })
332
- if (isAnon) {
333
- return respond(401, { status: 'rejected', trace })
334
- }
335
- } catch (err: any) {
336
- trace.push({
337
- step: '2_api_key_validation',
338
- status: 'ERROR',
339
- detail: { message: err.message, stack: err.stack?.split('\n').slice(0, 3) }
340
- })
341
- return respond(401, { status: 'rejected', trace })
158
+ const { data: deliveryRow, error: deliveryInsertError } = await adminDb
159
+ .from('webhook_deliveries')
160
+ .insert(deliveryPayload)
161
+ .select('id')
162
+ .single()
163
+
164
+ if (deliveryInsertError || !deliveryRow) {
165
+ throw new Error('Failed to create inbound delivery record')
342
166
  }
343
167
 
344
- // ── Step 3: Permission check ────────────────────────────────────────
345
- const permViaIntegrations = (principal as any).permissions?.integrations?.[integrationSlug]
346
- const permViaScopes = principal.scopes?.includes('webhook:execute')
347
- const hasPermission = !!permViaIntegrations?.includes?.('execute') || !!permViaScopes
348
- trace.push({
349
- step: '3_permission_check',
350
- status: hasPermission ? 'PASS' : 'FAIL',
351
- detail: {
352
- checked_permissions_path: `principal.permissions.integrations.${integrationSlug}`,
353
- permissions_value: permViaIntegrations ?? 'undefined (property does not exist)',
354
- checked_scopes: 'webhook:execute',
355
- scopes_value: principal.scopes,
356
- scopes_match: permViaScopes,
357
- final_result: hasPermission
358
- }
168
+ const deliveryId = deliveryRow.id
169
+
170
+ await emitLog(ctx, 'integration.inbound.started', { type: 'integration', id: integration.id }, undefined, {
171
+ delivery_id: deliveryId,
172
+ integration_id: integration.id,
173
+ api_key_id: apiKey.id,
174
+ account_id: integration.account_id,
175
+ event
359
176
  })
360
- if (!hasPermission) {
361
- return respond(403, { status: 'rejected', trace })
177
+
178
+ const logger = {
179
+ info: (msg: string, meta?: any) => console.log(`[integration-routes] ${msg}`, meta),
180
+ warn: (msg: string, meta?: any) => console.warn(`[integration-routes] ${msg}`, meta),
181
+ error: (msg: string, meta?: any) => console.error(`[integration-routes] ${msg}`, meta)
362
182
  }
363
183
 
364
- // ── Step 4: Resolve integration record ──────────────────────────────
365
- let integration: any
366
- try {
367
- integration = await resolveIntegration(integrationSlug, 'webhook')
368
- trace.push({
369
- step: '4_integration_resolution',
370
- status: 'PASS',
371
- detail: {
372
- id: integration.id,
373
- name: integration.name,
374
- type: integration.integration_type,
375
- is_active: integration.is_active,
376
- is_configured: integration.is_configured,
377
- has_config: !!integration.config,
378
- config_keys: integration.config ? Object.keys(integration.config) : []
379
- }
380
- })
381
- } catch (err: any) {
382
- trace.push({
383
- step: '4_integration_resolution',
384
- status: 'ERROR',
385
- detail: { message: err.message }
386
- })
387
- return respond(500, { status: 'rejected', trace })
184
+ const adapterCtx: InboundAdapterContext = {
185
+ payload: body,
186
+ integration,
187
+ apiKey,
188
+ accountId: integration.account_id,
189
+ appId: integration.app_id,
190
+ event,
191
+ headers: (ctx as any).headers || {},
192
+ logger
388
193
  }
389
194
 
390
- // ── Step 5: Parse request body ──────────────────────────────────────
391
- let body: any = {}
392
195
  try {
393
- body = event.body ? JSON.parse(event.body) : {}
394
- trace.push({
395
- step: '5_body_parse',
396
- status: 'PASS',
397
- detail: { parsed_keys: Object.keys(body), parsed_body: body }
398
- })
399
- } catch (e: any) {
400
- body = { raw: event.body }
401
- trace.push({
402
- step: '5_body_parse',
403
- status: 'WARN',
404
- detail: { message: 'Not valid JSON, wrapped as { raw: ... }', raw_length: event.body?.length }
405
- })
406
- }
196
+ const adapterResult: AdapterResult = await adapter(adapterCtx.payload, adapterCtx)
197
+
198
+ if (adapterResult.error) {
199
+ const status = adapterResult.retryable ? 'pending' : 'failed'
200
+ const nextAttempt = adapterResult.retryable ? computeNextAttempt(1).toISOString() : null
201
+
202
+ await adminDb
203
+ .from('webhook_deliveries')
204
+ .update({
205
+ status,
206
+ attempt_count: 1,
207
+ next_attempt_at: nextAttempt,
208
+ last_error: adapterResult.error,
209
+ updated_at: new Date().toISOString()
210
+ })
211
+ .eq('id', deliveryId)
212
+
213
+ await emitLog(ctx, adapterResult.retryable ? 'integration.inbound.adapter_retryable' : 'integration.inbound.adapter_failed', { type: 'integration', id: integration.id }, undefined, {
214
+ delivery_id: deliveryId,
215
+ error: adapterResult.error,
216
+ account_id: integration.account_id
217
+ })
218
+
219
+ throw new Error(adapterResult.error)
220
+ }
407
221
 
408
- // ── Step 6: Schema validation ───────────────────────────────────────
409
- const config = integration.config || {}
410
- if (config.validation?.schema) {
411
- const validation = validateSchema(body, config.validation.schema, requestId)
412
- trace.push({
413
- step: '6_schema_validation',
414
- status: validation.valid ? 'PASS' : 'FAIL',
415
- detail: {
416
- schema_fields: Object.keys(config.validation.schema),
417
- body_fields: Object.keys(body),
418
- errors: validation.errors,
419
- unknown_fields: validation.unknownFields,
420
- reject_unknown: config.validation.rejectUnknownFields
222
+ if (!adapterResult.type_slug) {
223
+ throw new Error('Adapter result is missing type_slug')
224
+ }
225
+
226
+ const adminCtx: any = {
227
+ ...ctx,
228
+ db: adminDb,
229
+ accountId: integration.account_id,
230
+ principal: {
231
+ ...ctx.principal,
232
+ id: null,
233
+ accountId: integration.account_id
421
234
  }
422
- })
423
- if (!validation.valid) {
424
- return respond(400, { status: 'rejected', trace })
425
235
  }
426
- } else {
427
- trace.push({ step: '6_schema_validation', status: 'SKIP', detail: 'No validation.schema in config' })
428
- }
429
236
 
430
- // ── Step 7: Sanitize data ───────────────────────────────────────────
431
- const sanitizedData = sanitizeData(body, config.validation?.sanitization || 'strict')
432
- trace.push({
433
- step: '7_sanitization',
434
- status: 'PASS',
435
- detail: {
436
- input_keys: Object.keys(body),
437
- output_keys: Object.keys(sanitizedData),
438
- sanitized_data: sanitizedData,
439
- note: 'Key sanitizer strips non-alphanumeric/underscore chars (hyphens removed)'
237
+ const createBody: any = {
238
+ entity: 'items',
239
+ type_slug: adapterResult.type_slug,
240
+ title: adapterResult.title,
241
+ status: adapterResult.status,
242
+ data: adapterResult.data
440
243
  }
441
- })
442
244
 
443
- // ── Step 8: Locate custom handler ───────────────────────────────────
444
- if (!config.handler?.path) {
445
- trace.push({ step: '8_handler_lookup', status: 'SKIP', detail: 'No handler.path in config' })
446
- return respond(200, { status: 'success', trace, result: sanitizedData })
447
- }
245
+ const record = await adminDataCreate(adminCtx, createBody)
448
246
 
449
- const scriptHandler = await loadCustomScript(config.handler.path)
450
- const registeredHandlers = await listRegisteredHandlers()
451
- trace.push({
452
- step: '8_handler_lookup',
453
- status: scriptHandler ? 'PASS' : 'FAIL',
454
- detail: {
455
- handler_key: config.handler.path,
456
- found_in_registry: !!scriptHandler,
457
- registered_handlers: registeredHandlers,
458
- note: 'Dynamic registry - handlers self-register via webhook_handlers table'
247
+ if (adapterResult.thread) {
248
+ // TODO: create thread via admin-data if needed
459
249
  }
460
- })
461
- if (!scriptHandler) {
462
- return respond(500, { status: 'rejected', trace })
463
- }
464
250
 
465
- // ── Step 9: Prepare handler arguments ───────────────────────────────
466
- const scriptContext = {
467
- integrationId: integration.id,
468
- accountId: integration.account_id,
469
- slug: config.slug || integration.name,
470
- principal: { id: principal.id, type: principal.type, accountId: principal.accountId },
471
- requestId,
472
- headers: event.headers || {}
473
- }
474
- const scriptEvent = {
475
- httpMethod: event.httpMethod,
476
- headers: event.headers || {},
477
- body: body,
478
- path: event.path,
479
- queryStringParameters: event.queryStringParameters || {}
480
- }
481
- trace.push({
482
- step: '9_handler_args',
483
- status: 'PASS',
484
- detail: {
485
- arg1_sanitized_data: sanitizedData,
486
- arg2_context_keys: Object.keys(scriptContext),
487
- arg3_event_keys: Object.keys(scriptEvent)
251
+ if (adapterResult.links?.length) {
252
+ // TODO: create links via admin-data if needed
488
253
  }
489
- })
490
254
 
491
- // ── Step 10: Execute handler ────────────────────────────────────────
492
- try {
493
- const result = await scriptHandler(sanitizedData, scriptContext, scriptEvent)
494
- trace.push({
495
- step: '10_handler_execution',
496
- status: 'PASS',
497
- detail: { result_type: typeof result, result }
255
+ await adminDb
256
+ .from('webhook_deliveries')
257
+ .update({
258
+ status: 'delivered',
259
+ attempt_count: 1,
260
+ next_attempt_at: null,
261
+ response_status: 200,
262
+ updated_at: new Date().toISOString()
263
+ })
264
+ .eq('id', deliveryId)
265
+
266
+ await emitLog(ctx, 'integration.inbound.delivered', { type: 'integration', id: integration.id }, undefined, {
267
+ delivery_id: deliveryId,
268
+ record_id: record?.id,
269
+ account_id: integration.account_id
498
270
  })
499
- return respond(200, { status: 'success', trace, result })
271
+
272
+ return record
500
273
  } catch (err: any) {
501
- trace.push({
502
- step: '10_handler_execution',
503
- status: 'ERROR',
504
- detail: { message: err.message, stack: err.stack?.split('\n').slice(0, 5) }
274
+ await adminDb
275
+ .from('webhook_deliveries')
276
+ .update({
277
+ status: 'pending',
278
+ attempt_count: 1,
279
+ next_attempt_at: computeNextAttempt(1).toISOString(),
280
+ last_error: err.message || String(err),
281
+ updated_at: new Date().toISOString()
282
+ })
283
+ .eq('id', deliveryId)
284
+
285
+ await emitLog(ctx, 'integration.inbound.error', { type: 'integration', id: integration.id }, undefined, {
286
+ delivery_id: deliveryId,
287
+ error: err.message || String(err),
288
+ account_id: integration.account_id
505
289
  })
506
- return respond(500, { status: 'rejected', trace })
507
- }
508
- }
509
-
510
- // ─── HELPERS ──────────────────────────────────────────────────────────────
511
290
 
512
- function respond(statusCode: number, body: any) {
513
- return {
514
- statusCode,
515
- headers: { 'Content-Type': 'application/json' },
516
- body: JSON.stringify(body, null, 2)
291
+ throw err
517
292
  }
518
293
  }
519
294
 
520
- // ─── EXPORTS ─────────────────────────────────────────────────────────────
295
+ export default createPublicHandler(execute)
521
296
 
522
- const handler = integrationHandler
523
- export { handler }
297
+ // CIL[END chunk_id="integration_routes_module_header_1784200006"]