spine-framework 0.3.144 → 0.4.0

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 +5051 -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 +20 -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
@@ -1,3 +1,14 @@
1
+ // CIL[BEGIN stable_id="~/.framework/functions/_shared/principal.ts:module_header:module_header" type="module_header" identity="module_header" parent="~/.framework/functions/_shared/principal.ts"]
2
+ // CIL[DOC-MACRO purpose="Shared principal and authorization module for Spine functions"]
3
+ // CIL[DOC-RESPONSIBILITY responsibility="Defines the Principal model, resolves request principals from headers and tokens, and provides role/scope predicates"]
4
+ // CIL[DOC-CONTEXT module="functions/_shared/principal" layer="shared" related_files=".framework/functions/_shared/db.ts"]
5
+ // CIL[DOC-CONTRACT inputs="HTTP event headers and tokens" outputs="Principal objects and authorization booleans" errors="Invalid or inactive credentials, rate limit exceeded, lookup errors" side_effects="Writes API key usage logs; updates people.auth_uid when linking by email"]
6
+ // CIL[DOC-MICRO logic_summary="Imports shared db/supabase clients and crypto; declares the Principal interface and anonymous/system constants; exports resolvers and helper functions" notable_decisions="[]"]
7
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
8
+ // CIL[DOC-STABILITY stability="stable"]
9
+ // CIL[DOC-AUDIENCE audience="framework developers, ai agents, security reviewers"]
10
+
11
+
1
12
 
2
13
 
3
14
 
@@ -14,36 +25,26 @@
14
25
 
15
26
 
16
27
 
17
- // CIL[BEGIN chunk_id="principal_imports_1784020784" type="module_header" identity="imports" parent="none"]
18
- // CIL[DOC-MACRO purpose="Import database, auth client, and WebSocket dependencies"]
19
- // CIL[DOC-RESPONSIBILITY responsibility="Import required modules for principal resolution"]
20
- // CIL[DOC-CONTEXT module="principal" layer="shared" related_files="[\"custom/apps/portal/pages/ContentPage.tsx\",\"custom/apps/cortex/pages/operations/OperationsDashboard.tsx\",\".framework/src/pages/admin/RolesPage.tsx\",\".framework/src/components/ui/dialog.tsx\",\"\\\"]"]
21
- // CIL[DOC-CONTRACT inputs="none" outputs="database, auth client, WebSocket" errors="none" side_effects="none"]
22
- // CIL[DOC-MICRO logic_summary="Import database connections, Supabase client, and WebSocket" notable_decisions="[]"]
23
- // CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
24
- // CIL[DOC-STABILITY stability="stable"]
25
- // CIL[DOC-AUDIENCE audience="developer"]
26
- // CIL[DOC-VECTOR type="context" content="Layer: shared. Direct callers: 0. Direct calls: 0. Related files: custom/apps/portal/pages/ContentPage.tsx, custom/apps/cortex/pages/operations/OperationsDashboard.tsx, .framework/src/pages/admin/RolesPage.tsx, .framework/src/components/ui/dialog.tsx, \."]
27
- // CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
28
- // CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
29
- // CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
30
- // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
31
- // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
32
28
  import { getUserDb, adminDb } from './db'
33
29
  import { createClient } from '@supabase/supabase-js'
34
30
  import WebSocket from 'ws'
35
- import { timingSafeEqual } from 'crypto'
36
- // CIL[END chunk_id="principal_imports_1784020784"]
37
- // CIL[BEGIN chunk_id="principal_internalsecret_1784020785" type="function" identity="isInternalInvokeAuthorized" parent="none"]
38
- // CIL[DOC-MACRO purpose="Gate internal-only principal headers behind a shared secret"]
39
- // CIL[DOC-RESPONSIBILITY responsibility="Prevent x-cron-id/x-trigger-id header spoofing on public endpoints"]
40
- // CIL[DOC-CONTEXT module="principal" layer="shared" related_files="[\".framework/functions/system-cron.ts\"]"]
41
- // CIL[DOC-CONTRACT inputs="event" outputs="boolean" errors="none" side_effects="none"]
42
- // CIL[DOC-MICRO logic_summary="Compares x-internal-secret header against INTERNAL_INVOKE_SECRET env var using a timing-safe comparison; returns false (deny) if the secret is unset or missing/mismatched" notable_decisions="[\"Without this gate, any caller could set x-cron-id/x-trigger-id and impersonate the associated machine principal by guessing or reusing a leaked schedule/trigger id.\"]"]
43
- // CIL[DOC-CONFIDENCE pass="first" score="0.95" flags="none"]
31
+ import { timingSafeEqual, createHash } from 'crypto'
32
+ // CIL[DOC-VECTOR type="context" content="purpose='Shared principal and authorization module for Spine functions'; No verified external callers; imports: .framework/functions/_shared/db.ts"]
33
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only"]
34
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified"]
35
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented"]
36
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
37
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
38
+ // CIL[END stable_id="~/.framework/functions/_shared/principal.ts:module_header:module_header"]
39
+
40
+ // CIL[BEGIN stable_id="~/.framework/functions/_shared/principal.ts:isInternalInvokeAuthorized:function" type="function" identity="isInternalInvokeAuthorized" parent="~/.framework/functions/_shared/principal.ts"]
41
+ // CIL[DOC-MACRO purpose="Verify that a request carries the trusted internal invocation secret"]
42
+ // CIL[DOC-RESPONSIBILITY responsibility="Validate shared secret header using timing-safe comparison"]
43
+ // CIL[DOC-CONTEXT module="functions/_shared/principal" layer="shared" related_files=".framework/functions/_shared/db.ts,.framework/functions/_shared/principal.ts"]
44
+ // CIL[DOC-CONTRACT inputs="event object with headers" outputs="boolean" errors="none (returns false on any failure)" side_effects="none"]
45
+ // CIL[DOC-MICRO logic_summary="Compare x-internal-secret against INTERNAL_INVOKE_SECRET using timingSafeEqual" notable_decisions="[]"]
46
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
44
47
  // CIL[DOC-STABILITY stability="stable"]
45
- // CIL[DOC-AUDIENCE audience="developer"]
46
- // CIL[DOC-NOTE type="caution" content="INTERNAL_INVOKE_SECRET must be set wherever the framework itself issues x-cron-id/x-trigger-id headers (currently no code path does), or those callers will silently fall back to ANONYMOUS_PRINCIPAL."]
47
48
  function isInternalInvokeAuthorized(event: any): boolean {
48
49
  const expected = (globalThis as any).process?.env?.INTERNAL_INVOKE_SECRET
49
50
  if (!expected) return false
@@ -51,32 +52,56 @@ function isInternalInvokeAuthorized(event: any): boolean {
51
52
  const provided = event.headers?.['x-internal-secret'] || event.headers?.['X-Internal-Secret']
52
53
  if (!provided || typeof provided !== 'string') return false
53
54
 
54
- const expectedBuf = Buffer.from(expected)
55
- const providedBuf = Buffer.from(provided)
56
- if (expectedBuf.length !== providedBuf.length) return false
55
+ // Hash both sides so the comparison always runs on equal-length (32-byte)
56
+ // inputs — an early-exit on length mismatch would leak the expected secret's
57
+ // length via response timing.
58
+ const expectedHash = createHash('sha256').update(expected).digest()
59
+ const providedHash = createHash('sha256').update(provided).digest()
57
60
 
58
61
  try {
59
- return timingSafeEqual(expectedBuf, providedBuf)
62
+ return timingSafeEqual(expectedHash, providedHash)
60
63
  } catch {
61
64
  return false
62
65
  }
63
66
  }
64
- // CIL[END chunk_id="principal_internalsecret_1784020785"]
65
- // CIL[BEGIN chunk_id="principal_authclient_1784020784" type="variable" identity="supabase" parent="none"]
66
- // CIL[DOC-MACRO purpose="Create Supabase auth client for JWT validation"]
67
- // CIL[DOC-RESPONSIBILITY responsibility="Initialize auth client with environment configuration"]
68
- // CIL[DOC-CONTEXT module="principal" layer="shared" related_files="[\".framework/functions/_shared/schema-utils.ts\",\".framework/src/pages/admin/TypeDetailPage.tsx\",\"custom/apps/cortex/pages/operations/OperationsDashboard.tsx\",\".framework/src/components/layout/Sidebar.tsx\",\"\\\"]"]
69
- // CIL[DOC-CONTRACT inputs="none" outputs="supabase client" errors="none" side_effects="none"]
70
- // CIL[DOC-MICRO logic_summary="Configures Supabase client with auth and realtime settings" notable_decisions="[]"]
71
- // CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
72
- // CIL[DOC-STABILITY stability="stable"]
73
- // CIL[DOC-VECTOR type="context" content="Layer: shared. Direct callers: 0. Direct calls: 0. Related files: .framework/functions/_shared/schema-utils.ts, .framework/src/pages/admin/TypeDetailPage.tsx, custom/apps/cortex/pages/operations/OperationsDashboard.tsx, .framework/src/components/layout/Sidebar.tsx, \."]
74
- // CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
75
- // CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
76
- // CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
67
+ // CIL[DOC-VECTOR type="context" content="purpose='Verify that a request carries the trusted internal invocation secret'; No verified external callers; imports: .framework/functions/_shared/db.ts"]
68
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only"]
69
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified"]
70
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented"]
77
71
  // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
78
72
  // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
73
+ // CIL[END stable_id="~/.framework/functions/_shared/principal.ts:isInternalInvokeAuthorized:function"]
74
+
75
+
76
+
77
+ // CIL[BEGIN stable_id="~/.framework/functions/_shared/principal.ts:env:constant" type="constant" identity="env" parent="~/.framework/functions/_shared/principal.ts"]
78
+ // CIL[DOC-MACRO purpose="Access process environment variables"]
79
+ // CIL[DOC-RESPONSIBILITY responsibility="Provide a safe fallback for environment lookups"]
80
+ // CIL[DOC-CONTEXT module="functions/_shared/principal" layer="shared" related_files=".framework/functions/_shared/db.ts,.framework/functions/_shared/principal.ts"]
81
+ // CIL[DOC-CONTRACT inputs="none" outputs="Record of environment variables" errors="none" side_effects="none"]
82
+ // CIL[DOC-MICRO logic_summary="Return global process.env or an empty object" notable_decisions="[]"]
83
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
84
+ // CIL[DOC-STABILITY stability="stable"]
79
85
  const env = (globalThis as any).process?.env || {}
86
+ // CIL[DOC-VECTOR type="context" content="purpose='Access process environment variables'; No verified external callers; imports: .framework/functions/_shared/db.ts"]
87
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only"]
88
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified"]
89
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented"]
90
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
91
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
92
+ // CIL[END stable_id="~/.framework/functions/_shared/principal.ts:env:constant"]
93
+
94
+
95
+
96
+
97
+ // CIL[BEGIN stable_id="~/.framework/functions/_shared/principal.ts:supabase:constant" type="constant" identity="supabase" parent="~/.framework/functions/_shared/principal.ts"]
98
+ // CIL[DOC-MACRO purpose="Create a Supabase client configured for function use"]
99
+ // CIL[DOC-RESPONSIBILITY responsibility="Provide an authenticated Supabase client with realtime transport"]
100
+ // CIL[DOC-CONTEXT module="functions/_shared/principal" layer="shared" related_files=".framework/functions/_shared/db.ts,.framework/functions/_shared/principal.ts"]
101
+ // CIL[DOC-CONTRACT inputs="none" outputs="Supabase client" errors="none" side_effects="none"]
102
+ // CIL[DOC-MICRO logic_summary="Instantiate createClient with URL, anon key and auth/realtime options" notable_decisions="[]"]
103
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
104
+ // CIL[DOC-STABILITY stability="stable"]
80
105
  const supabase = createClient(
81
106
  env.SUPABASE_URL || 'https://placeholder.supabase.co',
82
107
  env.SUPABASE_ANON_KEY || 'placeholder',
@@ -84,22 +109,26 @@ const supabase = createClient(
84
109
  auth: { autoRefreshToken: false, persistSession: false },
85
110
  realtime: { transport: WebSocket as any }
86
111
  }
112
+
113
+
87
114
  )
88
- // CIL[END chunk_id="principal_authclient_1784020784"]
89
- // CIL[BEGIN chunk_id="principal_interface_1784020784" type="interface" identity="Principal" parent="none"]
90
- // CIL[DOC-MACRO purpose="Define principal interface for authentication and authorization"]
91
- // CIL[DOC-RESPONSIBILITY responsibility="Provide comprehensive principal structure with provenance"]
92
- // CIL[DOC-CONTEXT module="principal" layer="shared" related_files="[\".framework/functions/_shared/schema-utils.ts\",\"custom/apps/portal/pages/ContentPage.tsx\",\".framework/src/components/layout/Sidebar.tsx\",\"custom/apps/cortex/pages/operations/OperationsDashboard.tsx\",\"\\\"]"]
93
- // CIL[DOC-CONTRACT inputs="none" outputs="Principal interface" errors="none" side_effects="none"]
94
- // CIL[DOC-MICRO logic_summary="Interface with identity, permissions, provenance, and auth context" notable_decisions="[]"]
95
- // CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
96
- // CIL[DOC-STABILITY stability="stable"]
97
- // CIL[DOC-VECTOR type="context" content="Layer: shared. Direct callers: 0. Direct calls: 0. Related files: .framework/functions/_shared/schema-utils.ts, custom/apps/portal/pages/ContentPage.tsx, .framework/src/components/layout/Sidebar.tsx, custom/apps/cortex/pages/operations/OperationsDashboard.tsx, \."]
98
- // CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
99
- // CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
100
- // CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
115
+ // CIL[DOC-VECTOR type="context" content="purpose='Create a Supabase client configured for function use'; No verified external callers; imports: .framework/functions/_shared/db.ts"]
116
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only"]
117
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified"]
118
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented"]
101
119
  // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
102
120
  // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
121
+ // CIL[END stable_id="~/.framework/functions/_shared/principal.ts:supabase:constant"]
122
+
123
+
124
+ // CIL[BEGIN stable_id="~/.framework/functions/_shared/principal.ts:Principal:interface" type="interface" identity="Principal" parent="~/.framework/functions/_shared/principal.ts"]
125
+ // CIL[DOC-MACRO purpose="Define the canonical identity shape used across Spine"]
126
+ // CIL[DOC-RESPONSIBILITY responsibility="Describe a principal's id, type, account, roles, scopes, machine metadata and provenance"]
127
+ // CIL[DOC-CONTEXT module="functions/_shared/principal" layer="shared" related_files=".framework/functions/_shared/db.ts,.framework/functions/_shared/principal.ts"]
128
+ // CIL[DOC-CONTRACT inputs="none" outputs="TypeScript interface" errors="none" side_effects="none"]
129
+ // CIL[DOC-MICRO logic_summary="Declare human or machine identity with provenance and optional auth context" notable_decisions="[]"]
130
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
131
+ // CIL[DOC-STABILITY stability="stable"]
103
132
  export interface Principal {
104
133
  id: string
105
134
  type: 'human' | 'machine'
@@ -123,26 +152,36 @@ export interface Principal {
123
152
  ipAddress?: string
124
153
  userAgent?: string
125
154
  }
155
+
156
+
157
+ // SECURITY: authContext carries raw credentials needed to build the
158
+ // user-scoped DB client (getPrincipalDb). It is transport-only — never pass
159
+ // a Principal to JSON.stringify, emitAudit, or any logging path. Use
160
+ // formatPrincipalForAudit() for serialized views. apiKey is deliberately
161
+ // absent: raw API keys are verified and discarded during resolution and must
162
+ // never be stored on objects that may be cached or serialized.
126
163
  authContext?: {
127
164
  jwt?: string
128
- apiKey?: string
129
165
  }
130
166
  }
131
- // CIL[END chunk_id="principal_interface_1784020784"]
132
- // CIL[BEGIN chunk_id="principal_anonymous_1784020784" type="constant" identity="ANONYMOUS_PRINCIPAL" parent="none"]
133
- // CIL[DOC-MACRO purpose="Define anonymous principal for unauthenticated access"]
134
- // CIL[DOC-RESPONSIBILITY responsibility="Provide fallback principal when no authentication present"]
135
- // CIL[DOC-CONTEXT module="principal" layer="shared" related_files="[\".framework/src/components/layout/Sidebar.tsx\",\".framework/functions/_shared/schema-utils.ts\",\".framework/src/pages/admin/TypeDetailPage.tsx\",\".framework/src/pages/admin/IntegrationsPage.tsx\",\"\\\"]"]
136
- // CIL[DOC-CONTRACT inputs="none" outputs="ANONYMOUS_PRINCIPAL constant" errors="none" side_effects="none"]
137
- // CIL[DOC-MICRO logic_summary="Machine principal with no permissions and manual provenance" notable_decisions="[]"]
138
- // CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
139
- // CIL[DOC-STABILITY stability="stable"]
140
- // CIL[DOC-VECTOR type="context" content="Layer: shared. Direct callers: 0. Direct calls: 0. Related files: .framework/src/components/layout/Sidebar.tsx, .framework/functions/_shared/schema-utils.ts, .framework/src/pages/admin/TypeDetailPage.tsx, .framework/src/pages/admin/IntegrationsPage.tsx, \."]
141
- // CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
142
- // CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
143
- // CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
167
+ // CIL[DOC-VECTOR type="context" content="purpose='Define the canonical identity shape used across Spine'; No verified external callers; imports: .framework/functions/_shared/db.ts"]
168
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only"]
169
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified"]
170
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented"]
144
171
  // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
145
172
  // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
173
+ // CIL[END stable_id="~/.framework/functions/_shared/principal.ts:Principal:interface"]
174
+
175
+
176
+
177
+ // CIL[BEGIN stable_id="~/.framework/functions/_shared/principal.ts:ANONYMOUS_PRINCIPAL:constant" type="constant" identity="ANONYMOUS_PRINCIPAL" parent="~/.framework/functions/_shared/principal.ts"]
178
+ // CIL[DOC-MACRO purpose="Provide a default principal for unauthenticated requests"]
179
+ // CIL[DOC-RESPONSIBILITY responsibility="Represent an anonymous machine principal with no scopes"]
180
+ // CIL[DOC-CONTEXT module="functions/_shared/principal" layer="shared" related_files=".framework/functions/_shared/db.ts,.framework/functions/_shared/principal.ts"]
181
+ // CIL[DOC-CONTRACT inputs="none" outputs="Principal" errors="none" side_effects="none"]
182
+ // CIL[DOC-MICRO logic_summary="Return an anonymous machine principal with manual provenance" notable_decisions="[]"]
183
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
184
+ // CIL[DOC-STABILITY stability="stable"]
146
185
  export const ANONYMOUS_PRINCIPAL: Principal = {
147
186
  id: 'anonymous',
148
187
  type: 'machine',
@@ -154,21 +193,27 @@ export const ANONYMOUS_PRINCIPAL: Principal = {
154
193
  invokedAt: new Date().toISOString()
155
194
  }
156
195
  }
157
- // CIL[END chunk_id="principal_anonymous_1784020784"]
158
- // CIL[BEGIN chunk_id="principal_system_1784020784" type="constant" identity="SYSTEM_PRINCIPAL" parent="none"]
159
- // CIL[DOC-MACRO purpose="Define system principal for internal operations"]
160
- // CIL[DOC-RESPONSIBILITY responsibility="Provide unrestricted internal principal for system tasks"]
161
- // CIL[DOC-CONTEXT module="principal" layer="shared" related_files="[\"custom/apps/cortex/pages/operations/OperationsDashboard.tsx\",\".framework/src/components/layout/Sidebar.tsx\",\".framework/functions/_shared/schema-utils.ts\",\".framework/src/pages/admin/TypeDetailPage.tsx\"]"]
162
- // CIL[DOC-CONTRACT inputs="none" outputs="SYSTEM_PRINCIPAL constant" errors="none" side_effects="none"]
163
- // CIL[DOC-MICRO logic_summary="Internal machine principal with full permissions" notable_decisions="[]"]
164
- // CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
165
- // CIL[DOC-STABILITY stability="stable"]
166
- // CIL[DOC-VECTOR type="context" content="Layer: shared. Direct callers: 0. Direct calls: 0. Related files: custom/apps/cortex/pages/operations/OperationsDashboard.tsx, .framework/src/components/layout/Sidebar.tsx, .framework/functions/_shared/schema-utils.ts, .framework/src/pages/admin/TypeDetailPage.tsx."]
167
- // CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
168
- // CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
169
- // CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
196
+ // CIL[DOC-VECTOR type="context" content="purpose='Provide a default principal for unauthenticated requests'; No verified external callers; imports: .framework/functions/_shared/db.ts"]
197
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only"]
198
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified"]
199
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented"]
170
200
  // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
171
201
  // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
202
+ // CIL[END stable_id="~/.framework/functions/_shared/principal.ts:ANONYMOUS_PRINCIPAL:constant"]
203
+
204
+
205
+
206
+
207
+
208
+
209
+ // CIL[BEGIN stable_id="~/.framework/functions/_shared/principal.ts:SYSTEM_PRINCIPAL:constant" type="constant" identity="SYSTEM_PRINCIPAL" parent="~/.framework/functions/_shared/principal.ts"]
210
+ // CIL[DOC-MACRO purpose="Provide a privileged internal principal for system operations"]
211
+ // CIL[DOC-RESPONSIBILITY responsibility="Represent the system machine principal with full scope"]
212
+ // CIL[DOC-CONTEXT module="functions/_shared/principal" layer="shared" related_files=".framework/functions/_shared/db.ts,.framework/functions/_shared/principal.ts"]
213
+ // CIL[DOC-CONTRACT inputs="none" outputs="Principal" errors="none" side_effects="none"]
214
+ // CIL[DOC-MICRO logic_summary="Return a machine principal with wildcard scope and internal flag" notable_decisions="[]"]
215
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
216
+ // CIL[DOC-STABILITY stability="stable"]
172
217
  export const SYSTEM_PRINCIPAL: Principal = {
173
218
  id: 'system',
174
219
  type: 'machine',
@@ -182,21 +227,26 @@ export const SYSTEM_PRINCIPAL: Principal = {
182
227
  invokedAt: new Date().toISOString()
183
228
  }
184
229
  }
185
- // CIL[END chunk_id="principal_system_1784020784"]
186
- // CIL[BEGIN chunk_id="principal_resolve_1784020784" type="function" identity="resolvePrincipal" parent="none"]
187
- // CIL[DOC-MACRO purpose="Resolve principal from event headers and authentication"]
188
- // CIL[DOC-RESPONSIBILITY responsibility="Determine caller identity from various auth methods"]
189
- // CIL[DOC-CONTEXT module="principal" layer="shared" related_files="[]"]
190
- // CIL[DOC-CONTRACT inputs="none" outputs="Principal" errors="authentication failures" side_effects="none"]
191
- // CIL[DOC-MICRO logic_summary="Checks API key, then (only if isInternalInvokeAuthorized) cron/trigger, then JWT auth in priority order" notable_decisions="[\"cron/trigger headers require INTERNAL_INVOKE_SECRET to be honored, closing a header-spoofing auth bypass\"]"]
192
- // CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
193
- // CIL[DOC-STABILITY stability="stable"]
194
- // CIL[DOC-VECTOR type="context" content="Layer: shared. Direct callers: 0. Direct calls: 0. Related files: none."]
195
- // CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
196
- // CIL[DOC-VECTOR type="configuration" content="Requires INTERNAL_INVOKE_SECRET env var for cron/trigger principal resolution."]
197
- // CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
230
+ // CIL[DOC-VECTOR type="context" content="purpose='Provide a privileged internal principal for system operations'; No verified external callers; imports: .framework/functions/_shared/db.ts"]
231
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only"]
232
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified"]
233
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented"]
198
234
  // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
199
235
  // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
236
+ // CIL[END stable_id="~/.framework/functions/_shared/principal.ts:SYSTEM_PRINCIPAL:constant"]
237
+
238
+
239
+
240
+
241
+
242
+ // CIL[BEGIN stable_id="~/.framework/functions/_shared/principal.ts:resolvePrincipal:function" type="function" identity="resolvePrincipal" parent="~/.framework/functions/_shared/principal.ts"]
243
+ // CIL[DOC-MACRO purpose="Resolve a Principal from incoming request headers"]
244
+ // CIL[DOC-RESPONSIBILITY responsibility="Dispatch to machine, cron, trigger or human resolution based on headers"]
245
+ // CIL[DOC-CONTEXT module="functions/_shared/principal" layer="shared" related_files=".framework/functions/_shared/db.ts,.framework/functions/_shared/principal.ts"]
246
+ // CIL[DOC-CONTRACT inputs="event object with headers" outputs="Principal" errors="Invalid or inactive credentials" side_effects="none"]
247
+ // CIL[DOC-MICRO logic_summary="Check x-api-key, internal cron/trigger headers, or Bearer token, falling back to ANONYMOUS_PRINCIPAL" notable_decisions="[]"]
248
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
249
+ // CIL[DOC-STABILITY stability="stable"]
200
250
  export async function resolvePrincipal(event: any): Promise<Principal> {
201
251
  const apiKey = event.headers?.['x-api-key'] || event.headers?.['X-Api-Key']
202
252
  if (apiKey) {
@@ -228,21 +278,22 @@ export async function resolvePrincipal(event: any): Promise<Principal> {
228
278
 
229
279
  return ANONYMOUS_PRINCIPAL
230
280
  }
231
- // CIL[END chunk_id="principal_resolve_1784020784"]
232
- // CIL[BEGIN chunk_id="principal_resolvemachine_1784020784" type="function" identity="resolveMachinePrincipal" parent="none"]
233
- // CIL[DOC-MACRO purpose="Resolve machine principal from API key"]
234
- // CIL[DOC-RESPONSIBILITY responsibility="Validate API key and return machine principal"]
235
- // CIL[DOC-CONTEXT module="principal" layer="shared" related_files="[]"]
236
- // CIL[DOC-CONTRACT inputs="none" outputs="Principal" errors="invalid API key" side_effects="database query"]
237
- // CIL[DOC-MICRO logic_summary="Validates machine principal via RPC and builds principal object" notable_decisions="[]"]
238
- // CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
239
- // CIL[DOC-STABILITY stability="stable"]
240
- // CIL[DOC-VECTOR type="context" content="Layer: shared. Direct callers: 0. Direct calls: 0. Related files: none."]
241
- // CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
242
- // CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
243
- // CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
281
+ // CIL[DOC-VECTOR type="context" content="purpose='Resolve a Principal from incoming request headers'; No verified external callers; imports: .framework/functions/_shared/db.ts"]
282
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only"]
283
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified"]
284
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented"]
244
285
  // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
245
286
  // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
287
+ // CIL[END stable_id="~/.framework/functions/_shared/principal.ts:resolvePrincipal:function"]
288
+
289
+ // CIL[BEGIN stable_id="~/.framework/functions/_shared/principal.ts:resolveMachinePrincipal:function" type="function" identity="resolveMachinePrincipal" parent="~/.framework/functions/_shared/principal.ts"]
290
+ // CIL[DOC-MACRO purpose="Resolve a machine principal from an API key"]
291
+ // CIL[DOC-RESPONSIBILITY responsibility="Validate key prefix and hash, enforce rate limits, and return the principal"]
292
+ // CIL[DOC-CONTEXT module="functions/_shared/principal" layer="shared" related_files=".framework/functions/_shared/db.ts,.framework/functions/_shared/principal.ts"]
293
+ // CIL[DOC-CONTRACT inputs="apiKey: string, event: any" outputs="Principal" errors="Invalid or inactive machine principal, rate limit exceeded" side_effects="Inserts api_key_usage_logs row"]
294
+ // CIL[DOC-MICRO logic_summary="Query api_keys by prefix, verify hash, count recent usage, log request, and build the principal" notable_decisions="[]"]
295
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
296
+ // CIL[DOC-STABILITY stability="stable"]
246
297
  async function resolveMachinePrincipal(apiKey: string, event: any): Promise<Principal> {
247
298
  const keyPrefix = apiKey.slice(0, 8)
248
299
 
@@ -251,14 +302,18 @@ async function resolveMachinePrincipal(apiKey: string, event: any): Promise<Prin
251
302
  .select('*')
252
303
  .eq('key_prefix', keyPrefix)
253
304
  .eq('is_active', true)
254
- .or(`expires_at.is.null,expires_at.gte.${new Date().toISOString()}`)
255
- .limit(1)
305
+ .order('created_at', { ascending: false })
306
+ .limit(10)
256
307
 
257
308
  if (error || !rows || rows.length === 0) {
258
309
  throw new Error('Invalid or inactive machine principal')
259
310
  }
260
311
 
261
- const key = rows[0]
312
+ const now = Date.now()
313
+ const key = rows.find((k: any) => !k.expires_at || new Date(k.expires_at).getTime() >= now)
314
+ if (!key) {
315
+ throw new Error('Invalid or inactive machine principal')
316
+ }
262
317
  const { verifyApiKey } = await import('./hash-api-key')
263
318
  if (!verifyApiKey(apiKey, key.key_hash)) {
264
319
  throw new Error('Invalid or inactive machine principal')
@@ -278,6 +333,32 @@ async function resolveMachinePrincipal(apiKey: string, event: any): Promise<Prin
278
333
  throw new Error('Rate limit exceeded for this API key')
279
334
  }
280
335
 
336
+ // Secondary per-IP ceiling: the per-key limit alone cannot catch a caller
337
+ // rotating through many valid keys. rate_limit_hit (migration 035) is the
338
+ // atomic DB counter — key on the trusted client IP + endpoint so expensive
339
+ // endpoints can be tuned separately. Fail-open: a limiter outage must not
340
+ // take down machine authentication.
341
+ const clientIp = getClientIp(event)
342
+ if (clientIp) {
343
+ const ipWindowSeconds = 60
344
+ const ipLimit = Number(env.SPINE_IP_RATE_LIMIT || 300)
345
+ const ipWindowStart = new Date(
346
+ Math.floor(now / (ipWindowSeconds * 1000)) * ipWindowSeconds * 1000
347
+ ).toISOString()
348
+ try {
349
+ const { data: ipCount } = await adminDb.rpc('rate_limit_hit', {
350
+ p_key: `ip:${clientIp}:${event.path || '/'}`,
351
+ p_window_start: ipWindowStart,
352
+ })
353
+ if (typeof ipCount === 'number' && ipCount > ipLimit) {
354
+ throw new Error('Rate limit exceeded for this address')
355
+ }
356
+ } catch (err: any) {
357
+ if (err?.message === 'Rate limit exceeded for this address') throw err
358
+ console.warn('IP rate limit check failed (fail-open):', err?.message)
359
+ }
360
+ }
361
+
281
362
  await adminDb.from('api_key_usage_logs').insert({
282
363
  api_key_id: key.id,
283
364
  account_id: key.account_id,
@@ -289,11 +370,14 @@ async function resolveMachinePrincipal(apiKey: string, event: any): Promise<Prin
289
370
  metadata: { source: 'resolve_machine_principal' }
290
371
  })
291
372
 
373
+ const isGlobal = (key.scopes || []).includes('*:*')
374
+
292
375
  return {
293
376
  id: key.id,
294
377
  type: 'machine',
295
378
  accountId: key.account_id,
296
379
  scopes: key.scopes || [],
380
+ roles: isGlobal ? ['system_admin'] : [],
297
381
  machineType: (key.machine_type as any) || 'integration',
298
382
  isInternal: key.is_internal,
299
383
  provenance: {
@@ -303,27 +387,30 @@ async function resolveMachinePrincipal(apiKey: string, event: any): Promise<Prin
303
387
  apiKeyId: key.id,
304
388
  ipAddress: getClientIp(event),
305
389
  userAgent: event.headers?.['user-agent'] || event.headers?.['User-Agent']
306
- },
307
- authContext: { apiKey }
390
+ }
391
+ // The raw API key is intentionally NOT stored on the principal — the key is
392
+ // verified above and discarded. getPrincipalDb() returns adminDb for machine
393
+ // principals regardless, so no downstream consumer needs the credential.
308
394
  }
309
395
  }
396
+ // CIL[DOC-VECTOR type="context" content="purpose='Resolve a machine principal from an API key'; No verified external callers; imports: .framework/functions/_shared/db.ts"]
397
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only"]
398
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified"]
399
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented"]
400
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
401
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
402
+ // CIL[END stable_id="~/.framework/functions/_shared/principal.ts:resolveMachinePrincipal:function"]
403
+
310
404
 
311
405
 
312
- // CIL[END chunk_id="principal_resolvemachine_1784020784"]
313
- // CIL[BEGIN chunk_id="principal_resolvecron_1784020784" type="function" identity="resolveCronPrincipal" parent="none"]
314
- // CIL[DOC-MACRO purpose="Resolve machine principal for scheduled execution"]
315
- // CIL[DOC-RESPONSIBILITY responsibility="Validate schedule and return associated machine principal"]
316
- // CIL[DOC-CONTEXT module="principal" layer="shared" related_files="[]"]
317
- // CIL[DOC-CONTRACT inputs="none" outputs="Principal" errors="invalid schedule" side_effects="database query"]
318
- // CIL[DOC-MICRO logic_summary="Validates schedule and creator, returns machine principal" notable_decisions="[]"]
319
- // CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
406
+ // CIL[BEGIN stable_id="~/.framework/functions/_shared/principal.ts:resolveCronPrincipal:function" type="function" identity="resolveCronPrincipal" parent="~/.framework/functions/_shared/principal.ts"]
407
+ // CIL[DOC-MACRO purpose="Resolve a machine principal for a cron schedule"]
408
+ // CIL[DOC-RESPONSIBILITY responsibility="Lookup a schedule, validate its creator, and return its associated machine principal"]
409
+ // CIL[DOC-CONTEXT module="functions/_shared/principal" layer="shared" related_files=".framework/functions/_shared/db.ts,.framework/functions/_shared/principal.ts"]
410
+ // CIL[DOC-CONTRACT inputs="scheduleId string" outputs="Principal" errors="Invalid or inactive schedule, validation failed" side_effects="none"]
411
+ // CIL[DOC-MICRO logic_summary="Fetch schedule with machine join, call validate_schedule_creator RPC, and build principal with cron provenance" notable_decisions="[]"]
412
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
320
413
  // CIL[DOC-STABILITY stability="stable"]
321
- // CIL[DOC-VECTOR type="context" content="Layer: shared. Direct callers: 0. Direct calls: 0. Related files: none."]
322
- // CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
323
- // CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
324
- // CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
325
- // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
326
- // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
327
414
  async function resolveCronPrincipal(scheduleId: string): Promise<Principal> {
328
415
  const { data: schedule, error: scheduleError } = await adminDb
329
416
  .from('schedules')
@@ -363,21 +450,22 @@ async function resolveCronPrincipal(scheduleId: string): Promise<Principal> {
363
450
  }
364
451
  }
365
452
  }
366
- // CIL[END chunk_id="principal_resolvecron_1784020784"]
367
- // CIL[BEGIN chunk_id="principal_resolvetrigger_1784020784" type="function" identity="resolveTriggerPrincipal" parent="none"]
368
- // CIL[DOC-MACRO purpose="Resolve machine principal for trigger execution"]
369
- // CIL[DOC-RESPONSIBILITY responsibility="Validate trigger and return configured machine principal"]
370
- // CIL[DOC-CONTEXT module="principal" layer="shared" related_files="[]"]
371
- // CIL[DOC-CONTRACT inputs="none" outputs="Principal" errors="invalid trigger" side_effects="database query"]
372
- // CIL[DOC-MICRO logic_summary="Validates trigger, finds action's machine principal, returns principal" notable_decisions="[]"]
373
- // CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
374
- // CIL[DOC-STABILITY stability="stable"]
375
- // CIL[DOC-VECTOR type="context" content="Layer: shared. Direct callers: 0. Direct calls: 0. Related files: none."]
376
- // CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
377
- // CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
378
- // CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
453
+ // CIL[DOC-VECTOR type="context" content="purpose='Resolve a machine principal for a cron schedule'; No verified external callers; imports: .framework/functions/_shared/db.ts"]
454
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only"]
455
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified"]
456
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented"]
379
457
  // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
380
458
  // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
459
+ // CIL[END stable_id="~/.framework/functions/_shared/principal.ts:resolveCronPrincipal:function"]
460
+
461
+ // CIL[BEGIN stable_id="~/.framework/functions/_shared/principal.ts:resolveTriggerPrincipal:function" type="function" identity="resolveTriggerPrincipal" parent="~/.framework/functions/_shared/principal.ts"]
462
+ // CIL[DOC-MACRO purpose="Resolve a machine principal for a trigger invocation"]
463
+ // CIL[DOC-RESPONSIBILITY responsibility="Load trigger, find its action's default machine, and return the principal"]
464
+ // CIL[DOC-CONTEXT module="functions/_shared/principal" layer="shared" related_files=".framework/functions/_shared/db.ts,.framework/functions/_shared/principal.ts"]
465
+ // CIL[DOC-CONTRACT inputs="triggerId: string, event: any" outputs="Principal" errors="Invalid trigger, no machine configured" side_effects="none"]
466
+ // CIL[DOC-MICRO logic_summary="Fetch trigger with action join, resolve default machine from api_keys, and build trigger provenance" notable_decisions="[]"]
467
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
468
+ // CIL[DOC-STABILITY stability="stable"]
381
469
  async function resolveTriggerPrincipal(triggerId: string, event: any): Promise<Principal> {
382
470
  const { data: trigger, error: triggerError } = await adminDb
383
471
  .from('triggers')
@@ -423,22 +511,57 @@ async function resolveTriggerPrincipal(triggerId: string, event: any): Promise<P
423
511
  }
424
512
  }
425
513
  }
426
- // CIL[END chunk_id="principal_resolvetrigger_1784020784"]
427
- // CIL[BEGIN chunk_id="principal_resolvehuman_1784020784" type="function" identity="resolveHumanPrincipal" parent="none"]
428
- // CIL[DOC-MACRO purpose="Resolve human principal from JWT token"]
429
- // CIL[DOC-RESPONSIBILITY responsibility="Validate JWT and return human principal with roles"]
430
- // CIL[DOC-CONTEXT module="principal" layer="shared" related_files="[]"]
431
- // CIL[DOC-CONTRACT inputs="none" outputs="Principal" errors="invalid token" side_effects="database query"]
432
- // CIL[DOC-MICRO logic_summary="Validates JWT, finds person record, builds principal with role" notable_decisions="[]"]
433
- // CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
434
- // CIL[DOC-STABILITY stability="stable"]
435
- // CIL[DOC-VECTOR type="context" content="Layer: shared. Direct callers: 0. Direct calls: 0. Related files: none."]
436
- // CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
437
- // CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
438
- // CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
514
+ // CIL[DOC-VECTOR type="context" content="purpose='Resolve a machine principal for a trigger invocation'; No verified external callers; imports: .framework/functions/_shared/db.ts"]
515
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only"]
516
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified"]
517
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented"]
439
518
  // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
440
519
  // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
520
+ // CIL[END stable_id="~/.framework/functions/_shared/principal.ts:resolveTriggerPrincipal:function"]
521
+
522
+ // CIL[BEGIN stable_id="~/.framework/functions/_shared/principal.ts:resolveHumanPrincipal:function" type="function" identity="resolveHumanPrincipal" parent="~/.framework/functions/_shared/principal.ts"]
523
+ // CIL[DOC-MACRO purpose="Resolve a human principal from a JWT bearer token"]
524
+ // CIL[DOC-RESPONSIBILITY responsibility="Validate Supabase user, map to internal person, and build the principal"]
525
+ // CIL[DOC-CONTEXT module="functions/_shared/principal" layer="shared" related_files=".framework/functions/_shared/db.ts,.framework/functions/_shared/principal.ts"]
526
+ // CIL[DOC-CONTRACT inputs="token: string, event: any" outputs="Principal" errors="Person lookup error, person not found" side_effects="none"]
527
+ // CIL[DOC-MICRO logic_summary="Get Supabase user, resolve internal person id, load person with role, and construct human principal; result cached ~60s keyed by token hash, provenance refreshed on hit; principals lacking both account_id and role_id are never cached" notable_decisions="cache avoids three sequential auth/people lookups on every polled request; provenance timestamps are refreshed per request so audit stays honest; unplaced (mid-registration, both fields null) principals skip the cache so placement lands on the next request instead of stalling 60s"]
528
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
529
+ // CIL[DOC-STABILITY stability="stable"]
530
+ // Module-level cache: sha256(token) → resolved human principal. Resolution
531
+ // costs 3 sequential round-trips (supabase.auth.getUser, person-id mapping,
532
+ // people+role join) and runs on every request — polling-heavy clients were
533
+ // paying ~1s of auth overhead per poll. 60s TTL bounds staleness for
534
+ // role/account changes. The authContext.jwt in the cached entry is only ever
535
+ // served back to the request that presented the same token. Principals for
536
+ // people missing BOTH account_id and role_id (registration placement still
537
+ // in flight) are never cached — see the comment at the cache write.
538
+ //
539
+ // SECURITY NOTE: role revocations, account deactivations, and account
540
+ // reassignments take up to 60 seconds to propagate to in-flight requests on
541
+ // warm function instances. This is a deliberate performance trade-off. Incident
542
+ // response: a full function redeploy clears the in-memory cache immediately. A
543
+ // DB-backed invalidation channel (e.g. a version counter on the people row)
544
+ // would close the window but costs a round-trip per cached hit.
545
+ const humanPrincipalCache = new Map<string, { principal: Principal; expiresAt: number }>()
546
+ const PRINCIPAL_CACHE_TTL_MS = 60_000
547
+ const PRINCIPAL_CACHE_MAX = 500
548
+
441
549
  async function resolveHumanPrincipal(token: string, event: any): Promise<Principal> {
550
+ const cacheKey = createHash('sha256').update(token).digest('hex')
551
+ const cached = humanPrincipalCache.get(cacheKey)
552
+ if (cached && cached.expiresAt > Date.now()) {
553
+ // Refresh request-scoped provenance so audit/log rows keep honest metadata
554
+ return {
555
+ ...cached.principal,
556
+ provenance: {
557
+ ...cached.principal.provenance,
558
+ invokedAt: new Date().toISOString(),
559
+ ipAddress: getClientIp(event),
560
+ userAgent: event.headers?.['user-agent'] || event.headers?.['User-Agent']
561
+ }
562
+ }
563
+ }
564
+
442
565
  const { data: { user }, error } = await supabase.auth.getUser(token)
443
566
 
444
567
  if (error || !user) {
@@ -464,7 +587,7 @@ async function resolveHumanPrincipal(token: string, event: any): Promise<Princip
464
587
  const role = Array.isArray(person.role) ? person.role[0] : person.role
465
588
  const roleSlugs = role?.slug ? [role.slug] : []
466
589
 
467
- return {
590
+ const principal: Principal = {
468
591
  id: personId,
469
592
  type: 'human',
470
593
  accountId: person.account_id || null,
@@ -480,32 +603,70 @@ async function resolveHumanPrincipal(token: string, event: any): Promise<Princip
480
603
  },
481
604
  authContext: { jwt: token }
482
605
  }
606
+
607
+ // Never cache an unplaced principal. Between signUp (people row created)
608
+ // and complete_registration_placement (account_id + role_id stamped
609
+ // atomically), requests would otherwise pin accountId:null for the full
610
+ // TTL — the documented ~60s "Account context required" stall. Both-null
611
+ // is the exact signature of "placement hasn't run yet" — a contact with
612
+ // an account but no role is a valid steady state and still caches.
613
+ if (!person.account_id && !person.role_id) {
614
+ return principal
615
+ }
616
+
617
+ if (humanPrincipalCache.size >= PRINCIPAL_CACHE_MAX) {
618
+ for (const [key, entry] of humanPrincipalCache) {
619
+ if (entry.expiresAt <= Date.now()) humanPrincipalCache.delete(key)
620
+ }
621
+ if (humanPrincipalCache.size >= PRINCIPAL_CACHE_MAX) humanPrincipalCache.clear()
622
+ }
623
+ humanPrincipalCache.set(cacheKey, { principal, expiresAt: Date.now() + PRINCIPAL_CACHE_TTL_MS })
624
+
625
+ return principal
483
626
  }
484
- // CIL[END chunk_id="principal_resolvehuman_1784020784"]
485
- // CIL[BEGIN chunk_id="principal_resolveinternal_1784020784" type="function" identity="resolveInternalPersonId" parent="none"]
486
- // CIL[DOC-MACRO purpose="Resolve internal person ID from auth user ID and email"]
487
- // CIL[DOC-RESPONSIBILITY responsibility="Map external auth ID to internal person record"]
488
- // CIL[DOC-CONTEXT module="principal" layer="shared" related_files="[]"]
489
- // CIL[DOC-CONTRACT inputs="none" outputs="internal person ID" errors="none" side_effects="database update"]
490
- // CIL[DOC-MICRO logic_summary="Looks up by email first, then auth ID, updates auth_uid if missing" notable_decisions="[]"]
491
- // CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
492
- // CIL[DOC-STABILITY stability="stable"]
493
- // CIL[DOC-VECTOR type="context" content="Layer: shared. Direct callers: 0. Direct calls: 0. Related files: none."]
494
- // CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
495
- // CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
496
- // CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
627
+ // CIL[DOC-VECTOR type="context" content="purpose='Resolve a human principal from a JWT bearer token'; No verified external callers; imports: .framework/functions/_shared/db.ts"]
628
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only"]
629
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified"]
630
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented"]
497
631
  // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
498
632
  // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
499
- async function resolveInternalPersonId(authUserId: string, email?: string): Promise<string> {
633
+ // CIL[END stable_id="~/.framework/functions/_shared/principal.ts:resolveHumanPrincipal:function"]
634
+
635
+ // CIL[BEGIN stable_id="~/.framework/functions/_shared/principal.ts:resolveInternalPersonId:function" type="function" identity="resolveInternalPersonId" parent="~/.framework/functions/_shared/principal.ts"]
636
+ // CIL[DOC-MACRO purpose="Map a Supabase auth user to an internal person id"]
637
+ // CIL[DOC-RESPONSIBILITY responsibility="Link auth user to person by email or auth_uid, falling back to the auth id"]
638
+ // CIL[DOC-CONTEXT module="functions/_shared/principal" layer="shared" related_files=".framework/functions/_shared/db.ts,.framework/functions/_shared/principal.ts"]
639
+ // CIL[DOC-CONTRACT inputs="authUserId: string, email?: string" outputs="person id string" errors="none" side_effects="Updates people.auth_uid when linking by email"]
640
+ // CIL[DOC-MICRO logic_summary="auth_uid lookup first (authoritative); email fallback only binds rows that are NOT invite stubs — invited/pending status or a stored invite_token is never claimed by email alone" notable_decisions="Invite stubs are claimed exclusively by the token-gated path in handle_new_auth_user (migration 052); binding one here by email would bypass token/expiry/revocation checks"]
641
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
642
+ // CIL[DOC-STABILITY stability="stable"]
643
+ // CIL[DOC-NOTE type="security" content="Registration/invitation hardening: previously this bound auth_uid to ANY person row matching the JWT email with auth_uid NULL — including unclaimed invite stubs — bypassing the invite token entirely."]
644
+ export async function resolveInternalPersonId(authUserId: string, email?: string): Promise<string> {
645
+ // auth_uid binding is authoritative — resolve it before any email fallback.
646
+ const { data: byAuthId } = await adminDb
647
+ .from('people')
648
+ .select('id')
649
+ .eq('auth_uid', authUserId)
650
+ .maybeSingle()
651
+
652
+ if (byAuthId) return byAuthId.id
653
+
500
654
  if (email) {
501
655
  const { data: byEmail } = await adminDb
502
656
  .from('people')
503
- .select('id, auth_uid')
657
+ .select('id, auth_uid, status, data')
504
658
  .eq('email', email)
505
659
  .maybeSingle()
506
-
660
+
507
661
  if (byEmail) {
508
662
  if (!byEmail.auth_uid) {
663
+ // Never claim an invite stub by email alone — only bind rows that
664
+ // are not invited/pending and carry no stored invite token.
665
+ const isInviteStub =
666
+ byEmail.status === 'invited' ||
667
+ byEmail.status === 'pending' ||
668
+ !!(byEmail.data && (byEmail.data as any).invite_token)
669
+ if (isInviteStub) return authUserId
509
670
  await adminDb
510
671
  .from('people')
511
672
  .update({ auth_uid: authUserId })
@@ -514,54 +675,54 @@ async function resolveInternalPersonId(authUserId: string, email?: string): Prom
514
675
  return byEmail.id
515
676
  }
516
677
  }
517
-
518
- const { data: byAuthId } = await adminDb
519
- .from('people')
520
- .select('id')
521
- .eq('auth_uid', authUserId)
522
- .maybeSingle()
523
-
524
- if (byAuthId) return byAuthId.id
525
-
678
+
526
679
  return authUserId
527
680
  }
528
- // CIL[END chunk_id="principal_resolveinternal_1784020784"]
529
- // CIL[BEGIN chunk_id="principal_getclientip_1784020784" type="function" identity="getClientIp" parent="none"]
530
- // CIL[DOC-MACRO purpose="Extract client IP address from event headers"]
531
- // CIL[DOC-RESPONSIBILITY responsibility="Parse various IP header formats for provenance"]
532
- // CIL[DOC-CONTEXT module="principal" layer="shared" related_files="[]"]
533
- // CIL[DOC-CONTRACT inputs="none" outputs="IP string or undefined" errors="none" side_effects="none"]
534
- // CIL[DOC-MICRO logic_summary="Checks common IP headers in priority order" notable_decisions="[]"]
535
- // CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
536
- // CIL[DOC-STABILITY stability="stable"]
537
- // CIL[DOC-VECTOR type="context" content="Layer: shared. Direct callers: 0. Direct calls: 0. Related files: none."]
538
- // CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
539
- // CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
540
- // CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
681
+ // CIL[DOC-VECTOR type="context" content="purpose='Map a Supabase auth user to an internal person id'; No verified external callers; imports: .framework/functions/_shared/db.ts"]
682
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only"]
683
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified"]
684
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented"]
541
685
  // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
542
686
  // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
687
+ // CIL[END stable_id="~/.framework/functions/_shared/principal.ts:resolveInternalPersonId:function"]
688
+
689
+ // CIL[BEGIN stable_id="~/.framework/functions/_shared/principal.ts:getClientIp:function" type="function" identity="getClientIp" parent="~/.framework/functions/_shared/principal.ts"]
690
+ // CIL[DOC-MACRO purpose="Extract the client IP from request headers or context"]
691
+ // CIL[DOC-RESPONSIBILITY responsibility="Return the most likely originating IP for a request"]
692
+ // CIL[DOC-CONTEXT module="functions/_shared/principal" layer="shared" related_files=".framework/functions/_shared/db.ts,.framework/functions/_shared/principal.ts"]
693
+ // CIL[DOC-CONTRACT inputs="event with headers and requestContext" outputs="ip string or undefined" errors="none" side_effects="none"]
694
+ // CIL[DOC-MICRO logic_summary="Prefer Netlify's x-nf-client-connection-ip (set by the edge, not client-controllable), then x-forwarded-for's first hop, x-real-ip, and requestContext.identity.sourceIp" notable_decisions="x-nf-client-connection-ip first because x-forwarded-for is client-spoofable and feeds rate limiting/provenance"]
695
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
696
+ // CIL[DOC-STABILITY stability="stable"]
543
697
  function getClientIp(event: any): string | undefined {
544
- return event.headers?.['x-forwarded-for'] ||
545
- event.headers?.['X-Forwarded-For'] ||
698
+ // x-nf-client-connection-ip is set by Netlify's edge and cannot be spoofed by
699
+ // the client; x-forwarded-for's first hop is client-controllable, so it is a
700
+ // fallback only. Used for provenance AND IP rate limiting — spoofability
701
+ // would let a caller mint fresh buckets by rotating the header.
702
+ return event.headers?.['x-nf-client-connection-ip'] ||
703
+ event.headers?.['X-Nf-Client-Connection-Ip'] ||
704
+ (event.headers?.['x-forwarded-for'] || event.headers?.['X-Forwarded-For'] || '').split(',')[0].trim() ||
546
705
  event.headers?.['x-real-ip'] ||
547
706
  event.headers?.['X-Real-Ip'] ||
548
- event.requestContext?.identity?.sourceIp
707
+ event.requestContext?.identity?.sourceIp ||
708
+ undefined
549
709
  }
550
- // CIL[END chunk_id="principal_getclientip_1784020784"]
551
- // CIL[BEGIN chunk_id="principal_machinescope_1784020784" type="function" identity="machineHasScope" parent="none"]
552
- // CIL[DOC-MACRO purpose="Check if machine principal has required scope"]
553
- // CIL[DOC-RESPONSIBILITY responsibility="Validate machine principal permissions"]
554
- // CIL[DOC-CONTEXT module="principal" layer="shared" related_files="[]"]
555
- // CIL[DOC-CONTRACT inputs="none" outputs="boolean" errors="none" side_effects="none"]
556
- // CIL[DOC-MICRO logic_summary="Checks exact scope, resource wildcard, or global wildcard" notable_decisions="[]"]
557
- // CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
558
- // CIL[DOC-STABILITY stability="stable"]
559
- // CIL[DOC-VECTOR type="context" content="Layer: shared. Direct callers: 0. Direct calls: 0. Related files: none."]
560
- // CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
561
- // CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
562
- // CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
710
+ // CIL[DOC-VECTOR type="context" content="purpose='Extract the client IP from request headers or context'; No verified external callers; imports: .framework/functions/_shared/db.ts"]
711
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only"]
712
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified"]
713
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented"]
563
714
  // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
564
715
  // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
716
+ // CIL[END stable_id="~/.framework/functions/_shared/principal.ts:getClientIp:function"]
717
+
718
+ // CIL[BEGIN stable_id="~/.framework/functions/_shared/principal.ts:machineHasScope:function" type="function" identity="machineHasScope" parent="~/.framework/functions/_shared/principal.ts"]
719
+ // CIL[DOC-MACRO purpose="Check whether a machine principal holds a given scope"]
720
+ // CIL[DOC-RESPONSIBILITY responsibility="Evaluate scope including resource wildcard and global wildcard matches"]
721
+ // CIL[DOC-CONTEXT module="functions/_shared/principal" layer="shared" related_files=".framework/functions/_shared/db.ts,.framework/functions/_shared/principal.ts"]
722
+ // CIL[DOC-CONTRACT inputs="principal: Principal, scope: string" outputs="boolean" errors="none" side_effects="none"]
723
+ // CIL[DOC-MICRO logic_summary="Return true if scope, resource wildcard, or global wildcard is present" notable_decisions="[]"]
724
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
725
+ // CIL[DOC-STABILITY stability="stable"]
565
726
  export function machineHasScope(principal: Principal, scope: string): boolean {
566
727
  if (principal.type !== 'machine') return false
567
728
 
@@ -576,58 +737,61 @@ export function machineHasScope(principal: Principal, scope: string): boolean {
576
737
 
577
738
  return false
578
739
  }
579
- // CIL[END chunk_id="principal_machinescope_1784020784"]
580
- // CIL[BEGIN chunk_id="principal_humanrole_1784020784" type="function" identity="humanHasRole" parent="none"]
581
- // CIL[DOC-MACRO purpose="Check if human principal has required role"]
582
- // CIL[DOC-RESPONSIBILITY responsibility="Validate human principal role membership"]
583
- // CIL[DOC-CONTEXT module="principal" layer="shared" related_files="[]"]
584
- // CIL[DOC-CONTRACT inputs="none" outputs="boolean" errors="none" side_effects="none"]
585
- // CIL[DOC-MICRO logic_summary="Checks if principal type is human and role is in roles array" notable_decisions="[]"]
586
- // CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
587
- // CIL[DOC-STABILITY stability="stable"]
588
- // CIL[DOC-VECTOR type="context" content="Layer: shared. Direct callers: 0. Direct calls: 0. Related files: none."]
589
- // CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
590
- // CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
591
- // CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
740
+ // CIL[DOC-VECTOR type="context" content="purpose='Check whether a machine principal holds a given scope'; No verified external callers; imports: .framework/functions/_shared/db.ts"]
741
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only"]
742
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified"]
743
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented"]
592
744
  // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
593
745
  // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
746
+ // CIL[END stable_id="~/.framework/functions/_shared/principal.ts:machineHasScope:function"]
747
+
748
+ // CIL[BEGIN stable_id="~/.framework/functions/_shared/principal.ts:humanHasRole:function" type="function" identity="humanHasRole" parent="~/.framework/functions/_shared/principal.ts"]
749
+ // CIL[DOC-MACRO purpose="Check whether a human principal has a role"]
750
+ // CIL[DOC-RESPONSIBILITY responsibility="Test principal roles array for a role slug"]
751
+ // CIL[DOC-CONTEXT module="functions/_shared/principal" layer="shared" related_files=".framework/functions/_shared/db.ts,.framework/functions/_shared/principal.ts"]
752
+ // CIL[DOC-CONTRACT inputs="principal: Principal, roleSlug: string" outputs="boolean" errors="none" side_effects="none"]
753
+ // CIL[DOC-MICRO logic_summary="Return true if principal is human and roles includes the slug" notable_decisions="[]"]
754
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
755
+ // CIL[DOC-STABILITY stability="stable"]
594
756
  export function humanHasRole(principal: Principal, roleSlug: string): boolean {
595
757
  if (principal.type !== 'human') return false
596
758
  return principal.roles?.includes(roleSlug) || false
597
759
  }
598
- // CIL[END chunk_id="principal_humanrole_1784020784"]
599
- // CIL[BEGIN chunk_id="principal_systemadmin_1784020784" type="function" identity="isSystemAdmin" parent="none"]
600
- // CIL[DOC-MACRO purpose="Check if principal is system administrator"]
601
- // CIL[DOC-RESPONSIBILITY responsibility="Validate system admin role for privileged operations"]
602
- // CIL[DOC-CONTEXT module="principal" layer="shared" related_files="[]"]
603
- // CIL[DOC-CONTRACT inputs="none" outputs="boolean" errors="none" side_effects="none"]
604
- // CIL[DOC-MICRO logic_summary="Checks if human principal has system_admin role" notable_decisions="[]"]
605
- // CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
606
- // CIL[DOC-STABILITY stability="stable"]
607
- // CIL[DOC-VECTOR type="context" content="Layer: shared. Direct callers: 0. Direct calls: 0. Related files: none."]
608
- // CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
609
- // CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
610
- // CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
760
+ // CIL[DOC-VECTOR type="context" content="purpose='Check whether a human principal has a role'; No verified external callers; imports: .framework/functions/_shared/db.ts"]
761
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only"]
762
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified"]
763
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented"]
611
764
  // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
612
765
  // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
766
+ // CIL[END stable_id="~/.framework/functions/_shared/principal.ts:humanHasRole:function"]
767
+
768
+ // CIL[BEGIN stable_id="~/.framework/functions/_shared/principal.ts:isSystemAdmin:function" type="function" identity="isSystemAdmin" parent="~/.framework/functions/_shared/principal.ts"]
769
+ // CIL[DOC-MACRO purpose="Determine if a principal is a system administrator"]
770
+ // CIL[DOC-RESPONSIBILITY responsibility="Check roles for system_admin by slug or object shape"]
771
+ // CIL[DOC-CONTEXT module="functions/_shared/principal" layer="shared" related_files=".framework/functions/_shared/db.ts,.framework/functions/_shared/principal.ts"]
772
+ // CIL[DOC-CONTRACT inputs="principal" outputs="boolean" errors="none" side_effects="none"]
773
+ // CIL[DOC-MICRO logic_summary="Return true if roles contains system_admin as a string or slug property" notable_decisions="[]"]
774
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
775
+ // CIL[DOC-STABILITY stability="stable"]
613
776
  export function isSystemAdmin(principal: Principal): boolean {
614
- return humanHasRole(principal, 'system_admin')
777
+ return principal?.roles?.some(r => r === 'system_admin' || r?.slug === 'system_admin') || false
615
778
  }
616
- // CIL[END chunk_id="principal_systemadmin_1784020784"]
617
- // CIL[BEGIN chunk_id="principal_getdb_1784020784" type="function" identity="getPrincipalDb" parent="none"]
618
- // CIL[DOC-MACRO purpose="Get appropriate database connection for principal"]
619
- // CIL[DOC-RESPONSIBILITY responsibility="Return user DB for humans, admin DB for machines"]
620
- // CIL[DOC-CONTEXT module="principal" layer="shared" related_files="[]"]
621
- // CIL[DOC-CONTRACT inputs="none" outputs="database connection" errors="none" side_effects="none"]
622
- // CIL[DOC-MICRO logic_summary="Returns user DB for authenticated humans, admin DB otherwise" notable_decisions="[]"]
623
- // CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
624
- // CIL[DOC-STABILITY stability="stable"]
625
- // CIL[DOC-VECTOR type="context" content="Layer: shared. Direct callers: 0. Direct calls: 0. Related files: none."]
626
- // CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
627
- // CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
628
- // CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
779
+ // CIL[DOC-VECTOR type="context" content="purpose='Determine if a principal is a system administrator'; No verified external callers; imports: .framework/functions/_shared/db.ts"]
780
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only"]
781
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified"]
782
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented"]
629
783
  // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
630
784
  // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
785
+ // CIL[END stable_id="~/.framework/functions/_shared/principal.ts:isSystemAdmin:function"]
786
+
787
+ // CIL[BEGIN stable_id="~/.framework/functions/_shared/principal.ts:getPrincipalDb:function" type="function" identity="getPrincipalDb" parent="~/.framework/functions/_shared/principal.ts"]
788
+ // CIL[DOC-MACRO purpose="Select the appropriate database client for a principal"]
789
+ // CIL[DOC-RESPONSIBILITY responsibility="Return user-scoped db for humans with JWT, otherwise admin db"]
790
+ // CIL[DOC-CONTEXT module="functions/_shared/principal" layer="shared" related_files=".framework/functions/_shared/db.ts,.framework/functions/_shared/principal.ts"]
791
+ // CIL[DOC-CONTRACT inputs="principal" outputs="Supabase client" errors="none" side_effects="none"]
792
+ // CIL[DOC-MICRO logic_summary="Use getUserDb with JWT for humans, otherwise return adminDb" notable_decisions="[]"]
793
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
794
+ // CIL[DOC-STABILITY stability="stable"]
631
795
  export function getPrincipalDb(principal: Principal) {
632
796
  if (principal.type === 'human' && principal.authContext?.jwt) {
633
797
  return getUserDb(principal.authContext.jwt)
@@ -635,21 +799,24 @@ export function getPrincipalDb(principal: Principal) {
635
799
 
636
800
  return adminDb
637
801
  }
638
- // CIL[END chunk_id="principal_getdb_1784020784"]
639
- // CIL[BEGIN chunk_id="principal_audit_1784020784" type="function" identity="formatPrincipalForAudit" parent="none"]
640
- // CIL[DOC-MACRO purpose="Format principal for audit logging"]
641
- // CIL[DOC-RESPONSIBILITY responsibility="Create audit-safe representation of principal"]
642
- // CIL[DOC-CONTEXT module="principal" layer="shared" related_files="[]"]
643
- // CIL[DOC-CONTRACT inputs="none" outputs="audit object" errors="none" side_effects="none"]
644
- // CIL[DOC-MICRO logic_summary="Returns principal fields formatted for audit logs" notable_decisions="[]"]
645
- // CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
646
- // CIL[DOC-STABILITY stability="stable"]
647
- // CIL[DOC-VECTOR type="context" content="Layer: shared. Direct callers: 0. Direct calls: 0. Related files: none."]
648
- // CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
649
- // CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
650
- // CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
802
+ // CIL[DOC-VECTOR type="context" content="purpose='Select the appropriate database client for a principal'; No verified external callers; imports: .framework/functions/_shared/db.ts"]
803
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only"]
804
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified"]
805
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented"]
651
806
  // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
652
807
  // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
808
+ // CIL[END stable_id="~/.framework/functions/_shared/principal.ts:getPrincipalDb:function"]
809
+
810
+ // CIL[BEGIN stable_id="~/.framework/functions/_shared/principal.ts:formatPrincipalForAudit:function" type="function" identity="formatPrincipalForAudit" parent="~/.framework/functions/_shared/principal.ts"]
811
+ // CIL[DOC-MACRO purpose="Serialize a principal for audit logs"]
812
+ // CIL[DOC-RESPONSIBILITY responsibility="Produce a safe, structured view of principal identity and provenance"]
813
+ // CIL[DOC-CONTEXT module="functions/_shared/principal" layer="shared" related_files=".framework/functions/_shared/db.ts,.framework/functions/_shared/principal.ts"]
814
+ // CIL[DOC-CONTRACT inputs="principal" outputs="audit object" errors="none" side_effects="none"]
815
+ // CIL[DOC-MICRO logic_summary="Return id, type, account_id, human/machine fields, and provenance spread" notable_decisions="[]"]
816
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
817
+ // CIL[DOC-STABILITY stability="stable"]
818
+ // Deliberately omits authContext — it holds the raw credential (JWT) and must
819
+ // never reach audit rows, log output, or any serialized payload.
653
820
  export function formatPrincipalForAudit(principal: Principal): object {
654
821
  return {
655
822
  id: principal.id,
@@ -667,4 +834,10 @@ export function formatPrincipalForAudit(principal: Principal): object {
667
834
  provenance: principal.provenance
668
835
  }
669
836
  }
670
- // CIL[END chunk_id="principal_audit_1784020784"]
837
+ // CIL[DOC-VECTOR type="context" content="purpose='Serialize a principal for audit logs'; No verified external callers; imports: .framework/functions/_shared/db.ts"]
838
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only"]
839
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified"]
840
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented"]
841
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
842
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
843
+ // CIL[END stable_id="~/.framework/functions/_shared/principal.ts:formatPrincipalForAudit:function"]