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