spine-framework 0.3.145 → 0.4.1

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 (451) hide show
  1. package/.env.example +76 -0
  2. package/.framework/.spine-manifest.json +19 -0
  3. package/.framework/cli/commands/agents.ts +61 -33
  4. package/.framework/cli/commands/auth.ts +60 -28
  5. package/.framework/cli/commands/create-app.ts +212 -80
  6. package/.framework/cli/commands/dev.ts +184 -68
  7. package/.framework/cli/commands/doctor.ts +324 -152
  8. package/.framework/cli/commands/generate.ts +119 -68
  9. package/.framework/cli/commands/init.ts +141 -242
  10. package/.framework/cli/commands/install-app.ts +1336 -314
  11. package/.framework/cli/commands/items.ts +31 -29
  12. package/.framework/cli/commands/migrate.ts +299 -156
  13. package/.framework/cli/commands/migrations.ts +62 -30
  14. package/.framework/cli/commands/pipelines.ts +31 -29
  15. package/.framework/cli/commands/status.ts +105 -45
  16. package/.framework/cli/commands/system.ts +47 -42
  17. package/.framework/cli/commands/test.ts +161 -102
  18. package/.framework/cli/commands/upgrade.ts +260 -0
  19. package/.framework/cli/commands/validate-app.ts +517 -161
  20. package/.framework/cli/context.ts +117 -85
  21. package/.framework/cli/env-loader.ts +74 -43
  22. package/.framework/cli/index.ts +70 -46
  23. package/.framework/cli/lib/project-files.ts +404 -0
  24. package/.framework/docs/guides/testing-guide.md +1 -1
  25. package/.framework/docs/runtime-types.md +72 -72
  26. package/.framework/functions/_shared/access.ts +84 -0
  27. package/.framework/functions/_shared/agent-runner.ts +5067 -1026
  28. package/.framework/functions/_shared/app-manifest.ts +214 -94
  29. package/.framework/functions/_shared/ast.ts +214 -0
  30. package/.framework/functions/_shared/audit.ts +91 -55
  31. package/.framework/functions/_shared/cil.ts +368 -127
  32. package/.framework/functions/_shared/db.ts +337 -68
  33. package/.framework/functions/_shared/funnel.ts +228 -0
  34. package/.framework/functions/_shared/geo.ts +105 -0
  35. package/.framework/functions/_shared/hash-api-key.ts +94 -17
  36. package/.framework/functions/_shared/index.ts +22 -224
  37. package/.framework/functions/_shared/kb.ts +832 -58
  38. package/.framework/functions/_shared/llm.ts +118 -18
  39. package/.framework/functions/_shared/logging.ts +317 -19
  40. package/.framework/functions/_shared/middleware.ts +701 -131
  41. package/.framework/functions/_shared/notify.ts +70 -0
  42. package/.framework/functions/_shared/outcomes.ts +34 -19
  43. package/.framework/functions/_shared/permissions.ts +501 -315
  44. package/.framework/functions/_shared/pipeline-runner.ts +704 -155
  45. package/.framework/functions/_shared/principal.ts +436 -263
  46. package/.framework/functions/_shared/rate-limit.ts +233 -0
  47. package/.framework/functions/_shared/resolve-credentials.ts +86 -19
  48. package/.framework/functions/_shared/resolve-ids.ts +203 -139
  49. package/.framework/functions/_shared/retry-deliveries.ts +51 -16
  50. package/.framework/functions/_shared/schema-utils.ts +602 -521
  51. package/.framework/functions/_shared/security.ts +345 -0
  52. package/.framework/functions/_shared/testing.ts +197 -172
  53. package/.framework/functions/_shared/tracing.ts +351 -21
  54. package/.framework/functions/_shared/trigger-engine.ts +425 -136
  55. package/.framework/functions/_shared/webhook-registration.ts +71 -70
  56. package/.framework/functions/_shared/webhook-registry.ts +126 -83
  57. package/.framework/functions/account-nodes.ts +30 -30
  58. package/.framework/functions/adapters/generic-webhook-inbound.ts +49 -16
  59. package/.framework/functions/adapters/generic-webhook-outbound.ts +17 -17
  60. package/.framework/functions/adapters/openai-outbound.ts +15 -16
  61. package/.framework/functions/admin-data.ts +596 -173
  62. package/.framework/functions/admin-funnel.ts +64 -60
  63. package/.framework/functions/admin-install-funnel.ts +84 -66
  64. package/.framework/functions/admin-signals.ts +69 -51
  65. package/.framework/functions/ai-agents.ts +431 -106
  66. package/.framework/functions/ai-interactions.ts +186 -112
  67. package/.framework/functions/api-keys.ts +107 -107
  68. package/.framework/functions/apps.ts +154 -136
  69. package/.framework/functions/auth.ts +83 -65
  70. package/.framework/functions/cil-agent-ingest.ts +189 -232
  71. package/.framework/functions/cil-agent.ts +433 -177
  72. package/.framework/functions/cil-callmap.ts +642 -430
  73. package/.framework/functions/cil-config.ts +139 -135
  74. package/.framework/functions/cil-context.ts +214 -127
  75. package/.framework/functions/cil-doc.ts +587 -458
  76. package/.framework/functions/cil-embeddings.ts +453 -364
  77. package/.framework/functions/cil-graph.ts +427 -142
  78. package/.framework/functions/cil-ingest.ts +1418 -1154
  79. package/.framework/functions/cil-pipeline.ts +276 -21
  80. package/.framework/functions/cil-project.ts +659 -132
  81. package/.framework/functions/cil-validate.ts +198 -164
  82. package/.framework/functions/debug-auth.ts +57 -55
  83. package/.framework/functions/deliveries.ts +80 -21
  84. package/.framework/functions/embeddings.ts +1023 -318
  85. package/.framework/functions/eval-harness.ts +146 -108
  86. package/.framework/functions/integration-outbound.ts +205 -75
  87. package/.framework/functions/integration-routes.ts +128 -23
  88. package/.framework/functions/integrations.ts +125 -100
  89. package/.framework/functions/invites.ts +1095 -460
  90. package/.framework/functions/item-progress.ts +158 -102
  91. package/.framework/functions/kb.ts +34 -10
  92. package/.framework/functions/logs.ts +133 -133
  93. package/.framework/functions/observability.ts +152 -122
  94. package/.framework/functions/pipeline-executions.ts +147 -132
  95. package/.framework/functions/pipelines.ts +134 -119
  96. package/.framework/functions/prompt-configs.ts +96 -80
  97. package/.framework/functions/roles.ts +112 -82
  98. package/.framework/functions/security.ts +450 -0
  99. package/.framework/functions/system-cron.ts +117 -80
  100. package/.framework/functions/system.ts +158 -98
  101. package/.framework/functions/tests.ts +29 -29
  102. package/.framework/functions/timers.ts +94 -95
  103. package/.framework/functions/trigger-scheduler.ts +10 -0
  104. package/.framework/functions/triggers.ts +179 -149
  105. package/.framework/functions/types.ts +137 -125
  106. package/.framework/mcp/index.ts +133 -0
  107. package/.framework/mcp/load-env.ts +60 -0
  108. package/.framework/mcp/shared/app-manifest.ts +275 -0
  109. package/.framework/mcp/shared/db.ts +477 -0
  110. package/.framework/mcp/shared/kb.ts +118 -0
  111. package/.framework/mcp/tools/architect.ts +431 -0
  112. package/.framework/mcp/tools/cil/context.ts +345 -0
  113. package/.framework/mcp/tools/cil/graph.ts +242 -0
  114. package/.framework/mcp/tools/cil/ingest.ts +1217 -0
  115. package/.framework/mcp/tools/cil/pipeline-orchestrator.ts +693 -0
  116. package/.framework/mcp/tools/cil/pipeline.ts +767 -0
  117. package/.framework/mcp/tools/cil/project.ts +791 -0
  118. package/.framework/mcp/tools/cil/retrieval.ts +336 -0
  119. package/.framework/mcp/tools/cil/search-core.ts +454 -0
  120. package/.framework/mcp/tools/cil/search.ts +97 -0
  121. package/.framework/mcp/tools/cil/suggest.ts +186 -0
  122. package/.framework/mcp/tools/cil/validate.ts +1369 -0
  123. package/.framework/mcp/tools/kb/delete.ts +139 -0
  124. package/.framework/mcp/tools/kb/index.ts +52 -0
  125. package/.framework/mcp/tools/kb/ingest.ts +84 -0
  126. package/.framework/mcp/tools/kb/list-types.ts +102 -0
  127. package/.framework/mcp/tools/kb/publish.ts +100 -0
  128. package/.framework/mcp/tools/kb/reindex.ts +86 -0
  129. package/.framework/mcp/tools/kb/revoke.ts +85 -0
  130. package/.framework/mcp/tools/kb/search.ts +101 -0
  131. package/.framework/mcp/tools/kb/upsert.ts +125 -0
  132. package/.framework/mcp/tools/observability.ts +510 -0
  133. package/.framework/migrations/001_core_schema.sql +15 -0
  134. package/.framework/migrations/002_core_rls_and_functions.sql +15 -0
  135. package/.framework/migrations/003_core_system_data.sql +16 -1
  136. package/.framework/migrations/004_core_rpc_and_broker.sql +17 -1
  137. package/.framework/migrations/005_core_observability.sql +17 -1
  138. package/.framework/migrations/006_security_hardening_rls.sql +15 -0
  139. package/.framework/migrations/007_kb_article_list.sql +17 -1
  140. package/.framework/migrations/008_items_target.sql +15 -0
  141. package/.framework/migrations/009_roles_slug_unique.sql +15 -0
  142. package/.framework/migrations/010_auth_user_trigger.sql +15 -0
  143. package/.framework/migrations/011_spine_system_account.sql +16 -0
  144. package/.framework/migrations/012_service_role_grants.sql +16 -0
  145. package/.framework/migrations/013_get_account_apps_role_filter.sql +15 -0
  146. package/.framework/migrations/014_apps_required_roles.sql +16 -0
  147. package/.framework/migrations/015_types_kind_slug_unique.sql +15 -0
  148. package/.framework/migrations/016_cil_types.sql +281 -0
  149. package/.framework/migrations/017_cil_stable_ids.sql +37 -0
  150. package/.framework/migrations/018_runtime_tables.sql +129 -0
  151. package/.framework/migrations/019_agent_prompt_columns.sql +56 -0
  152. package/.framework/migrations/020_schema_reconciliation.sql +67 -0
  153. package/.framework/migrations/021_kb_embedding_lifecycle.sql +31 -0
  154. package/.framework/migrations/032_cil_status_migration.sql +73 -0
  155. package/.framework/migrations/033_cil_versioning.sql +73 -0
  156. package/.framework/migrations/034_match_embeddings_knn.sql +115 -0
  157. package/.framework/migrations/035_rate_limits.sql +55 -0
  158. package/.framework/migrations/036_demo_record_flags.sql +72 -0
  159. package/.framework/migrations/037_kb_types_canonical.sql +244 -0
  160. package/.framework/migrations/038_drop_demo_record_flags.sql +38 -0
  161. package/.framework/migrations/042_registration_hierarchy.sql +309 -0
  162. package/.framework/migrations/043_policy_actor_cleanup.sql +39 -0
  163. package/.framework/migrations/044_match_embeddings_filtered_knn.sql +151 -0
  164. package/.framework/migrations/045_security_critical_rls.sql +139 -0
  165. package/.framework/migrations/046_security_missing_rls.sql +79 -0
  166. package/.framework/migrations/047_people_updated_by.sql +27 -0
  167. package/.framework/migrations/048_watcher_last_seen_at.sql +24 -0
  168. package/.framework/migrations/049_security_blocklist.sql +148 -0
  169. package/.framework/migrations/050_notifications.sql +49 -0
  170. package/.framework/migrations/051_message_agent_fields.sql +74 -0
  171. package/.framework/migrations/052_registration_security.sql +421 -0
  172. package/.framework/migrations/053_builtin_action_seeds.sql +66 -0
  173. package/.framework/migrations/054_kb_ingest_lease.sql +157 -0
  174. package/.framework/migrations/055_type_id_drift_reconciliation.sql +33 -0
  175. package/.framework/migrations/056_slug_column_reconciliation.sql +38 -0
  176. package/.framework/migrations/057_pipelines_slug_identity.sql +14 -0
  177. package/.framework/migrations/058_account_data_field_declarations.sql +53 -0
  178. package/.framework/migrations/059_registration_placement_fixes.sql +269 -0
  179. package/.framework/migrations/060_account_person_record_permissions.sql +38 -0
  180. package/.framework/migrations/061_fk_dedup.sql +32 -0
  181. package/.framework/public/spine-logo.svg +10 -3
  182. package/.framework/scripts/app-install-cli.ts +190 -93
  183. package/.framework/scripts/assemble-frontend.sh +127 -44
  184. package/.framework/scripts/assemble-functions.sh +128 -30
  185. package/.framework/scripts/boundary-check.sh +6 -2
  186. package/.framework/scripts/build-manifest.sh +24 -68
  187. package/.framework/scripts/check-core-integrity.sh +15 -76
  188. package/.framework/scripts/checksum-manifest.mts +343 -0
  189. package/.framework/scripts/cil-migrate-markers.ts +403 -0
  190. package/.framework/scripts/fix-cil-markers.mjs +176 -0
  191. package/.framework/scripts/verify-integrity.sh +21 -57
  192. package/.framework/scripts/verify-manifest.mts +305 -0
  193. package/.framework/seeds/types/funnel_signal_use.json +74 -21
  194. package/.framework/seeds/types/spine_instance.json +73 -21
  195. package/.framework/src/App.tsx +118 -72
  196. package/.framework/src/apps/admin/index.tsx +855 -58
  197. package/.framework/src/components/AppWrapper.tsx +43 -45
  198. package/.framework/src/components/CustomAppLoader.tsx +160 -116
  199. package/.framework/src/components/admin/AdminListPage.tsx +46 -44
  200. package/.framework/src/components/admin/AdminSidebar.tsx +57 -47
  201. package/.framework/src/components/admin/AdminStatsCard.tsx +46 -44
  202. package/.framework/src/components/admin/PageInstructions.tsx +98 -2
  203. package/.framework/src/components/admin/SortableTableHeader.tsx +44 -45
  204. package/.framework/src/components/app-shell/GenericAppShell.tsx +64 -65
  205. package/.framework/src/components/app-shell/GenericDetailPage.tsx +44 -47
  206. package/.framework/src/components/app-shell/GenericListPage.tsx +45 -49
  207. package/.framework/src/components/app-sidebar.tsx +42 -45
  208. package/.framework/src/components/auth/ProtectedRoute.tsx +48 -41
  209. package/.framework/src/components/auth/TurnstileWidget.tsx +27 -0
  210. package/.framework/src/components/kb/KBArticleEditor.tsx +231 -22
  211. package/.framework/src/components/kb/KBArticleList.tsx +251 -10
  212. package/.framework/src/components/layout/AppShell.tsx +51 -45
  213. package/.framework/src/components/layout/Header.tsx +42 -43
  214. package/.framework/src/components/layout/Layout.tsx +46 -46
  215. package/.framework/src/components/layout/Sidebar.tsx +196 -96
  216. package/.framework/src/components/runtime/DataDetailHeader.tsx +55 -30
  217. package/.framework/src/components/runtime/DataDetailPage.tsx +31 -269
  218. package/.framework/src/components/runtime/DataFilters.tsx +49 -74
  219. package/.framework/src/components/runtime/DataHeader.tsx +53 -32
  220. package/.framework/src/components/runtime/DataListPage.tsx +51 -155
  221. package/.framework/src/components/runtime/DataStats.tsx +66 -44
  222. package/.framework/src/components/runtime/DataTable.tsx +49 -149
  223. package/.framework/src/components/runtime/SchemaDetailForm.tsx +45 -75
  224. package/.framework/src/components/runtime/index.ts +119 -16
  225. package/.framework/src/components/search-form.tsx +46 -29
  226. package/.framework/src/components/shared/AgentView.tsx +65 -48
  227. package/.framework/src/components/shared/FieldRenderer.tsx +144 -50
  228. package/.framework/src/components/shared/InviteManager.tsx +207 -97
  229. package/.framework/src/components/shared/MemberList.tsx +97 -0
  230. package/.framework/src/components/shared/SchemaFields.tsx +107 -86
  231. package/.framework/src/components/ui/DataTable.tsx +80 -68
  232. package/.framework/src/components/ui/Form.tsx +157 -146
  233. package/.framework/src/components/ui/ItemCard.tsx +74 -67
  234. package/.framework/src/components/ui/ItemListView.tsx +73 -68
  235. package/.framework/src/components/ui/LoadingSpinner.tsx +43 -48
  236. package/.framework/src/components/ui/Modal.tsx +78 -68
  237. package/.framework/src/components/ui/RichTextEditor.tsx +89 -69
  238. package/.framework/src/components/ui/accordion.tsx +85 -86
  239. package/.framework/src/components/ui/alert-dialog.tsx +197 -184
  240. package/.framework/src/components/ui/alert.tsx +105 -94
  241. package/.framework/src/components/ui/aspect-ratio.tsx +57 -43
  242. package/.framework/src/components/ui/avatar.tsx +122 -107
  243. package/.framework/src/components/ui/badge.tsx +60 -55
  244. package/.framework/src/components/ui/breadcrumb.tsx +127 -128
  245. package/.framework/src/components/ui/button-group.tsx +86 -72
  246. package/.framework/src/components/ui/button.tsx +71 -50
  247. package/.framework/src/components/ui/calendar.tsx +72 -57
  248. package/.framework/src/components/ui/card.tsx +136 -129
  249. package/.framework/src/components/ui/carousel.tsx +226 -107
  250. package/.framework/src/components/ui/chart.tsx +252 -147
  251. package/.framework/src/components/ui/checkbox.tsx +43 -44
  252. package/.framework/src/components/ui/collapsible.tsx +86 -71
  253. package/.framework/src/components/ui/combobox.tsx +282 -249
  254. package/.framework/src/components/ui/command.tsx +158 -146
  255. package/.framework/src/components/ui/context-menu.tsx +239 -240
  256. package/.framework/src/components/ui/dialog.tsx +174 -157
  257. package/.framework/src/components/ui/direction.tsx +72 -56
  258. package/.framework/src/components/ui/drawer.tsx +168 -169
  259. package/.framework/src/components/ui/dropdown-menu.tsx +254 -223
  260. package/.framework/src/components/ui/empty.tsx +131 -120
  261. package/.framework/src/components/ui/field.tsx +205 -173
  262. package/.framework/src/components/ui/hover-card.tsx +71 -72
  263. package/.framework/src/components/ui/input-group.tsx +148 -133
  264. package/.framework/src/components/ui/input-otp.tsx +99 -86
  265. package/.framework/src/components/ui/input.tsx +43 -41
  266. package/.framework/src/components/ui/item.tsx +207 -185
  267. package/.framework/src/components/ui/kbd.tsx +57 -56
  268. package/.framework/src/components/ui/label.tsx +45 -41
  269. package/.framework/src/components/ui/menubar.tsx +268 -236
  270. package/.framework/src/components/ui/native-select.tsx +85 -86
  271. package/.framework/src/components/ui/navigation-menu.tsx +166 -156
  272. package/.framework/src/components/ui/pagination.tsx +131 -141
  273. package/.framework/src/components/ui/popover.tsx +125 -128
  274. package/.framework/src/components/ui/progress.tsx +47 -44
  275. package/.framework/src/components/ui/radio-group.tsx +60 -55
  276. package/.framework/src/components/ui/resizable.tsx +85 -72
  277. package/.framework/src/components/ui/scroll-area.tsx +57 -58
  278. package/.framework/src/components/ui/select.tsx +182 -169
  279. package/.framework/src/components/ui/separator.tsx +42 -43
  280. package/.framework/src/components/ui/sheet.tsx +170 -159
  281. package/.framework/src/components/ui/sidebar.tsx +395 -398
  282. package/.framework/src/components/ui/skeleton.tsx +42 -43
  283. package/.framework/src/components/ui/slider.tsx +61 -44
  284. package/.framework/src/components/ui/sonner.tsx +44 -44
  285. package/.framework/src/components/ui/spinner.tsx +47 -42
  286. package/.framework/src/components/ui/switch.tsx +56 -42
  287. package/.framework/src/components/ui/table-primitives.tsx +272 -132
  288. package/.framework/src/components/ui/table.tsx +140 -142
  289. package/.framework/src/components/ui/tabs.tsx +110 -93
  290. package/.framework/src/components/ui/textarea.tsx +43 -44
  291. package/.framework/src/components/ui/toggle-group.tsx +87 -71
  292. package/.framework/src/components/ui/toggle.tsx +56 -56
  293. package/.framework/src/components/ui/tooltip.tsx +99 -85
  294. package/.framework/src/contexts/AppContext.tsx +166 -114
  295. package/.framework/src/contexts/AuthContext.tsx +232 -137
  296. package/.framework/src/hooks/use-mobile.ts +43 -44
  297. package/.framework/src/hooks/useApi.ts +185 -172
  298. package/.framework/src/hooks/useAppPath.ts +31 -29
  299. package/.framework/src/hooks/useApps.ts +77 -42
  300. package/.framework/src/hooks/useEntityList.ts +66 -29
  301. package/.framework/src/hooks/useEntityRecord.ts +76 -32
  302. package/.framework/src/hooks/useForm.ts +62 -30
  303. package/.framework/src/hooks/useListSchema.ts +60 -43
  304. package/.framework/src/hooks/useSchemaRecord.ts +115 -95
  305. package/.framework/src/lib/api.ts +258 -134
  306. package/.framework/src/lib/registration.ts +181 -0
  307. package/.framework/src/lib/supabase.ts +118 -81
  308. package/.framework/src/lib/utils.ts +266 -266
  309. package/.framework/src/main.tsx +43 -44
  310. package/.framework/src/pages/DashboardPage.tsx +44 -43
  311. package/.framework/src/pages/NoAppPage.tsx +50 -0
  312. package/.framework/src/pages/NotFoundPage.tsx +32 -29
  313. package/.framework/src/pages/SetupPage.tsx +34 -33
  314. package/.framework/src/pages/admin/AIAgentDetailPage.tsx +69 -58
  315. package/.framework/src/pages/admin/AIAgentsPage.tsx +43 -40
  316. package/.framework/src/pages/admin/APIKeyDetailPage.tsx +63 -55
  317. package/.framework/src/pages/admin/APIKeysPage.tsx +44 -42
  318. package/.framework/src/pages/admin/AlertsConfigPage.tsx +117 -94
  319. package/.framework/src/pages/admin/AppDetailPage.tsx +48 -42
  320. package/.framework/src/pages/admin/AppsPage.tsx +45 -42
  321. package/.framework/src/pages/admin/DeliveriesPage.tsx +49 -17
  322. package/.framework/src/pages/admin/DesignedPage.tsx +31 -30
  323. package/.framework/src/pages/admin/EmbeddingDetailPage.tsx +58 -0
  324. package/.framework/src/pages/admin/EmbeddingsPage.tsx +49 -0
  325. package/.framework/src/pages/admin/ExtendedShadcnTestPage.tsx +31 -29
  326. package/.framework/src/pages/admin/IncrementalShadcnTestPage.tsx +46 -29
  327. package/.framework/src/pages/admin/IntegratedDashboard.tsx +32 -29
  328. package/.framework/src/pages/admin/IntegrationDetailPage.tsx +68 -55
  329. package/.framework/src/pages/admin/IntegrationsPage.tsx +45 -42
  330. package/.framework/src/pages/admin/InvitesPage.tsx +60 -44
  331. package/.framework/src/pages/admin/LogsPage.tsx +87 -137
  332. package/.framework/src/pages/admin/MinimalShadcnTestPage.tsx +31 -29
  333. package/.framework/src/pages/admin/ObservabilityDashboard.tsx +150 -89
  334. package/.framework/src/pages/admin/PipelineDetailPage.tsx +77 -104
  335. package/.framework/src/pages/admin/PipelineExecutionsPage.tsx +44 -43
  336. package/.framework/src/pages/admin/PipelinesPage.tsx +59 -145
  337. package/.framework/src/pages/admin/PromptConfigDetailPage.tsx +53 -42
  338. package/.framework/src/pages/admin/PromptConfigsPage.tsx +47 -117
  339. package/.framework/src/pages/admin/ProperlyDesignedPage.tsx +31 -44
  340. package/.framework/src/pages/admin/RoleDetailPage.tsx +49 -92
  341. package/.framework/src/pages/admin/RolesPage.tsx +47 -164
  342. package/.framework/src/pages/admin/SecurityPage.tsx +310 -0
  343. package/.framework/src/pages/admin/SelectTestPage.tsx +30 -32
  344. package/.framework/src/pages/admin/ShadcnTestPage.tsx +37 -44
  345. package/.framework/src/pages/admin/SimpleDashboard.tsx +32 -44
  346. package/.framework/src/pages/admin/TestRunDetailPage.tsx +97 -44
  347. package/.framework/src/pages/admin/TestingDashboard.tsx +126 -71
  348. package/.framework/src/pages/admin/TimerDetailPage.tsx +67 -55
  349. package/.framework/src/pages/admin/TimersPage.tsx +44 -42
  350. package/.framework/src/pages/admin/TriggerDetailPage.tsx +47 -42
  351. package/.framework/src/pages/admin/TriggersPage.tsx +58 -57
  352. package/.framework/src/pages/admin/TypeDetailPage.tsx +91 -55
  353. package/.framework/src/pages/admin/TypesPage.tsx +45 -42
  354. package/.framework/src/pages/admin/cil/CILAgentPage.tsx +258 -128
  355. package/.framework/src/pages/admin/cil/CILChunkDetailPage.tsx +127 -123
  356. package/.framework/src/pages/admin/cil/CILDashboardPage.tsx +308 -235
  357. package/.framework/src/pages/admin/cil/CILFileListPage.tsx +159 -110
  358. package/.framework/src/pages/admin/cil/CILFileSelectPage.tsx +432 -0
  359. package/.framework/src/pages/admin/cil/CILGraphPage.tsx +200 -178
  360. package/.framework/src/pages/admin/cil/CILIngestionPage.tsx +286 -239
  361. package/.framework/src/pages/admin/cil/CILProjectsPage.tsx +211 -90
  362. package/.framework/src/pages/admin/cil/CILSearchPage.tsx +156 -143
  363. package/.framework/src/pages/admin/cil/CILSuggestionsPage.tsx +379 -0
  364. package/.framework/src/pages/admin/cil/CILVersionsPage.tsx +337 -0
  365. package/.framework/src/pages/admin/cil/FlexiblePipelinePage.tsx +219 -214
  366. package/.framework/src/pages/admin/kb/KBAdminPage.tsx +64 -0
  367. package/.framework/src/pages/auth/AuthConfirmPage.tsx +135 -65
  368. package/.framework/src/pages/auth/ForgotPasswordPage.tsx +39 -31
  369. package/.framework/src/pages/auth/LoginPage.tsx +38 -32
  370. package/.framework/src/pages/auth/PendingApprovalPage.tsx +31 -28
  371. package/.framework/src/pages/auth/RegisterPage.tsx +429 -155
  372. package/.framework/src/pages/spine-framework/APIPage.tsx +33 -30
  373. package/.framework/src/pages/spine-framework/CLIPage.tsx +32 -30
  374. package/.framework/src/types/auth.ts +101 -93
  375. package/.framework/src/types/types.ts +283 -262
  376. package/.framework/templates/project.json +87 -0
  377. package/.framework/templates/spine.config.neutral.json +7 -0
  378. package/.framework/tests/api/api-surface.test.ts +381 -0
  379. package/.framework/tests/fixtures/seed.ts +427 -0
  380. package/.framework/tests/fixtures/teardown.ts +228 -0
  381. package/.framework/tests/reporter.ts +351 -0
  382. package/.framework/tests/setup.ts +135 -0
  383. package/.framework/tests/ui/ui-sweep.ts +278 -0
  384. package/.framework/tests/unit/admin-data-logger.test.ts +310 -0
  385. package/.framework/tests/unit/agent-runner.test.ts +2389 -0
  386. package/.framework/tests/unit/ai-interactions.test.ts +81 -0
  387. package/.framework/tests/unit/app-manifest.test.ts +359 -0
  388. package/.framework/tests/unit/assembly-scripts.test.ts +172 -0
  389. package/.framework/tests/unit/cil-determinism.test.ts +85 -0
  390. package/.framework/tests/unit/core-isolation.test.ts +153 -0
  391. package/.framework/tests/unit/funnel-signals.test.ts +135 -0
  392. package/.framework/tests/unit/install-app.test.ts +1759 -0
  393. package/.framework/tests/unit/invites-security.test.ts +572 -0
  394. package/.framework/tests/unit/kb-ingest-lease.test.ts +287 -0
  395. package/.framework/tests/unit/kb-search.test.ts +119 -0
  396. package/.framework/tests/unit/migrate.test.ts +509 -0
  397. package/.framework/tests/unit/notify.test.ts +136 -0
  398. package/.framework/tests/unit/permissions.test.ts +269 -0
  399. package/.framework/tests/unit/pipeline-runner.test.ts +755 -0
  400. package/.framework/tests/unit/principal-cache.test.ts +138 -0
  401. package/.framework/tests/unit/retrieval-golden.test.ts +520 -0
  402. package/.framework/tests/unit/runtime-isolation.test.ts +642 -0
  403. package/.framework/tests/unit/schema-utils.test.ts +225 -0
  404. package/.framework/tests/unit/trigger-engine.test.ts +513 -0
  405. package/.framework/tests/unit/validate-app.test.ts +527 -0
  406. package/README.md +34 -3
  407. package/bin/spine-framework.cjs +1 -0
  408. package/config/tailwind.config.cjs +7 -0
  409. package/config/tsconfig.functions.json +42 -0
  410. package/config/tsconfig.json +37 -9
  411. package/config/vite.config.ts +31 -2
  412. package/netlify.toml +31 -3
  413. package/package.json +46 -29
  414. package/scripts/assemble-frontend.sh +127 -44
  415. package/scripts/assemble-functions.sh +127 -29
  416. package/scripts/boundary-check.sh +6 -2
  417. package/scripts/build-manifest.sh +24 -68
  418. package/scripts/checksum-manifest.mts +345 -0
  419. package/scripts/dev-clean.sh +86 -0
  420. package/scripts/dev-netlify.sh +49 -0
  421. package/scripts/netlify-dev-wrapper.sh +2 -0
  422. package/scripts/verify-install.ts +358 -0
  423. package/scripts/verify-integrity.sh +21 -57
  424. package/scripts/verify-manifest.mts +305 -0
  425. package/templates/project/gitignore +45 -0
  426. package/{config → templates/project}/package-lock.json +22335 -9366
  427. package/vitest.config.ts +46 -0
  428. package/.framework/public/spine.config.json +0 -91
  429. package/config/deno.lock +0 -108
  430. package/config/tsconfig.node.tsbuildinfo +0 -1
  431. package/config/tsconfig.tsbuildinfo +0 -1
  432. package/config/vite.config.d.ts +0 -2
  433. package/custom/.devin/AGENT.md +0 -357
  434. package/custom/.devin/skills/install-app/SKILL.md +0 -39
  435. package/custom/.devin/skills/spine-workbench-discovery/SKILL.md +0 -39
  436. package/custom/.devin/skills/spine-workbench-idea/SKILL.md +0 -36
  437. package/custom/.devin/skills/spine-workbench-implement/SKILL.md +0 -46
  438. package/custom/.devin/skills/spine-workbench-initiative/SKILL.md +0 -28
  439. package/custom/.devin/skills/spine-workbench-plan/SKILL.md +0 -38
  440. package/custom/.devin/workflows/app-install.yaml +0 -83
  441. package/custom/README.md +0 -79
  442. package/docs/README.md +0 -15
  443. package/scripts/app-install-cli.ts +0 -286
  444. package/scripts/check-core-integrity.sh +0 -82
  445. package/scripts/ingest-chunks.cjs +0 -202
  446. package/scripts/kb-chunk-parser.cjs +0 -312
  447. package/scripts/kb-chunk-parser.ts +0 -330
  448. package/scripts/kb-ingest-docs.ts +0 -203
  449. package/scripts/load-test-app-install.ts +0 -484
  450. package/scripts/test-login.js +0 -33
  451. package/spine.config.json +0 -91
@@ -0,0 +1,450 @@
1
+ /**
2
+ * @module security
3
+ * @audience internal
4
+ * @layer security
5
+ * @stability experimental
6
+ *
7
+ * System-admin security operations: flagged-actor report, three-layer
8
+ * ban (internal blocklist + Supabase Auth ban + Netlify edge deny rule),
9
+ * unban, and blocklist CRUD. INVARIANT: every action requires a
10
+ * system-admin principal; the Netlify edge layer degrades gracefully
11
+ * when NETLIFY_ACCESS_TOKEN / NETLIFY_SITE_ID are unset (local dev).
12
+ *
13
+ * @seeAlso .framework/functions/_shared/security.ts (enforcement helpers)
14
+ * @seeAlso .framework/migrations/049_security_blocklist.sql (schema)
15
+ *
16
+ * @cil-generated
17
+ */
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
+ // CIL[DOC-MACRO purpose="System-admin security operations: flagged-actor report, ban/unban, blocklist CRUD, Netlify edge sync"]
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
+ // 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"]
22
+ // CIL[DOC-CONTRACT inputs="?action= flagged_accounts|ban_user|unban_user|blocklist|blocklist_add|blocklist_revoke + body" outputs="JSON report / action result" errors="403 non-system-admin; 400 bad action" side_effects="security_blocklist writes, auth.admin updates, Netlify firewall PUT, audit logs"]
23
+ // CIL[DOC-MICRO logic_summary="Report groups security.* log events by principal id then enriches with people/auth data; ban writes person+email+ip blocklist rows, bans auth user (876600h), and syncs one managed Netlify ip-deny rule labelled spine-security:blocklist" notable_decisions="Edge rule is a single shared deny rule whose data.addresses array is mutated — preserves user-managed geo rules and stays within tight free-tier rule limits; all edge calls skip silently without NETLIFY_ACCESS_TOKEN/SITE_ID"]
24
+ // CIL[DOC-STABILITY stability="experimental"]
25
+
26
+ import { createHandler, requireSystemContextWithAudit } from './_shared/middleware'
27
+ import type { RequestContext } from './_shared/middleware'
28
+ import { adminDb } from './_shared/db'
29
+ import { emitAudit } from './_shared/audit'
30
+ // CIL[END stable_id="~/.framework/functions/security.ts:module_header:module_header"]
31
+
32
+ // CIL[BEGIN stable_id="~/.framework/functions/security.ts:netlifyedgesync:comment" type="comment" identity="netlifyEdgeSync" parent="~/.framework/functions/security.ts"]
33
+ // CIL[DOC-MACRO purpose="Netlify Firewall Traffic Rules sync for banned IPs"]
34
+ // CIL[DOC-RESPONSIBILITY responsibility="Keep a single managed ip-deny rule (description spine-security:blocklist) whose data.addresses mirrors active ip blocklist entries"]
35
+ // 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
+ // 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
+ // CIL[DOC-STABILITY stability="experimental"]
38
+ const MANAGED_RULE_DESC = 'spine-security:blocklist'
39
+ const NETLIFY_API = 'https://api.netlify.com'
40
+
41
+ async function netlifyEdgeSync(ipsToAdd: string[], ipsToRemove: string[] = []): Promise<{ synced: boolean; skipped?: boolean; error?: string }> {
42
+ const token = process.env.NETLIFY_ACCESS_TOKEN
43
+ const siteId = process.env.NETLIFY_SITE_ID
44
+ if (!token || !siteId) return { synced: false, skipped: true }
45
+ const ips = [...new Set([...ipsToAdd, ...ipsToRemove].filter(Boolean))]
46
+ if (!ips.length) return { synced: false, skipped: true }
47
+
48
+ try {
49
+ const base = `${NETLIFY_API}/api/v1/sites/${siteId}/firewall_rule_set`
50
+ const getRes = await fetch(base, { headers: { Authorization: `Bearer ${token}` } })
51
+ if (!getRes.ok) return { synced: false, error: `Netlify GET ${getRes.status}` }
52
+ const payload = await getRes.json().catch(() => ({}))
53
+ const ruleset = payload?.published ?? payload ?? { default: 'allow', rules: [] }
54
+ const rules: any[] = Array.isArray(ruleset.rules) ? ruleset.rules : []
55
+
56
+ const managedIdx = rules.findIndex(
57
+ (r) => r?.type === 'ip' && r?.action === 'deny' && String(r?.description || '').startsWith(MANAGED_RULE_DESC)
58
+ )
59
+
60
+ let addresses: string[] = managedIdx >= 0
61
+ ? [...(rules[managedIdx]?.data?.addresses || [])]
62
+ : []
63
+
64
+ for (const ip of ipsToAdd) if (ip && !addresses.includes(ip)) addresses.push(ip)
65
+ addresses = addresses.filter((a) => !ipsToRemove.includes(a))
66
+
67
+ if (managedIdx >= 0) {
68
+ if (addresses.length) {
69
+ rules[managedIdx] = { ...rules[managedIdx], data: { ...rules[managedIdx].data, addresses } }
70
+ } else {
71
+ rules.splice(managedIdx, 1)
72
+ }
73
+ } else if (addresses.length) {
74
+ rules.push({
75
+ type: 'ip',
76
+ action: 'deny',
77
+ data: { addresses },
78
+ description: `${MANAGED_RULE_DESC} — managed by Spine security admin`,
79
+ })
80
+ }
81
+
82
+ const putRes = await fetch(base, {
83
+ method: 'PUT',
84
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
85
+ body: JSON.stringify({ default: ruleset.default || 'allow', rules }),
86
+ })
87
+ if (!putRes.ok) return { synced: false, error: `Netlify PUT ${putRes.status}` }
88
+ return { synced: true }
89
+ } catch (err: any) {
90
+ return { synced: false, error: err?.message || 'Netlify sync failed' }
91
+ }
92
+ }
93
+ // CIL[END stable_id="~/.framework/functions/security.ts:netlifyedgesync:comment"]
94
+
95
+ // CIL[BEGIN stable_id="~/.framework/functions/security.ts:flaggedaccounts:function" type="function" identity="flaggedAccounts" parent="~/.framework/functions/security.ts"]
96
+ // CIL[DOC-MACRO purpose="Aggregate security.* audit events into a flagged-actors report"]
97
+ // 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
+ // 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"]
100
+ // CIL[DOC-STABILITY stability="experimental"]
101
+ const SECURITY_SIGNALS = [
102
+ 'security.rate_limit_exceeded',
103
+ 'security.injection_detected',
104
+ 'security.registration_blocked',
105
+ ]
106
+
107
+ async function flaggedAccounts(ctx: RequestContext) {
108
+ const { data: events, error } = await adminDb
109
+ .from('logs')
110
+ .select('account_id, started_at, metadata')
111
+ .in('span_name', SECURITY_SIGNALS)
112
+ .order('started_at', { ascending: false })
113
+ .limit(500)
114
+ if (error) {
115
+ console.warn('flaggedAccounts query failed (missing logs?):', error.message)
116
+ return { rows: [] }
117
+ }
118
+
119
+ // Bucket by acting principal id, falling back to account id for
120
+ // anonymous signals (e.g. registration_blocked carries no person).
121
+ const buckets = new Map<string, {
122
+ person_id: string | null
123
+ account_id: string | null
124
+ signals: number
125
+ kinds: Set<string>
126
+ patterns: Set<string>
127
+ last_seen_at: string | null
128
+ }>()
129
+
130
+ for (const ev of events || []) {
131
+ const meta = ev.metadata || {}
132
+ const pid = meta.principal?.id && meta.principal.id !== 'anonymous' ? meta.principal.id : null
133
+ const key = pid || `acct:${ev.account_id || meta.target?.account_id || 'unknown'}`
134
+ const b = buckets.get(key) || {
135
+ person_id: pid,
136
+ account_id: ev.account_id || meta.target?.account_id || null,
137
+ signals: 0,
138
+ kinds: new Set<string>(),
139
+ patterns: new Set<string>(),
140
+ last_seen_at: null,
141
+ }
142
+ b.signals += 1
143
+ if (meta.action) b.kinds.add(String(meta.action).replace('security.', ''))
144
+ if (meta.pattern) b.patterns.add(String(meta.pattern))
145
+ if (!b.last_seen_at || ev.started_at > b.last_seen_at) b.last_seen_at = ev.started_at
146
+ buckets.set(key, b)
147
+ }
148
+
149
+ const personIds = [...buckets.values()].map(b => b.person_id).filter(Boolean) as string[]
150
+ const accountIds = [...new Set([...buckets.values()].map(b => b.account_id).filter(Boolean))] as string[]
151
+
152
+ const [{ data: people }, { data: accounts }, { data: bl }] = await Promise.all([
153
+ personIds.length
154
+ ? adminDb.from('people').select('id, email, account_id, auth_uid, status, data').in('id', personIds)
155
+ : Promise.resolve({ data: [] as any[] }),
156
+ accountIds.length
157
+ ? adminDb.from('accounts').select('id, slug, display_name').in('id', accountIds)
158
+ : Promise.resolve({ data: [] as any[] }),
159
+ adminDb.from('security_blocklist').select('type, value').is('revoked_at', null).or(`expires_at.is.null,expires_at.gt.${new Date().toISOString()}`),
160
+ ])
161
+
162
+ const personById = new Map((people || []).map((p: any) => [p.id, p]))
163
+ const accountById = new Map((accounts || []).map((a: any) => [a.id, a]))
164
+ const activeBlocks = new Set((bl || []).map((r: any) => `${r.type}:${r.value}`))
165
+
166
+ const rows = [...buckets.values()].map((b) => {
167
+ const p = b.person_id ? personById.get(b.person_id) : null
168
+ const acctId = b.account_id || p?.account_id || null
169
+ const acct = acctId ? accountById.get(acctId) : null
170
+ const email = p?.email || null
171
+ const ip = p?.data?.last_ip || p?.data?.registration_ip || null
172
+ const banned = !!(
173
+ (p && activeBlocks.has(`person_id:${p.id}`)) ||
174
+ (email && activeBlocks.has(`email:${String(email).toLowerCase()}`)) ||
175
+ (ip && activeBlocks.has(`ip:${ip}`)) ||
176
+ (acctId && activeBlocks.has(`account_id:${acctId}`))
177
+ )
178
+ return {
179
+ person_id: b.person_id,
180
+ email,
181
+ account_id: acctId,
182
+ account_name: acct?.display_name || acct?.slug || null,
183
+ last_ip: ip,
184
+ last_country: p?.data?.last_country || p?.data?.registration_country || null,
185
+ person_status: p?.status || null,
186
+ signals: b.signals,
187
+ kinds: [...b.kinds],
188
+ patterns: [...b.patterns],
189
+ last_seen_at: b.last_seen_at,
190
+ banned,
191
+ }
192
+ })
193
+
194
+ rows.sort((a, b) => b.signals - a.signals)
195
+ return { rows }
196
+ }
197
+ // CIL[END stable_id="~/.framework/functions/security.ts:flaggedaccounts:function"]
198
+
199
+ // CIL[BEGIN stable_id="~/.framework/functions/security.ts:addblockrows:function" type="function" identity="addBlockRows" parent="~/.framework/functions/security.ts"]
200
+ // CIL[DOC-MACRO purpose="Upsert active hard-block rows into security_blocklist"]
201
+ // CIL[DOC-RESPONSIBILITY responsibility="Insert rows with revoked_at cleared on (type,value) conflict so a re-ban re-activates a revoked row"]
202
+ // CIL[DOC-CONTRACT inputs="rows[], bannedBy, reason" outputs="void" errors="throws on DB error" side_effects="security_blocklist upsert"]
203
+ // CIL[DOC-MICRO logic_summary="map rows → upsert onConflict type,value with revoked_at:null" notable_decisions="[]"]
204
+ // CIL[DOC-STABILITY stability="stable"]
205
+ async function addBlockRows(
206
+ rows: Array<{ type: string; value: string }>,
207
+ bannedBy: string | undefined,
208
+ reason?: string,
209
+ ): Promise<void> {
210
+ const clean = rows.filter(r => r.value)
211
+ if (!clean.length) return
212
+ const { error } = await adminDb
213
+ .from('security_blocklist')
214
+ .upsert(
215
+ clean.map(r => ({
216
+ type: r.type,
217
+ value: r.value,
218
+ severity: 'hard',
219
+ reason: reason || null,
220
+ banned_by: bannedBy || null,
221
+ revoked_at: null,
222
+ expires_at: null,
223
+ })),
224
+ { onConflict: 'type,value' }
225
+ )
226
+ if (error) throw new Error('security_blocklist upsert failed: ' + error.message)
227
+ }
228
+ // CIL[END stable_id="~/.framework/functions/security.ts:addblockrows:function"]
229
+
230
+ // CIL[BEGIN stable_id="~/.framework/functions/security.ts:banuser:function" type="function" identity="banUser" parent="~/.framework/functions/security.ts"]
231
+ // CIL[DOC-MACRO purpose="Three-layer ban: internal blocklist + Supabase Auth ban + Netlify edge deny"]
232
+ // 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
+ // 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
+ // 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
+ // CIL[DOC-STABILITY stability="experimental"]
236
+ async function banUser(ctx: RequestContext, body: any) {
237
+ const { person_id, email: rawEmail, ip: rawIp, reason, block_ip = true, block_email = true } = body || {}
238
+ if (!person_id && !rawEmail && !rawIp) {
239
+ throw new Error('person_id, email, or ip is required')
240
+ }
241
+
242
+ let person: any = null
243
+ if (person_id) {
244
+ const { data } = await adminDb
245
+ .from('people')
246
+ .select('id, email, account_id, auth_uid, status, data')
247
+ .eq('id', person_id)
248
+ .maybeSingle()
249
+ if (!data) throw new Error(`Person not found: ${person_id}`)
250
+ person = data
251
+ } else if (rawEmail) {
252
+ const { data } = await adminDb
253
+ .from('people')
254
+ .select('id, email, account_id, auth_uid, status, data')
255
+ .eq('email', String(rawEmail).toLowerCase())
256
+ .maybeSingle()
257
+ person = data // may be null — banning a bare email is still valid
258
+ }
259
+
260
+ const email = (person?.email || rawEmail || '').toLowerCase() || null
261
+ const ip = person?.data?.last_ip || person?.data?.registration_ip || rawIp || null
262
+
263
+ const layers: Record<string, any> = { blocklist: 'pending', auth: 'skipped', edge: 'skipped' }
264
+
265
+ // Layer 1 (authoritative): internal blocklist rows.
266
+ const rows: Array<{ type: string; value: string }> = []
267
+ if (person?.id) rows.push({ type: 'person_id', value: person.id })
268
+ if (person?.account_id) rows.push({ type: 'account_id', value: person.account_id })
269
+ if (email && block_email) rows.push({ type: 'email', value: email })
270
+ if (ip && block_ip) rows.push({ type: 'ip', value: ip })
271
+ await addBlockRows(rows, ctx.principal?.id, reason)
272
+ layers.blocklist = 'applied'
273
+
274
+ // Layer 2: Supabase Auth ban — ~100y duration is effectively permanent.
275
+ if (person?.auth_uid) {
276
+ const { error: authErr } = await adminDb.auth.admin.updateUserById(person.auth_uid, {
277
+ ban_duration: '876600h',
278
+ })
279
+ layers.auth = authErr ? `error: ${authErr.message}` : 'applied'
280
+ }
281
+
282
+ // Layer 3: Netlify edge IP deny rule.
283
+ if (ip && block_ip) {
284
+ const edge = await netlifyEdgeSync([ip])
285
+ layers.edge = edge.synced ? 'applied' : edge.skipped ? 'skipped' : `error: ${edge.error}`
286
+ }
287
+
288
+ await emitAudit(ctx, 'security.user_banned', { type: 'person', id: person?.id, account_id: person?.account_id }, {
289
+ result: 'success',
290
+ target_email: email,
291
+ target_ip: ip,
292
+ reason: reason || null,
293
+ layers,
294
+ })
295
+
296
+ return { ok: true, person_id: person?.id || null, email, ip, layers }
297
+ }
298
+ // CIL[END stable_id="~/.framework/functions/security.ts:banuser:function"]
299
+
300
+ // CIL[BEGIN stable_id="~/.framework/functions/security.ts:unbanuser:function" type="function" identity="unbanUser" parent="~/.framework/functions/security.ts"]
301
+ // CIL[DOC-MACRO purpose="Reverse a ban: revoke blocklist rows, lift auth ban, remove edge IP"]
302
+ // 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
+ // 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
+ // 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
+ // CIL[DOC-STABILITY stability="experimental"]
306
+ async function unbanUser(ctx: RequestContext, body: any) {
307
+ const { person_id } = body || {}
308
+ if (!person_id) throw new Error('person_id is required')
309
+
310
+ const { data: person } = await adminDb
311
+ .from('people')
312
+ .select('id, email, account_id, auth_uid, data')
313
+ .eq('id', person_id)
314
+ .maybeSingle()
315
+ if (!person) throw new Error(`Person not found: ${person_id}`)
316
+
317
+ const email = (person.email || '').toLowerCase()
318
+ const ip = person.data?.last_ip || person.data?.registration_ip || null
319
+
320
+ const pairs: Array<{ type: string; value: string }> = [
321
+ { type: 'person_id', value: person.id },
322
+ ]
323
+ if (email) pairs.push({ type: 'email', value: email })
324
+ if (ip) pairs.push({ type: 'ip', value: ip })
325
+
326
+ const or = pairs.map(p => `and(type.eq.${p.type},value.eq.${p.value})`).join(',')
327
+ const { data: revoked } = await adminDb
328
+ .from('security_blocklist')
329
+ .update({ revoked_at: new Date().toISOString() })
330
+ .is('revoked_at', null)
331
+ .or(or)
332
+ .select('id')
333
+
334
+ const layers: Record<string, any> = { blocklist: `revoked ${revoked?.length || 0}`, auth: 'skipped', edge: 'skipped' }
335
+
336
+ if (person.auth_uid) {
337
+ const { error: authErr } = await adminDb.auth.admin.updateUserById(person.auth_uid, {
338
+ ban_duration: 'none',
339
+ })
340
+ layers.auth = authErr ? `error: ${authErr.message}` : 'applied'
341
+ }
342
+
343
+ if (ip) {
344
+ const edge = await netlifyEdgeSync([], [ip])
345
+ layers.edge = edge.synced ? 'applied' : edge.skipped ? 'skipped' : `error: ${edge.error}`
346
+ }
347
+
348
+ await emitAudit(ctx, 'security.user_unbanned', { type: 'person', id: person.id, account_id: person.account_id }, {
349
+ result: 'success',
350
+ target_email: email,
351
+ layers,
352
+ })
353
+
354
+ return { ok: true, person_id: person.id, layers }
355
+ }
356
+ // CIL[END stable_id="~/.framework/functions/security.ts:unbanuser:function"]
357
+
358
+ // CIL[BEGIN stable_id="~/.framework/functions/security.ts:blocklistcrud:comment" type="comment" identity="blocklistCrud" parent="~/.framework/functions/security.ts"]
359
+ // CIL[DOC-MACRO purpose="Blocklist list/add/revoke for the admin UI"]
360
+ // CIL[DOC-RESPONSIBILITY responsibility="Expose the security_blocklist table to system admins; ip entries sync to the Netlify edge on add/revoke"]
361
+ // 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
+ // CIL[DOC-STABILITY stability="experimental"]
363
+ async function listBlocklist() {
364
+ const { data, error } = await adminDb
365
+ .from('security_blocklist')
366
+ .select('id, type, value, severity, reason, banned_by, expires_at, revoked_at, created_at')
367
+ .order('created_at', { ascending: false })
368
+ .limit(500)
369
+ if (error) throw new Error('blocklist read failed: ' + error.message)
370
+ return { rows: data || [] }
371
+ }
372
+
373
+ async function blocklistAdd(ctx: RequestContext, body: any) {
374
+ const { type, value, severity = 'hard', reason, expires_at } = body || {}
375
+ if (!type || !value) throw new Error('type and value are required')
376
+ if (!['ip', 'email', 'email_domain', 'person_id', 'account_id'].includes(type)) {
377
+ throw new Error(`invalid type: ${type}`)
378
+ }
379
+ const normalized = type === 'email' || type === 'email_domain' ? String(value).toLowerCase() : String(value)
380
+
381
+ const { error } = await adminDb
382
+ .from('security_blocklist')
383
+ .upsert({
384
+ type, value: normalized, severity, reason: reason || null,
385
+ banned_by: ctx.principal?.id || null,
386
+ expires_at: expires_at || null,
387
+ revoked_at: null,
388
+ }, { onConflict: 'type,value' })
389
+ if (error) throw new Error('blocklist write failed: ' + error.message)
390
+
391
+ const edge = type === 'ip' ? await netlifyEdgeSync([normalized]) : { synced: false, skipped: true }
392
+ await emitAudit(ctx, 'security.blocklist_add', { type: 'security_blocklist' }, {
393
+ result: 'success', block_type: type, value: normalized, severity,
394
+ edge: edge.synced ? 'applied' : edge.skipped ? 'skipped' : `error: ${edge.error}`,
395
+ })
396
+ return { ok: true }
397
+ }
398
+
399
+ async function blocklistRevoke(ctx: RequestContext, body: any) {
400
+ const { id, type, value } = body || {}
401
+ if (!id && !(type && value)) throw new Error('id or (type, value) is required')
402
+
403
+ let query = adminDb
404
+ .from('security_blocklist')
405
+ .update({ revoked_at: new Date().toISOString() })
406
+ .is('revoked_at', null)
407
+ .select('id, type, value')
408
+ query = id ? query.eq('id', id) : query.eq('type', type).eq('value', String(value).toLowerCase())
409
+ const { data: revoked, error } = await query
410
+ if (error) throw new Error('blocklist revoke failed: ' + error.message)
411
+
412
+ const ip = (revoked || []).find((r: any) => r.type === 'ip')?.value
413
+ const edge = ip ? await netlifyEdgeSync([], [ip]) : { synced: false, skipped: true }
414
+ await emitAudit(ctx, 'security.blocklist_revoked', { type: 'security_blocklist', id }, {
415
+ result: 'success', revoked: revoked?.length || 0,
416
+ edge: edge.synced ? 'applied' : edge.skipped ? 'skipped' : `error: ${edge.error}`,
417
+ })
418
+ return { ok: true, revoked: revoked?.length || 0 }
419
+ }
420
+ // CIL[END stable_id="~/.framework/functions/security.ts:blocklistcrud:comment"]
421
+
422
+ // CIL[BEGIN stable_id="~/.framework/functions/security.ts:handler:constant" type="constant" identity="handler" parent="~/.framework/functions/security.ts"]
423
+ // CIL[DOC-MACRO purpose="Action dispatcher — system-admin only"]
424
+ // CIL[DOC-RESPONSIBILITY responsibility="Route ?action= to the security operations; every action is gated by requireSystemContextWithAudit"]
425
+ // CIL[DOC-CONTRACT inputs="ctx, body" outputs="action result JSON" errors="401/403 unauthenticated/non-admin; 400 unknown action" side_effects="delegated"]
426
+ // CIL[DOC-MICRO logic_summary="Gate once, then switch on ctx.query.action || body.action" notable_decisions="[]"]
427
+ // CIL[DOC-STABILITY stability="experimental"]
428
+ export const handler = createHandler(async (ctx, body) => {
429
+ const deny = requireSystemContextWithAudit(ctx, 'security_admin')
430
+ if (deny) return deny
431
+
432
+ const action = (ctx.query?.action as string) || (body?.action as string) || ''
433
+ switch (action) {
434
+ case 'flagged_accounts': return flaggedAccounts(ctx)
435
+ case 'ban_user': return banUser(ctx, body)
436
+ case 'unban_user': return unbanUser(ctx, body)
437
+ case 'blocklist': return listBlocklist()
438
+ case 'blocklist_add': return blocklistAdd(ctx, body)
439
+ case 'blocklist_revoke': return blocklistRevoke(ctx, body)
440
+ default: {
441
+ const e: any = new Error(`Unknown security action: ${action}`)
442
+ e.statusCode = 400
443
+ throw e
444
+ }
445
+ }
446
+ })
447
+ // CIL[END stable_id="~/.framework/functions/security.ts:handler:constant"]
448
+
449
+ // CIL[DOC-VECTOR type="context" content="purpose='System-admin security operations endpoint'; exposes flagged_accounts report, three-layer ban/unban, blocklist CRUD, Netlify edge IP sync" status="ready"]
450
+ // CIL[DOC-VECTOR type="blast_radius" content="middleware.ts enforcement reads security_blocklist; invites.ts registration gate; admin SecurityPage consumes this API" status="ready"]