spine-framework 0.4.4 → 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/hash-api-key.ts +183 -79
- package/.framework/functions/_shared/logging.ts +562 -224
- package/.framework/functions/_shared/middleware.ts +47 -44
- package/.framework/functions/admin-data.ts +39 -28
- package/.framework/functions/ai-agents.ts +10 -6
- 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/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 +240 -127
- package/.framework/functions/tests.ts +7 -6
- package/.framework/functions/timers.ts +310 -107
- package/.framework/functions/triggers.ts +28 -13
- package/.framework/functions/types.ts +13 -10
- 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/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 +88 -79
- package/.framework/src/components/admin/EntitySelect.tsx +181 -0
- package/.framework/src/components/admin/PageInstructions.tsx +86 -127
- package/.framework/src/components/app-shell/GenericDetailPage.tsx +37 -13
- package/.framework/src/components/app-shell/GenericListPage.tsx +28 -32
- package/.framework/src/components/layout/Sidebar.tsx +300 -177
- 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/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/pages/NotFoundPage.tsx +62 -31
- 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/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 +4 -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 +479 -44
- 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/integration-outbound-credentials.test.ts +167 -0
- package/.framework/tests/unit/integrations-audit-redaction.test.ts +130 -0
- package/.framework/tests/unit/logging-app-id.test.ts +75 -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/README.md +125 -166
- package/config/tsconfig.cli.json +2 -2
- 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,121 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: pipeline-runner-background
|
|
3
|
+
audience: developers, agents
|
|
4
|
+
kind: reference
|
|
5
|
+
verified_against: f69c37d
|
|
6
|
+
verified_on: 2026-10-30
|
|
7
|
+
source_files:
|
|
8
|
+
- .framework/functions/pipeline-runner-background.ts
|
|
9
|
+
- .framework/functions/_shared/pipeline-runner.ts
|
|
10
|
+
- .framework/functions/_shared/trigger-engine.ts
|
|
11
|
+
- .framework/functions/_shared/principal.ts
|
|
12
|
+
- netlify.toml
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# pipeline-runner-background
|
|
16
|
+
|
|
17
|
+
## What it does
|
|
18
|
+
|
|
19
|
+
The isolated executor for trigger-dispatched pipeline runs. When a record
|
|
20
|
+
write matches a trigger, `.framework/functions/_shared/trigger-engine.ts`
|
|
21
|
+
inserts a `pipeline_executions` row with `status: 'queued'` and POSTs the
|
|
22
|
+
dispatch payload here. This function authenticates the call, reconstructs
|
|
23
|
+
a machine `RequestContext`, and hands the pre-created `execution_id` to
|
|
24
|
+
`runPipeline` — so the queued row transitions `queued → running → terminal`
|
|
25
|
+
instead of a duplicate execution being inserted.
|
|
26
|
+
|
|
27
|
+
The `-background` filename suffix makes Netlify treat it as a background
|
|
28
|
+
function: the dispatcher gets a 202 acknowledgement immediately and the
|
|
29
|
+
run itself gets the fixed 15-minute invocation budget in its own process
|
|
30
|
+
and connection pool (see the note in `netlify.toml`). It is **not** a
|
|
31
|
+
scheduled function — it is invoked once per trigger fire, over HTTP.
|
|
32
|
+
|
|
33
|
+
## Route and methods
|
|
34
|
+
|
|
35
|
+
`POST /.netlify/functions/pipeline-runner-background` — internal
|
|
36
|
+
function-to-function traffic only. The only caller in the framework is the
|
|
37
|
+
trigger engine's dispatch path, which sends an `X-Internal-Secret` header
|
|
38
|
+
and awaits only the ack (bounded by a 5s timeout), never the pipeline's
|
|
39
|
+
completion.
|
|
40
|
+
|
|
41
|
+
## Authentication
|
|
42
|
+
|
|
43
|
+
Not `createHandler` — auth is checked inline, by design: the middleware
|
|
44
|
+
path would resolve the secret-bearing request to anonymous and 401 before
|
|
45
|
+
the handler ran.
|
|
46
|
+
|
|
47
|
+
`isDispatchAuthorized` compares the `X-Internal-Secret` header against the
|
|
48
|
+
`INTERNAL_INVOKE_SECRET` environment variable. Both sides are SHA-256
|
|
49
|
+
hashed before `timingSafeEqual`, so length-mismatch early-exit cannot leak
|
|
50
|
+
the expected secret via timing. If `INTERNAL_INVOKE_SECRET` is unset, the
|
|
51
|
+
check always fails — every dispatch gets 403 (fail closed).
|
|
52
|
+
|
|
53
|
+
On success the handler fabricates a context with `SYSTEM_PRINCIPAL`
|
|
54
|
+
(wildcard `*:*` machine principal), `adminDb`, the dispatching
|
|
55
|
+
`account_id`, and provenance `sourceType: 'trigger'` carrying the
|
|
56
|
+
`trigger_id`.
|
|
57
|
+
|
|
58
|
+
## Request contract
|
|
59
|
+
|
|
60
|
+
JSON body:
|
|
61
|
+
|
|
62
|
+
| Field | Required | Purpose |
|
|
63
|
+
|---|---|---|
|
|
64
|
+
| `pipeline_id` | yes | Pipeline to run; missing → 400 |
|
|
65
|
+
| `execution_id` | no | Pre-created `queued` execution row to claim |
|
|
66
|
+
| `trigger_data` | no | Trigger envelope passed to stage templates |
|
|
67
|
+
| `account_id` | no | Account scope for the fabricated context |
|
|
68
|
+
| `trigger_id` | no | Recorded in principal provenance |
|
|
69
|
+
|
|
70
|
+
Malformed JSON → 400 `Invalid JSON body`.
|
|
71
|
+
|
|
72
|
+
## Response contract
|
|
73
|
+
|
|
74
|
+
- `200` — `{ ok, execution_id, status }` where `ok` is true only when the
|
|
75
|
+
run finished `completed`. (In practice Netlify answers background
|
|
76
|
+
invocations with 202 to the dispatcher; the JSON body matters to
|
|
77
|
+
`netlify functions:invoke` and logs.)
|
|
78
|
+
- `400` — `{ error: 'Invalid JSON body' }` or
|
|
79
|
+
`{ error: 'pipeline_id is required' }`.
|
|
80
|
+
- `403` — `{ error: 'Forbidden — invalid internal secret' }`.
|
|
81
|
+
- `500` — `{ error, execution_id }` when `runPipeline` throws (missing or
|
|
82
|
+
inactive pipeline, claim failure, stage abort).
|
|
83
|
+
|
|
84
|
+
## Side effects
|
|
85
|
+
|
|
86
|
+
- On entry, `sweepStaleExecutions()` runs fire-and-forget — fails
|
|
87
|
+
`running`/`pending`/`queued` execution rows untouched for over 10
|
|
88
|
+
minutes.
|
|
89
|
+
- `runPipeline` claims the `queued` execution row (or inserts one with the
|
|
90
|
+
same id when the claim finds nothing), executes the pipeline stages, and
|
|
91
|
+
writes terminal status, stage results, and `duration_ms` back to
|
|
92
|
+
`pipeline_executions`. Whatever the stages do — record writes, outbound
|
|
93
|
+
HTTP, embeddings, notifications — happens here, outside the triggering
|
|
94
|
+
request's process.
|
|
95
|
+
|
|
96
|
+
## Errors
|
|
97
|
+
|
|
98
|
+
- Missing/invalid secret → 403.
|
|
99
|
+
- Missing `pipeline_id` or unparseable body → 400.
|
|
100
|
+
- `Pipeline not found or inactive`, claim/insert failures, and stage
|
|
101
|
+
errors → 500 with the thrown message. Stage-level failures are normally
|
|
102
|
+
captured on the execution row rather than thrown, unless the stage
|
|
103
|
+
aborts the run.
|
|
104
|
+
|
|
105
|
+
## Agent notes
|
|
106
|
+
|
|
107
|
+
- Duplicate-delivery safety lives in `runPipeline`: it only claims rows
|
|
108
|
+
still `status = 'queued'`; when the claim matches nothing it inserts a
|
|
109
|
+
row with the same `execution_id`, so a redelivered dispatch dies on the
|
|
110
|
+
primary-key conflict instead of running the pipeline twice
|
|
111
|
+
(`.framework/functions/_shared/pipeline-runner.ts`).
|
|
112
|
+
- If `INTERNAL_INVOKE_SECRET` is not set in the environment, trigger
|
|
113
|
+
dispatch is a silent no-op — triggers still insert `queued` rows but the
|
|
114
|
+
POST to this function 403s, and the sweeper eventually marks those rows
|
|
115
|
+
`failed`.
|
|
116
|
+
- The timing-safe secret check duplicates
|
|
117
|
+
`isInternalInvokeAuthorized` in `.framework/functions/_shared/principal.ts`;
|
|
118
|
+
keep the two implementations in sync if either changes.
|
|
119
|
+
- Do not call this endpoint from client code. It exists for the trigger
|
|
120
|
+
engine; manual pipeline runs go through `runPipeline` callers (CLI,
|
|
121
|
+
timers, `system-cron`) or the `pipelines` API.
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: pipelines
|
|
3
|
+
audience: both
|
|
4
|
+
kind: reference
|
|
5
|
+
verified_against: f69c37d
|
|
6
|
+
verified_on: 2026-10-30
|
|
7
|
+
source_files:
|
|
8
|
+
- .framework/functions/pipelines.ts
|
|
9
|
+
- .framework/functions/_shared/middleware.ts
|
|
10
|
+
- .framework/functions/_shared/pipeline-runner.ts
|
|
11
|
+
- .framework/migrations/020_schema_reconciliation.sql
|
|
12
|
+
- .framework/public/_redirects
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# pipelines
|
|
16
|
+
|
|
17
|
+
## What it does
|
|
18
|
+
|
|
19
|
+
CRUD plus `toggle`, `by-trigger` lookup, and `executions` listing over
|
|
20
|
+
`public.pipelines` — the config rows that define a `trigger_type` and an
|
|
21
|
+
ordered `stages` array. **This function manages config only**: pipeline
|
|
22
|
+
execution is done by `.framework/functions/_shared/pipeline-runner.ts`
|
|
23
|
+
(`runPipeline`), invoked by `system-cron` (due timers), by
|
|
24
|
+
`pipeline-runner-background` (trigger dispatches), or by the
|
|
25
|
+
`generate_embeddings`/kb stage handlers — and run bookkeeping lives in
|
|
26
|
+
`public.pipeline_executions` via the sibling `pipeline-executions`
|
|
27
|
+
function. See [Automation](../../concepts/automation.md).
|
|
28
|
+
|
|
29
|
+
## Route and methods
|
|
30
|
+
|
|
31
|
+
`/.netlify/functions/pipelines`, reachable as `/api/pipelines` (`/api/*`
|
|
32
|
+
→ `/.netlify/functions/:splat`, `.framework/public/_redirects`).
|
|
33
|
+
Dispatch is by `?action=` + method:
|
|
34
|
+
|
|
35
|
+
| Call | Handler |
|
|
36
|
+
|---|---|
|
|
37
|
+
| `GET /api/pipelines` | `list` |
|
|
38
|
+
| `GET /api/pipelines?id=<uuid>` | `get` |
|
|
39
|
+
| `GET /api/pipelines?action=by-trigger&trigger_type=<t>` | `listByTrigger` |
|
|
40
|
+
| `GET /api/pipelines?action=executions&pipeline_id=<uuid>` | `getExecutions` |
|
|
41
|
+
| `POST /api/pipelines` | `create` |
|
|
42
|
+
| `POST /api/pipelines?action=toggle` | `toggle` |
|
|
43
|
+
| `PATCH /api/pipelines` (`id` in body or query) | `update` |
|
|
44
|
+
| `DELETE /api/pipelines?id=<uuid>` | `remove` |
|
|
45
|
+
|
|
46
|
+
A matched action with the wrong method falls through to
|
|
47
|
+
`Invalid action or method` — e.g. `POST ?action=executions`.
|
|
48
|
+
|
|
49
|
+
## Authentication
|
|
50
|
+
|
|
51
|
+
Standard `createHandler` auth: `x-api-key` → machine principal,
|
|
52
|
+
`x-internal-secret` → internal principal, `Bearer <jwt>` → human;
|
|
53
|
+
anonymous → 401.
|
|
54
|
+
|
|
55
|
+
- `list`/`listByTrigger`/`create`/`update`/`toggle` require
|
|
56
|
+
`ctx.accountId` (or person+account context).
|
|
57
|
+
- No role gate — system admins use `adminDb`, other humans use
|
|
58
|
+
JWT-scoped `ctx.db` (RLS); machines run service-role.
|
|
59
|
+
- Reads are sanitized: non-admins get `{ id }` per row (pipeline rows
|
|
60
|
+
carry no `design_schema`). `getExecutions` rows are returned
|
|
61
|
+
**unsanitized**.
|
|
62
|
+
|
|
63
|
+
## Request contract
|
|
64
|
+
|
|
65
|
+
**list** (query): `app_id`. `include_inactive` is destructured but
|
|
66
|
+
**never applied** — `list` always returns all pipelines including
|
|
67
|
+
inactive ones (only `listByTrigger` honors it). Ordered by `name`.
|
|
68
|
+
|
|
69
|
+
**listByTrigger** (query): `trigger_type` required; `app_id`,
|
|
70
|
+
`include_inactive` (`'true'`) optional.
|
|
71
|
+
|
|
72
|
+
**get** (query): `id`.
|
|
73
|
+
|
|
74
|
+
**getExecutions** (query): `pipeline_id` required; `limit` (default 50),
|
|
75
|
+
`offset` — reads `public.pipeline_executions` ordered `created_at` desc.
|
|
76
|
+
|
|
77
|
+
**create** (body): `name`, `trigger_type`, `stages` required. Optional:
|
|
78
|
+
`app_id`, `description`, `config` `{}`, `metadata` `{}`.
|
|
79
|
+
`account_id`/`created_by` stamped. (`trigger_type` and `stages` were
|
|
80
|
+
added by `.framework/migrations/020_schema_reconciliation.sql`.)
|
|
81
|
+
|
|
82
|
+
**update** (`id` in body or query + body): named fields only — `name`,
|
|
83
|
+
`description`, `config`, `stages`, `metadata`, `trigger_type`,
|
|
84
|
+
`is_active`, `app_id`.
|
|
85
|
+
|
|
86
|
+
**toggle** (body): `id` and `is_active` **both required** — unlike the
|
|
87
|
+
`timers`/`triggers` toggles, this sets an explicit target state.
|
|
88
|
+
|
|
89
|
+
**remove** (query): `id` — **hard delete**.
|
|
90
|
+
|
|
91
|
+
## Response contract
|
|
92
|
+
|
|
93
|
+
Standard `{ data, error, meta }` envelope. `list`/`listByTrigger`/
|
|
94
|
+
`get`/`create`/`update`/`toggle` → sanitized row(s); `getExecutions` →
|
|
95
|
+
raw `pipeline_executions` array; `remove` → `{ success: true }`.
|
|
96
|
+
|
|
97
|
+
## Side effects
|
|
98
|
+
|
|
99
|
+
- `emitLog` writes `pipeline.created` / `.updated` / `.toggled` /
|
|
100
|
+
`.deleted` audit rows to `public.logs`.
|
|
101
|
+
- `is_active = false` stops the pipeline at dispatch time —
|
|
102
|
+
`runPipeline` throws when the pipeline is missing or inactive, so
|
|
103
|
+
queued dispatches fail rather than queue silently.
|
|
104
|
+
- No execution side effects: creating/updating a pipeline does not
|
|
105
|
+
enqueue anything. Enqueue paths are triggers (event → dispatch),
|
|
106
|
+
timers (cron → `runPipeline`), and other pipelines' `run_pipeline`
|
|
107
|
+
stages.
|
|
108
|
+
|
|
109
|
+
## Errors
|
|
110
|
+
|
|
111
|
+
| Condition | HTTP | Message |
|
|
112
|
+
|---|---|---|
|
|
113
|
+
| `by-trigger` without `trigger_type` | 500 | `trigger_type is required` |
|
|
114
|
+
| No account context | 500 | `Account context required` |
|
|
115
|
+
| Missing create fields | 500 | `name, trigger_type, and stages are required` |
|
|
116
|
+
| Missing toggle fields | 500 | `Pipeline ID and is_active are required` |
|
|
117
|
+
| Missing `id` | 500 | `Pipeline ID is required` |
|
|
118
|
+
| Missing row | 500 | `Pipeline not found` |
|
|
119
|
+
| `executions` without `pipeline_id` | 500 | `Pipeline ID is required` |
|
|
120
|
+
| Bad action/method | 500 | `Invalid action or method` |
|
|
121
|
+
|
|
122
|
+
## Agent notes
|
|
123
|
+
|
|
124
|
+
- `stages` is a free-form JSON array — nothing here validates stage
|
|
125
|
+
`stage_type`/`config`; unknown stage types fail at run time inside
|
|
126
|
+
`runPipeline`, not at write time. Author against the stage handler set
|
|
127
|
+
in `.framework/functions/_shared/pipeline-runner.ts`.
|
|
128
|
+
- `trigger_type` is a coarse grouping used by `listByTrigger`; actual
|
|
129
|
+
event→pipeline matching is on `triggers.event_type`, a different
|
|
130
|
+
column on a different table.
|
|
131
|
+
- `remove` hard-deletes the pipeline row — `pipeline_executions`
|
|
132
|
+
history is not cascaded or checked, so deleting a pipeline orphans its
|
|
133
|
+
run records.
|
|
134
|
+
- `toggle` differs from siblings: `timers.toggle`/`triggers.toggle`
|
|
135
|
+
flip the current value; `pipelines.toggle` requires the target
|
|
136
|
+
`is_active` in the body.
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: prompt-configs
|
|
3
|
+
audience: both
|
|
4
|
+
kind: reference
|
|
5
|
+
verified_against: f69c37d
|
|
6
|
+
verified_on: 2026-10-30
|
|
7
|
+
source_files:
|
|
8
|
+
- .framework/functions/prompt-configs.ts
|
|
9
|
+
- .framework/functions/_shared/middleware.ts
|
|
10
|
+
- .framework/functions/_shared/permissions.ts
|
|
11
|
+
- .framework/public/_redirects
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# prompt-configs
|
|
15
|
+
|
|
16
|
+
## What it does
|
|
17
|
+
|
|
18
|
+
CRUD over `public.prompt_configs` — reusable prompt/model
|
|
19
|
+
configurations (`system_prompt`, `context_template`, `model`,
|
|
20
|
+
temperature/token limits, escalation and output bindings,
|
|
21
|
+
`knowledge_sources`, `available_tools`) that agents resolve at run time
|
|
22
|
+
(see [AI and prompts](../../concepts/ai-and-prompts.md)).
|
|
23
|
+
|
|
24
|
+
## Route and methods
|
|
25
|
+
|
|
26
|
+
`/.netlify/functions/prompt-configs`, reachable as
|
|
27
|
+
`/api/prompt-configs` (`/api/*` → `/.netlify/functions/:splat`,
|
|
28
|
+
`.framework/public/_redirects`). `?action=` is **ignored** — the
|
|
29
|
+
dispatcher has only a `default` branch keyed on method:
|
|
30
|
+
|
|
31
|
+
| Call | Handler |
|
|
32
|
+
|---|---|
|
|
33
|
+
| `GET /api/prompt-configs` | `list` |
|
|
34
|
+
| `GET /api/prompt-configs?id=<uuid>` | `get` |
|
|
35
|
+
| `POST /api/prompt-configs` | `create` |
|
|
36
|
+
| `PATCH /api/prompt-configs` (`id` in body or query) | `update` |
|
|
37
|
+
| `DELETE /api/prompt-configs?id=<uuid>` | `remove` |
|
|
38
|
+
|
|
39
|
+
## Authentication
|
|
40
|
+
|
|
41
|
+
Standard `createHandler` auth: `x-api-key` → machine principal,
|
|
42
|
+
`x-internal-secret` → internal principal, `Bearer <jwt>` → human;
|
|
43
|
+
anonymous → 401.
|
|
44
|
+
|
|
45
|
+
- `list`/`create` additionally require `ctx.accountId` (`Account context
|
|
46
|
+
required`).
|
|
47
|
+
- There is **no role check** in this file. System admins use `adminDb`;
|
|
48
|
+
everyone else uses `ctx.db` — JWT-scoped (RLS) for humans, service
|
|
49
|
+
role for machine principals. Non-admin row access is whatever RLS
|
|
50
|
+
allows, and returned rows are sanitized to `{ id }` (prompt-config
|
|
51
|
+
rows carry no `design_schema`).
|
|
52
|
+
|
|
53
|
+
## Request contract
|
|
54
|
+
|
|
55
|
+
**list** (query): `model`, `is_active` (`'true'`/`'false'`), `limit`
|
|
56
|
+
(default 100), `offset` (default 0). Ordered by `name`.
|
|
57
|
+
|
|
58
|
+
**get** (query): `id`.
|
|
59
|
+
|
|
60
|
+
**create** (body): `name`, `slug` required. Optional with defaults:
|
|
61
|
+
`app_id` `null`, `description`/`system_prompt`/`context_template`
|
|
62
|
+
`null`, `model` `'gpt-4o'`, `temperature` `0.7`, `max_tokens` `4000`,
|
|
63
|
+
`is_multi_turn` `true`, `max_history_messages` `20`,
|
|
64
|
+
`confidence_threshold`/`escalation_action`/`escalation_target`/
|
|
65
|
+
`output_mode`/`output_field` `null`, `requires_review` `false`,
|
|
66
|
+
`knowledge_sources` `[]`, `available_tools` `[]`, `tool_constraints`
|
|
67
|
+
`{}`, `metadata` `{}`. `account_id` is stamped from context.
|
|
68
|
+
|
|
69
|
+
**update** (`id` in body or query + body): allowlist only — `name`,
|
|
70
|
+
`slug`, `description`, `app_id`, `system_prompt`, `context_template`,
|
|
71
|
+
`model`, `temperature`, `max_tokens`, `is_multi_turn`,
|
|
72
|
+
`max_history_messages`, `confidence_threshold`, `escalation_action`,
|
|
73
|
+
`escalation_target`, `output_mode`, `output_field`, `requires_review`,
|
|
74
|
+
`knowledge_sources`, `available_tools`, `tool_constraints`, `metadata`,
|
|
75
|
+
`is_active`. Unknown keys are ignored; `updated_at` is stamped.
|
|
76
|
+
|
|
77
|
+
**remove** (query): `id` — **hard delete**.
|
|
78
|
+
|
|
79
|
+
## Response contract
|
|
80
|
+
|
|
81
|
+
Standard `{ data, error, meta }` envelope.
|
|
82
|
+
|
|
83
|
+
- `list`/`get`/`create`/`update` → row(s) passed through
|
|
84
|
+
`sanitizeRecordData`: full row for system admins, `{ id }` for
|
|
85
|
+
non-admins (first-surface path, no `design_schema` on the row).
|
|
86
|
+
- `remove` → `{ success: true }`.
|
|
87
|
+
|
|
88
|
+
## Side effects
|
|
89
|
+
|
|
90
|
+
- `create` inserts into `public.prompt_configs` with the caller's
|
|
91
|
+
`account_id`.
|
|
92
|
+
- `remove` issues a real `DELETE` — unlike `types`/`apps`/`roles`/
|
|
93
|
+
`integrations`, deletion here is permanent.
|
|
94
|
+
- `emitLog` writes `prompt_config.created` / `.updated` / `.deleted`
|
|
95
|
+
audit rows to `public.logs`.
|
|
96
|
+
|
|
97
|
+
## Errors
|
|
98
|
+
|
|
99
|
+
| Condition | HTTP | Message |
|
|
100
|
+
|---|---|---|
|
|
101
|
+
| No account context (list, create) | 500 | `Account context required` |
|
|
102
|
+
| Missing create fields | 500 | `name and slug are required` |
|
|
103
|
+
| Missing `id` | 500 | `Config ID is required` |
|
|
104
|
+
| Row missing on delete | 500 | `Prompt config not found` |
|
|
105
|
+
| Unmatched method | 500 | `Invalid action or method` |
|
|
106
|
+
|
|
107
|
+
Validation failures surface as HTTP 500 — thrown `Error`s without a
|
|
108
|
+
`statusCode` map to 500 in the shared error path.
|
|
109
|
+
|
|
110
|
+
## Agent notes
|
|
111
|
+
|
|
112
|
+
- `?action=get&id=…` works only by accident — every action value lands
|
|
113
|
+
in the `default` branch. Don't rely on action names here; dispatch is
|
|
114
|
+
method + `id` presence.
|
|
115
|
+
- `remove` is a hard delete; there is no `is_active` restore path.
|
|
116
|
+
Prefer `PATCH { is_active: false }` if reversibility matters.
|
|
117
|
+
- Create stamps `account_id` from the caller's context — there is no
|
|
118
|
+
way to create a config for another account through this endpoint.
|
|
119
|
+
- Consumers resolve configs at run time by `prompt_config_id` stored on
|
|
120
|
+
threads/agents, not through this endpoint; this function is pure
|
|
121
|
+
config management.
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: roles
|
|
3
|
+
audience: both
|
|
4
|
+
kind: reference
|
|
5
|
+
verified_against: f69c37d
|
|
6
|
+
verified_on: 2026-10-30
|
|
7
|
+
source_files:
|
|
8
|
+
- .framework/functions/roles.ts
|
|
9
|
+
- .framework/functions/_shared/middleware.ts
|
|
10
|
+
- .framework/functions/_shared/permissions.ts
|
|
11
|
+
- .framework/migrations/009_roles_slug_unique.sql
|
|
12
|
+
- .framework/public/_redirects
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# roles
|
|
16
|
+
|
|
17
|
+
## What it does
|
|
18
|
+
|
|
19
|
+
CRUD over `public.roles` — role definitions (slug, `permissions` JSON,
|
|
20
|
+
`is_system`, `is_protected`) that principals carry and the permission
|
|
21
|
+
engine matches against `design_schema.record_permissions`.
|
|
22
|
+
|
|
23
|
+
## Route and methods
|
|
24
|
+
|
|
25
|
+
`/.netlify/functions/roles`, reachable as `/api/roles` (`/api/*` →
|
|
26
|
+
`/.netlify/functions/:splat`, `.framework/public/_redirects`). Dispatch
|
|
27
|
+
is by HTTP method only (`?action=` is ignored):
|
|
28
|
+
|
|
29
|
+
| Call | Handler |
|
|
30
|
+
|---|---|
|
|
31
|
+
| `GET /api/roles` | `list` |
|
|
32
|
+
| `GET /api/roles?id=<uuid>` | `get` |
|
|
33
|
+
| `POST /api/roles` | `create` |
|
|
34
|
+
| `PATCH /api/roles` (`id` in **body**) | `update` |
|
|
35
|
+
| `DELETE /api/roles` (`id` in **body**) | `remove` |
|
|
36
|
+
|
|
37
|
+
## Authentication
|
|
38
|
+
|
|
39
|
+
Standard `createHandler` auth: `x-api-key` → machine principal,
|
|
40
|
+
`x-internal-secret` → internal principal, `Bearer <jwt>` → human;
|
|
41
|
+
anonymous → 401.
|
|
42
|
+
|
|
43
|
+
- `create` requires `system_admin`, or a `canCreate` grant from
|
|
44
|
+
`resolveFirstSurfacePermissions(..., 'role', 'create')`. Creating the
|
|
45
|
+
`system_admin` slug itself always requires `system_admin`.
|
|
46
|
+
- `update`/`remove` have no explicit role gate — writes go through
|
|
47
|
+
`adminDb` for system admins and `ctx.db` (JWT-scoped, RLS) for other
|
|
48
|
+
humans, so a non-admin's update fails at the database as
|
|
49
|
+
`Role not found or access denied`.
|
|
50
|
+
- `list`/`get` are readable by any authenticated principal.
|
|
51
|
+
|
|
52
|
+
## Request contract
|
|
53
|
+
|
|
54
|
+
**list** (query): `app_id` (`'null'` filters to global roles),
|
|
55
|
+
`is_system` (`'true'`/`'false'`), `limit` (default 50), `offset`
|
|
56
|
+
(default 0). Active rows only, ordered `is_system` desc then `name`,
|
|
57
|
+
joining `app:apps(id, slug, name)`.
|
|
58
|
+
|
|
59
|
+
**get** (query): `id` — active rows only.
|
|
60
|
+
|
|
61
|
+
**create** (body): `slug`, `name` required. Optional: `app_id`,
|
|
62
|
+
`description`, `permissions` (default `{}`), `is_system` (default
|
|
63
|
+
`false`). `is_active: true` is forced.
|
|
64
|
+
|
|
65
|
+
**update** (body): `id` plus any subset of role columns — **no field
|
|
66
|
+
allowlist**; every key except `id` is written plus `updated_at`, after
|
|
67
|
+
`validateUpdatePermissions(ctx, updates, current, 'role')` passes.
|
|
68
|
+
|
|
69
|
+
**remove** (body): `id` — soft delete (`is_active = false`).
|
|
70
|
+
|
|
71
|
+
## Response contract
|
|
72
|
+
|
|
73
|
+
Standard `{ data, error, meta }` envelope.
|
|
74
|
+
|
|
75
|
+
- `list` → array of role rows; `get`/`create`/`update`/`remove` → single
|
|
76
|
+
row.
|
|
77
|
+
- System admins get raw rows. Other principals get `sanitizeRecordData`
|
|
78
|
+
output — role records carry no `design_schema`, so non-admin reads
|
|
79
|
+
collapse to `{ id }` (and `list` returns an array of `{ id }`
|
|
80
|
+
objects). Row-level access for humans is additionally bounded by RLS
|
|
81
|
+
on `ctx.db`.
|
|
82
|
+
|
|
83
|
+
## Side effects
|
|
84
|
+
|
|
85
|
+
- `create` inserts into `public.roles` after a duplicate-slug pre-check
|
|
86
|
+
scoped to `slug` + `app_id` (or `app_id IS NULL` for global roles).
|
|
87
|
+
- `remove` is a soft delete; `update`/`remove` stamp `updated_at`.
|
|
88
|
+
- `emitLog` writes `role.created` / `role.updated` / `role.deleted`
|
|
89
|
+
audit rows to `public.logs` with before/after payloads.
|
|
90
|
+
|
|
91
|
+
## Errors
|
|
92
|
+
|
|
93
|
+
| Condition | HTTP | Message |
|
|
94
|
+
|---|---|---|
|
|
95
|
+
| Missing create fields | 500 | `slug and name are required` |
|
|
96
|
+
| Non-admin creating `system_admin` | 500 | `system_admin role can only be created by system administrators` |
|
|
97
|
+
| Non-admin without create grant | 500 | `Insufficient permissions to create roles` |
|
|
98
|
+
| Duplicate slug for app scope | 500 | `Role slug already exists for this app` |
|
|
99
|
+
| Missing/inaccessible row | 500 | `Role not found or access denied` |
|
|
100
|
+
| Missing `id` | 500 | `Role ID is required` |
|
|
101
|
+
| Unknown method | 500 | `Unsupported method: <METHOD>` |
|
|
102
|
+
|
|
103
|
+
Validation failures surface as HTTP 500 — thrown `Error`s without a
|
|
104
|
+
`statusCode` map to 500 in the shared error path.
|
|
105
|
+
|
|
106
|
+
## Agent notes
|
|
107
|
+
|
|
108
|
+
- `update` and `remove` read `id` from the **body** only — a
|
|
109
|
+
`?id=` query param is not consulted (unlike most sibling functions).
|
|
110
|
+
- The pre-check scopes slug uniqueness to `app_id`, but the database
|
|
111
|
+
enforces a **global** unique index on `roles(slug)`
|
|
112
|
+
(`roles_slug_key`, `.framework/migrations/009_roles_slug_unique.sql`)
|
|
113
|
+
— a slug already used by another app passes the pre-check then fails
|
|
114
|
+
the insert. The pre-check also ignores `is_active`, so soft-deleted
|
|
115
|
+
slugs still conflict.
|
|
116
|
+
- `update` accepts arbitrary columns (no allowlist): `slug`,
|
|
117
|
+
`is_system`, `is_protected`, `permissions` can all be rewritten by a
|
|
118
|
+
caller that passes `validateUpdatePermissions`.
|
|
119
|
+
- Role `permissions` is a free-form JSON column — the grant semantics
|
|
120
|
+
live in `design_schema.record_permissions` matching on role **slug**,
|
|
121
|
+
not in this table.
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: security
|
|
3
|
+
audience: both
|
|
4
|
+
kind: reference
|
|
5
|
+
verified_against: f69c37d
|
|
6
|
+
verified_on: 2026-10-30
|
|
7
|
+
source_files:
|
|
8
|
+
- .framework/functions/security.ts
|
|
9
|
+
- .framework/functions/_shared/middleware.ts
|
|
10
|
+
- .framework/functions/_shared/audit.ts
|
|
11
|
+
- .framework/public/_redirects
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# security
|
|
15
|
+
|
|
16
|
+
## What it does
|
|
17
|
+
|
|
18
|
+
Security-admin console API: aggregates abuse signals into a
|
|
19
|
+
"flagged accounts" report, bans/unbans people across three enforcement
|
|
20
|
+
layers (the `security_blocklist` table, Supabase auth ban, and the Netlify
|
|
21
|
+
edge firewall), and manages blocklist entries directly.
|
|
22
|
+
|
|
23
|
+
## Route and methods
|
|
24
|
+
|
|
25
|
+
`/.netlify/functions/security`, reachable as `/api/security` (`/api/*` →
|
|
26
|
+
`/.netlify/functions/:splat`). No method check — the action comes from
|
|
27
|
+
`?action=` or `body.action`:
|
|
28
|
+
|
|
29
|
+
| `action` | Purpose |
|
|
30
|
+
|---|---|
|
|
31
|
+
| `flagged_accounts` | 30-day rollup of security signals per person/account |
|
|
32
|
+
| `ban_user` | Blocklist + Supabase ban + edge IP deny |
|
|
33
|
+
| `unban_user` | Revoke blocklist rows + lift Supabase ban + remove edge rule |
|
|
34
|
+
| `blocklist` | List `security_blocklist` rows (latest 500) |
|
|
35
|
+
| `blocklist_add` | Upsert a blocklist entry |
|
|
36
|
+
| `blocklist_revoke` | Stamp `revoked_at` on entries |
|
|
37
|
+
|
|
38
|
+
Unknown action → `Unknown security action: <action>` (HTTP 400).
|
|
39
|
+
|
|
40
|
+
## Authentication
|
|
41
|
+
|
|
42
|
+
Standard `createHandler` plus `requireSystemContextWithAudit` — only
|
|
43
|
+
`system_admin` principals (human `system_admin` role, `*:*` machine key,
|
|
44
|
+
or internal secret) get in; anonymous → 401. All DB access uses `adminDb`.
|
|
45
|
+
|
|
46
|
+
## Request contract
|
|
47
|
+
|
|
48
|
+
**`flagged_accounts`** — no parameters. Scans `logs` for `span_name` in
|
|
49
|
+
`security.rate_limit_exceeded`, `security.injection_detected`,
|
|
50
|
+
`security.registration_blocked` over the last 30 days (limit 500), buckets
|
|
51
|
+
by `person_id` (falling back to account), and joins `people`, `accounts`,
|
|
52
|
+
and active blocklist rows.
|
|
53
|
+
|
|
54
|
+
**`ban_user`** (body): at least one of `person_id`, `email`, `ip`
|
|
55
|
+
required. Optional: `reason`, `block_ip` (default `true`), `block_email`
|
|
56
|
+
(default `true`).
|
|
57
|
+
|
|
58
|
+
**`unban_user`** (body): `person_id` required.
|
|
59
|
+
|
|
60
|
+
**`blocklist`** — no parameters.
|
|
61
|
+
|
|
62
|
+
**`blocklist_add`** (body): `type` ∈ `ip | email | email_domain |
|
|
63
|
+
person_id | account_id` and `value` required; optional `severity`
|
|
64
|
+
(default `'hard'`), `reason`, `expires_at`. Email and email-domain values
|
|
65
|
+
are lowercased.
|
|
66
|
+
|
|
67
|
+
**`blocklist_revoke`** (body): `id` *or* (`type`, `value`).
|
|
68
|
+
|
|
69
|
+
## Response contract
|
|
70
|
+
|
|
71
|
+
Standard `{ data, error, meta }` envelope.
|
|
72
|
+
|
|
73
|
+
- `flagged_accounts` → `{ rows: [...] }`, each row
|
|
74
|
+
`{ person_id, email, account_id, account_name, last_ip, last_country,
|
|
75
|
+
person_status, signals, kinds, patterns, last_seen_at, banned }`
|
|
76
|
+
sorted by signal count descending. `banned` is true when any matching
|
|
77
|
+
person/email/IP/account blocklist entry is active.
|
|
78
|
+
- `ban_user` → `{ ok: true, person_id, email, ip, layers }` where `layers`
|
|
79
|
+
reports `blocklist` / `auth` / `edge` as `applied`, `skipped`, or
|
|
80
|
+
`error: <msg>` per layer.
|
|
81
|
+
- `unban_user` → `{ ok: true, person_id, layers }`.
|
|
82
|
+
- `blocklist` → `{ rows: [...] }`.
|
|
83
|
+
- `blocklist_add` → `{ ok: true }`; `blocklist_revoke` → `{ ok: true,
|
|
84
|
+
revoked: <n> }`.
|
|
85
|
+
|
|
86
|
+
## Side effects
|
|
87
|
+
|
|
88
|
+
- `security_blocklist` upserts keyed on `(type, value)` with
|
|
89
|
+
`banned_by`, `severity`, `reason`, `expires_at`; revoke stamps
|
|
90
|
+
`revoked_at` (rows are never deleted).
|
|
91
|
+
- `ban_user`/`unban_user` call `auth.admin.updateUserById` with
|
|
92
|
+
`ban_duration: '876600h'` (~100 years) / `'none'` when the person has an
|
|
93
|
+
`auth_uid`.
|
|
94
|
+
- **Netlify edge sync**: when `NETLIFY_ACCESS_TOKEN` + `NETLIFY_SITE_ID`
|
|
95
|
+
are set, IP entries are added to/removed from a managed
|
|
96
|
+
`spine-security:blocklist` deny rule on the site's firewall ruleset.
|
|
97
|
+
Without those env vars the edge layer reports `skipped`. Failures are
|
|
98
|
+
reported in `layers.edge`, not thrown.
|
|
99
|
+
- `emitAudit` writes `security.user_banned`, `security.user_unbanned`,
|
|
100
|
+
`security.blocklist_add`, `security.blocklist_revoked` audit rows.
|
|
101
|
+
- The blocklist is enforced elsewhere: `isBlockedRequest`/`isBlockedIp`
|
|
102
|
+
in the request middleware and `checkRegistrationBlocklist` during
|
|
103
|
+
registration read `security_blocklist` — this endpoint only writes it.
|
|
104
|
+
|
|
105
|
+
## Errors
|
|
106
|
+
|
|
107
|
+
| Condition | HTTP | Message |
|
|
108
|
+
|---|---|---|
|
|
109
|
+
| Unknown action | 400 | `Unknown security action: <action>` |
|
|
110
|
+
| Missing ban target | 500 | `person_id, email, or ip is required` |
|
|
111
|
+
| Person not found | 500 | `Person not found: <id>` |
|
|
112
|
+
| Missing blocklist fields | 500 | `type and value are required` / `id or (type, value) is required` |
|
|
113
|
+
| Invalid `type` | 500 | `invalid type: <type>` |
|
|
114
|
+
| Blocklist write failures | 500 | `security_blocklist upsert failed: …` etc. |
|
|
115
|
+
|
|
116
|
+
The system-admin denial is returned as the handler result, so it lands
|
|
117
|
+
inside `data` (a `{ statusCode, headers, body }` object) with HTTP 200 —
|
|
118
|
+
not as a real 403. See "Agent notes".
|
|
119
|
+
|
|
120
|
+
## Agent notes
|
|
121
|
+
|
|
122
|
+
- Banning by bare `email` works even when no `people` row exists — the
|
|
123
|
+
email/domain still lands on the blocklist and blocks future
|
|
124
|
+
registrations via `checkRegistrationBlocklist`.
|
|
125
|
+
- `ban_user` also blocklists the person's whole `account_id` — banning a
|
|
126
|
+
person can lock out their entire account. Check `layers` in the
|
|
127
|
+
response to confirm what was applied.
|
|
128
|
+
- `unban_user` revokes only the rows matching `person_id`, the person's
|
|
129
|
+
current email, and their current IP — the `account_id` row written by
|
|
130
|
+
`ban_user` is **not** revoked, and neither are manually added
|
|
131
|
+
`email_domain` entries. Revoke those separately via `blocklist_revoke`.
|
|
132
|
+
- Per-layer results mean "success" is conditional: `layers.edge:
|
|
133
|
+
'skipped'` just means Netlify env vars are absent, while
|
|
134
|
+
`layers.auth: 'skipped'` means the person has no `auth_uid` (e.g. an
|
|
135
|
+
unregistered invite stub).
|
|
136
|
+
- Denial quirk: a non-system-admin caller gets HTTP 200 with the denial
|
|
137
|
+
response object nested under `data`, because
|
|
138
|
+
`requireSystemContextWithAudit(ctx)` returns a response-shaped value
|
|
139
|
+
that the outer envelope wraps. Check `data.statusCode === 403` rather
|
|
140
|
+
than the HTTP status.
|