spine-framework 0.4.3 → 0.5.0-rc.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (320) 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/agent-runner.ts +1000 -81
  15. package/.framework/functions/_shared/cil.ts +2 -1
  16. package/.framework/functions/_shared/db.ts +3 -6
  17. package/.framework/functions/_shared/hash-api-key.ts +183 -79
  18. package/.framework/functions/_shared/kb.ts +61 -7
  19. package/.framework/functions/_shared/logging.ts +562 -224
  20. package/.framework/functions/_shared/message-sequence.ts +53 -0
  21. package/.framework/functions/_shared/middleware.ts +47 -44
  22. package/.framework/functions/_shared/pipeline-runner.ts +150 -37
  23. package/.framework/functions/_shared/principal.ts +9 -6
  24. package/.framework/functions/_shared/trigger-engine.ts +149 -40
  25. package/.framework/functions/admin-data.ts +48 -31
  26. package/.framework/functions/ai-agents.ts +103 -33
  27. package/.framework/functions/api-keys.ts +301 -122
  28. package/.framework/functions/apps.ts +43 -26
  29. package/.framework/functions/cil-ingest.ts +48 -15
  30. package/.framework/functions/debug-auth.ts +14 -8
  31. package/.framework/functions/deliveries.ts +39 -12
  32. package/.framework/functions/embeddings.ts +145 -76
  33. package/.framework/functions/integration-outbound.ts +67 -27
  34. package/.framework/functions/integrations.ts +254 -127
  35. package/.framework/functions/logs.ts +273 -133
  36. package/.framework/functions/observability.ts +277 -147
  37. package/.framework/functions/pipeline-executions.ts +1 -1
  38. package/.framework/functions/pipeline-runner-background.ts +120 -0
  39. package/.framework/functions/pipelines.ts +14 -9
  40. package/.framework/functions/prompt-configs.ts +7 -4
  41. package/.framework/functions/roles.ts +8 -5
  42. package/.framework/functions/security.ts +30 -17
  43. package/.framework/functions/system-cron.ts +250 -128
  44. package/.framework/functions/tests.ts +7 -6
  45. package/.framework/functions/timers.ts +310 -107
  46. package/.framework/functions/trigger-scheduler.ts +65 -5
  47. package/.framework/functions/triggers.ts +28 -13
  48. package/.framework/functions/types.ts +13 -10
  49. package/.framework/migrations/064_episode_leases.sql +196 -0
  50. package/.framework/migrations/065_execution_tables_updated_at.sql +20 -0
  51. package/.framework/migrations/066_episode_steal_thread_merge.sql +148 -0
  52. package/.framework/migrations/067_episode_lease_stale_steal.sql +93 -0
  53. package/.framework/migrations/068_cron_schedule_timer_rpcs.sql +251 -0
  54. package/.framework/migrations/069_log_stats_window.sql +54 -0
  55. package/.framework/migrations/070_app_type_schema_rpcs.sql +166 -0
  56. package/.framework/migrations/071_timer_interval_keys.sql +96 -0
  57. package/.framework/migrations/072_slug_autofill.sql +77 -0
  58. package/.framework/migrations/073_seed_member_roles.sql +26 -0
  59. package/.framework/schemas/spine.config.schema.json +62 -0
  60. package/.framework/src/App.tsx +3 -1
  61. package/.framework/src/apps/admin/index.tsx +1141 -775
  62. package/.framework/src/components/ErrorBoundary.tsx +136 -0
  63. package/.framework/src/components/admin/AdminListPage.tsx +81 -73
  64. package/.framework/src/components/admin/AdminSidebar.tsx +92 -81
  65. package/.framework/src/components/admin/EntitySelect.tsx +181 -0
  66. package/.framework/src/components/admin/PageInstructions.tsx +86 -127
  67. package/.framework/src/components/app-shell/GenericAppShell.tsx +3 -1
  68. package/.framework/src/components/app-shell/GenericDetailPage.tsx +37 -13
  69. package/.framework/src/components/app-shell/GenericListPage.tsx +28 -32
  70. package/.framework/src/components/auth/TurnstileWidget.tsx +9 -6
  71. package/.framework/src/components/kb/KBArticleEditor.tsx +49 -6
  72. package/.framework/src/components/layout/Layout.tsx +4 -2
  73. package/.framework/src/components/layout/Sidebar.tsx +304 -179
  74. package/.framework/src/components/runtime/DataDetailPage.tsx +67 -35
  75. package/.framework/src/components/runtime/DataHeader.tsx +96 -48
  76. package/.framework/src/components/runtime/DataListPage.tsx +3 -26
  77. package/.framework/src/components/runtime/DataStats.tsx +10 -10
  78. package/.framework/src/components/runtime/DataTable.tsx +12 -10
  79. package/.framework/src/components/ui/sheet.tsx +24 -26
  80. package/.framework/src/components/ui/textarea.tsx +74 -48
  81. package/.framework/src/hooks/useBranding.ts +47 -0
  82. package/.framework/src/hooks/useEntityList.ts +12 -9
  83. package/.framework/src/hooks/useListSchema.ts +108 -58
  84. package/.framework/src/instructions/README.md +57 -0
  85. package/.framework/src/instructions/ai-agents.md +69 -0
  86. package/.framework/src/instructions/alerts.md +80 -0
  87. package/.framework/src/instructions/api-keys.md +87 -0
  88. package/.framework/src/instructions/apps.md +77 -0
  89. package/.framework/src/instructions/concepts/ai-and-prompts.md +150 -0
  90. package/.framework/src/instructions/concepts/apps-and-renderers.md +162 -0
  91. package/.framework/src/instructions/concepts/auth-and-api-keys.md +133 -0
  92. package/.framework/src/instructions/concepts/automation.md +186 -0
  93. package/.framework/src/instructions/concepts/concurrency-and-idempotency.md +125 -0
  94. package/.framework/src/instructions/concepts/core-vs-custom.md +124 -0
  95. package/.framework/src/instructions/concepts/design-schema.md +152 -0
  96. package/.framework/src/instructions/concepts/integrations-and-webhooks.md +127 -0
  97. package/.framework/src/instructions/concepts/observability.md +116 -0
  98. package/.framework/src/instructions/concepts/permissions.md +150 -0
  99. package/.framework/src/instructions/concepts/runtime-records.md +111 -0
  100. package/.framework/src/instructions/concepts/scope-and-accounts.md +112 -0
  101. package/.framework/src/instructions/concepts/spine-way.md +172 -0
  102. package/.framework/src/instructions/deliveries.md +59 -0
  103. package/.framework/src/instructions/guide/00-orientation.md +106 -0
  104. package/.framework/src/instructions/guide/01-install-and-first-run.md +253 -0
  105. package/.framework/src/instructions/guide/02-project-map.md +147 -0
  106. package/.framework/src/instructions/guide/03-domain-model.md +223 -0
  107. package/.framework/src/instructions/guide/04-first-app.md +227 -0
  108. package/.framework/src/instructions/guide/05-glossary.md +183 -0
  109. package/.framework/src/instructions/integrations.md +96 -0
  110. package/.framework/src/instructions/internal/cil-markers.md +83 -0
  111. package/.framework/src/instructions/invites.md +60 -0
  112. package/.framework/src/instructions/kb.md +80 -0
  113. package/.framework/src/instructions/logs.md +77 -0
  114. package/.framework/src/instructions/observability.md +61 -0
  115. package/.framework/src/instructions/operations/deployment.md +115 -0
  116. package/.framework/src/instructions/operations/migrations.md +125 -0
  117. package/.framework/src/instructions/operations/security-checklist.md +148 -0
  118. package/.framework/src/instructions/operations/troubleshooting.md +170 -0
  119. package/.framework/src/instructions/operations/upgrade.md +116 -0
  120. package/.framework/src/instructions/pipeline-executions.md +62 -0
  121. package/.framework/src/instructions/pipelines.md +78 -0
  122. package/.framework/src/instructions/prompts.md +95 -0
  123. package/.framework/src/instructions/reference/api/admin-data.md +162 -0
  124. package/.framework/src/instructions/reference/api/auth.md +113 -0
  125. package/.framework/src/instructions/reference/api/errors-and-envelopes.md +113 -0
  126. package/.framework/src/instructions/reference/app-manifest.md +150 -0
  127. package/.framework/src/instructions/reference/cli.md +124 -0
  128. package/.framework/src/instructions/reference/database.md +132 -0
  129. package/.framework/src/instructions/reference/design-schema.md +280 -0
  130. package/.framework/src/instructions/reference/env-vars.md +133 -0
  131. package/.framework/src/instructions/reference/functions/account-nodes.md +75 -0
  132. package/.framework/src/instructions/reference/functions/admin-data.md +195 -0
  133. package/.framework/src/instructions/reference/functions/admin-funnel.md +90 -0
  134. package/.framework/src/instructions/reference/functions/admin-install-funnel.md +107 -0
  135. package/.framework/src/instructions/reference/functions/admin-signals.md +102 -0
  136. package/.framework/src/instructions/reference/functions/ai-agents.md +148 -0
  137. package/.framework/src/instructions/reference/functions/ai-interactions.md +156 -0
  138. package/.framework/src/instructions/reference/functions/api-keys.md +130 -0
  139. package/.framework/src/instructions/reference/functions/apps.md +140 -0
  140. package/.framework/src/instructions/reference/functions/auth.md +99 -0
  141. package/.framework/src/instructions/reference/functions/debug-auth.md +91 -0
  142. package/.framework/src/instructions/reference/functions/deliveries.md +110 -0
  143. package/.framework/src/instructions/reference/functions/embeddings.md +166 -0
  144. package/.framework/src/instructions/reference/functions/eval-harness.md +149 -0
  145. package/.framework/src/instructions/reference/functions/index.md +103 -0
  146. package/.framework/src/instructions/reference/functions/integration-outbound.md +200 -0
  147. package/.framework/src/instructions/reference/functions/integration-routes.md +151 -0
  148. package/.framework/src/instructions/reference/functions/integrations.md +130 -0
  149. package/.framework/src/instructions/reference/functions/invites.md +182 -0
  150. package/.framework/src/instructions/reference/functions/item-progress.md +107 -0
  151. package/.framework/src/instructions/reference/functions/kb.md +166 -0
  152. package/.framework/src/instructions/reference/functions/logs.md +141 -0
  153. package/.framework/src/instructions/reference/functions/observability.md +105 -0
  154. package/.framework/src/instructions/reference/functions/pipeline-executions.md +132 -0
  155. package/.framework/src/instructions/reference/functions/pipeline-runner-background.md +121 -0
  156. package/.framework/src/instructions/reference/functions/pipelines.md +136 -0
  157. package/.framework/src/instructions/reference/functions/prompt-configs.md +121 -0
  158. package/.framework/src/instructions/reference/functions/roles.md +121 -0
  159. package/.framework/src/instructions/reference/functions/security.md +140 -0
  160. package/.framework/src/instructions/reference/functions/system-cron.md +136 -0
  161. package/.framework/src/instructions/reference/functions/system.md +106 -0
  162. package/.framework/src/instructions/reference/functions/tests.md +97 -0
  163. package/.framework/src/instructions/reference/functions/timers.md +117 -0
  164. package/.framework/src/instructions/reference/functions/trigger-scheduler.md +102 -0
  165. package/.framework/src/instructions/reference/functions/triggers.md +140 -0
  166. package/.framework/src/instructions/reference/functions/types.md +146 -0
  167. package/.framework/src/instructions/reference/seeds.md +126 -0
  168. package/.framework/src/instructions/reference/shared-modules.md +127 -0
  169. package/.framework/src/instructions/reference/spine-config.md +114 -0
  170. package/.framework/src/instructions/roles.md +80 -0
  171. package/.framework/src/instructions/runtime-detail.md +70 -0
  172. package/.framework/src/instructions/runtime-list.md +77 -0
  173. package/.framework/src/instructions/security.md +67 -0
  174. package/.framework/src/instructions/tasks/add-a-pipeline.md +110 -0
  175. package/.framework/src/instructions/tasks/add-a-role.md +96 -0
  176. package/.framework/src/instructions/tasks/add-a-timer.md +114 -0
  177. package/.framework/src/instructions/tasks/add-a-trigger.md +106 -0
  178. package/.framework/src/instructions/tasks/add-a-type.md +151 -0
  179. package/.framework/src/instructions/tasks/add-a-webhook.md +108 -0
  180. package/.framework/src/instructions/tasks/add-an-ai-agent.md +108 -0
  181. package/.framework/src/instructions/tasks/add-an-integration.md +124 -0
  182. package/.framework/src/instructions/tasks/build-a-custom-page.md +118 -0
  183. package/.framework/src/instructions/tasks/package-and-install-an-app.md +127 -0
  184. package/.framework/src/instructions/tasks/test-your-code.md +108 -0
  185. package/.framework/src/instructions/tasks/write-a-function.md +132 -0
  186. package/.framework/src/instructions/tasks/write-a-migration.md +110 -0
  187. package/.framework/src/instructions/testing.md +59 -0
  188. package/.framework/src/instructions/timers.md +82 -0
  189. package/.framework/src/instructions/triggers.md +69 -0
  190. package/.framework/src/instructions/types.md +93 -0
  191. package/.framework/src/lib/branding.ts +128 -0
  192. package/.framework/src/pages/NoAppPage.tsx +3 -1
  193. package/.framework/src/pages/NotFoundPage.tsx +62 -31
  194. package/.framework/src/pages/SetupPage.tsx +5 -2
  195. package/.framework/src/pages/admin/AIAgentDetailPage.tsx +117 -106
  196. package/.framework/src/pages/admin/AIAgentsPage.tsx +86 -48
  197. package/.framework/src/pages/admin/APIKeyDetailPage.tsx +153 -152
  198. package/.framework/src/pages/admin/APIKeysPage.tsx +91 -90
  199. package/.framework/src/pages/admin/AlertsConfigPage.tsx +29 -18
  200. package/.framework/src/pages/admin/AppDetailPage.tsx +6 -60
  201. package/.framework/src/pages/admin/AppsPage.tsx +104 -66
  202. package/.framework/src/pages/admin/DeliveriesPage.tsx +5 -5
  203. package/.framework/src/pages/admin/EmbeddingDetailPage.tsx +5 -23
  204. package/.framework/src/pages/admin/EmbeddingsPage.tsx +5 -26
  205. package/.framework/src/pages/admin/IntegrationDetailPage.tsx +7 -7
  206. package/.framework/src/pages/admin/IntegrationsPage.tsx +85 -43
  207. package/.framework/src/pages/admin/LogsPage.tsx +144 -76
  208. package/.framework/src/pages/admin/ObservabilityDashboard.tsx +223 -140
  209. package/.framework/src/pages/admin/PipelineDetailPage.tsx +134 -113
  210. package/.framework/src/pages/admin/PipelineExecutionsPage.tsx +108 -45
  211. package/.framework/src/pages/admin/PipelinesPage.tsx +160 -115
  212. package/.framework/src/pages/admin/PromptConfigDetailPage.tsx +96 -98
  213. package/.framework/src/pages/admin/PromptConfigsPage.tsx +101 -59
  214. package/.framework/src/pages/admin/RoleDetailPage.tsx +90 -79
  215. package/.framework/src/pages/admin/RolesPage.tsx +86 -43
  216. package/.framework/src/pages/admin/SecurityPage.tsx +1 -1
  217. package/.framework/src/pages/admin/TestingDashboard.tsx +16 -28
  218. package/.framework/src/pages/admin/TimerDetailPage.tsx +117 -100
  219. package/.framework/src/pages/admin/TimersPage.tsx +90 -62
  220. package/.framework/src/pages/admin/TriggerDetailPage.tsx +90 -83
  221. package/.framework/src/pages/admin/TriggersPage.tsx +124 -99
  222. package/.framework/src/pages/admin/TypeDetailPage.tsx +152 -138
  223. package/.framework/src/pages/admin/TypesPage.tsx +90 -48
  224. package/.framework/src/pages/admin/cil/CILAgentPage.tsx +17 -25
  225. package/.framework/src/pages/admin/cil/CILChunkDetailPage.tsx +17 -27
  226. package/.framework/src/pages/admin/cil/CILDashboardPage.tsx +72 -40
  227. package/.framework/src/pages/admin/cil/CILFileListPage.tsx +321 -129
  228. package/.framework/src/pages/admin/cil/CILFileSelectPage.tsx +18 -29
  229. package/.framework/src/pages/admin/cil/CILGraphPage.tsx +26 -38
  230. package/.framework/src/pages/admin/cil/CILIngestionPage.tsx +34 -46
  231. package/.framework/src/pages/admin/cil/CILProjectsPage.tsx +189 -103
  232. package/.framework/src/pages/admin/cil/CILSearchPage.tsx +246 -136
  233. package/.framework/src/pages/admin/cil/CILSuggestionsPage.tsx +11 -22
  234. package/.framework/src/pages/admin/cil/CILVersionsPage.tsx +9 -20
  235. package/.framework/src/pages/admin/cil/FlexiblePipelinePage.tsx +26 -38
  236. package/.framework/src/pages/admin/kb/KBAdminPage.tsx +7 -26
  237. package/.framework/src/pages/auth/AuthConfirmPage.tsx +3 -1
  238. package/.framework/src/pages/auth/ForgotPasswordPage.tsx +3 -1
  239. package/.framework/src/pages/auth/LoginPage.tsx +4 -2
  240. package/.framework/src/pages/auth/RegisterPage.tsx +3 -1
  241. package/.framework/templates/agent/AGENTS.md +150 -0
  242. package/.framework/templates/agent/skills/spine-add-ai-agent/SKILL.md +58 -0
  243. package/.framework/templates/agent/skills/spine-add-automation/SKILL.md +62 -0
  244. package/.framework/templates/agent/skills/spine-add-integration/SKILL.md +63 -0
  245. package/.framework/templates/agent/skills/spine-architect/SKILL.md +86 -0
  246. package/.framework/templates/agent/skills/spine-install-app/SKILL.md +72 -0
  247. package/.framework/templates/agent/skills/spine-migration/SKILL.md +75 -0
  248. package/.framework/templates/agent/skills/spine-new-app/SKILL.md +67 -0
  249. package/.framework/templates/agent/skills/spine-new-function/SKILL.md +65 -0
  250. package/.framework/templates/agent/skills/spine-new-type/SKILL.md +65 -0
  251. package/.framework/templates/agent/skills/spine-schema/SKILL.md +77 -0
  252. package/.framework/templates/agent/skills/spine-test/SKILL.md +67 -0
  253. package/.framework/templates/agent/skills/spine-troubleshoot/SKILL.md +74 -0
  254. package/.framework/templates/agent/skills/spine-upgrade/SKILL.md +69 -0
  255. package/.framework/templates/project.json +5 -1
  256. package/.framework/tests/unit/account-scoping-gates.test.ts +392 -0
  257. package/.framework/tests/unit/agent-files.test.ts +355 -0
  258. package/.framework/tests/unit/agent-runner.test.ts +496 -47
  259. package/.framework/tests/unit/api-keys-create-scope-gate.test.ts +160 -0
  260. package/.framework/tests/unit/api-keys-omit-secret-fields.test.ts +38 -0
  261. package/.framework/tests/unit/bootstrap.test.ts +242 -0
  262. package/.framework/tests/unit/cli-context-api-key.test.ts +114 -0
  263. package/.framework/tests/unit/debug-auth.test.ts +38 -0
  264. package/.framework/tests/unit/deliveries-account-scoping.test.ts +185 -0
  265. package/.framework/tests/unit/episode-lease.test.ts +695 -0
  266. package/.framework/tests/unit/integration-outbound-credentials.test.ts +167 -0
  267. package/.framework/tests/unit/integrations-audit-redaction.test.ts +130 -0
  268. package/.framework/tests/unit/kb-ingest-lease.test.ts +19 -0
  269. package/.framework/tests/unit/logging-app-id.test.ts +75 -0
  270. package/.framework/tests/unit/pipeline-runner-background.test.ts +120 -0
  271. package/.framework/tests/unit/pipeline-runner.test.ts +153 -1
  272. package/.framework/tests/unit/principal-cache.test.ts +15 -0
  273. package/.framework/tests/unit/system-context-denial.test.ts +145 -0
  274. package/.framework/tests/unit/system-cron.test.ts +240 -0
  275. package/.framework/tests/unit/timers.test.ts +159 -0
  276. package/.framework/tests/unit/trigger-engine.test.ts +278 -91
  277. package/.framework/tests/unit/trigger-scheduler.test.ts +78 -0
  278. package/README.md +125 -166
  279. package/config/tsconfig.cli.json +2 -2
  280. package/netlify.toml +5 -0
  281. package/package.json +15 -4
  282. package/templates/project/package-lock.json +2 -2
  283. package/.framework/docs/admin-routes.md +0 -76
  284. package/.framework/docs/api-docs-progress.md +0 -38
  285. package/.framework/docs/api-governance.md +0 -146
  286. package/.framework/docs/api-testing-results.md +0 -212
  287. package/.framework/docs/assembly-launch-guide.md +0 -138
  288. package/.framework/docs/audit-results.md +0 -590
  289. package/.framework/docs/db-api-inventory.md +0 -95
  290. package/.framework/docs/gap-remediation-backlog.md +0 -103
  291. package/.framework/docs/ui-adoption-verification.md +0 -111
  292. package/.framework/docs/ui-api-coverage.md +0 -84
  293. package/.framework/docs/v2-compatibility-audit.md +0 -228
  294. package/.framework/mcp/index.ts +0 -133
  295. package/.framework/mcp/load-env.ts +0 -60
  296. package/.framework/mcp/shared/app-manifest.ts +0 -275
  297. package/.framework/mcp/shared/db.ts +0 -477
  298. package/.framework/mcp/shared/kb.ts +0 -118
  299. package/.framework/mcp/tools/architect.ts +0 -431
  300. package/.framework/mcp/tools/cil/context.ts +0 -345
  301. package/.framework/mcp/tools/cil/graph.ts +0 -242
  302. package/.framework/mcp/tools/cil/ingest.ts +0 -1217
  303. package/.framework/mcp/tools/cil/pipeline-orchestrator.ts +0 -693
  304. package/.framework/mcp/tools/cil/pipeline.ts +0 -767
  305. package/.framework/mcp/tools/cil/project.ts +0 -791
  306. package/.framework/mcp/tools/cil/retrieval.ts +0 -336
  307. package/.framework/mcp/tools/cil/search-core.ts +0 -454
  308. package/.framework/mcp/tools/cil/search.ts +0 -97
  309. package/.framework/mcp/tools/cil/suggest.ts +0 -186
  310. package/.framework/mcp/tools/cil/validate.ts +0 -1369
  311. package/.framework/mcp/tools/kb/delete.ts +0 -139
  312. package/.framework/mcp/tools/kb/index.ts +0 -52
  313. package/.framework/mcp/tools/kb/ingest.ts +0 -84
  314. package/.framework/mcp/tools/kb/list-types.ts +0 -102
  315. package/.framework/mcp/tools/kb/publish.ts +0 -100
  316. package/.framework/mcp/tools/kb/reindex.ts +0 -86
  317. package/.framework/mcp/tools/kb/revoke.ts +0 -85
  318. package/.framework/mcp/tools/kb/search.ts +0 -101
  319. package/.framework/mcp/tools/kb/upsert.ts +0 -125
  320. package/.framework/mcp/tools/observability.ts +0 -510
@@ -0,0 +1,120 @@
1
+ // CIL[BEGIN stable_id="~/.framework/functions/pipeline-runner-background.ts:module_header:module_header" type="module_header" identity="module_header" parent="~/.framework/functions/pipeline-runner-background.ts"]
2
+ // CIL[DOC-MACRO purpose="Background entry point for event-triggered pipeline executions — receives dispatched payloads from checkAndFireTriggers and runs the pipeline in an isolated process with its own connection budget"]
3
+ // CIL[DOC-RESPONSIBILITY responsibility="Authenticate internal dispatch calls via INTERNAL_INVOKE_SECRET, reconstruct a machine RequestContext, and invoke runPipeline with the pre-created execution_id so the queued row transitions to running/completed rather than being duplicated"]
4
+ // CIL[DOC-CONTEXT module="pipeline-runner-background" layer="functions" related_files=".framework/functions/_shared/trigger-engine.ts,.framework/functions/_shared/pipeline-runner.ts,.framework/functions/_shared/principal.ts,.framework/functions/_shared/middleware.ts"]
5
+ // CIL[DOC-CONTRACT inputs="POST body { pipeline_id, trigger_data, account_id, execution_id, trigger_id } with X-Internal-Secret header" outputs="{ ok: true, execution_id } | { error }" errors="403 on missing/wrong secret, 400 on missing pipeline_id, 500 on pipeline failure" side_effects="Runs the named pipeline; updates pipeline_executions row from queued→running→terminal"]
6
+ // CIL[DOC-MICRO logic_summary="Validates the shared internal secret (timing-safe, same as principal.isInternalInvokeAuthorized), builds a SYSTEM_PRINCIPAL RequestContext carrying the original account scope, then calls runPipeline with the caller's pre-created execution_id. Status 'queued' → 'running' is handled inside runPipeline via the new opts.executionId parameter." notable_decisions="Does NOT use createHandler — it would resolve the secret to anonymous and 401 before reaching the handler. Auth is checked inline and SYSTEM_PRINCIPAL provides the machine context directly."]
7
+ // CIL[DOC-CONFIDENCE pass="first" score="0.9" flags="none"]
8
+ // CIL[DOC-STABILITY stability="stable"]
9
+ // CIL[DOC-AUDIENCE audience="framework"]
10
+
11
+ import { createHash, timingSafeEqual } from 'node:crypto'
12
+ import { runPipeline, sweepStaleExecutions } from './_shared/pipeline-runner'
13
+ import { SYSTEM_PRINCIPAL } from './_shared/principal'
14
+ import { adminDb } from './_shared/db'
15
+ // CIL[END stable_id="~/.framework/functions/pipeline-runner-background.ts:module_header:module_header"]
16
+
17
+
18
+ // CIL[BEGIN stable_id="~/.framework/functions/pipeline-runner-background.ts:isdispatchauthorized:function" type="function" identity="isDispatchAuthorized" parent="~/.framework/functions/pipeline-runner-background.ts"]
19
+ // CIL[DOC-MACRO purpose="Verify the X-Internal-Secret header using timing-safe comparison (mirrors principal.isInternalInvokeAuthorized)"]
20
+ // CIL[DOC-RESPONSIBILITY responsibility="Return true only when the request carries a valid INTERNAL_INVOKE_SECRET"]
21
+ // CIL[DOC-CONTEXT module="pipeline-runner-background" layer="functions" related_files=".framework/functions/_shared/principal.ts"]
22
+ // CIL[DOC-CONTRACT inputs="event headers" outputs="boolean" errors="none" side_effects="none"]
23
+ // CIL[DOC-MICRO logic_summary="SHA-256 both sides before timingSafeEqual so early-exit on length mismatch cannot leak the expected secret length via response timing — identical pattern to principal.isInternalInvokeAuthorized" notable_decisions="duplicated here rather than imported because isInternalInvokeAuthorized is unexported from principal.ts"]
24
+ // CIL[DOC-CONFIDENCE pass="first" score="0.9" flags="none"]
25
+ // CIL[DOC-STABILITY stability="stable"]
26
+ function isDispatchAuthorized(event: any): boolean {
27
+ const expected = (globalThis as any).process?.env?.INTERNAL_INVOKE_SECRET
28
+ if (!expected) return false
29
+
30
+ const provided = event.headers?.['x-internal-secret'] || event.headers?.['X-Internal-Secret']
31
+ if (!provided || typeof provided !== 'string') return false
32
+
33
+ const expectedHash = createHash('sha256').update(expected).digest()
34
+ const providedHash = createHash('sha256').update(provided).digest()
35
+
36
+ try {
37
+ return timingSafeEqual(expectedHash, providedHash)
38
+ } catch {
39
+ return false
40
+ }
41
+ }
42
+ // CIL[END stable_id="~/.framework/functions/pipeline-runner-background.ts:isdispatchauthorized:function"]
43
+
44
+
45
+ // CIL[BEGIN stable_id="~/.framework/functions/pipeline-runner-background.ts:handler:constant" type="constant" identity="handler" parent="~/.framework/functions/pipeline-runner-background.ts"]
46
+ // CIL[DOC-MACRO purpose="Netlify function handler for dispatched pipeline executions"]
47
+ // CIL[DOC-RESPONSIBILITY responsibility="Authenticate, sweep stale executions, then runPipeline with the caller's pre-created execution_id"]
48
+ // CIL[DOC-CONTEXT module="pipeline-runner-background" layer="functions" related_files=".framework/functions/_shared/pipeline-runner.ts"]
49
+ // CIL[DOC-CONTRACT inputs="POST body { pipeline_id, trigger_data, account_id, execution_id, trigger_id }" outputs="{ ok, execution_id, status } | { error }" errors="403 unauthorized, 400 missing pipeline_id" side_effects="Pipeline runs; pipeline_executions transitions queued→running→terminal"]
50
+ // CIL[DOC-MICRO logic_summary="Reject without INTERNAL_INVOKE_SECRET; build SYSTEM_PRINCIPAL ctx scoped to the dispatching account; runPipeline with opts.executionId so the pre-created row is claimed rather than duplicated; return the final ExecutionResult" notable_decisions="sweepStaleExecutions is called opportunistically on entry — same fire-and-forget pattern as runPipeline itself, but on a fresh process it costs nothing"]
51
+ // CIL[DOC-CONFIDENCE pass="first" score="0.9" flags="none"]
52
+ // CIL[DOC-STABILITY stability="stable"]
53
+ export const handler = async (event: any, _context: any) => {
54
+ const requestId = crypto.randomUUID()
55
+
56
+ if (!isDispatchAuthorized(event)) {
57
+ return {
58
+ statusCode: 403,
59
+ body: JSON.stringify({ error: 'Forbidden — invalid internal secret' }),
60
+ }
61
+ }
62
+
63
+ let body: any = {}
64
+ try {
65
+ body = typeof event.body === 'string' ? JSON.parse(event.body || '{}') : (event.body || {})
66
+ } catch {
67
+ return { statusCode: 400, body: JSON.stringify({ error: 'Invalid JSON body' }) }
68
+ }
69
+
70
+ const { pipeline_id, trigger_data, account_id, execution_id, trigger_id } = body
71
+
72
+ if (!pipeline_id) {
73
+ return { statusCode: 400, body: JSON.stringify({ error: 'pipeline_id is required' }) }
74
+ }
75
+
76
+ // Opportunistic sweep — picks up any executions orphaned by a prior dead
77
+ // invocation before this one starts its own work.
78
+ void sweepStaleExecutions().catch(() => {})
79
+
80
+ const ctx = {
81
+ requestId,
82
+ principal: {
83
+ ...SYSTEM_PRINCIPAL,
84
+ provenance: {
85
+ ...SYSTEM_PRINCIPAL.provenance,
86
+ sourceType: 'trigger' as const,
87
+ invokedAt: new Date().toISOString(),
88
+ triggerId: trigger_id || null,
89
+ },
90
+ },
91
+ db: adminDb,
92
+ accountId: account_id || null,
93
+ appId: null,
94
+ query: {},
95
+ requestPath: '/.netlify/functions/pipeline-runner-background',
96
+ headers: event.headers || {},
97
+ }
98
+
99
+ try {
100
+ const result = await runPipeline(pipeline_id, trigger_data || {}, ctx as any, {
101
+ executionId: execution_id,
102
+ })
103
+
104
+ return {
105
+ statusCode: 200,
106
+ body: JSON.stringify({
107
+ ok: result.status === 'completed',
108
+ execution_id: result.executionId,
109
+ status: result.status,
110
+ }),
111
+ }
112
+ } catch (err: any) {
113
+ console.error(`[${requestId}] pipeline-runner-background failed for pipeline ${pipeline_id}:`, err)
114
+ return {
115
+ statusCode: 500,
116
+ body: JSON.stringify({ error: err.message || 'Pipeline execution failed', execution_id }),
117
+ }
118
+ }
119
+ }
120
+ // CIL[END stable_id="~/.framework/functions/pipeline-runner-background.ts:handler:constant"]
@@ -176,7 +176,8 @@ export const create = createHandler(async (ctx, body) => {
176
176
  throw new Error('User context (person and account) required')
177
177
  }
178
178
 
179
- const { data, error: err } = await ctx.db
179
+ const db = isSystemAdmin(ctx.principal) ? adminDb : ctx.db
180
+ const { data, error: err } = await db
180
181
  .from('pipelines')
181
182
  .insert({
182
183
  app_id: app_id || null,
@@ -187,7 +188,7 @@ export const create = createHandler(async (ctx, body) => {
187
188
  config: config || {},
188
189
  stages,
189
190
  metadata: metadata || {},
190
- created_by: ctx.principal.id
191
+ created_by: ctx.principal.type === 'human' ? ctx.principal.id : null
191
192
  })
192
193
  .select()
193
194
  .single()
@@ -228,7 +229,8 @@ export const update = createHandler(async (ctx, body) => {
228
229
  throw new Error('User context (person and account) required')
229
230
  }
230
231
 
231
- const { data: current } = await ctx.db
232
+ const db = isSystemAdmin(ctx.principal) ? adminDb : ctx.db
233
+ const { data: current } = await db
232
234
  .from('pipelines')
233
235
  .select('*')
234
236
  .eq('id', id)
@@ -248,7 +250,7 @@ export const update = createHandler(async (ctx, body) => {
248
250
  if (is_active !== undefined) updateData.is_active = is_active
249
251
  if (app_id !== undefined) updateData.app_id = app_id
250
252
 
251
- const { data, error: err } = await ctx.db
253
+ const { data, error: err } = await db
252
254
  .from('pipelines')
253
255
  .update(updateData)
254
256
  .eq('id', id)
@@ -290,7 +292,8 @@ export const toggle = createHandler(async (ctx, body) => {
290
292
  throw new Error('User context (person and account) required')
291
293
  }
292
294
 
293
- const { data: current } = await ctx.db
295
+ const db = isSystemAdmin(ctx.principal) ? adminDb : ctx.db
296
+ const { data: current } = await db
294
297
  .from('pipelines')
295
298
  .select('*')
296
299
  .eq('id', id)
@@ -300,7 +303,7 @@ export const toggle = createHandler(async (ctx, body) => {
300
303
  throw new Error('Pipeline not found')
301
304
  }
302
305
 
303
- const { data, error: err } = await ctx.db
306
+ const { data, error: err } = await db
304
307
  .from('pipelines')
305
308
  .update({ is_active, updated_at: new Date().toISOString() })
306
309
  .eq('id', id)
@@ -338,7 +341,8 @@ export const getExecutions = createHandler(async (ctx, _body) => {
338
341
  throw new Error('Pipeline ID is required')
339
342
  }
340
343
 
341
- const { data, error: err } = await ctx.db
344
+ const db = isSystemAdmin(ctx.principal) ? adminDb : ctx.db
345
+ const { data, error: err } = await db
342
346
  .from('pipeline_executions')
343
347
  .select('*')
344
348
  .eq('pipeline_id', pipeline_id)
@@ -374,7 +378,8 @@ export const remove = createHandler(async (ctx, _body) => {
374
378
  throw new Error('Pipeline ID is required')
375
379
  }
376
380
 
377
- const { data: current } = await ctx.db
381
+ const db = isSystemAdmin(ctx.principal) ? adminDb : ctx.db
382
+ const { data: current } = await db
378
383
  .from('pipelines')
379
384
  .select('id, name')
380
385
  .eq('id', id)
@@ -382,7 +387,7 @@ export const remove = createHandler(async (ctx, _body) => {
382
387
 
383
388
  if (!current) throw new Error('Pipeline not found')
384
389
 
385
- const { error: err } = await ctx.db
390
+ const { error: err } = await db
386
391
  .from('pipelines')
387
392
  .delete()
388
393
  .eq('id', id)
@@ -131,7 +131,8 @@ export const create = createHandler(async (ctx, body) => {
131
131
  throw new Error('Account context required')
132
132
  }
133
133
 
134
- const { data, error: err } = await ctx.db
134
+ const db = isSystemAdmin(ctx.principal) ? adminDb : ctx.db
135
+ const { data, error: err } = await db
135
136
  .from('prompt_configs')
136
137
  .insert({
137
138
  app_id: app_id || null,
@@ -201,7 +202,8 @@ export const update = createHandler(async (ctx, body) => {
201
202
  if (updates[key] !== undefined) updateData[key] = updates[key]
202
203
  }
203
204
 
204
- const { data, error: err } = await ctx.db
205
+ const db = isSystemAdmin(ctx.principal) ? adminDb : ctx.db
206
+ const { data, error: err } = await db
205
207
  .from('prompt_configs')
206
208
  .update(updateData)
207
209
  .eq('id', id)
@@ -239,7 +241,8 @@ export const remove = createHandler(async (ctx, _body) => {
239
241
  throw new Error('Config ID is required')
240
242
  }
241
243
 
242
- const { data: current } = await ctx.db
244
+ const db = isSystemAdmin(ctx.principal) ? adminDb : ctx.db
245
+ const { data: current } = await db
243
246
  .from('prompt_configs')
244
247
  .select('id, name, slug')
245
248
  .eq('id', id)
@@ -247,7 +250,7 @@ export const remove = createHandler(async (ctx, _body) => {
247
250
 
248
251
  if (!current) throw new Error('Prompt config not found')
249
252
 
250
- const { error: err } = await ctx.db
253
+ const { error: err } = await db
251
254
  .from('prompt_configs')
252
255
  .delete()
253
256
  .eq('id', id)
@@ -188,7 +188,8 @@ export const create = createHandler(async (ctx, body) => {
188
188
  throw new Error('Role slug already exists for this app')
189
189
  }
190
190
 
191
- const { data, error: err } = await ctx.db
191
+ const db = permissions.isSystemAdmin(ctx) ? adminDb : ctx.db
192
+ const { data, error: err } = await db
192
193
  .from('roles')
193
194
  .insert({
194
195
  app_id,
@@ -234,7 +235,8 @@ export const update = createHandler(async (ctx, body) => {
234
235
  throw new Error('User context (person and account) required')
235
236
  }
236
237
 
237
- const { data: current, error: fetchErr } = await ctx.db
238
+ const db = permissions.isSystemAdmin(ctx) ? adminDb : ctx.db
239
+ const { data: current, error: fetchErr } = await db
238
240
  .from('roles')
239
241
  .select('*')
240
242
  .eq('id', id)
@@ -255,7 +257,7 @@ export const update = createHandler(async (ctx, body) => {
255
257
  throw new Error(fieldValidation.error)
256
258
  }
257
259
 
258
- const { data, error: err } = await ctx.db
260
+ const { data, error: err } = await db
259
261
  .from('roles')
260
262
  .update({
261
263
  ...updates,
@@ -297,7 +299,8 @@ export const remove = createHandler(async (ctx, body) => {
297
299
  throw new Error('User context (person and account) required')
298
300
  }
299
301
 
300
- const { data: current, error: fetchErr } = await ctx.db
302
+ const db = permissions.isSystemAdmin(ctx) ? adminDb : ctx.db
303
+ const { data: current, error: fetchErr } = await db
301
304
  .from('roles')
302
305
  .select('*')
303
306
  .eq('id', id)
@@ -307,7 +310,7 @@ export const remove = createHandler(async (ctx, body) => {
307
310
  throw new Error('Role not found or access denied')
308
311
  }
309
312
 
310
- const { data, error: err } = await ctx.db
313
+ const { data, error: err } = await db
311
314
  .from('roles')
312
315
  .update({
313
316
  is_active: false,
@@ -1,3 +1,4 @@
1
+ // CIL[BEGIN stable_id="~/.framework/functions/security.ts:module_header:module_header" type="module_header" identity="module_header" parent="~/.framework/functions/security.ts"]
1
2
  /**
2
3
  * @module security
3
4
  * @audience internal
@@ -15,7 +16,6 @@
15
16
  *
16
17
  * @cil-generated
17
18
  */
18
- // CIL[BEGIN stable_id="~/.framework/functions/security.ts:module_header:module_header" type="module_header" identity="module_header" parent="~/.framework/functions/security.ts"]
19
19
  // CIL[DOC-MACRO purpose="System-admin security operations: flagged-actor report, ban/unban, blocklist CRUD, Netlify edge sync"]
20
20
  // CIL[DOC-RESPONSIBILITY responsibility="Aggregate security.* audit events into a flagged-accounts report; apply three-layer bans (security_blocklist + auth.admin ban_duration + Netlify firewall rule); expose blocklist list/add/revoke"]
21
21
  // CIL[DOC-CONTEXT module="functions" layer="security" related_files=".framework/functions/_shared/security.ts,.framework/migrations/049_security_blocklist.sql,.framework/src/pages/admin/SecurityPage.tsx"]
@@ -29,15 +29,19 @@ import { adminDb } from './_shared/db'
29
29
  import { emitAudit } from './_shared/audit'
30
30
  // CIL[END stable_id="~/.framework/functions/security.ts:module_header:module_header"]
31
31
 
32
- // CIL[BEGIN stable_id="~/.framework/functions/security.ts:netlifyedgesync:comment" type="comment" identity="netlifyEdgeSync" parent="~/.framework/functions/security.ts"]
33
32
  // CIL[DOC-MACRO purpose="Netlify Firewall Traffic Rules sync for banned IPs"]
34
33
  // CIL[DOC-RESPONSIBILITY responsibility="Keep a single managed ip-deny rule (description spine-security:blocklist) whose data.addresses mirrors active ip blocklist entries"]
35
34
  // CIL[DOC-CONTRACT inputs="ips to add/remove" outputs="{ synced: boolean, skipped?: boolean, error?: string }" errors="never throws — returns error field" side_effects="Netlify API GET/PUT of full ruleset"]
36
35
  // CIL[DOC-MICRO logic_summary="GET firewall_rule_set → mutate managed rule's addresses (or create rule) → PUT whole published ruleset back; user-managed rules are preserved verbatim" notable_decisions="PUT replaces the entire ruleset, so the full published rule list is round-tripped; concurrent edits could still race — acceptable at admin-action frequency"]
37
36
  // CIL[DOC-STABILITY stability="experimental"]
37
+ // CIL[BEGIN stable_id="~/.framework/functions/security.ts:MANAGED_RULE_DESC:constant" type="constant" identity="MANAGED_RULE_DESC" parent="~/.framework/functions/security.ts"]
38
38
  const MANAGED_RULE_DESC = 'spine-security:blocklist'
39
+ // CIL[END stable_id="~/.framework/functions/security.ts:MANAGED_RULE_DESC:constant"]
40
+ // CIL[BEGIN stable_id="~/.framework/functions/security.ts:NETLIFY_API:constant" type="constant" identity="NETLIFY_API" parent="~/.framework/functions/security.ts"]
39
41
  const NETLIFY_API = 'https://api.netlify.com'
42
+ // CIL[END stable_id="~/.framework/functions/security.ts:NETLIFY_API:constant"]
40
43
 
44
+ // CIL[BEGIN stable_id="~/.framework/functions/security.ts:netlifyEdgeSync:function" type="function" identity="netlifyEdgeSync" parent="~/.framework/functions/security.ts"]
41
45
  async function netlifyEdgeSync(ipsToAdd: string[], ipsToRemove: string[] = []): Promise<{ synced: boolean; skipped?: boolean; error?: string }> {
42
46
  const token = process.env.NETLIFY_ACCESS_TOKEN
43
47
  const siteId = process.env.NETLIFY_SITE_ID
@@ -90,25 +94,29 @@ async function netlifyEdgeSync(ipsToAdd: string[], ipsToRemove: string[] = []):
90
94
  return { synced: false, error: err?.message || 'Netlify sync failed' }
91
95
  }
92
96
  }
93
- // CIL[END stable_id="~/.framework/functions/security.ts:netlifyedgesync:comment"]
97
+ // CIL[END stable_id="~/.framework/functions/security.ts:netlifyEdgeSync:function"]
94
98
 
95
- // CIL[BEGIN stable_id="~/.framework/functions/security.ts:flaggedaccounts:function" type="function" identity="flaggedAccounts" parent="~/.framework/functions/security.ts"]
96
99
  // CIL[DOC-MACRO purpose="Aggregate security.* audit events into a flagged-actors report"]
97
100
  // CIL[DOC-RESPONSIBILITY responsibility="Group recent security.rate_limit_exceeded / injection_detected / registration_blocked log rows by acting person or account, enrich with people + account + blocklist state"]
98
101
  // CIL[DOC-CONTRACT inputs="ctx" outputs="{ rows: FlaggedRow[] }" errors="returns empty rows on missing tables" side_effects="logs + people + accounts + security_blocklist reads"]
99
- // CIL[DOC-MICRO logic_summary="Fetch last 500 security.* events → bucket by person id (fallback account id) → count signals + collect kinds/patterns/ips → join people(email, geo, status), accounts(display_name), active blocklist entries → sort by signal count desc" notable_decisions="IP shown comes from the person's registration_ip/last_ip (trusted edge capture), not spoofable request data"]
102
+ // CIL[DOC-MICRO logic_summary="Fetch last 500 security.* events within 30 days → bucket by person id (fallback account id) → count signals + collect kinds/patterns/ips → join people(email, geo, status), accounts(display_name), active blocklist entries → sort by signal count desc" notable_decisions="IP shown comes from the person's registration_ip/last_ip (trusted edge capture), not spoofable request data; started_at lower bound keeps the span_name scan index-bounded (no span_name index, and zero-signal scans would otherwise walk the whole logs table)"]
100
103
  // CIL[DOC-STABILITY stability="experimental"]
104
+ // CIL[BEGIN stable_id="~/.framework/functions/security.ts:SECURITY_SIGNALS:constant" type="constant" identity="SECURITY_SIGNALS" parent="~/.framework/functions/security.ts"]
101
105
  const SECURITY_SIGNALS = [
102
106
  'security.rate_limit_exceeded',
103
107
  'security.injection_detected',
104
108
  'security.registration_blocked',
105
109
  ]
110
+ // CIL[END stable_id="~/.framework/functions/security.ts:SECURITY_SIGNALS:constant"]
106
111
 
112
+ // CIL[BEGIN stable_id="~/.framework/functions/security.ts:flaggedAccounts:function" type="function" identity="flaggedAccounts" parent="~/.framework/functions/security.ts"]
107
113
  async function flaggedAccounts(ctx: RequestContext) {
114
+ const since = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString()
108
115
  const { data: events, error } = await adminDb
109
116
  .from('logs')
110
117
  .select('account_id, started_at, metadata')
111
118
  .in('span_name', SECURITY_SIGNALS)
119
+ .gte('started_at', since)
112
120
  .order('started_at', { ascending: false })
113
121
  .limit(500)
114
122
  if (error) {
@@ -194,14 +202,14 @@ async function flaggedAccounts(ctx: RequestContext) {
194
202
  rows.sort((a, b) => b.signals - a.signals)
195
203
  return { rows }
196
204
  }
197
- // CIL[END stable_id="~/.framework/functions/security.ts:flaggedaccounts:function"]
205
+ // CIL[END stable_id="~/.framework/functions/security.ts:flaggedAccounts:function"]
198
206
 
199
- // CIL[BEGIN stable_id="~/.framework/functions/security.ts:addblockrows:function" type="function" identity="addBlockRows" parent="~/.framework/functions/security.ts"]
200
207
  // CIL[DOC-MACRO purpose="Upsert active hard-block rows into security_blocklist"]
201
208
  // CIL[DOC-RESPONSIBILITY responsibility="Insert rows with revoked_at cleared on (type,value) conflict so a re-ban re-activates a revoked row"]
202
209
  // CIL[DOC-CONTRACT inputs="rows[], bannedBy, reason" outputs="void" errors="throws on DB error" side_effects="security_blocklist upsert"]
203
210
  // CIL[DOC-MICRO logic_summary="map rows → upsert onConflict type,value with revoked_at:null" notable_decisions="[]"]
204
211
  // CIL[DOC-STABILITY stability="stable"]
212
+ // CIL[BEGIN stable_id="~/.framework/functions/security.ts:addBlockRows:function" type="function" identity="addBlockRows" parent="~/.framework/functions/security.ts"]
205
213
  async function addBlockRows(
206
214
  rows: Array<{ type: string; value: string }>,
207
215
  bannedBy: string | undefined,
@@ -225,14 +233,14 @@ async function addBlockRows(
225
233
  )
226
234
  if (error) throw new Error('security_blocklist upsert failed: ' + error.message)
227
235
  }
228
- // CIL[END stable_id="~/.framework/functions/security.ts:addblockrows:function"]
236
+ // CIL[END stable_id="~/.framework/functions/security.ts:addBlockRows:function"]
229
237
 
230
- // CIL[BEGIN stable_id="~/.framework/functions/security.ts:banuser:function" type="function" identity="banUser" parent="~/.framework/functions/security.ts"]
231
238
  // CIL[DOC-MACRO purpose="Three-layer ban: internal blocklist + Supabase Auth ban + Netlify edge deny"]
232
239
  // CIL[DOC-RESPONSIBILITY responsibility="Given a person_id (or bare email/ip), persist blocklist rows for person+email+account+IP, ban the auth user, and push the IP into the managed Netlify deny rule"]
233
240
  // CIL[DOC-CONTRACT inputs="ctx, body {person_id?|email?|ip?, reason?, block_ip?, block_email?}" outputs="{ ok, person_id, layers }" errors="400 missing target; 404 person not found; 500 blocklist write failure" side_effects="security_blocklist upserts, auth.admin.updateUserById, Netlify ruleset PUT, audit events"]
234
241
  // CIL[DOC-MICRO logic_summary="Resolve person → write blocklist rows (fail = abort) → auth ban via ban_duration 876600h → edge sync for last/registration IP → audit security.user_banned with per-layer results" notable_decisions="Internal blocklist is authoritative and must succeed; auth/edge failures are recorded in the audit event and returned, not thrown — a ban must never half-apply silently"]
235
242
  // CIL[DOC-STABILITY stability="experimental"]
243
+ // CIL[BEGIN stable_id="~/.framework/functions/security.ts:banUser:function" type="function" identity="banUser" parent="~/.framework/functions/security.ts"]
236
244
  async function banUser(ctx: RequestContext, body: any) {
237
245
  const { person_id, email: rawEmail, ip: rawIp, reason, block_ip = true, block_email = true } = body || {}
238
246
  if (!person_id && !rawEmail && !rawIp) {
@@ -295,14 +303,14 @@ async function banUser(ctx: RequestContext, body: any) {
295
303
 
296
304
  return { ok: true, person_id: person?.id || null, email, ip, layers }
297
305
  }
298
- // CIL[END stable_id="~/.framework/functions/security.ts:banuser:function"]
306
+ // CIL[END stable_id="~/.framework/functions/security.ts:banUser:function"]
299
307
 
300
- // CIL[BEGIN stable_id="~/.framework/functions/security.ts:unbanuser:function" type="function" identity="unbanUser" parent="~/.framework/functions/security.ts"]
301
308
  // CIL[DOC-MACRO purpose="Reverse a ban: revoke blocklist rows, lift auth ban, remove edge IP"]
302
309
  // CIL[DOC-RESPONSIBILITY responsibility="Revoke active security_blocklist rows for the person's identifiers, lift the Supabase ban, and pull the IP out of the managed Netlify rule"]
303
310
  // CIL[DOC-CONTRACT inputs="ctx, body {person_id}" outputs="{ ok, revoked, layers }" errors="404 person not found" side_effects="blocklist updates, auth admin call, Netlify PUT, audit"]
304
311
  // CIL[DOC-MICRO logic_summary="Load person → revoke matching (type,value) rows → ban_duration 'none' → edge remove → audit security.user_unbanned" notable_decisions="account_id rows are intentionally left alone on unban — an org-level block is a separate decision"]
305
312
  // CIL[DOC-STABILITY stability="experimental"]
313
+ // CIL[BEGIN stable_id="~/.framework/functions/security.ts:unbanUser:function" type="function" identity="unbanUser" parent="~/.framework/functions/security.ts"]
306
314
  async function unbanUser(ctx: RequestContext, body: any) {
307
315
  const { person_id } = body || {}
308
316
  if (!person_id) throw new Error('person_id is required')
@@ -353,13 +361,13 @@ async function unbanUser(ctx: RequestContext, body: any) {
353
361
 
354
362
  return { ok: true, person_id: person.id, layers }
355
363
  }
356
- // CIL[END stable_id="~/.framework/functions/security.ts:unbanuser:function"]
364
+ // CIL[END stable_id="~/.framework/functions/security.ts:unbanUser:function"]
357
365
 
358
- // CIL[BEGIN stable_id="~/.framework/functions/security.ts:blocklistcrud:comment" type="comment" identity="blocklistCrud" parent="~/.framework/functions/security.ts"]
359
366
  // CIL[DOC-MACRO purpose="Blocklist list/add/revoke for the admin UI"]
360
367
  // CIL[DOC-RESPONSIBILITY responsibility="Expose the security_blocklist table to system admins; ip entries sync to the Netlify edge on add/revoke"]
361
368
  // CIL[DOC-MICRO logic_summary="list returns active+recent rows; add upserts (type,value,severity,reason,expires_at); revoke stamps revoked_at. Both edge-sync ip values" notable_decisions="[]"]
362
369
  // CIL[DOC-STABILITY stability="experimental"]
370
+ // CIL[BEGIN stable_id="~/.framework/functions/security.ts:listBlocklist:function" type="function" identity="listBlocklist" parent="~/.framework/functions/security.ts"]
363
371
  async function listBlocklist() {
364
372
  const { data, error } = await adminDb
365
373
  .from('security_blocklist')
@@ -369,7 +377,9 @@ async function listBlocklist() {
369
377
  if (error) throw new Error('blocklist read failed: ' + error.message)
370
378
  return { rows: data || [] }
371
379
  }
380
+ // CIL[END stable_id="~/.framework/functions/security.ts:listBlocklist:function"]
372
381
 
382
+ // CIL[BEGIN stable_id="~/.framework/functions/security.ts:blocklistAdd:function" type="function" identity="blocklistAdd" parent="~/.framework/functions/security.ts"]
373
383
  async function blocklistAdd(ctx: RequestContext, body: any) {
374
384
  const { type, value, severity = 'hard', reason, expires_at } = body || {}
375
385
  if (!type || !value) throw new Error('type and value are required')
@@ -395,7 +405,9 @@ async function blocklistAdd(ctx: RequestContext, body: any) {
395
405
  })
396
406
  return { ok: true }
397
407
  }
408
+ // CIL[END stable_id="~/.framework/functions/security.ts:blocklistAdd:function"]
398
409
 
410
+ // CIL[BEGIN stable_id="~/.framework/functions/security.ts:blocklistRevoke:function" type="function" identity="blocklistRevoke" parent="~/.framework/functions/security.ts"]
399
411
  async function blocklistRevoke(ctx: RequestContext, body: any) {
400
412
  const { id, type, value } = body || {}
401
413
  if (!id && !(type && value)) throw new Error('id or (type, value) is required')
@@ -417,17 +429,18 @@ async function blocklistRevoke(ctx: RequestContext, body: any) {
417
429
  })
418
430
  return { ok: true, revoked: revoked?.length || 0 }
419
431
  }
420
- // CIL[END stable_id="~/.framework/functions/security.ts:blocklistcrud:comment"]
432
+ // CIL[END stable_id="~/.framework/functions/security.ts:blocklistRevoke:function"]
421
433
 
422
- // CIL[BEGIN stable_id="~/.framework/functions/security.ts:handler:constant" type="constant" identity="handler" parent="~/.framework/functions/security.ts"]
423
434
  // CIL[DOC-MACRO purpose="Action dispatcher — system-admin only"]
424
435
  // CIL[DOC-RESPONSIBILITY responsibility="Route ?action= to the security operations; every action is gated by requireSystemContextWithAudit"]
425
436
  // CIL[DOC-CONTRACT inputs="ctx, body" outputs="action result JSON" errors="401/403 unauthenticated/non-admin; 400 unknown action" side_effects="delegated"]
426
437
  // CIL[DOC-MICRO logic_summary="Gate once, then switch on ctx.query.action || body.action" notable_decisions="[]"]
427
438
  // CIL[DOC-STABILITY stability="experimental"]
439
+ // CIL[BEGIN stable_id="~/.framework/functions/security.ts:handler:constant" type="constant" identity="handler" parent="~/.framework/functions/security.ts"]
428
440
  export const handler = createHandler(async (ctx, body) => {
429
- const deny = requireSystemContextWithAudit(ctx, 'security_admin')
430
- if (deny) return deny
441
+ // Throws a statusCode-carrying error on denial; createHandler maps it to a
442
+ // real HTTP 401/403 (F-051).
443
+ requireSystemContextWithAudit(ctx, 'security_admin')
431
444
 
432
445
  const action = (ctx.query?.action as string) || (body?.action as string) || ''
433
446
  switch (action) {