spine-framework 0.3.124 → 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.
- package/.framework/cli/commands/agents.ts +35 -37
- package/.framework/cli/commands/auth.ts +35 -34
- package/.framework/cli/commands/create-app.ts +104 -40
- package/.framework/cli/commands/dev.ts +104 -69
- package/.framework/cli/commands/doctor.ts +179 -69
- package/.framework/cli/commands/generate.ts +83 -143
- package/.framework/cli/commands/init.ts +72 -34
- package/.framework/cli/commands/install-app.ts +260 -125
- package/.framework/cli/commands/items.ts +33 -44
- package/.framework/cli/commands/migrate.ts +122 -40
- package/.framework/cli/commands/migrations.ts +33 -40
- package/.framework/cli/commands/pipelines.ts +31 -39
- package/.framework/cli/commands/status.ts +47 -23
- package/.framework/cli/commands/system.ts +47 -37
- package/.framework/cli/commands/test.ts +112 -68
- package/.framework/cli/commands/validate-app.ts +215 -121
- package/.framework/cli/context.ts +112 -130
- package/.framework/cli/env-loader.ts +47 -18
- package/.framework/cli/index.ts +81 -42
- package/.framework/docs/runtime-types.md +1024 -0
- package/.framework/functions/_shared/agent-runner.ts +1296 -961
- package/.framework/functions/_shared/app-manifest.ts +82 -68
- package/.framework/functions/_shared/audit.ts +135 -83
- package/.framework/functions/_shared/{cil-state.ts → cil.ts} +158 -56
- package/.framework/functions/_shared/db.ts +80 -126
- package/.framework/functions/_shared/hash-api-key.ts +63 -0
- package/.framework/functions/_shared/index.ts +224 -335
- package/.framework/functions/_shared/kb.ts +319 -0
- package/.framework/functions/_shared/llm.ts +92 -0
- package/.framework/functions/_shared/logging.ts +288 -0
- package/.framework/functions/_shared/middleware.ts +329 -365
- package/.framework/functions/_shared/outcomes.ts +36 -0
- package/.framework/functions/_shared/permissions.ts +359 -643
- package/.framework/functions/_shared/pipeline-runner.ts +243 -316
- package/.framework/functions/_shared/principal.ts +376 -468
- package/.framework/functions/_shared/resolve-credentials.ts +102 -0
- package/.framework/functions/_shared/resolve-ids.ts +188 -16
- package/.framework/functions/_shared/retry-deliveries.ts +104 -0
- package/.framework/functions/_shared/schema-utils.ts +611 -373
- package/.framework/functions/_shared/testing.ts +207 -137
- package/.framework/functions/_shared/tracing.ts +276 -0
- package/.framework/functions/_shared/trigger-engine.ts +159 -211
- package/.framework/functions/_shared/webhook-registration.ts +95 -81
- package/.framework/functions/_shared/webhook-registry.ts +153 -75
- package/.framework/functions/account-nodes.ts +31 -53
- package/.framework/functions/adapters/generic-webhook-inbound.ts +56 -0
- package/.framework/functions/adapters/generic-webhook-outbound.ts +43 -0
- package/.framework/functions/adapters/openai-outbound.ts +40 -0
- package/.framework/functions/admin-data.ts +685 -537
- package/.framework/functions/admin-funnel.ts +31 -20
- package/.framework/functions/admin-install-funnel.ts +47 -28
- package/.framework/functions/admin-signals.ts +47 -29
- package/.framework/functions/ai-agents.ts +122 -147
- package/.framework/functions/ai-interactions.ts +180 -90
- package/.framework/functions/api-keys.ts +250 -192
- package/.framework/functions/apps.ts +203 -197
- package/.framework/functions/auth.ts +98 -91
- package/.framework/functions/cil-agent-ingest.ts +773 -0
- package/.framework/functions/cil-agent.ts +592 -0
- package/.framework/functions/cil-callmap.ts +1227 -0
- package/.framework/functions/cil-config.ts +583 -0
- package/.framework/functions/cil-context.ts +445 -0
- package/.framework/functions/cil-doc.ts +1752 -0
- package/.framework/functions/cil-embeddings.ts +1202 -0
- package/.framework/functions/cil-graph.ts +585 -0
- package/.framework/functions/cil-ingest.ts +2416 -0
- package/.framework/functions/cil-pipeline.ts +318 -0
- package/.framework/functions/cil-project.ts +499 -0
- package/.framework/functions/cil-validate.ts +469 -0
- package/.framework/functions/debug-auth.ts +73 -53
- package/.framework/functions/deliveries.ts +117 -0
- package/.framework/functions/embeddings.ts +528 -516
- package/.framework/functions/eval-harness.ts +158 -106
- package/.framework/functions/integration-outbound.ts +311 -0
- package/.framework/functions/integration-routes.ts +240 -466
- package/.framework/functions/integrations.ts +132 -140
- package/.framework/functions/invites.ts +401 -201
- package/.framework/functions/item-progress.ts +113 -53
- package/.framework/functions/kb.ts +28 -0
- package/.framework/functions/logs.ts +239 -222
- package/.framework/functions/observability.ts +308 -259
- package/.framework/functions/pipeline-executions.ts +179 -235
- package/.framework/functions/pipelines.ts +163 -205
- package/.framework/functions/prompt-configs.ts +109 -156
- package/.framework/functions/roles.ts +97 -147
- package/.framework/functions/system-cron.ts +132 -218
- package/.framework/functions/system.ts +112 -52
- package/.framework/functions/tests.ts +32 -28
- package/.framework/functions/timers.ts +162 -210
- package/.framework/functions/triggers.ts +282 -346
- package/.framework/functions/types.ts +283 -400
- package/.framework/migrations/001_core_schema.sql +630 -0
- package/.framework/migrations/002_core_rls_and_functions.sql +532 -0
- package/.framework/migrations/{003_system_data.sql → 003_core_system_data.sql} +26 -153
- package/.framework/migrations/004_core_rpc_and_broker.sql +421 -0
- package/.framework/migrations/005_core_observability.sql +598 -0
- package/.framework/migrations/006_security_hardening_rls.sql +97 -0
- package/.framework/migrations/007_kb_article_list.sql +237 -0
- package/.framework/migrations/008_items_target.sql +8 -0
- package/.framework/migrations/009_roles_slug_unique.sql +11 -0
- package/.framework/public/spine.config.json +91 -0
- package/.framework/scripts/app-install-cli.ts +112 -98
- package/.framework/src/App.tsx +67 -20
- package/.framework/src/apps/admin/index.tsx +97 -27
- package/.framework/src/components/AppWrapper.tsx +50 -11
- package/.framework/src/components/CustomAppLoader.tsx +131 -22
- package/.framework/src/components/admin/AdminListPage.tsx +75 -8
- package/.framework/src/components/admin/AdminSidebar.tsx +148 -36
- package/.framework/src/components/admin/AdminStatsCard.tsx +48 -29
- package/.framework/src/components/admin/PageInstructions.tsx +49 -0
- package/.framework/src/components/admin/SortableTableHeader.tsx +49 -2
- package/.framework/src/components/app-shell/GenericAppShell.tsx +69 -11
- package/.framework/src/components/app-shell/GenericDetailPage.tsx +55 -10
- package/.framework/src/components/app-shell/GenericListPage.tsx +76 -9
- package/.framework/src/components/app-sidebar.tsx +50 -3
- package/.framework/src/components/auth/ProtectedRoute.tsx +53 -45
- package/.framework/src/components/kb/KBArticleEditor.tsx +409 -0
- package/.framework/src/components/kb/KBArticleList.tsx +557 -0
- package/.framework/src/components/layout/AppShell.tsx +54 -16
- package/.framework/src/components/layout/Header.tsx +50 -38
- package/.framework/src/components/layout/Layout.tsx +48 -32
- package/.framework/src/components/layout/Sidebar.tsx +167 -16
- package/.framework/src/components/runtime/DataDetailHeader.tsx +33 -1
- package/.framework/src/components/runtime/DataDetailPage.tsx +289 -4
- package/.framework/src/components/runtime/DataFilters.tsx +78 -1
- package/.framework/src/components/runtime/DataHeader.tsx +32 -29
- package/.framework/src/components/runtime/DataListPage.tsx +161 -2
- package/.framework/src/components/runtime/DataStats.tsx +48 -1
- package/.framework/src/components/runtime/DataTable.tsx +153 -1
- package/.framework/src/components/runtime/SchemaDetailForm.tsx +77 -46
- package/.framework/src/components/runtime/index.ts +16 -9
- package/.framework/src/components/search-form.tsx +33 -1
- package/.framework/src/components/shared/AgentView.tsx +48 -51
- package/.framework/src/components/shared/FieldRenderer.tsx +84 -67
- package/.framework/src/components/shared/InviteManager.tsx +113 -52
- package/.framework/src/components/shared/MemberList.tsx +182 -0
- package/.framework/src/components/shared/SchemaFields.tsx +96 -78
- package/.framework/src/components/ui/DataTable.tsx +86 -56
- package/.framework/src/components/ui/Form.tsx +188 -109
- package/.framework/src/components/ui/ItemCard.tsx +86 -58
- package/.framework/src/components/ui/ItemListView.tsx +88 -61
- package/.framework/src/components/ui/LoadingSpinner.tsx +52 -27
- package/.framework/src/components/ui/Modal.tsx +81 -4
- package/.framework/src/components/ui/RichTextEditor.tsx +101 -9
- package/.framework/src/components/ui/accordion.tsx +97 -5
- package/.framework/src/components/ui/alert-dialog.tsx +225 -13
- package/.framework/src/components/ui/alert.tsx +113 -6
- package/.framework/src/components/ui/aspect-ratio.tsx +49 -2
- package/.framework/src/components/ui/avatar.tsx +129 -7
- package/.framework/src/components/ui/badge.tsx +65 -3
- package/.framework/src/components/ui/breadcrumb.tsx +145 -8
- package/.framework/src/components/ui/button-group.tsx +81 -4
- package/.framework/src/components/ui/button.tsx +49 -2
- package/.framework/src/components/ui/calendar.tsx +65 -3
- package/.framework/src/components/ui/card.tsx +145 -8
- package/.framework/src/components/ui/carousel.tsx +129 -7
- package/.framework/src/components/ui/chart.tsx +161 -10
- package/.framework/src/components/ui/checkbox.tsx +49 -2
- package/.framework/src/components/ui/collapsible.tsx +81 -5
- package/.framework/src/components/ui/combobox.tsx +305 -20
- package/.framework/src/components/ui/command.tsx +177 -11
- package/.framework/src/components/ui/context-menu.tsx +273 -17
- package/.framework/src/components/ui/dialog.tsx +209 -27
- package/.framework/src/components/ui/direction.tsx +64 -4
- package/.framework/src/components/ui/drawer.tsx +193 -12
- package/.framework/src/components/ui/dropdown-menu.tsx +273 -18
- package/.framework/src/components/ui/empty.tsx +145 -9
- package/.framework/src/components/ui/field.tsx +209 -14
- package/.framework/src/components/ui/hover-card.tsx +81 -4
- package/.framework/src/components/ui/input-group.tsx +162 -10
- package/.framework/src/components/ui/input-otp.tsx +98 -5
- package/.framework/src/components/ui/input.tsx +49 -2
- package/.framework/src/components/ui/item.tsx +225 -13
- package/.framework/src/components/ui/kbd.tsx +65 -3
- package/.framework/src/components/ui/label.tsx +49 -2
- package/.framework/src/components/ui/menubar.tsx +289 -17
- package/.framework/src/components/ui/native-select.tsx +97 -5
- package/.framework/src/components/ui/navigation-menu.tsx +177 -10
- package/.framework/src/components/ui/pagination.tsx +161 -9
- package/.framework/src/components/ui/popover.tsx +145 -8
- package/.framework/src/components/ui/progress.tsx +49 -2
- package/.framework/src/components/ui/radio-group.tsx +65 -3
- package/.framework/src/components/ui/resizable.tsx +82 -4
- package/.framework/src/components/ui/scroll-area.tsx +65 -3
- package/.framework/src/components/ui/select.tsx +193 -11
- package/.framework/src/components/ui/separator.tsx +49 -2
- package/.framework/src/components/ui/sheet.tsx +193 -11
- package/.framework/src/components/ui/sidebar.tsx +481 -38
- package/.framework/src/components/ui/skeleton.tsx +49 -2
- package/.framework/src/components/ui/slider.tsx +49 -2
- package/.framework/src/components/ui/sonner.tsx +49 -2
- package/.framework/src/components/ui/spinner.tsx +49 -2
- package/.framework/src/components/ui/switch.tsx +49 -2
- package/.framework/src/components/ui/table-primitives.tsx +160 -33
- package/.framework/src/components/ui/table.tsx +161 -9
- package/.framework/src/components/ui/tabs.tsx +113 -6
- package/.framework/src/components/ui/textarea.tsx +49 -2
- package/.framework/src/components/ui/toggle-group.tsx +81 -4
- package/.framework/src/components/ui/toggle.tsx +65 -3
- package/.framework/src/components/ui/tooltip.tsx +97 -5
- package/.framework/src/contexts/AppContext.tsx +129 -45
- package/.framework/src/contexts/AuthContext.tsx +187 -144
- package/.framework/src/hooks/use-mobile.ts +48 -2
- package/.framework/src/hooks/useApi.ts +208 -159
- package/.framework/src/hooks/useAppPath.ts +32 -23
- package/.framework/src/hooks/useApps.ts +51 -20
- package/.framework/src/hooks/useEntityList.ts +32 -78
- package/.framework/src/hooks/useEntityRecord.ts +35 -96
- package/.framework/src/hooks/useForm.ts +32 -92
- package/.framework/src/hooks/useListSchema.ts +49 -78
- package/.framework/src/hooks/useSchemaRecord.ts +114 -105
- package/.framework/src/lib/api.ts +195 -116
- package/.framework/src/lib/supabase.ts +98 -64
- package/.framework/src/lib/utils.ts +287 -151
- package/.framework/src/main.tsx +49 -3
- package/.framework/src/pages/DashboardPage.tsx +52 -29
- package/.framework/src/pages/NotFoundPage.tsx +32 -10
- package/.framework/src/pages/SetupPage.tsx +32 -12
- package/.framework/src/pages/admin/AIAgentDetailPage.tsx +119 -10
- package/.framework/src/pages/admin/AIAgentsPage.tsx +48 -19
- package/.framework/src/pages/admin/APIKeyDetailPage.tsx +187 -17
- package/.framework/src/pages/admin/APIKeysPage.tsx +98 -22
- package/.framework/src/pages/admin/AlertsConfigPage.tsx +113 -26
- package/.framework/src/pages/admin/AppDetailPage.tsx +125 -8
- package/.framework/src/pages/admin/AppsPage.tsx +49 -2
- package/.framework/src/pages/admin/DeliveriesPage.tsx +196 -0
- package/.framework/src/pages/admin/DesignedPage.tsx +32 -18
- package/.framework/src/pages/admin/EmbeddingDetailPage.tsx +215 -484
- package/.framework/src/pages/admin/EmbeddingsPage.tsx +116 -319
- package/.framework/src/pages/admin/ExtendedShadcnTestPage.tsx +34 -9
- package/.framework/src/pages/admin/IncrementalShadcnTestPage.tsx +34 -9
- package/.framework/src/pages/admin/IntegratedDashboard.tsx +32 -16
- package/.framework/src/pages/admin/IntegrationDetailPage.tsx +183 -26
- package/.framework/src/pages/admin/IntegrationsPage.tsx +48 -19
- package/.framework/src/pages/admin/InvitesPage.tsx +48 -14
- package/.framework/src/pages/admin/LogsPage.tsx +422 -75
- package/.framework/src/pages/admin/MinimalShadcnTestPage.tsx +34 -9
- package/.framework/src/pages/admin/ObservabilityDashboard.tsx +113 -24
- package/.framework/src/pages/admin/PipelineDetailPage.tsx +206 -34
- package/.framework/src/pages/admin/PipelineExecutionsPage.tsx +50 -17
- package/.framework/src/pages/admin/PipelinesPage.tsx +160 -21
- package/.framework/src/pages/admin/PromptConfigDetailPage.tsx +378 -88
- package/.framework/src/pages/admin/PromptConfigsPage.tsx +125 -16
- package/.framework/src/pages/admin/ProperlyDesignedPage.tsx +49 -10
- package/.framework/src/pages/admin/RoleDetailPage.tsx +130 -10
- package/.framework/src/pages/admin/RolesPage.tsx +171 -18
- package/.framework/src/pages/admin/SelectTestPage.tsx +34 -9
- package/.framework/src/pages/admin/ShadcnTestPage.tsx +49 -11
- package/.framework/src/pages/admin/SimpleDashboard.tsx +49 -10
- package/.framework/src/pages/admin/TestRunDetailPage.tsx +49 -4
- package/.framework/src/pages/admin/TestingDashboard.tsx +98 -28
- package/.framework/src/pages/admin/TimerDetailPage.tsx +143 -33
- package/.framework/src/pages/admin/TimersPage.tsx +48 -20
- package/.framework/src/pages/admin/TriggerDetailPage.tsx +118 -30
- package/.framework/src/pages/admin/TriggersPage.tsx +64 -19
- package/.framework/src/pages/admin/TypeDetailPage.tsx +138 -15
- package/.framework/src/pages/admin/TypesPage.tsx +48 -20
- package/.framework/src/pages/admin/cil/CILAgentPage.tsx +465 -0
- package/.framework/src/pages/admin/cil/CILChunkDetailPage.tsx +787 -0
- package/.framework/src/pages/admin/cil/CILDashboardPage.tsx +699 -0
- package/.framework/src/pages/admin/cil/CILFileListPage.tsx +647 -0
- package/.framework/src/pages/admin/cil/CILGraphPage.tsx +616 -0
- package/.framework/src/pages/admin/cil/CILIngestionPage.tsx +1402 -0
- package/.framework/src/pages/admin/cil/CILProjectsPage.tsx +614 -0
- package/.framework/src/pages/admin/cil/CILSearchPage.tsx +571 -0
- package/.framework/src/pages/admin/cil/FlexiblePipelinePage.tsx +731 -0
- package/.framework/src/pages/admin/kb/KBAdminPage.tsx +64 -0
- package/.framework/src/pages/auth/AuthConfirmPage.tsx +52 -51
- package/.framework/src/pages/auth/ForgotPasswordPage.tsx +31 -22
- package/.framework/src/pages/auth/LoginPage.tsx +50 -23
- package/.framework/src/pages/auth/PendingApprovalPage.tsx +31 -15
- package/.framework/src/pages/auth/RegisterPage.tsx +281 -317
- package/.framework/src/pages/spine-framework/APIPage.tsx +33 -0
- package/.framework/src/pages/spine-framework/CLIPage.tsx +33 -0
- package/.framework/src/types/auth.ts +128 -71
- package/.framework/src/types/types.ts +373 -157
- package/custom/.devin/skills/spine-workbench-discovery/SKILL.md +39 -0
- package/custom/.devin/skills/spine-workbench-idea/SKILL.md +36 -0
- package/custom/.devin/skills/spine-workbench-implement/SKILL.md +46 -0
- package/custom/.devin/skills/spine-workbench-initiative/SKILL.md +28 -0
- package/custom/.devin/skills/spine-workbench-plan/SKILL.md +38 -0
- package/docs/README.md +9 -130
- package/netlify.toml +5 -2
- package/package.json +13 -25
- package/scripts/kb-ingest-docs.ts +203 -0
- package/spine.config.json +65 -4
- package/.framework/migrations/001_schema.sql +0 -425
- package/.framework/migrations/002_rls_and_functions.sql +0 -763
- package/.framework/migrations/004_enhancements.sql +0 -43
- package/.framework/migrations/005_auth_user_trigger.sql +0 -105
- package/.framework/migrations/006_fix_current_actor_id.sql +0 -21
- package/.framework/migrations/016_invites.sql +0 -27
- package/.framework/migrations/017_fix_get_account_apps.sql +0 -78
- package/.framework/migrations/018_apps_required_roles.sql +0 -17
- package/.framework/migrations/019_triggers_unique_constraint.sql +0 -13
- package/.framework/migrations/020_get_account_apps_role_filter.sql +0 -117
- package/.framework/migrations/021_app_domains.sql +0 -112
- package/.framework/migrations/022_route_prefix_domain_unique.sql +0 -21
- package/.framework/migrations/023_match_embeddings_rpc.sql +0 -78
- package/.framework/migrations/024_messages_account_id.sql +0 -9
- package/.framework/migrations/025_retype_threads_messages.sql +0 -240
- package/.framework/migrations/026_fix_rls_policies.sql +0 -47
- package/.framework/migrations/027_embedding_lifecycle.sql +0 -176
- package/.framework/migrations/028_fix_message_data_permissions.sql +0 -49
- package/.framework/migrations/029_tighten_support_triage_prompt.sql +0 -41
- package/.framework/migrations/030_messages_updated_by.sql +0 -6
- package/.framework/migrations/031_seed_upsert_constraints.sql +0 -35
- package/docs/APPS.md +0 -232
- package/docs/ARCHITECTURE.md +0 -246
- package/docs/INSTALL.md +0 -155
- package/docs/design-schema-spec.md +0 -1837
- package/docs/dev-guides/01-create-an-app.md +0 -183
- package/docs/dev-guides/02-create-a-function.md +0 -178
- package/docs/dev-guides/03-create-a-component.md +0 -178
- package/docs/dev-guides/04-webhook-handlers.md +0 -129
- package/docs/dev-guides/05-testing.md +0 -168
- package/docs/dev-guides/README.md +0 -40
- package/docs/npm-packages-plan.md +0 -288
- package/docs/spine-core--api-reference.md +0 -822
|
@@ -1,523 +1,297 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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 {
|
|
40
|
-
import { resolveHandler
|
|
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
|
|
23
|
+
interface InboundAdapterContext {
|
|
24
|
+
payload: any
|
|
45
25
|
integration: any
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
26
|
+
apiKey: any
|
|
27
|
+
accountId: string | null
|
|
28
|
+
appId: string | null
|
|
29
|
+
event: string | null
|
|
49
30
|
headers: Record<string, string>
|
|
50
|
-
|
|
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
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
62
|
-
|
|
46
|
+
function getClientIp(event: any): string {
|
|
47
|
+
return event.headers?.['x-forwarded-for'] || event.headers?.['X-Forwarded-For'] || 'unknown'
|
|
63
48
|
}
|
|
64
49
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
228
|
-
|
|
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 (
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
|
|
286
|
-
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
-
|
|
361
|
-
|
|
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
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
status: '
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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
|
-
|
|
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
|
-
|
|
450
|
-
|
|
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
|
-
|
|
466
|
-
|
|
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
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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
|
-
|
|
271
|
+
|
|
272
|
+
return record
|
|
500
273
|
} catch (err: any) {
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
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
|
-
|
|
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
|
-
|
|
295
|
+
export default createPublicHandler(execute)
|
|
521
296
|
|
|
522
|
-
|
|
523
|
-
export { handler }
|
|
297
|
+
// CIL[END chunk_id="integration_routes_module_header_1784200006"]
|