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