spine-framework 0.3.124 → 0.3.126

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 (319) hide show
  1. package/.framework/cli/commands/agents.ts +35 -37
  2. package/.framework/cli/commands/auth.ts +35 -34
  3. package/.framework/cli/commands/create-app.ts +104 -40
  4. package/.framework/cli/commands/dev.ts +104 -69
  5. package/.framework/cli/commands/doctor.ts +179 -69
  6. package/.framework/cli/commands/generate.ts +83 -143
  7. package/.framework/cli/commands/init.ts +72 -34
  8. package/.framework/cli/commands/install-app.ts +260 -125
  9. package/.framework/cli/commands/items.ts +33 -44
  10. package/.framework/cli/commands/migrate.ts +122 -40
  11. package/.framework/cli/commands/migrations.ts +33 -40
  12. package/.framework/cli/commands/pipelines.ts +31 -39
  13. package/.framework/cli/commands/status.ts +47 -23
  14. package/.framework/cli/commands/system.ts +47 -37
  15. package/.framework/cli/commands/test.ts +112 -68
  16. package/.framework/cli/commands/validate-app.ts +215 -121
  17. package/.framework/cli/context.ts +112 -130
  18. package/.framework/cli/env-loader.ts +47 -18
  19. package/.framework/cli/index.ts +81 -42
  20. package/.framework/docs/runtime-types.md +1024 -0
  21. package/.framework/functions/_shared/agent-runner.ts +1296 -961
  22. package/.framework/functions/_shared/app-manifest.ts +82 -68
  23. package/.framework/functions/_shared/audit.ts +135 -83
  24. package/.framework/functions/_shared/{cil-state.ts → cil.ts} +158 -56
  25. package/.framework/functions/_shared/db.ts +80 -126
  26. package/.framework/functions/_shared/hash-api-key.ts +63 -0
  27. package/.framework/functions/_shared/index.ts +224 -335
  28. package/.framework/functions/_shared/kb.ts +319 -0
  29. package/.framework/functions/_shared/llm.ts +92 -0
  30. package/.framework/functions/_shared/logging.ts +288 -0
  31. package/.framework/functions/_shared/middleware.ts +329 -365
  32. package/.framework/functions/_shared/outcomes.ts +36 -0
  33. package/.framework/functions/_shared/permissions.ts +359 -643
  34. package/.framework/functions/_shared/pipeline-runner.ts +243 -316
  35. package/.framework/functions/_shared/principal.ts +376 -468
  36. package/.framework/functions/_shared/resolve-credentials.ts +102 -0
  37. package/.framework/functions/_shared/resolve-ids.ts +188 -16
  38. package/.framework/functions/_shared/retry-deliveries.ts +104 -0
  39. package/.framework/functions/_shared/schema-utils.ts +611 -373
  40. package/.framework/functions/_shared/testing.ts +207 -137
  41. package/.framework/functions/_shared/tracing.ts +276 -0
  42. package/.framework/functions/_shared/trigger-engine.ts +159 -211
  43. package/.framework/functions/_shared/webhook-registration.ts +95 -81
  44. package/.framework/functions/_shared/webhook-registry.ts +153 -75
  45. package/.framework/functions/account-nodes.ts +31 -53
  46. package/.framework/functions/adapters/generic-webhook-inbound.ts +56 -0
  47. package/.framework/functions/adapters/generic-webhook-outbound.ts +43 -0
  48. package/.framework/functions/adapters/openai-outbound.ts +40 -0
  49. package/.framework/functions/admin-data.ts +685 -537
  50. package/.framework/functions/admin-funnel.ts +31 -20
  51. package/.framework/functions/admin-install-funnel.ts +47 -28
  52. package/.framework/functions/admin-signals.ts +47 -29
  53. package/.framework/functions/ai-agents.ts +122 -147
  54. package/.framework/functions/ai-interactions.ts +180 -90
  55. package/.framework/functions/api-keys.ts +250 -192
  56. package/.framework/functions/apps.ts +203 -197
  57. package/.framework/functions/auth.ts +98 -91
  58. package/.framework/functions/cil-agent-ingest.ts +773 -0
  59. package/.framework/functions/cil-agent.ts +592 -0
  60. package/.framework/functions/cil-callmap.ts +1227 -0
  61. package/.framework/functions/cil-config.ts +583 -0
  62. package/.framework/functions/cil-context.ts +445 -0
  63. package/.framework/functions/cil-doc.ts +1752 -0
  64. package/.framework/functions/cil-embeddings.ts +1202 -0
  65. package/.framework/functions/cil-graph.ts +585 -0
  66. package/.framework/functions/cil-ingest.ts +2416 -0
  67. package/.framework/functions/cil-pipeline.ts +318 -0
  68. package/.framework/functions/cil-project.ts +499 -0
  69. package/.framework/functions/cil-validate.ts +469 -0
  70. package/.framework/functions/debug-auth.ts +73 -53
  71. package/.framework/functions/deliveries.ts +117 -0
  72. package/.framework/functions/embeddings.ts +528 -516
  73. package/.framework/functions/eval-harness.ts +158 -106
  74. package/.framework/functions/integration-outbound.ts +311 -0
  75. package/.framework/functions/integration-routes.ts +240 -466
  76. package/.framework/functions/integrations.ts +132 -140
  77. package/.framework/functions/invites.ts +401 -201
  78. package/.framework/functions/item-progress.ts +113 -53
  79. package/.framework/functions/kb.ts +28 -0
  80. package/.framework/functions/logs.ts +239 -222
  81. package/.framework/functions/observability.ts +308 -259
  82. package/.framework/functions/pipeline-executions.ts +179 -235
  83. package/.framework/functions/pipelines.ts +163 -205
  84. package/.framework/functions/prompt-configs.ts +109 -156
  85. package/.framework/functions/roles.ts +97 -147
  86. package/.framework/functions/system-cron.ts +132 -218
  87. package/.framework/functions/system.ts +112 -52
  88. package/.framework/functions/tests.ts +32 -28
  89. package/.framework/functions/timers.ts +162 -210
  90. package/.framework/functions/triggers.ts +282 -346
  91. package/.framework/functions/types.ts +283 -400
  92. package/.framework/migrations/001_core_schema.sql +630 -0
  93. package/.framework/migrations/002_core_rls_and_functions.sql +532 -0
  94. package/.framework/migrations/{003_system_data.sql → 003_core_system_data.sql} +26 -153
  95. package/.framework/migrations/004_core_rpc_and_broker.sql +421 -0
  96. package/.framework/migrations/005_core_observability.sql +598 -0
  97. package/.framework/migrations/006_security_hardening_rls.sql +97 -0
  98. package/.framework/migrations/007_kb_article_list.sql +237 -0
  99. package/.framework/migrations/008_items_target.sql +8 -0
  100. package/.framework/migrations/009_roles_slug_unique.sql +11 -0
  101. package/.framework/public/spine.config.json +91 -0
  102. package/.framework/scripts/app-install-cli.ts +112 -98
  103. package/.framework/src/App.tsx +67 -20
  104. package/.framework/src/apps/admin/index.tsx +97 -27
  105. package/.framework/src/components/AppWrapper.tsx +50 -11
  106. package/.framework/src/components/CustomAppLoader.tsx +131 -22
  107. package/.framework/src/components/admin/AdminListPage.tsx +75 -8
  108. package/.framework/src/components/admin/AdminSidebar.tsx +148 -36
  109. package/.framework/src/components/admin/AdminStatsCard.tsx +48 -29
  110. package/.framework/src/components/admin/PageInstructions.tsx +49 -0
  111. package/.framework/src/components/admin/SortableTableHeader.tsx +49 -2
  112. package/.framework/src/components/app-shell/GenericAppShell.tsx +69 -11
  113. package/.framework/src/components/app-shell/GenericDetailPage.tsx +55 -10
  114. package/.framework/src/components/app-shell/GenericListPage.tsx +76 -9
  115. package/.framework/src/components/app-sidebar.tsx +50 -3
  116. package/.framework/src/components/auth/ProtectedRoute.tsx +53 -45
  117. package/.framework/src/components/kb/KBArticleEditor.tsx +409 -0
  118. package/.framework/src/components/kb/KBArticleList.tsx +557 -0
  119. package/.framework/src/components/layout/AppShell.tsx +54 -16
  120. package/.framework/src/components/layout/Header.tsx +50 -38
  121. package/.framework/src/components/layout/Layout.tsx +48 -32
  122. package/.framework/src/components/layout/Sidebar.tsx +167 -16
  123. package/.framework/src/components/runtime/DataDetailHeader.tsx +33 -1
  124. package/.framework/src/components/runtime/DataDetailPage.tsx +289 -4
  125. package/.framework/src/components/runtime/DataFilters.tsx +78 -1
  126. package/.framework/src/components/runtime/DataHeader.tsx +32 -29
  127. package/.framework/src/components/runtime/DataListPage.tsx +161 -2
  128. package/.framework/src/components/runtime/DataStats.tsx +48 -1
  129. package/.framework/src/components/runtime/DataTable.tsx +153 -1
  130. package/.framework/src/components/runtime/SchemaDetailForm.tsx +77 -46
  131. package/.framework/src/components/runtime/index.ts +16 -9
  132. package/.framework/src/components/search-form.tsx +33 -1
  133. package/.framework/src/components/shared/AgentView.tsx +48 -51
  134. package/.framework/src/components/shared/FieldRenderer.tsx +84 -67
  135. package/.framework/src/components/shared/InviteManager.tsx +113 -52
  136. package/.framework/src/components/shared/MemberList.tsx +182 -0
  137. package/.framework/src/components/shared/SchemaFields.tsx +96 -78
  138. package/.framework/src/components/ui/DataTable.tsx +86 -56
  139. package/.framework/src/components/ui/Form.tsx +188 -109
  140. package/.framework/src/components/ui/ItemCard.tsx +86 -58
  141. package/.framework/src/components/ui/ItemListView.tsx +88 -61
  142. package/.framework/src/components/ui/LoadingSpinner.tsx +52 -27
  143. package/.framework/src/components/ui/Modal.tsx +81 -4
  144. package/.framework/src/components/ui/RichTextEditor.tsx +101 -9
  145. package/.framework/src/components/ui/accordion.tsx +97 -5
  146. package/.framework/src/components/ui/alert-dialog.tsx +225 -13
  147. package/.framework/src/components/ui/alert.tsx +113 -6
  148. package/.framework/src/components/ui/aspect-ratio.tsx +49 -2
  149. package/.framework/src/components/ui/avatar.tsx +129 -7
  150. package/.framework/src/components/ui/badge.tsx +65 -3
  151. package/.framework/src/components/ui/breadcrumb.tsx +145 -8
  152. package/.framework/src/components/ui/button-group.tsx +81 -4
  153. package/.framework/src/components/ui/button.tsx +49 -2
  154. package/.framework/src/components/ui/calendar.tsx +65 -3
  155. package/.framework/src/components/ui/card.tsx +145 -8
  156. package/.framework/src/components/ui/carousel.tsx +129 -7
  157. package/.framework/src/components/ui/chart.tsx +161 -10
  158. package/.framework/src/components/ui/checkbox.tsx +49 -2
  159. package/.framework/src/components/ui/collapsible.tsx +81 -5
  160. package/.framework/src/components/ui/combobox.tsx +305 -20
  161. package/.framework/src/components/ui/command.tsx +177 -11
  162. package/.framework/src/components/ui/context-menu.tsx +273 -17
  163. package/.framework/src/components/ui/dialog.tsx +209 -27
  164. package/.framework/src/components/ui/direction.tsx +64 -4
  165. package/.framework/src/components/ui/drawer.tsx +193 -12
  166. package/.framework/src/components/ui/dropdown-menu.tsx +273 -18
  167. package/.framework/src/components/ui/empty.tsx +145 -9
  168. package/.framework/src/components/ui/field.tsx +209 -14
  169. package/.framework/src/components/ui/hover-card.tsx +81 -4
  170. package/.framework/src/components/ui/input-group.tsx +162 -10
  171. package/.framework/src/components/ui/input-otp.tsx +98 -5
  172. package/.framework/src/components/ui/input.tsx +49 -2
  173. package/.framework/src/components/ui/item.tsx +225 -13
  174. package/.framework/src/components/ui/kbd.tsx +65 -3
  175. package/.framework/src/components/ui/label.tsx +49 -2
  176. package/.framework/src/components/ui/menubar.tsx +289 -17
  177. package/.framework/src/components/ui/native-select.tsx +97 -5
  178. package/.framework/src/components/ui/navigation-menu.tsx +177 -10
  179. package/.framework/src/components/ui/pagination.tsx +161 -9
  180. package/.framework/src/components/ui/popover.tsx +145 -8
  181. package/.framework/src/components/ui/progress.tsx +49 -2
  182. package/.framework/src/components/ui/radio-group.tsx +65 -3
  183. package/.framework/src/components/ui/resizable.tsx +82 -4
  184. package/.framework/src/components/ui/scroll-area.tsx +65 -3
  185. package/.framework/src/components/ui/select.tsx +193 -11
  186. package/.framework/src/components/ui/separator.tsx +49 -2
  187. package/.framework/src/components/ui/sheet.tsx +193 -11
  188. package/.framework/src/components/ui/sidebar.tsx +481 -38
  189. package/.framework/src/components/ui/skeleton.tsx +49 -2
  190. package/.framework/src/components/ui/slider.tsx +49 -2
  191. package/.framework/src/components/ui/sonner.tsx +49 -2
  192. package/.framework/src/components/ui/spinner.tsx +49 -2
  193. package/.framework/src/components/ui/switch.tsx +49 -2
  194. package/.framework/src/components/ui/table-primitives.tsx +160 -33
  195. package/.framework/src/components/ui/table.tsx +161 -9
  196. package/.framework/src/components/ui/tabs.tsx +113 -6
  197. package/.framework/src/components/ui/textarea.tsx +49 -2
  198. package/.framework/src/components/ui/toggle-group.tsx +81 -4
  199. package/.framework/src/components/ui/toggle.tsx +65 -3
  200. package/.framework/src/components/ui/tooltip.tsx +97 -5
  201. package/.framework/src/contexts/AppContext.tsx +129 -45
  202. package/.framework/src/contexts/AuthContext.tsx +187 -144
  203. package/.framework/src/hooks/use-mobile.ts +48 -2
  204. package/.framework/src/hooks/useApi.ts +208 -159
  205. package/.framework/src/hooks/useAppPath.ts +32 -23
  206. package/.framework/src/hooks/useApps.ts +51 -20
  207. package/.framework/src/hooks/useEntityList.ts +32 -78
  208. package/.framework/src/hooks/useEntityRecord.ts +35 -96
  209. package/.framework/src/hooks/useForm.ts +32 -92
  210. package/.framework/src/hooks/useListSchema.ts +49 -78
  211. package/.framework/src/hooks/useSchemaRecord.ts +114 -105
  212. package/.framework/src/lib/api.ts +195 -116
  213. package/.framework/src/lib/supabase.ts +98 -64
  214. package/.framework/src/lib/utils.ts +287 -151
  215. package/.framework/src/main.tsx +49 -3
  216. package/.framework/src/pages/DashboardPage.tsx +52 -29
  217. package/.framework/src/pages/NotFoundPage.tsx +32 -10
  218. package/.framework/src/pages/SetupPage.tsx +32 -12
  219. package/.framework/src/pages/admin/AIAgentDetailPage.tsx +119 -10
  220. package/.framework/src/pages/admin/AIAgentsPage.tsx +48 -19
  221. package/.framework/src/pages/admin/APIKeyDetailPage.tsx +187 -17
  222. package/.framework/src/pages/admin/APIKeysPage.tsx +98 -22
  223. package/.framework/src/pages/admin/AlertsConfigPage.tsx +113 -26
  224. package/.framework/src/pages/admin/AppDetailPage.tsx +125 -8
  225. package/.framework/src/pages/admin/AppsPage.tsx +49 -2
  226. package/.framework/src/pages/admin/DeliveriesPage.tsx +196 -0
  227. package/.framework/src/pages/admin/DesignedPage.tsx +32 -18
  228. package/.framework/src/pages/admin/EmbeddingDetailPage.tsx +215 -484
  229. package/.framework/src/pages/admin/EmbeddingsPage.tsx +116 -319
  230. package/.framework/src/pages/admin/ExtendedShadcnTestPage.tsx +34 -9
  231. package/.framework/src/pages/admin/IncrementalShadcnTestPage.tsx +34 -9
  232. package/.framework/src/pages/admin/IntegratedDashboard.tsx +32 -16
  233. package/.framework/src/pages/admin/IntegrationDetailPage.tsx +183 -26
  234. package/.framework/src/pages/admin/IntegrationsPage.tsx +48 -19
  235. package/.framework/src/pages/admin/InvitesPage.tsx +48 -14
  236. package/.framework/src/pages/admin/LogsPage.tsx +422 -75
  237. package/.framework/src/pages/admin/MinimalShadcnTestPage.tsx +34 -9
  238. package/.framework/src/pages/admin/ObservabilityDashboard.tsx +113 -24
  239. package/.framework/src/pages/admin/PipelineDetailPage.tsx +206 -34
  240. package/.framework/src/pages/admin/PipelineExecutionsPage.tsx +50 -17
  241. package/.framework/src/pages/admin/PipelinesPage.tsx +160 -21
  242. package/.framework/src/pages/admin/PromptConfigDetailPage.tsx +378 -88
  243. package/.framework/src/pages/admin/PromptConfigsPage.tsx +125 -16
  244. package/.framework/src/pages/admin/ProperlyDesignedPage.tsx +49 -10
  245. package/.framework/src/pages/admin/RoleDetailPage.tsx +130 -10
  246. package/.framework/src/pages/admin/RolesPage.tsx +171 -18
  247. package/.framework/src/pages/admin/SelectTestPage.tsx +34 -9
  248. package/.framework/src/pages/admin/ShadcnTestPage.tsx +49 -11
  249. package/.framework/src/pages/admin/SimpleDashboard.tsx +49 -10
  250. package/.framework/src/pages/admin/TestRunDetailPage.tsx +49 -4
  251. package/.framework/src/pages/admin/TestingDashboard.tsx +98 -28
  252. package/.framework/src/pages/admin/TimerDetailPage.tsx +143 -33
  253. package/.framework/src/pages/admin/TimersPage.tsx +48 -20
  254. package/.framework/src/pages/admin/TriggerDetailPage.tsx +118 -30
  255. package/.framework/src/pages/admin/TriggersPage.tsx +64 -19
  256. package/.framework/src/pages/admin/TypeDetailPage.tsx +138 -15
  257. package/.framework/src/pages/admin/TypesPage.tsx +48 -20
  258. package/.framework/src/pages/admin/cil/CILAgentPage.tsx +465 -0
  259. package/.framework/src/pages/admin/cil/CILChunkDetailPage.tsx +787 -0
  260. package/.framework/src/pages/admin/cil/CILDashboardPage.tsx +699 -0
  261. package/.framework/src/pages/admin/cil/CILFileListPage.tsx +647 -0
  262. package/.framework/src/pages/admin/cil/CILGraphPage.tsx +616 -0
  263. package/.framework/src/pages/admin/cil/CILIngestionPage.tsx +1402 -0
  264. package/.framework/src/pages/admin/cil/CILProjectsPage.tsx +614 -0
  265. package/.framework/src/pages/admin/cil/CILSearchPage.tsx +571 -0
  266. package/.framework/src/pages/admin/cil/FlexiblePipelinePage.tsx +731 -0
  267. package/.framework/src/pages/admin/kb/KBAdminPage.tsx +64 -0
  268. package/.framework/src/pages/auth/AuthConfirmPage.tsx +52 -51
  269. package/.framework/src/pages/auth/ForgotPasswordPage.tsx +31 -22
  270. package/.framework/src/pages/auth/LoginPage.tsx +50 -23
  271. package/.framework/src/pages/auth/PendingApprovalPage.tsx +31 -15
  272. package/.framework/src/pages/auth/RegisterPage.tsx +281 -317
  273. package/.framework/src/pages/spine-framework/APIPage.tsx +33 -0
  274. package/.framework/src/pages/spine-framework/CLIPage.tsx +33 -0
  275. package/.framework/src/types/auth.ts +128 -71
  276. package/.framework/src/types/types.ts +373 -157
  277. package/custom/.devin/skills/spine-workbench-discovery/SKILL.md +39 -0
  278. package/custom/.devin/skills/spine-workbench-idea/SKILL.md +36 -0
  279. package/custom/.devin/skills/spine-workbench-implement/SKILL.md +46 -0
  280. package/custom/.devin/skills/spine-workbench-initiative/SKILL.md +28 -0
  281. package/custom/.devin/skills/spine-workbench-plan/SKILL.md +38 -0
  282. package/docs/README.md +9 -130
  283. package/netlify.toml +5 -2
  284. package/package.json +13 -25
  285. package/scripts/kb-ingest-docs.ts +203 -0
  286. package/spine.config.json +65 -4
  287. package/.framework/migrations/001_schema.sql +0 -425
  288. package/.framework/migrations/002_rls_and_functions.sql +0 -763
  289. package/.framework/migrations/004_enhancements.sql +0 -43
  290. package/.framework/migrations/005_auth_user_trigger.sql +0 -105
  291. package/.framework/migrations/006_fix_current_actor_id.sql +0 -21
  292. package/.framework/migrations/016_invites.sql +0 -27
  293. package/.framework/migrations/017_fix_get_account_apps.sql +0 -78
  294. package/.framework/migrations/018_apps_required_roles.sql +0 -17
  295. package/.framework/migrations/019_triggers_unique_constraint.sql +0 -13
  296. package/.framework/migrations/020_get_account_apps_role_filter.sql +0 -117
  297. package/.framework/migrations/021_app_domains.sql +0 -112
  298. package/.framework/migrations/022_route_prefix_domain_unique.sql +0 -21
  299. package/.framework/migrations/023_match_embeddings_rpc.sql +0 -78
  300. package/.framework/migrations/024_messages_account_id.sql +0 -9
  301. package/.framework/migrations/025_retype_threads_messages.sql +0 -240
  302. package/.framework/migrations/026_fix_rls_policies.sql +0 -47
  303. package/.framework/migrations/027_embedding_lifecycle.sql +0 -176
  304. package/.framework/migrations/028_fix_message_data_permissions.sql +0 -49
  305. package/.framework/migrations/029_tighten_support_triage_prompt.sql +0 -41
  306. package/.framework/migrations/030_messages_updated_by.sql +0 -6
  307. package/.framework/migrations/031_seed_upsert_constraints.sql +0 -35
  308. package/docs/APPS.md +0 -232
  309. package/docs/ARCHITECTURE.md +0 -246
  310. package/docs/INSTALL.md +0 -155
  311. package/docs/design-schema-spec.md +0 -1837
  312. package/docs/dev-guides/01-create-an-app.md +0 -183
  313. package/docs/dev-guides/02-create-a-function.md +0 -178
  314. package/docs/dev-guides/03-create-a-component.md +0 -178
  315. package/docs/dev-guides/04-webhook-handlers.md +0 -129
  316. package/docs/dev-guides/05-testing.md +0 -168
  317. package/docs/dev-guides/README.md +0 -40
  318. package/docs/npm-packages-plan.md +0 -288
  319. package/docs/spine-core--api-reference.md +0 -822
@@ -1,44 +1,81 @@
1
- /**
2
- * @module principal
3
- * @audience both
4
- * @layer shared-core
5
- * @stability stable
6
- *
7
- * Unified identity abstraction for all actors in Spine v2. Every request —
8
- * whether from a human via JWT, an integration via API key, a scheduled cron
9
- * job, or an internal trigger — resolves to a single `Principal` object before
10
- * any permission check or DB query occurs.
11
- *
12
- * Resolution order in `resolvePrincipal`:
13
- * 1. `x-api-key` header → machine principal (external integration)
14
- * 2. `x-cron-id` header → machine principal (scheduled job)
15
- * 3. `x-trigger-id` header → machine principal (event trigger)
16
- * 4. `Authorization: Bearer <jwt>` → human principal
17
- * 5. (none) → ANONYMOUS_PRINCIPAL (rejected by createHandler)
18
- *
19
- * INVARIANT: `resolvePrincipal` always returns a Principal, never null.
20
- * ANONYMOUS_PRINCIPAL is the sentinel for unauthenticated requests and is
21
- * rejected by `createHandler` before the handler runs.
22
- * INVARIANT: `adminDb` is used for all principal resolution lookups to avoid
23
- * circular dependencies with RLS (which itself depends on the resolved principal).
24
- * INVARIANT: never store `authContext.jwt` or `authContext.apiKey` in logs.
25
- * Use `formatPrincipalForAudit` which strips these fields.
26
- *
27
- * @seeAlso db.ts (adminDb, getUserDb — used for resolution and client selection)
28
- * @seeAlso middleware.ts (createHandler calls resolvePrincipal, getPrincipalDb)
29
- * @seeAlso permissions.ts (PermissionEngine.isSystemAdmin, canPrincipalAccessRecord)
30
- * @seeAlso audit.ts (formatPrincipalForAudit — safe audit serialization)
31
- */
32
1
 
2
+
3
+
4
+
5
+
6
+
7
+
8
+
9
+
10
+
11
+
12
+
13
+
14
+
15
+
16
+
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"]
33
32
  import { getUserDb, adminDb } from './db'
34
33
  import { createClient } from '@supabase/supabase-js'
35
34
  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"]
44
+ // 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
+ function isInternalInvokeAuthorized(event: any): boolean {
48
+ const expected = (globalThis as any).process?.env?.INTERNAL_INVOKE_SECRET
49
+ if (!expected) return false
50
+
51
+ const provided = event.headers?.['x-internal-secret'] || event.headers?.['X-Internal-Secret']
52
+ if (!provided || typeof provided !== 'string') return false
36
53
 
37
- // ─── AUTH CLIENT ─────────────────────────────────────────────────────────────
54
+ const expectedBuf = Buffer.from(expected)
55
+ const providedBuf = Buffer.from(provided)
56
+ if (expectedBuf.length !== providedBuf.length) return false
38
57
 
39
- // Supabase anon client used only for JWT validation (supabase.auth.getUser).
40
- // A separate client is used here to avoid importing the RLS-scoped client
41
- // before the principal is resolved.
58
+ try {
59
+ return timingSafeEqual(expectedBuf, providedBuf)
60
+ } catch {
61
+ return false
62
+ }
63
+ }
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."]
77
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
78
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
42
79
  const env = (globalThis as any).process?.env || {}
43
80
  const supabase = createClient(
44
81
  env.SUPABASE_URL || 'https://placeholder.supabase.co',
@@ -48,117 +85,64 @@ const supabase = createClient(
48
85
  realtime: { transport: WebSocket as any }
49
86
  }
50
87
  )
51
-
52
- // ─── PRINCIPAL INTERFACE ─────────────────────────────────────────────────────
53
-
54
- /**
55
- * Unified identity abstraction for all actors in Spine.
56
- *
57
- * Every request resolves to a `Principal` before any permission or DB access.
58
- * The `type` field gates which optional fields are populated:
59
- * - `'human'` → `roles`, `displayName`, `email`, `authContext.jwt`
60
- * - `'machine'` → `scopes`, `machineType`, `isInternal`, `authContext.apiKey`
61
- *
62
- * The `provenance` object is always populated and is the primary audit trail
63
- * field. It must not be modified after resolution.
64
- *
65
- * @inputSpec none — this is a pure type definition
66
- * @calledBy middleware.ts (RequestContext.principal), permissions.ts (all methods),
67
- * audit.ts (formatPrincipalForAudit), tests/integration/helpers.ts (makeTestCtx)
68
- */
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."]
101
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
102
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
69
103
  export interface Principal {
70
- /** Unique identifier — person UUID (human) or machine principal UUID (machine) */
71
104
  id: string
72
-
73
- /** Actor type — gates which optional fields are populated */
74
105
  type: 'human' | 'machine'
75
-
76
- /** Primary account context; null for internal system principals */
77
106
  accountId: string | null
78
-
79
- // ─ Human-specific (only populated when type === 'human') ─────────────────
80
- /** Role slugs from people.role_id → roles.slug; used by PermissionEngine */
81
107
  roles?: string[]
82
-
83
- /** Display name from people.display_name or people.email */
84
108
  displayName?: string
85
-
86
- /** Email address from people.email or Supabase auth user */
87
109
  email?: string
88
-
89
- // ─ Machine-specific (only populated when type === 'machine') ─────────────
90
- /** Explicit permission grants (e.g., ['items:read', 'people:write', '*:*']) */
91
110
  scopes?: string[]
92
-
93
- /** Machine classification — determines UI visibility and default scopes */
94
111
  machineType?: 'integration' | 'service_account' | 'internal' | 'timer'
95
-
96
- /** Internal machines (cron, trigger, pipeline) are hidden from the UI */
97
112
  isInternal?: boolean
98
-
99
- // ─ Universal provenance — always populated, never modified after resolution
100
113
  provenance: {
101
- /** How this principal was authenticated */
102
114
  sourceType: 'jwt' | 'api_key' | 'cron' | 'trigger' | 'manual' | 'webhook' | 'timer'
103
-
104
- /** Person who authorized this principal (may be self for humans) */
105
115
  createdBy: string | null
106
-
107
- /** Chain ID for trigger/pipeline sequences */
108
116
  parentExecutionId?: string
109
-
110
- /** When this principal context was created */
111
117
  invokedAt: string
112
-
113
- // Source-specific context
114
- /** API key ID (for api_key source) */
115
118
  apiKeyId?: string
116
-
117
- /** Schedule ID (for cron source) */
118
119
  cronId?: string
119
-
120
- /** Trigger ID (for trigger source) */
121
120
  triggerId?: string
122
-
123
- /** Timer ID (for timer source) */
124
121
  timerId?: string
125
-
126
- /** Event ID that triggered this execution */
127
122
  eventId?: string
128
-
129
- /** IP address of the requester */
130
123
  ipAddress?: string
131
-
132
- /** User agent string */
133
124
  userAgent?: string
134
125
  }
135
-
136
- // ============================================
137
- // Authentication context (for RLS client selection)
138
- // ============================================
139
126
  authContext?: {
140
- /** JWT token for human-scoped DB client */
141
127
  jwt?: string
142
-
143
- /** API key value for machine verification */
144
128
  apiKey?: string
145
129
  }
146
130
  }
147
-
148
- // ─── PRINCIPAL CONSTANTS ─────────────────────────────────────────────────────
149
-
150
- /**
151
- * Sentinel principal for unauthenticated requests.
152
- *
153
- * Returned by `resolvePrincipal` when no auth header is present. `createHandler`
154
- * checks for `principal.id === 'anonymous'` and rejects the request with 401.
155
- * Never use this principal for any DB access — it has no scopes or accountId.
156
- *
157
- * @stability stable
158
- * @calledBy resolvePrincipal (returned when no auth header is present)
159
- * @calledBy middleware.ts, requireUserContext, requireSystemContextWithAudit (checked against)
160
- * @calledBy permissions.ts (all surface methods check for 'anonymous' and deny)
161
- */
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."]
144
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
145
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
162
146
  export const ANONYMOUS_PRINCIPAL: Principal = {
163
147
  id: 'anonymous',
164
148
  type: 'machine',
@@ -170,37 +154,26 @@ export const ANONYMOUS_PRINCIPAL: Principal = {
170
154
  invokedAt: new Date().toISOString()
171
155
  }
172
156
  }
173
-
174
- /**
175
- * System principal for internal Spine operations (cron, pipeline runner, trigger engine).
176
- *
177
- * Has `'*:*'` scope (all resources, all actions) and `isInternal: true`. When
178
- * a `CoreContext` is constructed for a system operation (e.g. in v2-custom/ or
179
- * the CLI), use `SYSTEM_PRINCIPAL` as the principal and `adminDb` as the db.
180
- *
181
- * Never expose this principal in an HTTP response or log the `authContext` field.
182
- *
183
- * @stability stable
184
- * @calledBy tests/integration/helpers.ts (makeTestCtx)
185
- * @calledBy CLI context construction
186
- * @calledBy v2-custom/ system-level import callers
187
- *
188
- * @example Import usage (v2-custom/)
189
- * ```ts
190
- * import { CoreContext, SYSTEM_PRINCIPAL, adminDb } from '../_shared/index'
191
- * const ctx: CoreContext = {
192
- * principal: SYSTEM_PRINCIPAL,
193
- * accountId: MY_ACCOUNT_ID,
194
- * db: adminDb,
195
- * requestId: crypto.randomUUID()
196
- * }
197
- * ```
198
- */
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."]
170
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
171
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
199
172
  export const SYSTEM_PRINCIPAL: Principal = {
200
173
  id: 'system',
201
174
  type: 'machine',
202
175
  accountId: null,
203
- scopes: ['*:*'], // All scopes
176
+ scopes: ['*:*'],
204
177
  machineType: 'internal',
205
178
  isInternal: true,
206
179
  provenance: {
@@ -209,107 +182,125 @@ export const SYSTEM_PRINCIPAL: Principal = {
209
182
  invokedAt: new Date().toISOString()
210
183
  }
211
184
  }
212
-
213
- // ─── RESOLUTION FUNCTIONS ────────────────────────────────────────────────────
214
-
215
- /**
216
- * Main entry point for principal resolution. Called by `createHandler` on every
217
- * HTTP request before the handler runs.
218
- *
219
- * Examines request headers in priority order and delegates to the appropriate
220
- * resolver. Always returns a `Principal` — never throws for missing auth
221
- * (returns `ANONYMOUS_PRINCIPAL` instead). Throws only on invalid/expired
222
- * credentials (invalid API key, expired JWT, etc.).
223
- *
224
- * Resolution order:
225
- * 1. `x-api-key` / `X-Api-Key` header → `resolveMachinePrincipal`
226
- * 2. `x-cron-id` / `X-Cron-Id` header → `resolveCronPrincipal`
227
- * 3. `x-trigger-id` / `X-Trigger-Id` header → `resolveTriggerPrincipal`
228
- * 4. `Authorization: Bearer <jwt>` → `resolveHumanPrincipal`
229
- * 5. (none matched) → `ANONYMOUS_PRINCIPAL`
230
- *
231
- * @param event - Raw Netlify event object with `headers` and optional `body`
232
- * @returns Promise<Principal> — always resolves; throws on invalid credentials
233
- * @throws Error — on invalid API key, invalid JWT, or missing DB records
234
- * @inputSpec event.headers: Record<string, string> — HTTP request headers
235
- * @outputSpec Principal — fully resolved principal with provenance populated
236
- * @sideEffects DB reads: api_keys, schedules, triggers, people tables via sub-resolvers
237
- * @calledBy middleware.ts (createHandler) — once per HTTP request
238
- * @calls resolveMachinePrincipal | resolveCronPrincipal | resolveTriggerPrincipal |
239
- * resolveHumanPrincipal
240
- * @testUnit tests/unit/principal.test.ts — 'resolvePrincipal' describe block
241
- * @testIntegration tests/integration/auth.test.ts
242
- */
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."]
198
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
199
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
243
200
  export async function resolvePrincipal(event: any): Promise<Principal> {
244
- // Check for API key (external machine)
245
201
  const apiKey = event.headers?.['x-api-key'] || event.headers?.['X-Api-Key']
246
202
  if (apiKey) {
247
203
  return resolveMachinePrincipal(apiKey, event)
248
204
  }
249
-
250
- // Check for internal cron header
251
- const cronId = event.headers?.['x-cron-id'] || event.headers?.['X-Cron-Id']
252
- if (cronId) {
253
- return resolveCronPrincipal(cronId)
254
- }
255
-
256
- // Check for internal trigger header
257
- const triggerId = event.headers?.['x-trigger-id'] || event.headers?.['X-Trigger-Id']
258
- if (triggerId) {
259
- return resolveTriggerPrincipal(triggerId, event)
205
+
206
+ // x-cron-id / x-trigger-id let the caller assume a privileged machine
207
+ // principal by ID alone. They must never be honored unless the request
208
+ // also proves it came from a trusted internal caller via a shared
209
+ // secret (see isInternalInvokeAuthorized) — otherwise anyone who learns
210
+ // a valid schedule/trigger id (e.g. from the admin UI or an audit log)
211
+ // could impersonate that principal on any endpoint.
212
+ if (isInternalInvokeAuthorized(event)) {
213
+ const cronId = event.headers?.['x-cron-id'] || event.headers?.['X-Cron-Id']
214
+ if (cronId) {
215
+ return resolveCronPrincipal(cronId)
216
+ }
217
+
218
+ const triggerId = event.headers?.['x-trigger-id'] || event.headers?.['X-Trigger-Id']
219
+ if (triggerId) {
220
+ return resolveTriggerPrincipal(triggerId, event)
221
+ }
260
222
  }
261
-
262
- // Check for JWT Bearer (human)
223
+
263
224
  const authHeader = event.headers?.authorization || event.headers?.Authorization
264
225
  if (authHeader?.startsWith('Bearer ')) {
265
226
  return resolveHumanPrincipal(authHeader.replace('Bearer ', ''), event)
266
227
  }
267
228
 
268
- // No authentication - return anonymous
269
229
  return ANONYMOUS_PRINCIPAL
270
230
  }
271
-
272
- /**
273
- * Resolves a machine principal by validating an API key against the DB.
274
- *
275
- * Calls the `validate_machine_principal` RPC which checks the key hash,
276
- * expiry, and active status in one query. Throws on any validation failure —
277
- * invalid keys are not silently demoted to anonymous.
278
- *
279
- * @param apiKey - Raw API key string from `x-api-key` header
280
- * @param event - Raw Netlify event (used for IP/user-agent provenance)
281
- * @returns Promise<Principal> — machine principal with scopes from the DB record
282
- * @throws Error — on invalid, expired, or inactive API key
283
- * @inputSpec apiKey: string — raw key value; hashed by the RPC for comparison
284
- * @outputSpec Principal with type='machine', scopes from DB, provenance.sourceType='api_key'
285
- * @sideEffects DB read: validate_machine_principal RPC (api_keys table)
286
- * @calledBy resolvePrincipal (api_key branch)
287
- * @calls adminDb.rpc('validate_machine_principal')
288
- */
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."]
244
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
245
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
289
246
  async function resolveMachinePrincipal(apiKey: string, event: any): Promise<Principal> {
290
- // Validate the API key using the database function
291
- const { data: rows, error } = await adminDb.rpc('validate_machine_principal', {
292
- p_key_value: apiKey,
293
- p_required_scope: null // No specific scope required for resolution
294
- })
295
- const machine = Array.isArray(rows) ? rows[0] : rows
296
-
297
- if (error || !machine || !machine.is_valid) {
298
- throw new Error(machine?.error_message || 'Invalid or inactive machine principal')
247
+ const keyPrefix = apiKey.slice(0, 8)
248
+
249
+ const { data: rows, error } = await adminDb
250
+ .from('api_keys')
251
+ .select('*')
252
+ .eq('key_prefix', keyPrefix)
253
+ .eq('is_active', true)
254
+ .or(`expires_at.is.null,expires_at.gte.${new Date().toISOString()}`)
255
+ .limit(1)
256
+
257
+ if (error || !rows || rows.length === 0) {
258
+ throw new Error('Invalid or inactive machine principal')
299
259
  }
300
-
260
+
261
+ const key = rows[0]
262
+ const { verifyApiKey } = await import('./hash-api-key')
263
+ if (!verifyApiKey(apiKey, key.key_hash)) {
264
+ throw new Error('Invalid or inactive machine principal')
265
+ }
266
+
267
+ const rateWindowSeconds = (key.config?.rate_limit_window_seconds as number) || 60
268
+ const rateLimit = key.rate_limit || 100
269
+ const windowStart = new Date(Date.now() - rateWindowSeconds * 1000).toISOString()
270
+
271
+ const { count, error: countError } = await adminDb
272
+ .from('api_key_usage_logs')
273
+ .select('*', { count: 'exact', head: true })
274
+ .eq('api_key_id', key.id)
275
+ .gte('created_at', windowStart)
276
+
277
+ if (!countError && count !== null && count >= rateLimit) {
278
+ throw new Error('Rate limit exceeded for this API key')
279
+ }
280
+
281
+ await adminDb.from('api_key_usage_logs').insert({
282
+ api_key_id: key.id,
283
+ account_id: key.account_id,
284
+ request_method: event.httpMethod,
285
+ request_path: event.path,
286
+ request_ip: getClientIp(event),
287
+ user_agent: event.headers?.['user-agent'] || event.headers?.['User-Agent'],
288
+ success: true,
289
+ metadata: { source: 'resolve_machine_principal' }
290
+ })
291
+
301
292
  return {
302
- id: machine.machine_id,
293
+ id: key.id,
303
294
  type: 'machine',
304
- accountId: machine.account_id,
305
- scopes: machine.scopes || [],
306
- machineType: machine.machine_type as any,
307
- isInternal: machine.is_internal,
295
+ accountId: key.account_id,
296
+ scopes: key.scopes || [],
297
+ machineType: (key.machine_type as any) || 'integration',
298
+ isInternal: key.is_internal,
308
299
  provenance: {
309
300
  sourceType: 'api_key',
310
- createdBy: machine.created_by,
301
+ createdBy: key.created_by,
311
302
  invokedAt: new Date().toISOString(),
312
- apiKeyId: machine.machine_id,
303
+ apiKeyId: key.id,
313
304
  ipAddress: getClientIp(event),
314
305
  userAgent: event.headers?.['user-agent'] || event.headers?.['User-Agent']
315
306
  },
@@ -317,25 +308,23 @@ async function resolveMachinePrincipal(apiKey: string, event: any): Promise<Prin
317
308
  }
318
309
  }
319
310
 
320
- /**
321
- * Resolves a machine principal for a scheduled cron job execution.
322
- *
323
- * Loads the schedule record (with its linked machine principal) and validates
324
- * that the schedule creator is still active via `validate_schedule_creator` RPC.
325
- * Uses the schedule's `delegated_scopes` if set, falling back to the machine's
326
- * own scopes. This allows scopes to be narrowed per-schedule for least-privilege.
327
- *
328
- * @param scheduleId - UUID of the schedule from `x-cron-id` header
329
- * @returns Promise<Principal> — machine principal with delegated or own scopes
330
- * @throws Error — on invalid/inactive schedule, missing machine, or validation failure
331
- * @inputSpec scheduleId: string — valid UUID in the schedules table
332
- * @outputSpec Principal with type='machine', provenance.sourceType='cron', cronId set
333
- * @sideEffects DB reads: schedules table, validate_schedule_creator RPC
334
- * @calledBy resolvePrincipal (cron branch)
335
- * @calls adminDb.from('schedules'), adminDb.rpc('validate_schedule_creator')
336
- */
311
+
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"]
320
+ // 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"]
337
327
  async function resolveCronPrincipal(scheduleId: string): Promise<Principal> {
338
- // Load the schedule with its machine principal
339
328
  const { data: schedule, error: scheduleError } = await adminDb
340
329
  .from('schedules')
341
330
  .select(`
@@ -349,7 +338,6 @@ async function resolveCronPrincipal(scheduleId: string): Promise<Principal> {
349
338
  throw new Error('Invalid or inactive schedule: ' + scheduleId)
350
339
  }
351
340
 
352
- // Validate the schedule creator is still active
353
341
  const { data: validation, error: validationError } = await adminDb.rpc('validate_schedule_creator', {
354
342
  p_schedule_id: scheduleId
355
343
  })
@@ -375,26 +363,22 @@ async function resolveCronPrincipal(scheduleId: string): Promise<Principal> {
375
363
  }
376
364
  }
377
365
  }
378
-
379
- /**
380
- * Resolves a machine principal for an event trigger execution.
381
- *
382
- * Loads the trigger record and its associated action. The action must have a
383
- * `default_machine_principal_id` configured — this is the API key record that
384
- * provides identity and scopes for the trigger's execution context.
385
- *
386
- * @param triggerId - UUID of the trigger from `x-trigger-id` header
387
- * @param event - Raw Netlify event (used for eventId and provenance)
388
- * @returns Promise<Principal> — machine principal from the trigger's action config
389
- * @throws Error — on invalid trigger, missing action config, or missing machine
390
- * @inputSpec triggerId: string — valid UUID in the triggers table
391
- * @outputSpec Principal with type='machine', provenance.sourceType='trigger', triggerId set
392
- * @sideEffects DB reads: triggers table, api_keys table
393
- * @calledBy resolvePrincipal (trigger branch)
394
- * @calls adminDb.from('triggers'), adminDb.from('api_keys')
395
- */
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."]
379
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
380
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
396
381
  async function resolveTriggerPrincipal(triggerId: string, event: any): Promise<Principal> {
397
- // Load the trigger
398
382
  const { data: trigger, error: triggerError } = await adminDb
399
383
  .from('triggers')
400
384
  .select(`
@@ -408,7 +392,6 @@ async function resolveTriggerPrincipal(triggerId: string, event: any): Promise<P
408
392
  throw new Error('Invalid trigger: ' + triggerId)
409
393
  }
410
394
 
411
- // Get the action's default machine principal
412
395
  const action = trigger.action
413
396
  if (!action?.default_machine_principal_id) {
414
397
  throw new Error('Trigger action has no machine principal configured')
@@ -440,47 +423,36 @@ async function resolveTriggerPrincipal(triggerId: string, event: any): Promise<P
440
423
  }
441
424
  }
442
425
  }
443
-
444
- /**
445
- * Resolves a human principal from a Supabase JWT Bearer token.
446
- *
447
- * Validates the token with `supabase.auth.getUser`, resolves the internal
448
- * person UUID from the auth user, then loads the person's role via `role_id` FK.
449
- * Role slugs are the source of truth for `PermissionEngine.isSystemAdmin` checks.
450
- *
451
- * If the person's `auth_uid` is not yet set, it is backfilled on first login
452
- * (side effect on the people table).
453
- *
454
- * @param token - Raw JWT string extracted from `Authorization: Bearer <token>`
455
- * @param event - Raw Netlify event (used for IP/user-agent provenance)
456
- * @returns Promise<Principal> — human principal with roles from the DB
457
- * @throws Error('Invalid authentication token') — on expired or invalid JWT
458
- * @throws Error('Person not found') — if people record doesn't exist for this auth user
459
- * @inputSpec token: string — valid non-expired Supabase JWT
460
- * @outputSpec Principal with type='human', roles from people.role_id → roles.slug
461
- * @sideEffects DB reads: supabase.auth.getUser, people table (with role join)
462
- * @sideEffects DB write (conditional): people.auth_uid backfill on first login
463
- * @calledBy resolvePrincipal (jwt branch)
464
- * @calls supabase.auth.getUser, resolveInternalPersonId, adminDb.from('people')
465
- * @testIntegration tests/integration/auth.test.ts
466
- */
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."]
439
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
440
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
467
441
  async function resolveHumanPrincipal(token: string, event: any): Promise<Principal> {
468
- // Validate JWT with Supabase
469
442
  const { data: { user }, error } = await supabase.auth.getUser(token)
470
443
 
471
444
  if (error || !user) {
472
445
  return ANONYMOUS_PRINCIPAL
473
446
  }
474
447
 
475
- // Resolve internal person ID from auth user
476
448
  const personId = await resolveInternalPersonId(user.id, user.email)
449
+ const userDb = getUserDb(token)
477
450
 
478
- // Load person details
479
- const { data: person, error: personError } = await adminDb
451
+ const { data: person, error: personError } = await userDb
480
452
  .from('people')
481
- .select('*, role:roles!role_id(slug, name, is_system, is_protected)')
453
+ .select('*, role:roles(id, slug, name, is_system, permissions)')
482
454
  .eq('id', personId)
483
- .maybeSingle()
455
+ .single()
484
456
 
485
457
  if (personError) {
486
458
  throw new Error('Person lookup error: ' + personError.message + ' (id: ' + personId + ')')
@@ -489,8 +461,8 @@ async function resolveHumanPrincipal(token: string, event: any): Promise<Princip
489
461
  throw new Error('Person not found: ' + personId)
490
462
  }
491
463
 
492
- // Resolve role from role_id
493
- const roleSlugs = person.role?.slug ? [person.role.slug] : []
464
+ const role = Array.isArray(person.role) ? person.role[0] : person.role
465
+ const roleSlugs = role?.slug ? [role.slug] : []
494
466
 
495
467
  return {
496
468
  id: personId,
@@ -501,7 +473,7 @@ async function resolveHumanPrincipal(token: string, event: any): Promise<Princip
501
473
  email: person.email,
502
474
  provenance: {
503
475
  sourceType: 'jwt',
504
- createdBy: personId, // Self-created through auth
476
+ createdBy: personId,
505
477
  invokedAt: new Date().toISOString(),
506
478
  ipAddress: getClientIp(event),
507
479
  userAgent: event.headers?.['user-agent'] || event.headers?.['User-Agent']
@@ -509,32 +481,22 @@ async function resolveHumanPrincipal(token: string, event: any): Promise<Princip
509
481
  authContext: { jwt: token }
510
482
  }
511
483
  }
512
-
513
- /**
514
- * Resolves the internal people table UUID from a Supabase auth user ID.
515
- *
516
- * Lookup strategy (in order):
517
- * 1. Match by `email` — most reliable; also backfills `auth_uid` if missing
518
- * 2. Fallback: match by `auth_uid` directly
519
- * 3. Last resort: return `authUserId` as-is (person not yet in people table)
520
- *
521
- * The email-first strategy handles the case where a person was created manually
522
- * in the people table before the user completed Supabase Auth registration.
523
- *
524
- * @param authUserId - UUID from Supabase auth.users (supabase.auth.getUser result)
525
- * @param email - Email address from the Supabase auth user (optional but preferred)
526
- * @returns Promise<string> — internal person UUID, or authUserId as fallback
527
- * @throws never — graceful fallback to authUserId on any lookup failure
528
- * @inputSpec authUserId: string — valid Supabase auth user UUID
529
- * @inputSpec email: string | undefined — used for primary lookup
530
- * @outputSpec string — internal people.id UUID
531
- * @sideEffects DB reads: people table (by email, then by auth_uid)
532
- * @sideEffects DB write (conditional): people.auth_uid backfill when found by email
533
- * @calledBy resolveHumanPrincipal
534
- * @calls adminDb.from('people')
535
- */
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."]
497
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
498
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
536
499
  async function resolveInternalPersonId(authUserId: string, email?: string): Promise<string> {
537
- // Try to find by email first (more reliable)
538
500
  if (email) {
539
501
  const { data: byEmail } = await adminDb
540
502
  .from('people')
@@ -543,7 +505,6 @@ async function resolveInternalPersonId(authUserId: string, email?: string): Prom
543
505
  .maybeSingle()
544
506
 
545
507
  if (byEmail) {
546
- // Update auth_uid if not set
547
508
  if (!byEmail.auth_uid) {
548
509
  await adminDb
549
510
  .from('people')
@@ -554,7 +515,6 @@ async function resolveInternalPersonId(authUserId: string, email?: string): Prom
554
515
  }
555
516
  }
556
517
 
557
- // Fallback: try by auth_uid
558
518
  const { data: byAuthId } = await adminDb
559
519
  .from('people')
560
520
  .select('id')
@@ -563,28 +523,23 @@ async function resolveInternalPersonId(authUserId: string, email?: string): Prom
563
523
 
564
524
  if (byAuthId) return byAuthId.id
565
525
 
566
- // Not found - return the auth ID as fallback
567
526
  return authUserId
568
527
  }
569
-
570
- // ─── HELPER FUNCTIONS ────────────────────────────────────────────────────────
571
-
572
- /**
573
- * Extracts the client IP address from event headers.
574
- *
575
- * Checks headers in priority order: `x-forwarded-for` (load balancer),
576
- * `x-real-ip` (proxy), then `requestContext.identity.sourceIp` (API Gateway).
577
- * Returns `undefined` if none are present.
578
- *
579
- * @param event - Raw Netlify event object
580
- * @returns string | undefined — first non-empty IP found
581
- * @throws never
582
- * @inputSpec event.headers: Record<string, string> | undefined
583
- * @outputSpec string — IPv4 or IPv6 address; may contain comma-separated list
584
- * from x-forwarded-for (take first value if parsing is needed)
585
- * @sideEffects none
586
- * @calledBy resolveMachinePrincipal, resolveHumanPrincipal (for provenance)
587
- */
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."]
541
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
542
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
588
543
  function getClientIp(event: any): string | undefined {
589
544
  return event.headers?.['x-forwarded-for'] ||
590
545
  event.headers?.['X-Forwarded-For'] ||
@@ -592,157 +547,109 @@ function getClientIp(event: any): string | undefined {
592
547
  event.headers?.['X-Real-Ip'] ||
593
548
  event.requestContext?.identity?.sourceIp
594
549
  }
595
-
596
- /**
597
- * Checks whether a machine principal has been granted a specific scope.
598
- *
599
- * Scope matching supports three patterns:
600
- * 1. Exact: `'items:read'` matches only `'items:read'`
601
- * 2. Wildcard action: `'items:*'` matches `'items:read'`, `'items:write'`, etc.
602
- * 3. Global wildcard: `'*:*'` matches any scope
603
- *
604
- * Returns `false` for non-machine principals — role-based checks use `humanHasRole`.
605
- *
606
- * @param principal - The principal to check
607
- * @param scope - The required scope string in `'resource:action'` format
608
- * @returns boolean — true if any of the principal's scopes grant the required scope
609
- * @throws never
610
- * @inputSpec principal.type: 'machine' — returns false for human principals
611
- * @inputSpec scope: string — must be in 'resource:action' format
612
- * @inputSpec principal.scopes: string[] — list of granted scope strings
613
- * @outputSpec boolean
614
- * @sideEffects none
615
- * @calledBy permissions.ts (checkMachineScope), any custom code doing scope checks
616
- * @testUnit tests/unit/principal.test.ts — 'machineHasScope' describe block
617
- *
618
- * @example
619
- * ```ts
620
- * import { machineHasScope } from '../_shared/index'
621
- * if (!machineHasScope(principal, 'items:write')) {
622
- * return { error: 'Insufficient scope' }
623
- * }
624
- * ```
625
- */
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."]
563
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
564
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
626
565
  export function machineHasScope(principal: Principal, scope: string): boolean {
627
566
  if (principal.type !== 'machine') return false
628
567
 
629
568
  const scopes = principal.scopes || []
630
569
  const [resource, action] = scope.split(':')
631
570
 
632
- // Exact match
633
571
  if (scopes.includes(scope)) return true
634
572
 
635
- // Wildcard resource
636
573
  if (scopes.includes(`${resource}:*`)) return true
637
574
 
638
- // Global wildcard
639
575
  if (scopes.includes('*:*')) return true
640
576
 
641
577
  return false
642
578
  }
643
-
644
- /**
645
- * Checks whether a human principal has been assigned a specific role.
646
- *
647
- * Returns `false` for non-human (machine) principals — scope checks use
648
- * `machineHasScope`. Role slugs come from `people.role_id → roles.slug`
649
- * and are loaded at resolution time in `resolveHumanPrincipal`.
650
- *
651
- * @param principal - The principal to check
652
- * @param roleSlug - The role slug to look for (e.g. 'system_admin', 'agent')
653
- * @returns boolean — true if principal.roles includes the given slug
654
- * @throws never
655
- * @inputSpec principal.type: 'human' — returns false for machine principals
656
- * @inputSpec roleSlug: string — must match exactly (case-sensitive)
657
- * @inputSpec principal.roles: string[] | undefined
658
- * @outputSpec boolean
659
- * @sideEffects none
660
- * @calledBy isSystemAdmin, permissions.ts (PermissionEngine.isSystemAdmin)
661
- * @testUnit tests/unit/principal.test.ts — 'humanHasRole' describe block
662
- */
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."]
592
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
593
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
663
594
  export function humanHasRole(principal: Principal, roleSlug: string): boolean {
664
595
  if (principal.type !== 'human') return false
665
596
  return principal.roles?.includes(roleSlug) || false
666
597
  }
667
-
668
- /**
669
- * Returns true if the principal holds the `system_admin` role.
670
- *
671
- * This is the canonical system admin check used by `middleware.ts`
672
- * (`requireSystemContextWithAudit`) and re-exported from `permissions.ts`
673
- * (`PermissionEngine.isSystemAdmin`). system_admin bypasses all three
674
- * permission surfaces.
675
- *
676
- * @param principal - The principal to check
677
- * @returns boolean — true only if type='human' and roles includes 'system_admin'
678
- * @throws never
679
- * @inputSpec principal: Principal — any resolved principal
680
- * @outputSpec boolean — false for machine principals, anonymous, or missing roles
681
- * @sideEffects none
682
- * @calledBy middleware.ts (requireSystemContextWithAudit), permissions.ts (PermissionEngine),
683
- * any handler doing system-admin-only checks
684
- * @calls humanHasRole
685
- * @testUnit tests/unit/principal.test.ts — 'isSystemAdmin' describe block
686
- */
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."]
611
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
612
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
687
613
  export function isSystemAdmin(principal: Principal): boolean {
688
614
  return humanHasRole(principal, 'system_admin')
689
615
  }
690
-
691
- /**
692
- * Selects the correct Supabase database client for the given principal.
693
- *
694
- * This is the only place in the codebase where the two-client selection
695
- * decision is made. The result is stored in `ctx.db` and used for all
696
- * subsequent DB queries in the request.
697
- *
698
- * Selection logic:
699
- * - Human principal with JWT → `getUserDb(jwt)` — enforces RLS
700
- * - Machine principal → `adminDb` — RLS policies check machine ID in policies
701
- * - Anonymous → `adminDb` (but anonymous requests are rejected before DB access)
702
- *
703
- * @param principal - The resolved principal
704
- * @returns SupabaseClient — RLS-scoped for humans, admin for machines
705
- * @throws never
706
- * @inputSpec principal.type: 'human' | 'machine'
707
- * @inputSpec principal.authContext.jwt: string | undefined — required for human client
708
- * @outputSpec SupabaseClient — getUserDb result (human) or adminDb (machine)
709
- * @sideEffects none (client construction only)
710
- * @calledBy middleware.ts (createHandler — `const ctxDb = getPrincipalDb(principal)`)
711
- * @calls getUserDb (db.ts), adminDb (db.ts)
712
- * @testUnit tests/unit/principal.test.ts — 'getPrincipalDb' describe block
713
- */
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."]
629
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
630
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
714
631
  export function getPrincipalDb(principal: Principal) {
715
632
  if (principal.type === 'human' && principal.authContext?.jwt) {
716
633
  return getUserDb(principal.authContext.jwt)
717
634
  }
718
635
 
719
- // Machines use admin client - RLS policies check their ID
720
636
  return adminDb
721
637
  }
722
-
723
- /**
724
- * Serializes a principal into a safe, structured object for audit log entries.
725
- *
726
- * Strips `authContext` entirely (never log JWT or API key values). The returned
727
- * object is safe to store in the `metadata` JSONB column of the `logs` table.
728
- *
729
- * @param principal - Any resolved principal including ANONYMOUS_PRINCIPAL
730
- * @returns object — audit-safe principal summary
731
- * @throws never
732
- * @inputSpec principal: Principal — any resolved principal
733
- * @outputSpec { id, type, account_id } + role/scope fields depending on type
734
- * @outputSpec authContext is NEVER included in output
735
- * @sideEffects none
736
- * @calledBy audit.ts (emitAudit — in metadata.principal)
737
- * @testUnit tests/unit/principal.test.ts — 'formatPrincipalForAudit' describe block
738
- *
739
- * @example
740
- * ```ts
741
- * await emitAudit(ctx, 'items.delete', { type: 'item', id }, {
742
- * principal_snapshot: formatPrincipalForAudit(ctx.principal)
743
- * })
744
- * ```
745
- */
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."]
651
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
652
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
746
653
  export function formatPrincipalForAudit(principal: Principal): object {
747
654
  return {
748
655
  id: principal.id,
@@ -760,3 +667,4 @@ export function formatPrincipalForAudit(principal: Principal): object {
760
667
  provenance: principal.provenance
761
668
  }
762
669
  }
670
+ // CIL[END chunk_id="principal_audit_1784020784"]