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