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