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.
- package/.framework/README.md +129 -0
- package/.framework/cli/bin.cjs +14 -0
- package/.framework/cli/commands/agents.ts +153 -0
- package/.framework/cli/commands/auth.ts +94 -0
- package/.framework/cli/commands/create-app.ts +185 -0
- package/.framework/cli/commands/dev.ts +295 -0
- package/.framework/cli/commands/doctor.ts +442 -0
- package/.framework/cli/commands/generate.ts +332 -0
- package/.framework/cli/commands/init.ts +272 -0
- package/.framework/cli/commands/install-app.ts +391 -0
- package/.framework/cli/commands/items.ts +253 -0
- package/.framework/cli/commands/migrations.ts +141 -0
- package/.framework/cli/commands/pipelines.ts +166 -0
- package/.framework/cli/commands/status.ts +197 -0
- package/.framework/cli/commands/system.ts +184 -0
- package/.framework/cli/commands/test.ts +227 -0
- package/.framework/cli/commands/uninstall-app.ts +166 -0
- package/.framework/cli/context.ts +268 -0
- package/.framework/cli/env-loader.ts +36 -0
- package/.framework/cli/index.ts +106 -0
- package/.framework/cli/welcome.cjs +45 -0
- package/.framework/docs/API.md +384 -0
- package/.framework/docs/STABILITY.md +52 -0
- package/.framework/docs/admin-routes.md +76 -0
- package/.framework/docs/api-docs-progress.md +38 -0
- package/.framework/docs/api-governance.md +146 -0
- package/.framework/docs/api-testing-results.md +212 -0
- package/.framework/docs/apis/admin-configs.md +567 -0
- package/.framework/docs/apis/admin-data.md +272 -0
- package/.framework/docs/apis/index.md +231 -0
- package/.framework/docs/apis/internal.md +295 -0
- package/.framework/docs/apis/runtime.md +537 -0
- package/.framework/docs/assembly-launch-guide.md +138 -0
- package/.framework/docs/audit-results.md +590 -0
- package/.framework/docs/authorization-model.md +170 -0
- package/.framework/docs/db-api-inventory.md +95 -0
- package/.framework/docs/examples/custom-app/README.md +77 -0
- package/.framework/docs/examples/custom-function/README.md +27 -0
- package/.framework/docs/examples/custom-function/handler.ts +48 -0
- package/.framework/docs/examples/custom-webhook/README.md +68 -0
- package/.framework/docs/gap-remediation-backlog.md +103 -0
- package/.framework/docs/guides/cli-guide.md +224 -0
- package/.framework/docs/guides/getting-started.md +103 -0
- package/.framework/docs/guides/import-guide.md +193 -0
- package/.framework/docs/guides/testing-guide.md +229 -0
- package/.framework/docs/permission-examples.md +326 -0
- package/.framework/docs/ui-adoption-verification.md +111 -0
- package/.framework/docs/ui-api-coverage.md +84 -0
- package/.framework/docs/v2-compatibility-audit.md +228 -0
- package/.framework/functions/.gitkeep +1 -0
- package/.framework/functions/_shared/agent-runner.ts +1097 -0
- package/.framework/functions/_shared/app-manifest.ts +184 -0
- package/.framework/functions/_shared/audit.ts +150 -0
- package/.framework/functions/_shared/db.ts +174 -0
- package/.framework/functions/_shared/index.ts +382 -0
- package/.framework/functions/_shared/middleware.ts +490 -0
- package/.framework/functions/_shared/permissions.ts +1325 -0
- package/.framework/functions/_shared/pipeline-runner.ts +731 -0
- package/.framework/functions/_shared/principal.ts +760 -0
- package/.framework/functions/_shared/schema-utils.ts +967 -0
- package/.framework/functions/_shared/testing.ts +258 -0
- package/.framework/functions/_shared/trigger-engine.ts +425 -0
- package/.framework/functions/_shared/webhook-registration.ts +168 -0
- package/.framework/functions/_shared/webhook-registry.ts +129 -0
- package/.framework/functions/account-nodes.ts +111 -0
- package/.framework/functions/admin-data.ts +606 -0
- package/.framework/functions/ai-agents.ts +323 -0
- package/.framework/functions/api-keys.ts +376 -0
- package/.framework/functions/apps.ts +483 -0
- package/.framework/functions/auth.ts +196 -0
- package/.framework/functions/debug-auth.ts +107 -0
- package/.framework/functions/embeddings.ts +556 -0
- package/.framework/functions/integration-routes.ts +523 -0
- package/.framework/functions/integrations.ts +319 -0
- package/.framework/functions/item-progress.ts +272 -0
- package/.framework/functions/logs.ts +438 -0
- package/.framework/functions/observability.ts +275 -0
- package/.framework/functions/pipeline-executions.ts +494 -0
- package/.framework/functions/pipelines.ts +485 -0
- package/.framework/functions/prompt-configs.ts +339 -0
- package/.framework/functions/roles.ts +387 -0
- package/.framework/functions/system-cron.ts +742 -0
- package/.framework/functions/system.ts +323 -0
- package/.framework/functions/tests.ts +119 -0
- package/.framework/functions/timers.ts +357 -0
- package/.framework/functions/triggers.ts +563 -0
- package/.framework/functions/types.ts +604 -0
- package/.framework/migrations/000_foundation.sql +1256 -0
- package/.framework/migrations/001_seed.sql +92 -0
- package/.framework/migrations/002_seed_constraints.sql +13 -0
- package/.framework/migrations/003_auth_user_trigger.sql +59 -0
- package/.framework/src/App.tsx +126 -0
- package/.framework/src/apps/admin/index.tsx +173 -0
- package/.framework/src/components/AppWrapper.tsx +56 -0
- package/.framework/src/components/CustomAppLoader.tsx +116 -0
- package/.framework/src/components/admin/AdminListPage.tsx +151 -0
- package/.framework/src/components/admin/AdminSidebar.tsx +166 -0
- package/.framework/src/components/admin/AdminStatsCard.tsx +62 -0
- package/.framework/src/components/admin/SortableTableHeader.tsx +42 -0
- package/.framework/src/components/app-shell/GenericAppShell.tsx +181 -0
- package/.framework/src/components/app-shell/GenericDetailPage.tsx +200 -0
- package/.framework/src/components/app-shell/GenericListPage.tsx +116 -0
- package/.framework/src/components/app-sidebar.tsx +228 -0
- package/.framework/src/components/auth/ProtectedRoute.tsx +88 -0
- package/.framework/src/components/layout/AppShell.tsx +91 -0
- package/.framework/src/components/layout/Header.tsx +88 -0
- package/.framework/src/components/layout/Layout.tsx +95 -0
- package/.framework/src/components/layout/Sidebar.tsx +329 -0
- package/.framework/src/components/runtime/DataDetailHeader.tsx +77 -0
- package/.framework/src/components/runtime/DataDetailPage.tsx +171 -0
- package/.framework/src/components/runtime/DataFilters.tsx +91 -0
- package/.framework/src/components/runtime/DataHeader.tsx +68 -0
- package/.framework/src/components/runtime/DataListPage.tsx +124 -0
- package/.framework/src/components/runtime/DataStats.tsx +70 -0
- package/.framework/src/components/runtime/DataTable.tsx +174 -0
- package/.framework/src/components/runtime/SchemaDetailForm.tsx +134 -0
- package/.framework/src/components/runtime/index.ts +18 -0
- package/.framework/src/components/search-form.tsx +29 -0
- package/.framework/src/components/shared/AgentView.tsx +213 -0
- package/.framework/src/components/shared/FieldRenderer.tsx +478 -0
- package/.framework/src/components/shared/SchemaFields.tsx +226 -0
- package/.framework/src/components/ui/DataTable.tsx +343 -0
- package/.framework/src/components/ui/Form.tsx +281 -0
- package/.framework/src/components/ui/ItemCard.tsx +296 -0
- package/.framework/src/components/ui/ItemListView.tsx +308 -0
- package/.framework/src/components/ui/LoadingSpinner.tsx +52 -0
- package/.framework/src/components/ui/Modal.tsx +61 -0
- package/.framework/src/components/ui/RichTextEditor.tsx +210 -0
- package/.framework/src/components/ui/accordion.tsx +82 -0
- package/.framework/src/components/ui/alert-dialog.tsx +197 -0
- package/.framework/src/components/ui/alert.tsx +76 -0
- package/.framework/src/components/ui/aspect-ratio.tsx +11 -0
- package/.framework/src/components/ui/avatar.tsx +110 -0
- package/.framework/src/components/ui/badge.tsx +49 -0
- package/.framework/src/components/ui/breadcrumb.tsx +122 -0
- package/.framework/src/components/ui/button-group.tsx +83 -0
- package/.framework/src/components/ui/button.tsx +65 -0
- package/.framework/src/components/ui/calendar.tsx +222 -0
- package/.framework/src/components/ui/card.tsx +100 -0
- package/.framework/src/components/ui/carousel.tsx +240 -0
- package/.framework/src/components/ui/chart.tsx +373 -0
- package/.framework/src/components/ui/checkbox.tsx +31 -0
- package/.framework/src/components/ui/collapsible.tsx +33 -0
- package/.framework/src/components/ui/combobox.tsx +299 -0
- package/.framework/src/components/ui/command.tsx +193 -0
- package/.framework/src/components/ui/context-menu.tsx +261 -0
- package/.framework/src/components/ui/dialog.tsx +165 -0
- package/.framework/src/components/ui/direction.tsx +22 -0
- package/.framework/src/components/ui/drawer.tsx +132 -0
- package/.framework/src/components/ui/dropdown-menu.tsx +269 -0
- package/.framework/src/components/ui/empty.tsx +104 -0
- package/.framework/src/components/ui/field.tsx +238 -0
- package/.framework/src/components/ui/hover-card.tsx +42 -0
- package/.framework/src/components/ui/input-group.tsx +153 -0
- package/.framework/src/components/ui/input-otp.tsx +87 -0
- package/.framework/src/components/ui/input.tsx +19 -0
- package/.framework/src/components/ui/item.tsx +196 -0
- package/.framework/src/components/ui/kbd.tsx +26 -0
- package/.framework/src/components/ui/label.tsx +22 -0
- package/.framework/src/components/ui/menubar.tsx +277 -0
- package/.framework/src/components/ui/native-select.tsx +61 -0
- package/.framework/src/components/ui/navigation-menu.tsx +164 -0
- package/.framework/src/components/ui/pagination.tsx +129 -0
- package/.framework/src/components/ui/popover.tsx +87 -0
- package/.framework/src/components/ui/progress.tsx +31 -0
- package/.framework/src/components/ui/radio-group.tsx +42 -0
- package/.framework/src/components/ui/resizable.tsx +50 -0
- package/.framework/src/components/ui/scroll-area.tsx +53 -0
- package/.framework/src/components/ui/select.tsx +195 -0
- package/.framework/src/components/ui/separator.tsx +26 -0
- package/.framework/src/components/ui/sheet.tsx +145 -0
- package/.framework/src/components/ui/sidebar.tsx +706 -0
- package/.framework/src/components/ui/skeleton.tsx +13 -0
- package/.framework/src/components/ui/slider.tsx +59 -0
- package/.framework/src/components/ui/sonner.tsx +47 -0
- package/.framework/src/components/ui/spinner.tsx +10 -0
- package/.framework/src/components/ui/switch.tsx +33 -0
- package/.framework/src/components/ui/table-primitives.tsx +141 -0
- package/.framework/src/components/ui/table.tsx +114 -0
- package/.framework/src/components/ui/tabs.tsx +90 -0
- package/.framework/src/components/ui/textarea.tsx +18 -0
- package/.framework/src/components/ui/toggle-group.tsx +89 -0
- package/.framework/src/components/ui/toggle.tsx +45 -0
- package/.framework/src/components/ui/tooltip.tsx +57 -0
- package/.framework/src/contexts/AppContext.tsx +133 -0
- package/.framework/src/contexts/AuthContext.tsx +371 -0
- package/.framework/src/hooks/use-mobile.ts +19 -0
- package/.framework/src/hooks/useApi.ts +526 -0
- package/.framework/src/hooks/useApps.ts +114 -0
- package/.framework/src/hooks/useEntityList.ts +190 -0
- package/.framework/src/hooks/useEntityRecord.ts +308 -0
- package/.framework/src/hooks/useForm.ts +307 -0
- package/.framework/src/hooks/useListSchema.ts +264 -0
- package/.framework/src/hooks/useSchemaRecord.ts +223 -0
- package/.framework/src/index.css +128 -0
- package/.framework/src/lib/api.ts +156 -0
- package/.framework/src/lib/supabase.ts +94 -0
- package/.framework/src/lib/utils.ts +317 -0
- package/.framework/src/main.tsx +27 -0
- package/.framework/src/pages/DashboardPage.tsx +181 -0
- package/.framework/src/pages/NotFoundPage.tsx +39 -0
- package/.framework/src/pages/admin/AIAgentDetailPage.tsx +161 -0
- package/.framework/src/pages/admin/AIAgentsPage.tsx +318 -0
- package/.framework/src/pages/admin/APIKeyDetailPage.tsx +199 -0
- package/.framework/src/pages/admin/APIKeysPage.tsx +303 -0
- package/.framework/src/pages/admin/AlertsConfigPage.tsx +523 -0
- package/.framework/src/pages/admin/AppDetailPage.tsx +493 -0
- package/.framework/src/pages/admin/AppsPage.tsx +355 -0
- package/.framework/src/pages/admin/DesignedPage.tsx +491 -0
- package/.framework/src/pages/admin/EmbeddingDetailPage.tsx +534 -0
- package/.framework/src/pages/admin/EmbeddingsPage.tsx +424 -0
- package/.framework/src/pages/admin/ExtendedShadcnTestPage.tsx +176 -0
- package/.framework/src/pages/admin/IncrementalShadcnTestPage.tsx +109 -0
- package/.framework/src/pages/admin/IntegratedDashboard.tsx +402 -0
- package/.framework/src/pages/admin/IntegrationDetailPage.tsx +187 -0
- package/.framework/src/pages/admin/IntegrationsPage.tsx +301 -0
- package/.framework/src/pages/admin/LogsPage.tsx +283 -0
- package/.framework/src/pages/admin/MinimalShadcnTestPage.tsx +85 -0
- package/.framework/src/pages/admin/ObservabilityDashboard.tsx +470 -0
- package/.framework/src/pages/admin/PipelineDetailPage.tsx +183 -0
- package/.framework/src/pages/admin/PipelineExecutionsPage.tsx +279 -0
- package/.framework/src/pages/admin/PipelinesPage.tsx +390 -0
- package/.framework/src/pages/admin/PromptConfigDetailPage.tsx +299 -0
- package/.framework/src/pages/admin/PromptConfigsPage.tsx +292 -0
- package/.framework/src/pages/admin/ProperlyDesignedPage.tsx +434 -0
- package/.framework/src/pages/admin/RoleDetailPage.tsx +273 -0
- package/.framework/src/pages/admin/RolesPage.tsx +292 -0
- package/.framework/src/pages/admin/SelectTestPage.tsx +61 -0
- package/.framework/src/pages/admin/ShadcnTestPage.tsx +588 -0
- package/.framework/src/pages/admin/SimpleDashboard.tsx +387 -0
- package/.framework/src/pages/admin/TestRunDetailPage.tsx +172 -0
- package/.framework/src/pages/admin/TestingDashboard.tsx +257 -0
- package/.framework/src/pages/admin/TimerDetailPage.tsx +151 -0
- package/.framework/src/pages/admin/TimersPage.tsx +376 -0
- package/.framework/src/pages/admin/TriggerDetailPage.tsx +149 -0
- package/.framework/src/pages/admin/TriggersPage.tsx +381 -0
- package/.framework/src/pages/admin/TypeDetailPage.tsx +694 -0
- package/.framework/src/pages/admin/TypesPage.tsx +295 -0
- package/.framework/src/pages/auth/LoginPage.tsx +188 -0
- package/.framework/src/pages/auth/RegisterPage.tsx +163 -0
- package/.framework/src/pages/spine-framework/APIPage.tsx +17 -0
- package/.framework/src/pages/spine-framework/CLIPage.tsx +25 -0
- package/.framework/src/types/auth.ts +125 -0
- package/.framework/src/types/types.ts +407 -0
- package/STRUCTURE.md +150 -0
- package/config/components.json +25 -0
- package/config/deno.lock +108 -0
- package/config/package-lock.json +17183 -0
- package/config/postcss.config.cjs +10 -0
- package/config/tailwind.config.cjs +78 -0
- package/config/tsconfig.build.json +32 -0
- package/config/tsconfig.cli.json +18 -0
- package/config/tsconfig.json +41 -0
- package/config/tsconfig.node.json +17 -0
- package/config/tsconfig.node.tsbuildinfo +1 -0
- package/config/tsconfig.tsbuildinfo +1 -0
- package/config/typedoc.json +16 -0
- package/config/vite.config.d.ts +2 -0
- package/config/vite.config.ts +72 -0
- package/dist/cli/commands/agents.d.ts +39 -0
- package/dist/cli/commands/agents.d.ts.map +1 -0
- package/dist/cli/commands/auth.d.ts +36 -0
- package/dist/cli/commands/auth.d.ts.map +1 -0
- package/dist/cli/commands/create-app.d.ts +23 -0
- package/dist/cli/commands/create-app.d.ts.map +1 -0
- package/dist/cli/commands/dev.d.ts +39 -0
- package/dist/cli/commands/dev.d.ts.map +1 -0
- package/dist/cli/commands/doctor.d.ts +42 -0
- package/dist/cli/commands/doctor.d.ts.map +1 -0
- package/dist/cli/commands/generate.d.ts +36 -0
- package/dist/cli/commands/generate.d.ts.map +1 -0
- package/dist/cli/commands/init.d.ts +30 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/install-app.d.ts +30 -0
- package/dist/cli/commands/install-app.d.ts.map +1 -0
- package/dist/cli/commands/items.d.ts +45 -0
- package/dist/cli/commands/items.d.ts.map +1 -0
- package/dist/cli/commands/migrations.d.ts +41 -0
- package/dist/cli/commands/migrations.d.ts.map +1 -0
- package/dist/cli/commands/pipelines.d.ts +40 -0
- package/dist/cli/commands/pipelines.d.ts.map +1 -0
- package/dist/cli/commands/status.d.ts +23 -0
- package/dist/cli/commands/status.d.ts.map +1 -0
- package/dist/cli/commands/system.d.ts +29 -0
- package/dist/cli/commands/system.d.ts.map +1 -0
- package/dist/cli/commands/test.d.ts +46 -0
- package/dist/cli/commands/test.d.ts.map +1 -0
- package/dist/cli/commands/uninstall-app.d.ts +23 -0
- package/dist/cli/commands/uninstall-app.d.ts.map +1 -0
- package/dist/cli/context.d.ts +88 -0
- package/dist/cli/context.d.ts.map +1 -0
- package/dist/cli/env-loader.d.ts +14 -0
- package/dist/cli/env-loader.d.ts.map +1 -0
- package/dist/cli/index.d.ts +41 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/functions/_shared/agent-runner.d.ts +156 -0
- package/dist/functions/_shared/agent-runner.d.ts.map +1 -0
- package/dist/functions/_shared/app-manifest.d.ts +68 -0
- package/dist/functions/_shared/app-manifest.d.ts.map +1 -0
- package/dist/functions/_shared/audit.d.ts +91 -0
- package/dist/functions/_shared/audit.d.ts.map +1 -0
- package/dist/functions/_shared/db.d.ts +125 -0
- package/dist/functions/_shared/db.d.ts.map +1 -0
- package/dist/functions/_shared/index.d.ts +298 -0
- package/dist/functions/_shared/index.d.ts.map +1 -0
- package/dist/functions/_shared/middleware.d.ts +315 -0
- package/dist/functions/_shared/middleware.d.ts.map +1 -0
- package/dist/functions/_shared/permissions.d.ts +626 -0
- package/dist/functions/_shared/permissions.d.ts.map +1 -0
- package/dist/functions/_shared/pipeline-runner.d.ts +124 -0
- package/dist/functions/_shared/pipeline-runner.d.ts.map +1 -0
- package/dist/functions/_shared/principal.d.ts +284 -0
- package/dist/functions/_shared/principal.d.ts.map +1 -0
- package/dist/functions/_shared/schema-utils.d.ts +181 -0
- package/dist/functions/_shared/schema-utils.d.ts.map +1 -0
- package/dist/functions/_shared/testing.d.ts +172 -0
- package/dist/functions/_shared/testing.d.ts.map +1 -0
- package/dist/functions/_shared/trigger-engine.d.ts +140 -0
- package/dist/functions/_shared/trigger-engine.d.ts.map +1 -0
- package/dist/functions/_shared/webhook-registration.d.ts +81 -0
- package/dist/functions/_shared/webhook-registration.d.ts.map +1 -0
- package/dist/functions/_shared/webhook-registry.d.ts +57 -0
- package/dist/functions/_shared/webhook-registry.d.ts.map +1 -0
- package/dist/functions/account-nodes.d.ts +48 -0
- package/dist/functions/account-nodes.d.ts.map +1 -0
- package/dist/functions/admin-data.d.ts +178 -0
- package/dist/functions/admin-data.d.ts.map +1 -0
- package/dist/functions/ai-agents.d.ts +125 -0
- package/dist/functions/ai-agents.d.ts.map +1 -0
- package/dist/functions/api-keys.d.ts +140 -0
- package/dist/functions/api-keys.d.ts.map +1 -0
- package/dist/functions/apps.d.ts +163 -0
- package/dist/functions/apps.d.ts.map +1 -0
- package/dist/functions/auth.d.ts +74 -0
- package/dist/functions/auth.d.ts.map +1 -0
- package/dist/functions/debug-auth.d.ts +33 -0
- package/dist/functions/debug-auth.d.ts.map +1 -0
- package/dist/functions/embeddings.d.ts +205 -0
- package/dist/functions/embeddings.d.ts.map +1 -0
- package/dist/functions/integration-routes.d.ts +45 -0
- package/dist/functions/integration-routes.d.ts.map +1 -0
- package/dist/functions/integrations.d.ts +124 -0
- package/dist/functions/integrations.d.ts.map +1 -0
- package/dist/functions/item-progress.d.ts +41 -0
- package/dist/functions/item-progress.d.ts.map +1 -0
- package/dist/functions/logs.d.ts +162 -0
- package/dist/functions/logs.d.ts.map +1 -0
- package/dist/functions/observability.d.ts +123 -0
- package/dist/functions/observability.d.ts.map +1 -0
- package/dist/functions/pipeline-executions.d.ts +190 -0
- package/dist/functions/pipeline-executions.d.ts.map +1 -0
- package/dist/functions/pipelines.d.ts +171 -0
- package/dist/functions/pipelines.d.ts.map +1 -0
- package/dist/functions/prompt-configs.d.ts +125 -0
- package/dist/functions/prompt-configs.d.ts.map +1 -0
- package/dist/functions/roles.d.ts +118 -0
- package/dist/functions/roles.d.ts.map +1 -0
- package/dist/functions/system-cron.d.ts +65 -0
- package/dist/functions/system-cron.d.ts.map +1 -0
- package/dist/functions/system.d.ts +29 -0
- package/dist/functions/system.d.ts.map +1 -0
- package/dist/functions/tests.d.ts +28 -0
- package/dist/functions/tests.d.ts.map +1 -0
- package/dist/functions/timers.d.ts +139 -0
- package/dist/functions/timers.d.ts.map +1 -0
- package/dist/functions/triggers.d.ts +203 -0
- package/dist/functions/triggers.d.ts.map +1 -0
- package/dist/functions/types.d.ts +151 -0
- package/dist/functions/types.d.ts.map +1 -0
- package/dist/src/types/types.d.ts +364 -0
- package/dist/src/types/types.d.ts.map +1 -0
- package/package.json +192 -0
- package/scripts/app-install-cli.ts +286 -0
- package/scripts/assemble-frontend.sh +79 -0
- package/scripts/assemble-functions.sh +62 -0
- package/scripts/assemble.sh +35 -0
- package/scripts/boundary-check.sh +106 -0
- package/scripts/build-manifest.sh +80 -0
- package/scripts/check-core-integrity.sh +82 -0
- package/scripts/ingest-chunks.cjs +202 -0
- package/scripts/kb-chunk-parser.cjs +312 -0
- package/scripts/kb-chunk-parser.ts +330 -0
- package/scripts/load-test-app-install.ts +484 -0
- package/scripts/netlify-dev-wrapper.sh +22 -0
- 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
|
+
}
|