spine-framework 0.3.145 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +76 -0
- package/.framework/.spine-manifest.json +19 -0
- package/.framework/cli/commands/agents.ts +61 -33
- package/.framework/cli/commands/auth.ts +60 -28
- package/.framework/cli/commands/create-app.ts +212 -80
- package/.framework/cli/commands/dev.ts +184 -68
- package/.framework/cli/commands/doctor.ts +324 -152
- package/.framework/cli/commands/generate.ts +119 -68
- package/.framework/cli/commands/init.ts +141 -242
- package/.framework/cli/commands/install-app.ts +1336 -314
- package/.framework/cli/commands/items.ts +31 -29
- package/.framework/cli/commands/migrate.ts +299 -156
- package/.framework/cli/commands/migrations.ts +62 -30
- package/.framework/cli/commands/pipelines.ts +31 -29
- package/.framework/cli/commands/status.ts +105 -45
- package/.framework/cli/commands/system.ts +47 -42
- package/.framework/cli/commands/test.ts +161 -102
- package/.framework/cli/commands/upgrade.ts +260 -0
- package/.framework/cli/commands/validate-app.ts +517 -161
- package/.framework/cli/context.ts +117 -85
- package/.framework/cli/env-loader.ts +74 -43
- package/.framework/cli/index.ts +70 -46
- package/.framework/cli/lib/project-files.ts +404 -0
- package/.framework/docs/guides/testing-guide.md +1 -1
- package/.framework/docs/runtime-types.md +72 -72
- package/.framework/functions/_shared/access.ts +84 -0
- package/.framework/functions/_shared/agent-runner.ts +5051 -1026
- package/.framework/functions/_shared/app-manifest.ts +214 -94
- package/.framework/functions/_shared/ast.ts +214 -0
- package/.framework/functions/_shared/audit.ts +91 -55
- package/.framework/functions/_shared/cil.ts +368 -127
- package/.framework/functions/_shared/db.ts +337 -68
- package/.framework/functions/_shared/funnel.ts +228 -0
- package/.framework/functions/_shared/geo.ts +105 -0
- package/.framework/functions/_shared/hash-api-key.ts +94 -17
- package/.framework/functions/_shared/index.ts +22 -224
- package/.framework/functions/_shared/kb.ts +832 -58
- package/.framework/functions/_shared/llm.ts +118 -18
- package/.framework/functions/_shared/logging.ts +317 -19
- package/.framework/functions/_shared/middleware.ts +701 -131
- package/.framework/functions/_shared/notify.ts +70 -0
- package/.framework/functions/_shared/outcomes.ts +34 -19
- package/.framework/functions/_shared/permissions.ts +501 -315
- package/.framework/functions/_shared/pipeline-runner.ts +704 -155
- package/.framework/functions/_shared/principal.ts +436 -263
- package/.framework/functions/_shared/rate-limit.ts +233 -0
- package/.framework/functions/_shared/resolve-credentials.ts +86 -19
- package/.framework/functions/_shared/resolve-ids.ts +203 -139
- package/.framework/functions/_shared/retry-deliveries.ts +51 -16
- package/.framework/functions/_shared/schema-utils.ts +602 -521
- package/.framework/functions/_shared/security.ts +345 -0
- package/.framework/functions/_shared/testing.ts +197 -172
- package/.framework/functions/_shared/tracing.ts +351 -21
- package/.framework/functions/_shared/trigger-engine.ts +425 -136
- package/.framework/functions/_shared/webhook-registration.ts +71 -70
- package/.framework/functions/_shared/webhook-registry.ts +126 -83
- package/.framework/functions/account-nodes.ts +30 -30
- package/.framework/functions/adapters/generic-webhook-inbound.ts +49 -16
- package/.framework/functions/adapters/generic-webhook-outbound.ts +17 -17
- package/.framework/functions/adapters/openai-outbound.ts +15 -16
- package/.framework/functions/admin-data.ts +596 -173
- package/.framework/functions/admin-funnel.ts +64 -60
- package/.framework/functions/admin-install-funnel.ts +84 -66
- package/.framework/functions/admin-signals.ts +69 -51
- package/.framework/functions/ai-agents.ts +431 -106
- package/.framework/functions/ai-interactions.ts +186 -112
- package/.framework/functions/api-keys.ts +107 -107
- package/.framework/functions/apps.ts +154 -136
- package/.framework/functions/auth.ts +83 -65
- package/.framework/functions/cil-agent-ingest.ts +189 -232
- package/.framework/functions/cil-agent.ts +433 -177
- package/.framework/functions/cil-callmap.ts +642 -430
- package/.framework/functions/cil-config.ts +139 -135
- package/.framework/functions/cil-context.ts +214 -127
- package/.framework/functions/cil-doc.ts +587 -458
- package/.framework/functions/cil-embeddings.ts +453 -364
- package/.framework/functions/cil-graph.ts +427 -142
- package/.framework/functions/cil-ingest.ts +1418 -1154
- package/.framework/functions/cil-pipeline.ts +276 -21
- package/.framework/functions/cil-project.ts +659 -132
- package/.framework/functions/cil-validate.ts +198 -164
- package/.framework/functions/debug-auth.ts +57 -55
- package/.framework/functions/deliveries.ts +80 -21
- package/.framework/functions/embeddings.ts +1023 -318
- package/.framework/functions/eval-harness.ts +146 -108
- package/.framework/functions/integration-outbound.ts +205 -75
- package/.framework/functions/integration-routes.ts +128 -23
- package/.framework/functions/integrations.ts +125 -100
- package/.framework/functions/invites.ts +1095 -460
- package/.framework/functions/item-progress.ts +158 -102
- package/.framework/functions/kb.ts +34 -10
- package/.framework/functions/logs.ts +133 -133
- package/.framework/functions/observability.ts +152 -122
- package/.framework/functions/pipeline-executions.ts +147 -132
- package/.framework/functions/pipelines.ts +134 -119
- package/.framework/functions/prompt-configs.ts +96 -80
- package/.framework/functions/roles.ts +112 -82
- package/.framework/functions/security.ts +450 -0
- package/.framework/functions/system-cron.ts +117 -80
- package/.framework/functions/system.ts +158 -98
- package/.framework/functions/tests.ts +29 -29
- package/.framework/functions/timers.ts +94 -95
- package/.framework/functions/trigger-scheduler.ts +10 -0
- package/.framework/functions/triggers.ts +179 -149
- package/.framework/functions/types.ts +137 -125
- package/.framework/mcp/index.ts +133 -0
- package/.framework/mcp/load-env.ts +60 -0
- package/.framework/mcp/shared/app-manifest.ts +275 -0
- package/.framework/mcp/shared/db.ts +477 -0
- package/.framework/mcp/shared/kb.ts +118 -0
- package/.framework/mcp/tools/architect.ts +431 -0
- package/.framework/mcp/tools/cil/context.ts +345 -0
- package/.framework/mcp/tools/cil/graph.ts +242 -0
- package/.framework/mcp/tools/cil/ingest.ts +1217 -0
- package/.framework/mcp/tools/cil/pipeline-orchestrator.ts +693 -0
- package/.framework/mcp/tools/cil/pipeline.ts +767 -0
- package/.framework/mcp/tools/cil/project.ts +791 -0
- package/.framework/mcp/tools/cil/retrieval.ts +336 -0
- package/.framework/mcp/tools/cil/search-core.ts +454 -0
- package/.framework/mcp/tools/cil/search.ts +97 -0
- package/.framework/mcp/tools/cil/suggest.ts +186 -0
- package/.framework/mcp/tools/cil/validate.ts +1369 -0
- package/.framework/mcp/tools/kb/delete.ts +139 -0
- package/.framework/mcp/tools/kb/index.ts +52 -0
- package/.framework/mcp/tools/kb/ingest.ts +84 -0
- package/.framework/mcp/tools/kb/list-types.ts +102 -0
- package/.framework/mcp/tools/kb/publish.ts +100 -0
- package/.framework/mcp/tools/kb/reindex.ts +86 -0
- package/.framework/mcp/tools/kb/revoke.ts +85 -0
- package/.framework/mcp/tools/kb/search.ts +101 -0
- package/.framework/mcp/tools/kb/upsert.ts +125 -0
- package/.framework/mcp/tools/observability.ts +510 -0
- package/.framework/migrations/001_core_schema.sql +15 -0
- package/.framework/migrations/002_core_rls_and_functions.sql +15 -0
- package/.framework/migrations/003_core_system_data.sql +16 -1
- package/.framework/migrations/004_core_rpc_and_broker.sql +17 -1
- package/.framework/migrations/005_core_observability.sql +17 -1
- package/.framework/migrations/006_security_hardening_rls.sql +15 -0
- package/.framework/migrations/007_kb_article_list.sql +17 -1
- package/.framework/migrations/008_items_target.sql +15 -0
- package/.framework/migrations/009_roles_slug_unique.sql +15 -0
- package/.framework/migrations/010_auth_user_trigger.sql +15 -0
- package/.framework/migrations/011_spine_system_account.sql +16 -0
- package/.framework/migrations/012_service_role_grants.sql +16 -0
- package/.framework/migrations/013_get_account_apps_role_filter.sql +15 -0
- package/.framework/migrations/014_apps_required_roles.sql +16 -0
- package/.framework/migrations/015_types_kind_slug_unique.sql +15 -0
- package/.framework/migrations/016_cil_types.sql +281 -0
- package/.framework/migrations/017_cil_stable_ids.sql +37 -0
- package/.framework/migrations/018_runtime_tables.sql +129 -0
- package/.framework/migrations/019_agent_prompt_columns.sql +56 -0
- package/.framework/migrations/020_schema_reconciliation.sql +67 -0
- package/.framework/migrations/021_kb_embedding_lifecycle.sql +31 -0
- package/.framework/migrations/032_cil_status_migration.sql +73 -0
- package/.framework/migrations/033_cil_versioning.sql +73 -0
- package/.framework/migrations/034_match_embeddings_knn.sql +115 -0
- package/.framework/migrations/035_rate_limits.sql +55 -0
- package/.framework/migrations/036_demo_record_flags.sql +72 -0
- package/.framework/migrations/037_kb_types_canonical.sql +244 -0
- package/.framework/migrations/038_drop_demo_record_flags.sql +38 -0
- package/.framework/migrations/042_registration_hierarchy.sql +309 -0
- package/.framework/migrations/043_policy_actor_cleanup.sql +39 -0
- package/.framework/migrations/044_match_embeddings_filtered_knn.sql +151 -0
- package/.framework/migrations/045_security_critical_rls.sql +139 -0
- package/.framework/migrations/046_security_missing_rls.sql +79 -0
- package/.framework/migrations/047_people_updated_by.sql +27 -0
- package/.framework/migrations/048_watcher_last_seen_at.sql +24 -0
- package/.framework/migrations/049_security_blocklist.sql +148 -0
- package/.framework/migrations/050_notifications.sql +49 -0
- package/.framework/migrations/051_message_agent_fields.sql +74 -0
- package/.framework/migrations/052_registration_security.sql +421 -0
- package/.framework/migrations/053_builtin_action_seeds.sql +66 -0
- package/.framework/migrations/054_kb_ingest_lease.sql +157 -0
- package/.framework/migrations/055_type_id_drift_reconciliation.sql +33 -0
- package/.framework/migrations/056_slug_column_reconciliation.sql +38 -0
- package/.framework/migrations/057_pipelines_slug_identity.sql +14 -0
- package/.framework/migrations/058_account_data_field_declarations.sql +53 -0
- package/.framework/migrations/059_registration_placement_fixes.sql +269 -0
- package/.framework/migrations/060_account_person_record_permissions.sql +38 -0
- package/.framework/migrations/061_fk_dedup.sql +32 -0
- package/.framework/public/spine-logo.svg +10 -3
- package/.framework/scripts/app-install-cli.ts +190 -93
- package/.framework/scripts/assemble-frontend.sh +127 -44
- package/.framework/scripts/assemble-functions.sh +128 -30
- package/.framework/scripts/boundary-check.sh +6 -2
- package/.framework/scripts/build-manifest.sh +24 -68
- package/.framework/scripts/check-core-integrity.sh +15 -76
- package/.framework/scripts/checksum-manifest.mts +343 -0
- package/.framework/scripts/cil-migrate-markers.ts +403 -0
- package/.framework/scripts/fix-cil-markers.mjs +176 -0
- package/.framework/scripts/verify-integrity.sh +21 -57
- package/.framework/scripts/verify-manifest.mts +305 -0
- package/.framework/seeds/types/funnel_signal_use.json +74 -21
- package/.framework/seeds/types/spine_instance.json +73 -21
- package/.framework/src/App.tsx +118 -72
- package/.framework/src/apps/admin/index.tsx +855 -58
- package/.framework/src/components/AppWrapper.tsx +43 -45
- package/.framework/src/components/CustomAppLoader.tsx +160 -116
- package/.framework/src/components/admin/AdminListPage.tsx +46 -44
- package/.framework/src/components/admin/AdminSidebar.tsx +57 -47
- package/.framework/src/components/admin/AdminStatsCard.tsx +46 -44
- package/.framework/src/components/admin/PageInstructions.tsx +98 -2
- package/.framework/src/components/admin/SortableTableHeader.tsx +44 -45
- package/.framework/src/components/app-shell/GenericAppShell.tsx +64 -65
- package/.framework/src/components/app-shell/GenericDetailPage.tsx +44 -47
- package/.framework/src/components/app-shell/GenericListPage.tsx +45 -49
- package/.framework/src/components/app-sidebar.tsx +42 -45
- package/.framework/src/components/auth/ProtectedRoute.tsx +48 -41
- package/.framework/src/components/auth/TurnstileWidget.tsx +27 -0
- package/.framework/src/components/kb/KBArticleEditor.tsx +231 -22
- package/.framework/src/components/kb/KBArticleList.tsx +251 -10
- package/.framework/src/components/layout/AppShell.tsx +51 -45
- package/.framework/src/components/layout/Header.tsx +42 -43
- package/.framework/src/components/layout/Layout.tsx +46 -46
- package/.framework/src/components/layout/Sidebar.tsx +196 -96
- package/.framework/src/components/runtime/DataDetailHeader.tsx +55 -30
- package/.framework/src/components/runtime/DataDetailPage.tsx +31 -269
- package/.framework/src/components/runtime/DataFilters.tsx +49 -74
- package/.framework/src/components/runtime/DataHeader.tsx +53 -32
- package/.framework/src/components/runtime/DataListPage.tsx +51 -155
- package/.framework/src/components/runtime/DataStats.tsx +66 -44
- package/.framework/src/components/runtime/DataTable.tsx +49 -149
- package/.framework/src/components/runtime/SchemaDetailForm.tsx +45 -75
- package/.framework/src/components/runtime/index.ts +119 -16
- package/.framework/src/components/search-form.tsx +46 -29
- package/.framework/src/components/shared/AgentView.tsx +65 -48
- package/.framework/src/components/shared/FieldRenderer.tsx +144 -50
- package/.framework/src/components/shared/InviteManager.tsx +207 -97
- package/.framework/src/components/shared/MemberList.tsx +97 -0
- package/.framework/src/components/shared/SchemaFields.tsx +107 -86
- package/.framework/src/components/ui/DataTable.tsx +80 -68
- package/.framework/src/components/ui/Form.tsx +157 -146
- package/.framework/src/components/ui/ItemCard.tsx +74 -67
- package/.framework/src/components/ui/ItemListView.tsx +73 -68
- package/.framework/src/components/ui/LoadingSpinner.tsx +43 -48
- package/.framework/src/components/ui/Modal.tsx +78 -68
- package/.framework/src/components/ui/RichTextEditor.tsx +89 -69
- package/.framework/src/components/ui/accordion.tsx +85 -86
- package/.framework/src/components/ui/alert-dialog.tsx +197 -184
- package/.framework/src/components/ui/alert.tsx +105 -94
- package/.framework/src/components/ui/aspect-ratio.tsx +57 -43
- package/.framework/src/components/ui/avatar.tsx +122 -107
- package/.framework/src/components/ui/badge.tsx +60 -55
- package/.framework/src/components/ui/breadcrumb.tsx +127 -128
- package/.framework/src/components/ui/button-group.tsx +86 -72
- package/.framework/src/components/ui/button.tsx +71 -50
- package/.framework/src/components/ui/calendar.tsx +72 -57
- package/.framework/src/components/ui/card.tsx +136 -129
- package/.framework/src/components/ui/carousel.tsx +226 -107
- package/.framework/src/components/ui/chart.tsx +252 -147
- package/.framework/src/components/ui/checkbox.tsx +43 -44
- package/.framework/src/components/ui/collapsible.tsx +86 -71
- package/.framework/src/components/ui/combobox.tsx +282 -249
- package/.framework/src/components/ui/command.tsx +158 -146
- package/.framework/src/components/ui/context-menu.tsx +239 -240
- package/.framework/src/components/ui/dialog.tsx +174 -157
- package/.framework/src/components/ui/direction.tsx +72 -56
- package/.framework/src/components/ui/drawer.tsx +168 -169
- package/.framework/src/components/ui/dropdown-menu.tsx +254 -223
- package/.framework/src/components/ui/empty.tsx +131 -120
- package/.framework/src/components/ui/field.tsx +205 -173
- package/.framework/src/components/ui/hover-card.tsx +71 -72
- package/.framework/src/components/ui/input-group.tsx +148 -133
- package/.framework/src/components/ui/input-otp.tsx +99 -86
- package/.framework/src/components/ui/input.tsx +43 -41
- package/.framework/src/components/ui/item.tsx +207 -185
- package/.framework/src/components/ui/kbd.tsx +57 -56
- package/.framework/src/components/ui/label.tsx +45 -41
- package/.framework/src/components/ui/menubar.tsx +268 -236
- package/.framework/src/components/ui/native-select.tsx +85 -86
- package/.framework/src/components/ui/navigation-menu.tsx +166 -156
- package/.framework/src/components/ui/pagination.tsx +131 -141
- package/.framework/src/components/ui/popover.tsx +125 -128
- package/.framework/src/components/ui/progress.tsx +47 -44
- package/.framework/src/components/ui/radio-group.tsx +60 -55
- package/.framework/src/components/ui/resizable.tsx +85 -72
- package/.framework/src/components/ui/scroll-area.tsx +57 -58
- package/.framework/src/components/ui/select.tsx +182 -169
- package/.framework/src/components/ui/separator.tsx +42 -43
- package/.framework/src/components/ui/sheet.tsx +170 -159
- package/.framework/src/components/ui/sidebar.tsx +395 -398
- package/.framework/src/components/ui/skeleton.tsx +42 -43
- package/.framework/src/components/ui/slider.tsx +61 -44
- package/.framework/src/components/ui/sonner.tsx +44 -44
- package/.framework/src/components/ui/spinner.tsx +47 -42
- package/.framework/src/components/ui/switch.tsx +56 -42
- package/.framework/src/components/ui/table-primitives.tsx +272 -132
- package/.framework/src/components/ui/table.tsx +140 -142
- package/.framework/src/components/ui/tabs.tsx +110 -93
- package/.framework/src/components/ui/textarea.tsx +43 -44
- package/.framework/src/components/ui/toggle-group.tsx +87 -71
- package/.framework/src/components/ui/toggle.tsx +56 -56
- package/.framework/src/components/ui/tooltip.tsx +99 -85
- package/.framework/src/contexts/AppContext.tsx +166 -114
- package/.framework/src/contexts/AuthContext.tsx +232 -137
- package/.framework/src/hooks/use-mobile.ts +43 -44
- package/.framework/src/hooks/useApi.ts +185 -172
- package/.framework/src/hooks/useAppPath.ts +31 -29
- package/.framework/src/hooks/useApps.ts +77 -42
- package/.framework/src/hooks/useEntityList.ts +66 -29
- package/.framework/src/hooks/useEntityRecord.ts +76 -32
- package/.framework/src/hooks/useForm.ts +62 -30
- package/.framework/src/hooks/useListSchema.ts +60 -43
- package/.framework/src/hooks/useSchemaRecord.ts +115 -95
- package/.framework/src/lib/api.ts +258 -134
- package/.framework/src/lib/registration.ts +181 -0
- package/.framework/src/lib/supabase.ts +118 -81
- package/.framework/src/lib/utils.ts +266 -266
- package/.framework/src/main.tsx +43 -44
- package/.framework/src/pages/DashboardPage.tsx +44 -43
- package/.framework/src/pages/NoAppPage.tsx +50 -0
- package/.framework/src/pages/NotFoundPage.tsx +32 -29
- package/.framework/src/pages/SetupPage.tsx +34 -33
- package/.framework/src/pages/admin/AIAgentDetailPage.tsx +69 -58
- package/.framework/src/pages/admin/AIAgentsPage.tsx +43 -40
- package/.framework/src/pages/admin/APIKeyDetailPage.tsx +63 -55
- package/.framework/src/pages/admin/APIKeysPage.tsx +44 -42
- package/.framework/src/pages/admin/AlertsConfigPage.tsx +117 -94
- package/.framework/src/pages/admin/AppDetailPage.tsx +48 -42
- package/.framework/src/pages/admin/AppsPage.tsx +45 -42
- package/.framework/src/pages/admin/DeliveriesPage.tsx +49 -17
- package/.framework/src/pages/admin/DesignedPage.tsx +31 -30
- package/.framework/src/pages/admin/EmbeddingDetailPage.tsx +58 -0
- package/.framework/src/pages/admin/EmbeddingsPage.tsx +49 -0
- package/.framework/src/pages/admin/ExtendedShadcnTestPage.tsx +31 -29
- package/.framework/src/pages/admin/IncrementalShadcnTestPage.tsx +46 -29
- package/.framework/src/pages/admin/IntegratedDashboard.tsx +32 -29
- package/.framework/src/pages/admin/IntegrationDetailPage.tsx +68 -55
- package/.framework/src/pages/admin/IntegrationsPage.tsx +45 -42
- package/.framework/src/pages/admin/InvitesPage.tsx +60 -44
- package/.framework/src/pages/admin/LogsPage.tsx +87 -137
- package/.framework/src/pages/admin/MinimalShadcnTestPage.tsx +31 -29
- package/.framework/src/pages/admin/ObservabilityDashboard.tsx +150 -89
- package/.framework/src/pages/admin/PipelineDetailPage.tsx +77 -104
- package/.framework/src/pages/admin/PipelineExecutionsPage.tsx +44 -43
- package/.framework/src/pages/admin/PipelinesPage.tsx +59 -145
- package/.framework/src/pages/admin/PromptConfigDetailPage.tsx +53 -42
- package/.framework/src/pages/admin/PromptConfigsPage.tsx +47 -117
- package/.framework/src/pages/admin/ProperlyDesignedPage.tsx +31 -44
- package/.framework/src/pages/admin/RoleDetailPage.tsx +49 -92
- package/.framework/src/pages/admin/RolesPage.tsx +47 -164
- package/.framework/src/pages/admin/SecurityPage.tsx +310 -0
- package/.framework/src/pages/admin/SelectTestPage.tsx +30 -32
- package/.framework/src/pages/admin/ShadcnTestPage.tsx +37 -44
- package/.framework/src/pages/admin/SimpleDashboard.tsx +32 -44
- package/.framework/src/pages/admin/TestRunDetailPage.tsx +97 -44
- package/.framework/src/pages/admin/TestingDashboard.tsx +126 -71
- package/.framework/src/pages/admin/TimerDetailPage.tsx +67 -55
- package/.framework/src/pages/admin/TimersPage.tsx +44 -42
- package/.framework/src/pages/admin/TriggerDetailPage.tsx +47 -42
- package/.framework/src/pages/admin/TriggersPage.tsx +58 -57
- package/.framework/src/pages/admin/TypeDetailPage.tsx +91 -55
- package/.framework/src/pages/admin/TypesPage.tsx +45 -42
- package/.framework/src/pages/admin/cil/CILAgentPage.tsx +258 -128
- package/.framework/src/pages/admin/cil/CILChunkDetailPage.tsx +127 -123
- package/.framework/src/pages/admin/cil/CILDashboardPage.tsx +308 -235
- package/.framework/src/pages/admin/cil/CILFileListPage.tsx +159 -110
- package/.framework/src/pages/admin/cil/CILFileSelectPage.tsx +432 -0
- package/.framework/src/pages/admin/cil/CILGraphPage.tsx +200 -178
- package/.framework/src/pages/admin/cil/CILIngestionPage.tsx +286 -239
- package/.framework/src/pages/admin/cil/CILProjectsPage.tsx +211 -90
- package/.framework/src/pages/admin/cil/CILSearchPage.tsx +156 -143
- package/.framework/src/pages/admin/cil/CILSuggestionsPage.tsx +379 -0
- package/.framework/src/pages/admin/cil/CILVersionsPage.tsx +337 -0
- package/.framework/src/pages/admin/cil/FlexiblePipelinePage.tsx +219 -214
- package/.framework/src/pages/admin/kb/KBAdminPage.tsx +64 -0
- package/.framework/src/pages/auth/AuthConfirmPage.tsx +135 -65
- package/.framework/src/pages/auth/ForgotPasswordPage.tsx +39 -31
- package/.framework/src/pages/auth/LoginPage.tsx +38 -32
- package/.framework/src/pages/auth/PendingApprovalPage.tsx +31 -28
- package/.framework/src/pages/auth/RegisterPage.tsx +429 -155
- package/.framework/src/pages/spine-framework/APIPage.tsx +33 -30
- package/.framework/src/pages/spine-framework/CLIPage.tsx +32 -30
- package/.framework/src/types/auth.ts +101 -93
- package/.framework/src/types/types.ts +283 -262
- package/.framework/templates/project.json +87 -0
- package/.framework/templates/spine.config.neutral.json +7 -0
- package/.framework/tests/api/api-surface.test.ts +381 -0
- package/.framework/tests/fixtures/seed.ts +427 -0
- package/.framework/tests/fixtures/teardown.ts +228 -0
- package/.framework/tests/reporter.ts +351 -0
- package/.framework/tests/setup.ts +135 -0
- package/.framework/tests/ui/ui-sweep.ts +278 -0
- package/.framework/tests/unit/admin-data-logger.test.ts +310 -0
- package/.framework/tests/unit/agent-runner.test.ts +2389 -0
- package/.framework/tests/unit/ai-interactions.test.ts +81 -0
- package/.framework/tests/unit/app-manifest.test.ts +359 -0
- package/.framework/tests/unit/assembly-scripts.test.ts +172 -0
- package/.framework/tests/unit/cil-determinism.test.ts +85 -0
- package/.framework/tests/unit/core-isolation.test.ts +153 -0
- package/.framework/tests/unit/funnel-signals.test.ts +135 -0
- package/.framework/tests/unit/install-app.test.ts +1759 -0
- package/.framework/tests/unit/invites-security.test.ts +572 -0
- package/.framework/tests/unit/kb-ingest-lease.test.ts +287 -0
- package/.framework/tests/unit/kb-search.test.ts +119 -0
- package/.framework/tests/unit/migrate.test.ts +509 -0
- package/.framework/tests/unit/notify.test.ts +136 -0
- package/.framework/tests/unit/permissions.test.ts +269 -0
- package/.framework/tests/unit/pipeline-runner.test.ts +755 -0
- package/.framework/tests/unit/principal-cache.test.ts +138 -0
- package/.framework/tests/unit/retrieval-golden.test.ts +520 -0
- package/.framework/tests/unit/runtime-isolation.test.ts +642 -0
- package/.framework/tests/unit/schema-utils.test.ts +225 -0
- package/.framework/tests/unit/trigger-engine.test.ts +513 -0
- package/.framework/tests/unit/validate-app.test.ts +527 -0
- package/README.md +34 -3
- package/bin/spine-framework.cjs +1 -0
- package/config/tailwind.config.cjs +7 -0
- package/config/tsconfig.functions.json +42 -0
- package/config/tsconfig.json +37 -9
- package/config/vite.config.ts +31 -2
- package/netlify.toml +31 -3
- package/package.json +46 -29
- package/scripts/assemble-frontend.sh +127 -44
- package/scripts/assemble-functions.sh +127 -29
- package/scripts/boundary-check.sh +6 -2
- package/scripts/build-manifest.sh +24 -68
- package/scripts/checksum-manifest.mts +345 -0
- package/scripts/dev-clean.sh +86 -0
- package/scripts/dev-netlify.sh +49 -0
- package/scripts/netlify-dev-wrapper.sh +2 -0
- package/scripts/verify-install.ts +358 -0
- package/scripts/verify-integrity.sh +21 -57
- package/scripts/verify-manifest.mts +305 -0
- package/templates/project/gitignore +45 -0
- package/{config → templates/project}/package-lock.json +22335 -9366
- package/vitest.config.ts +46 -0
- package/.framework/public/spine.config.json +0 -91
- package/config/deno.lock +0 -108
- package/config/tsconfig.node.tsbuildinfo +0 -1
- package/config/tsconfig.tsbuildinfo +0 -1
- package/config/vite.config.d.ts +0 -2
- package/custom/.devin/AGENT.md +0 -357
- package/custom/.devin/skills/install-app/SKILL.md +0 -39
- package/custom/.devin/skills/spine-workbench-discovery/SKILL.md +0 -39
- package/custom/.devin/skills/spine-workbench-idea/SKILL.md +0 -36
- package/custom/.devin/skills/spine-workbench-implement/SKILL.md +0 -46
- package/custom/.devin/skills/spine-workbench-initiative/SKILL.md +0 -28
- package/custom/.devin/skills/spine-workbench-plan/SKILL.md +0 -38
- package/custom/.devin/workflows/app-install.yaml +0 -83
- package/custom/README.md +0 -79
- package/docs/README.md +0 -15
- package/scripts/app-install-cli.ts +0 -286
- package/scripts/check-core-integrity.sh +0 -82
- package/scripts/ingest-chunks.cjs +0 -202
- package/scripts/kb-chunk-parser.cjs +0 -312
- package/scripts/kb-chunk-parser.ts +0 -330
- package/scripts/kb-ingest-docs.ts +0 -203
- package/scripts/load-test-app-install.ts +0 -484
- package/scripts/test-login.js +0 -33
- package/spine.config.json +0 -91
|
@@ -1,3 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module invites
|
|
3
|
+
* @audience internal
|
|
4
|
+
* @layer app-specific
|
|
5
|
+
* @stability stable
|
|
6
|
+
*
|
|
7
|
+
* This module handles invite-related functionalities within the application, integrating with various services to manage invites effectively. It serves as an API handler for processing invite requests and includes middleware for authentication and rate limiting. INVARIANT: All invite requests must be authenticated before processing.
|
|
8
|
+
*
|
|
9
|
+
* @seeAlso middleware.ts (provides authentication and rate limiting)
|
|
10
|
+
* @seeAlso db.ts (database interactions)
|
|
11
|
+
* @seeAlso audit.ts (logging and auditing actions)
|
|
12
|
+
*
|
|
13
|
+
* @cil-generated
|
|
14
|
+
*/
|
|
15
|
+
// CIL[BEGIN stable_id="~/.framework/functions/invites.ts:module_header:module_header" type="module_header" identity="module_header" parent="~/.framework/functions/invites.ts"]
|
|
16
|
+
// CIL[DOC-MACRO purpose="This module handles invite-related functionalities within the application, integrating with various services to manage invites effectively."]
|
|
17
|
+
// CIL[DOC-RESPONSIBILITY responsibility="This module handles invite-related functionalities within the application, integrating with various services to manage invites effectively. It serves as an API handler for processing invite requests and includes middleware for authentication and rate limiting. INVARIANT: All invite requests must be authenticated before processing."]
|
|
18
|
+
// CIL[DOC-CONTEXT module="invites" layer="app-specific" related_files="middleware.ts (provides authentication and rate limiting),db.ts (database interactions),audit.ts (logging and auditing actions)"]
|
|
19
|
+
// CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
|
|
20
|
+
// CIL[DOC-MICRO logic_summary="This module handles invite-related functionalities within the application, integrating with various services to manage invites effectively. It serves as an API handler for processing invite requests and includes middleware for authentication and rate limiting. INVARIANT: All invite requests must be authenticated before processing." notable_decisions="none"]
|
|
21
|
+
// CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
|
|
22
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
23
|
+
// CIL[DOC-AUDIENCE audience="internal"]
|
|
1
24
|
|
|
2
25
|
|
|
3
26
|
|
|
@@ -14,84 +37,230 @@
|
|
|
14
37
|
|
|
15
38
|
|
|
16
39
|
|
|
17
|
-
// CIL[BEGIN chunk_id="invites_moduleheader_1783968140" type="module_header" identity="imports" parent="none"]
|
|
18
|
-
// CIL[DOC-MACRO purpose="Import dependencies for invite management API"]
|
|
19
|
-
// CIL[DOC-RESPONSIBILITY responsibility="Import handler utilities and database access"]
|
|
20
|
-
// CIL[DOC-CONTEXT module="invites" layer="api-handler" related_files="[\"custom/apps/cortex/pages/operations/OperationsDashboard.tsx\",\".framework/src/pages/admin/TypeDetailPage.tsx\",\".framework/functions/_shared/principal.ts\",\".framework/functions/_shared/schema-utils.ts\",\"custom/apps/portal/pages/ContentPage.tsx\",\"\\\"]"]
|
|
21
|
-
// CIL[DOC-CONTRACT inputs="none" outputs="Imported modules" errors="Import failures" side_effects="none"]
|
|
22
|
-
// CIL[DOC-MICRO logic_summary="Imports createHandler, createPublicHandler, adminDb, emitLog" 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: api-handler. Direct callers: 0. Direct calls: 0. Related files: custom/apps/cortex/pages/operations/OperationsDashboard.tsx, .framework/src/pages/admin/TypeDetailPage.tsx, .framework/functions/_shared/principal.ts, .framework/functions/_shared/schema-utils.ts, custom/apps/portal/pages/ContentPage.tsx, \."]
|
|
27
|
-
// CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
|
|
28
|
-
// CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
|
|
29
|
-
// CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
|
|
30
|
-
// CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
|
|
31
|
-
// CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
|
|
32
40
|
import { createHandler, createPublicHandler } from './_shared/middleware'
|
|
33
41
|
import { adminDb } from './_shared/db'
|
|
34
42
|
import { emitLog } from './_shared/audit'
|
|
43
|
+
import { checkRateLimit } from './_shared/rate-limit'
|
|
44
|
+
import { getClientGeo } from './_shared/geo'
|
|
45
|
+
import { checkRegistrationBlocklist, recordRegistrationGeo } from './_shared/security'
|
|
46
|
+
import { execute as executeOutbound } from './integration-outbound'
|
|
35
47
|
import { create as adminDataCreate, list as adminDataList, update as adminDataUpdate } from './admin-data'
|
|
36
|
-
// CIL[END
|
|
37
|
-
// CIL[
|
|
38
|
-
// CIL[DOC-
|
|
39
|
-
// CIL[DOC-
|
|
40
|
-
// CIL[DOC-
|
|
41
|
-
// CIL[DOC-CONTRACT inputs="none" outputs="Constants" errors="none" side_effects="none"]
|
|
42
|
-
// CIL[DOC-MICRO logic_summary="Sets token length, max attempts, expiry days, and personal email domains" notable_decisions="[]"]
|
|
43
|
-
// CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
|
|
44
|
-
// CIL[DOC-STABILITY stability="stable"]
|
|
45
|
-
// CIL[DOC-VECTOR type="context" content="Layer: api-handler. Direct callers: 0. Direct calls: 0. Related files: .framework/functions/_shared/schema-utils.ts, custom/apps/portal/pages/ContentPage.tsx, custom/apps/cortex/pages/operations/OperationsDashboard.tsx, .framework/functions/_shared/principal.ts, .framework/src/pages/admin/TypeDetailPage.tsx, \."]
|
|
46
|
-
// CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
|
|
47
|
-
// CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
|
|
48
|
-
// CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
|
|
48
|
+
// CIL[END stable_id="~/.framework/functions/invites.ts:module_header:module_header"]
|
|
49
|
+
// CIL[DOC-VECTOR type="context" content="purpose='This module handles invite-related functionalities within the application, integrating with various services to manage invites effectively.'; No verified external callers" status="ready"]
|
|
50
|
+
// CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only" status="ready"]
|
|
51
|
+
// CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified" status="ready"]
|
|
52
|
+
// CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented" status="ready"]
|
|
49
53
|
// CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
|
|
50
54
|
// CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
|
|
55
|
+
// CIL[DOC-MACRO purpose="TOKEN_LENGTH defines the length of the token used in the invite process, ensuring consistency across the application."]
|
|
56
|
+
// CIL[DOC-RESPONSIBILITY responsibility="TOKEN_LENGTH defines the length of the token used in the invite process, ensuring consistency across the application. This constant exists to centralize the token length configuration for easier maintenance and updates."]
|
|
57
|
+
// CIL[DOC-CONTEXT module="invites" layer="app-specific" related_files=".framework/functions/_shared/middleware,.framework/functions/_shared/db,.framework/functions/_shared/audit,.framework/functions/_shared/rate-limit,.framework/functions/_shared/geo,.framework/functions/integration-outbound,.framework/functions/admin-data"]
|
|
58
|
+
// CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
|
|
59
|
+
// CIL[DOC-MICRO logic_summary="TOKEN_LENGTH defines the length of the token used in the invite process, ensuring consistency across the application. This constant exists to centralize the token length configuration for easier maintenance and updates." notable_decisions="none"]
|
|
60
|
+
// CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
|
|
61
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
62
|
+
/**
|
|
63
|
+
* TOKEN_LENGTH defines the length of the token used in the invite process, ensuring consistency across the application. This constant exists to centralize the token length configuration for easier maintenance and updates.
|
|
64
|
+
*
|
|
65
|
+
* @sideEffects none
|
|
66
|
+
* @stability stable
|
|
67
|
+
* @cil-generated
|
|
68
|
+
*/
|
|
69
|
+
// CIL[BEGIN stable_id="~/.framework/functions/invites.ts:token_length:constant" type="constant" identity="TOKEN_LENGTH" parent="~/.framework/functions/invites.ts"]
|
|
70
|
+
// CIL[DOC-MACRO purpose="TOKEN_LENGTH constant"]
|
|
71
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Implements the TOKEN_LENGTH constant."]
|
|
72
|
+
// CIL[DOC-CONTEXT module=".framework/functions/invites.ts" layer="app-specific" related_files=""]
|
|
73
|
+
// CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
|
|
74
|
+
// CIL[DOC-MICRO logic_summary="Implements the TOKEN_LENGTH constant." notable_decisions="[]"]
|
|
75
|
+
// CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
|
|
76
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
51
77
|
const TOKEN_LENGTH = 16
|
|
78
|
+
// CIL[END stable_id="~/.framework/functions/invites.ts:token_length:constant"]
|
|
79
|
+
// CIL[DOC-MACRO purpose="TOKEN_LENGTH constant"]
|
|
80
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Implements the TOKEN_LENGTH constant."]
|
|
81
|
+
// CIL[DOC-CONTEXT module=".framework/functions/invites.ts" layer="app-specific" related_files=""]
|
|
82
|
+
// CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
|
|
83
|
+
// CIL[DOC-MICRO logic_summary="Implements the TOKEN_LENGTH constant." notable_decisions="[]"]
|
|
84
|
+
// CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
|
|
85
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
86
|
+
// CIL[DOC-VECTOR type="context" content="purpose='TOKEN_LENGTH defines the length of the token used in the invite process, ensuring consistency across the application.'; No verified external callers" status="ready"]
|
|
87
|
+
// CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only" status="ready"]
|
|
88
|
+
// CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified" status="ready"]
|
|
89
|
+
// CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented" status="ready"]
|
|
90
|
+
// CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
|
|
91
|
+
// CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
|
|
92
|
+
// CIL[DOC-MACRO purpose="MAX_ATTEMPTS defines the maximum number of attempts allowed for a specific operation in the invites module."]
|
|
93
|
+
// CIL[DOC-RESPONSIBILITY responsibility="MAX_ATTEMPTS defines the maximum number of attempts allowed for a specific operation in the invites module. This constant helps to enforce limits and prevent excessive retries."]
|
|
94
|
+
// CIL[DOC-CONTEXT module="invites" layer="app-specific" related_files=".framework/functions/_shared/middleware,.framework/functions/_shared/db,.framework/functions/_shared/audit,.framework/functions/_shared/rate-limit,.framework/functions/_shared/geo,.framework/functions/integration-outbound,.framework/functions/admin-data"]
|
|
95
|
+
// CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
|
|
96
|
+
// CIL[DOC-MICRO logic_summary="MAX_ATTEMPTS defines the maximum number of attempts allowed for a specific operation in the invites module. This constant helps to enforce limits and prevent excessive retries." notable_decisions="none"]
|
|
97
|
+
// CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
|
|
98
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
99
|
+
/**
|
|
100
|
+
* MAX_ATTEMPTS defines the maximum number of attempts allowed for a specific operation in the invites module. This constant helps to enforce limits and prevent excessive retries.
|
|
101
|
+
*
|
|
102
|
+
* @sideEffects none
|
|
103
|
+
* @stability stable
|
|
104
|
+
* @cil-generated
|
|
105
|
+
*/
|
|
106
|
+
// CIL[BEGIN stable_id="~/.framework/functions/invites.ts:max_attempts:constant" type="constant" identity="MAX_ATTEMPTS" parent="~/.framework/functions/invites.ts"]
|
|
107
|
+
// CIL[DOC-MACRO purpose="MAX_ATTEMPTS constant"]
|
|
108
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Implements the MAX_ATTEMPTS constant."]
|
|
109
|
+
// CIL[DOC-CONTEXT module=".framework/functions/invites.ts" layer="app-specific" related_files=""]
|
|
110
|
+
// CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
|
|
111
|
+
// CIL[DOC-MICRO logic_summary="Implements the MAX_ATTEMPTS constant." notable_decisions="[]"]
|
|
112
|
+
// CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
|
|
113
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
52
114
|
const MAX_ATTEMPTS = 5
|
|
115
|
+
// CIL[END stable_id="~/.framework/functions/invites.ts:max_attempts:constant"]
|
|
116
|
+
// CIL[DOC-MACRO purpose="MAX_ATTEMPTS constant"]
|
|
117
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Implements the MAX_ATTEMPTS constant."]
|
|
118
|
+
// CIL[DOC-CONTEXT module=".framework/functions/invites.ts" layer="app-specific" related_files=""]
|
|
119
|
+
// CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
|
|
120
|
+
// CIL[DOC-MICRO logic_summary="Implements the MAX_ATTEMPTS constant." notable_decisions="[]"]
|
|
121
|
+
// CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
|
|
122
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
123
|
+
// CIL[DOC-VECTOR type="context" content="purpose='MAX_ATTEMPTS defines the maximum number of attempts allowed for a specific operation in the invites module.'; No verified external callers" status="ready"]
|
|
124
|
+
// CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only" status="ready"]
|
|
125
|
+
// CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified" status="ready"]
|
|
126
|
+
// CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented" status="ready"]
|
|
127
|
+
// CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
|
|
128
|
+
// CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
|
|
129
|
+
// CIL[DOC-MACRO purpose="INVITE_EXPIRY_DAYS defines the duration in days that an invite remains valid."]
|
|
130
|
+
// CIL[DOC-RESPONSIBILITY responsibility="INVITE_EXPIRY_DAYS defines the duration in days that an invite remains valid. This constant is used to manage the expiration of invites in the application."]
|
|
131
|
+
// CIL[DOC-CONTEXT module="invites" layer="app-specific" related_files=".framework/functions/_shared/middleware,.framework/functions/_shared/db,.framework/functions/_shared/audit,.framework/functions/_shared/rate-limit,.framework/functions/_shared/geo,.framework/functions/integration-outbound,.framework/functions/admin-data"]
|
|
132
|
+
// CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
|
|
133
|
+
// CIL[DOC-MICRO logic_summary="INVITE_EXPIRY_DAYS defines the duration in days that an invite remains valid. This constant is used to manage the expiration of invites in the application." notable_decisions="none"]
|
|
134
|
+
// CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
|
|
135
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
136
|
+
/**
|
|
137
|
+
* INVITE_EXPIRY_DAYS defines the duration in days that an invite remains valid. This constant is used to manage the expiration of invites in the application.
|
|
138
|
+
*
|
|
139
|
+
* @sideEffects none
|
|
140
|
+
* @stability stable
|
|
141
|
+
* @cil-generated
|
|
142
|
+
*/
|
|
143
|
+
// CIL[BEGIN stable_id="~/.framework/functions/invites.ts:invite_expiry_days:constant" type="constant" identity="INVITE_EXPIRY_DAYS" parent="~/.framework/functions/invites.ts"]
|
|
144
|
+
// CIL[DOC-MACRO purpose="INVITE_EXPIRY_DAYS constant"]
|
|
145
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Implements the INVITE_EXPIRY_DAYS constant."]
|
|
146
|
+
// CIL[DOC-CONTEXT module=".framework/functions/invites.ts" layer="app-specific" related_files=""]
|
|
147
|
+
// CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
|
|
148
|
+
// CIL[DOC-MICRO logic_summary="Implements the INVITE_EXPIRY_DAYS constant." notable_decisions="[]"]
|
|
149
|
+
// CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
|
|
150
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
53
151
|
const INVITE_EXPIRY_DAYS = 7
|
|
152
|
+
// CIL[END stable_id="~/.framework/functions/invites.ts:invite_expiry_days:constant"]
|
|
153
|
+
// CIL[DOC-MACRO purpose="INVITE_EXPIRY_DAYS constant"]
|
|
154
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Implements the INVITE_EXPIRY_DAYS constant."]
|
|
155
|
+
// CIL[DOC-CONTEXT module=".framework/functions/invites.ts" layer="app-specific" related_files=""]
|
|
156
|
+
// CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
|
|
157
|
+
// CIL[DOC-MICRO logic_summary="Implements the INVITE_EXPIRY_DAYS constant." notable_decisions="[]"]
|
|
158
|
+
// CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
|
|
159
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
160
|
+
// CIL[DOC-VECTOR type="context" content="purpose='INVITE_EXPIRY_DAYS defines the duration in days that an invite remains valid.'; No verified external callers" status="ready"]
|
|
161
|
+
// CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only" status="ready"]
|
|
162
|
+
// CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified" status="ready"]
|
|
163
|
+
// CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented" status="ready"]
|
|
164
|
+
// CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
|
|
165
|
+
// CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
|
|
54
166
|
|
|
167
|
+
// CIL[DOC-MACRO purpose="PERSONAL_DOMAINS is a set of common email domains used to identify personal email addresses."]
|
|
168
|
+
// CIL[DOC-RESPONSIBILITY responsibility="PERSONAL_DOMAINS is a set of common email domains used to identify personal email addresses. It exists to facilitate validation or filtering of email inputs in the module."]
|
|
169
|
+
// CIL[DOC-CONTEXT module="invites" layer="app-specific" related_files=".framework/functions/_shared/middleware,.framework/functions/_shared/db,.framework/functions/_shared/audit,.framework/functions/_shared/rate-limit,.framework/functions/_shared/geo,.framework/functions/integration-outbound,.framework/functions/admin-data"]
|
|
170
|
+
// CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
|
|
171
|
+
// CIL[DOC-MICRO logic_summary="PERSONAL_DOMAINS is a set of common email domains used to identify personal email addresses. It exists to facilitate validation or filtering of email inputs in the module." notable_decisions="none"]
|
|
172
|
+
// CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
|
|
173
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
174
|
+
/**
|
|
175
|
+
* PERSONAL_DOMAINS is a set of common email domains used to identify personal email addresses. It exists to facilitate validation or filtering of email inputs in the module.
|
|
176
|
+
*
|
|
177
|
+
* @sideEffects none
|
|
178
|
+
* @stability stable
|
|
179
|
+
* @cil-generated
|
|
180
|
+
*/
|
|
181
|
+
// CIL[BEGIN stable_id="~/.framework/functions/invites.ts:personal_domains:constant" type="constant" identity="PERSONAL_DOMAINS" parent="~/.framework/functions/invites.ts"]
|
|
182
|
+
// CIL[DOC-MACRO purpose="PERSONAL_DOMAINS constant"]
|
|
183
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Implements the PERSONAL_DOMAINS constant."]
|
|
184
|
+
// CIL[DOC-CONTEXT module=".framework/functions/invites.ts" layer="app-specific" related_files=""]
|
|
185
|
+
// CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
|
|
186
|
+
// CIL[DOC-MICRO logic_summary="Implements the PERSONAL_DOMAINS constant." notable_decisions="[]"]
|
|
187
|
+
// CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
|
|
188
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
55
189
|
const PERSONAL_DOMAINS = new Set([
|
|
56
190
|
'gmail.com', 'yahoo.com', 'hotmail.com', 'outlook.com', 'icloud.com',
|
|
57
191
|
'aol.com', 'protonmail.com', 'me.com', 'mac.com', 'live.com',
|
|
58
192
|
'msn.com', 'ymail.com', 'googlemail.com', 'fastmail.com',
|
|
59
193
|
])
|
|
60
|
-
// CIL[END
|
|
61
|
-
// CIL[
|
|
62
|
-
// CIL[DOC-
|
|
63
|
-
// CIL[DOC-
|
|
64
|
-
// CIL[DOC-
|
|
65
|
-
// CIL[DOC-CONTRACT inputs="none" outputs="Random token string" errors="Crypto API failures" side_effects="none"]
|
|
66
|
-
// CIL[DOC-MICRO logic_summary="Uses crypto.getRandomValues to generate alphanumeric token" notable_decisions="[]"]
|
|
67
|
-
// CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
|
|
68
|
-
// CIL[DOC-STABILITY stability="stable"]
|
|
69
|
-
// CIL[DOC-VECTOR type="context" content="Layer: api-handler. Direct callers: 1. Direct calls: 0. Related files: .framework/src/pages/admin/TypeDetailPage.tsx, .framework/src/pages/admin/RolesPage.tsx, .framework/functions/_shared/principal.ts, custom/apps/cortex/pages/operations/OperationsDashboard.tsx, .framework/functions/_shared/schema-utils.ts, \."]
|
|
70
|
-
// CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 1. Total affected: 1."]
|
|
71
|
-
// CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
|
|
72
|
-
// CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
|
|
194
|
+
// CIL[END stable_id="~/.framework/functions/invites.ts:personal_domains:constant"]
|
|
195
|
+
// CIL[DOC-VECTOR type="context" content="purpose='PERSONAL_DOMAINS is a set of common email domains used to identify personal email addresses.'; No verified external callers" status="ready"]
|
|
196
|
+
// CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only" status="ready"]
|
|
197
|
+
// CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified" status="ready"]
|
|
198
|
+
// CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented" status="ready"]
|
|
73
199
|
// CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
|
|
74
200
|
// CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
|
|
201
|
+
// CIL[DOC-MACRO purpose="Generates a random token string composed of alphanumeric characters."]
|
|
202
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Generates a random token string composed of alphanumeric characters. This function is used to create unique identifiers for invites."]
|
|
203
|
+
// CIL[DOC-CONTEXT module="invites" layer="app-specific" related_files=".framework/functions/_shared/middleware,.framework/functions/_shared/db,.framework/functions/_shared/audit,.framework/functions/_shared/rate-limit,.framework/functions/_shared/geo,.framework/functions/integration-outbound,.framework/functions/admin-data"]
|
|
204
|
+
// CIL[DOC-CONTRACT inputs="none" outputs="string" errors="none" side_effects="none dependencies="crypto.getRandomValues"]
|
|
205
|
+
// CIL[DOC-MICRO logic_summary="Generates a random token string composed of alphanumeric characters. This function is used to create unique identifiers for invites." notable_decisions="none"]
|
|
206
|
+
// CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
|
|
207
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
208
|
+
/**
|
|
209
|
+
* Generates a random token string composed of alphanumeric characters. This function is used to create unique identifiers for invites.
|
|
210
|
+
*
|
|
211
|
+
* @outputSpec string
|
|
212
|
+
* @sideEffects none
|
|
213
|
+
* @stability stable
|
|
214
|
+
* @calls crypto.getRandomValues
|
|
215
|
+
* @cil-generated
|
|
216
|
+
*/
|
|
217
|
+
// CIL[BEGIN stable_id="~/.framework/functions/invites.ts:generatetoken:function" type="function" identity="generateToken" parent="~/.framework/functions/invites.ts"]
|
|
218
|
+
// CIL[DOC-MACRO purpose="generateToken function"]
|
|
219
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Implements the generateToken function."]
|
|
220
|
+
// CIL[DOC-CONTEXT module=".framework/functions/invites.ts" layer="app-specific" related_files=""]
|
|
221
|
+
// CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
|
|
222
|
+
// CIL[DOC-MICRO logic_summary="Implements the generateToken function." notable_decisions="[]"]
|
|
223
|
+
// CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
|
|
224
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
75
225
|
function generateToken(): string {
|
|
76
226
|
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
|
|
77
227
|
const bytes = crypto.getRandomValues(new Uint8Array(TOKEN_LENGTH))
|
|
78
228
|
return Array.from(bytes).map(b => chars[b % chars.length]).join('')
|
|
79
229
|
}
|
|
80
|
-
// CIL[END
|
|
81
|
-
// CIL[
|
|
82
|
-
// CIL[DOC-
|
|
83
|
-
// CIL[DOC-
|
|
84
|
-
// CIL[DOC-
|
|
85
|
-
// CIL[DOC-CONTRACT inputs="none" outputs="Unique account slug" errors="Invalid name format" side_effects="none"]
|
|
86
|
-
// CIL[DOC-MICRO logic_summary="Normalizes name to lowercase, adds hyphens, appends random hash" notable_decisions="[]"]
|
|
87
|
-
// CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
|
|
88
|
-
// CIL[DOC-STABILITY stability="stable"]
|
|
89
|
-
// CIL[DOC-VECTOR type="context" content="Layer: api-handler. Direct callers: 0. Direct calls: 0. Related files: .framework/src/pages/admin/RolesPage.tsx, .framework/functions/_shared/schema-utils.ts, .framework/src/components/ui/dialog.tsx, .framework/functions/_shared/principal.ts, .framework/src/components/ui/table-primitives.tsx, \."]
|
|
90
|
-
// CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
|
|
91
|
-
// CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
|
|
92
|
-
// CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
|
|
230
|
+
// CIL[END stable_id="~/.framework/functions/invites.ts:generatetoken:function"]
|
|
231
|
+
// CIL[DOC-VECTOR type="context" content="purpose='Generates a random token string composed of alphanumeric characters.'; No verified external callers" status="ready"]
|
|
232
|
+
// CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only" status="ready"]
|
|
233
|
+
// CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified" status="ready"]
|
|
234
|
+
// CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented" status="ready"]
|
|
93
235
|
// CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
|
|
94
236
|
// CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
|
|
237
|
+
// CIL[DOC-MACRO purpose="Slug generator for account names."]
|
|
238
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Generates a URL-friendly slug from a given account name by normalizing and truncating it. This function ensures that the slug is unique by appending a random hash."]
|
|
239
|
+
// CIL[DOC-CONTEXT module="invites" layer="app-specific" related_files=".framework/functions/_shared/middleware,.framework/functions/_shared/db,.framework/functions/_shared/audit,.framework/functions/_shared/rate-limit,.framework/functions/_shared/geo,.framework/functions/integration-outbound,.framework/functions/admin-data"]
|
|
240
|
+
// CIL[DOC-CONTRACT inputs="name: string (required) — the account name to be converted into a slug." outputs="string — a URL-friendly slug with a random hash appended." errors="none" side_effects="none"]
|
|
241
|
+
// CIL[DOC-MICRO logic_summary="Normalizes the name by converting it to lowercase and replacing non-alphanumeric characters with hyphens." notable_decisions="none"]
|
|
242
|
+
// CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
|
|
243
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
244
|
+
/**
|
|
245
|
+
* Generates a URL-friendly slug from a given account name by normalizing and truncating it. This function ensures that the slug is unique by appending a random hash.
|
|
246
|
+
*
|
|
247
|
+
* @macro Slug generator for account names.
|
|
248
|
+
* @micro Normalizes the name by converting it to lowercase and replacing non-alphanumeric characters with hyphens.
|
|
249
|
+
* @inputSpec
|
|
250
|
+
* name: string (required) — the account name to be converted into a slug.
|
|
251
|
+
* @outputSpec string — a URL-friendly slug with a random hash appended.
|
|
252
|
+
* @sideEffects none
|
|
253
|
+
* @stability stable
|
|
254
|
+
* @cil-generated
|
|
255
|
+
*/
|
|
256
|
+
// CIL[BEGIN stable_id="~/.framework/functions/invites.ts:generateaccountslug:function" type="function" identity="generateAccountSlug" parent="~/.framework/functions/invites.ts"]
|
|
257
|
+
// CIL[DOC-MACRO purpose="generateAccountSlug function"]
|
|
258
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Implements the generateAccountSlug function."]
|
|
259
|
+
// CIL[DOC-CONTEXT module=".framework/functions/invites.ts" layer="app-specific" related_files=""]
|
|
260
|
+
// CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
|
|
261
|
+
// CIL[DOC-MICRO logic_summary="Implements the generateAccountSlug function." notable_decisions="[]"]
|
|
262
|
+
// CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
|
|
263
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
95
264
|
function generateAccountSlug(name: string): string {
|
|
96
265
|
const base = name
|
|
97
266
|
.toLowerCase()
|
|
@@ -101,42 +270,195 @@ function generateAccountSlug(name: string): string {
|
|
|
101
270
|
const hash = Math.random().toString(36).slice(2, 8)
|
|
102
271
|
return `${base}-${hash}`
|
|
103
272
|
}
|
|
104
|
-
// CIL[END
|
|
105
|
-
// CIL[
|
|
106
|
-
// CIL[DOC-
|
|
107
|
-
// CIL[DOC-
|
|
108
|
-
// CIL[DOC-
|
|
109
|
-
// CIL[DOC-CONTRACT inputs="none" outputs="boolean - admin status" errors="none" side_effects="none"]
|
|
110
|
-
// CIL[DOC-MICRO logic_summary="Checks for system_admin or role ending with _admin" notable_decisions="[]"]
|
|
111
|
-
// CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
|
|
112
|
-
// CIL[DOC-STABILITY stability="stable"]
|
|
113
|
-
// CIL[DOC-VECTOR type="context" content="Layer: api-handler. Direct callers: 0. Direct calls: 0. Related files: .framework/src/pages/admin/RolesPage.tsx, .framework/migrations/015_simplify_apps_table.sql, .framework/src/pages/admin/TypeDetailPage.tsx, .framework/src/pages/admin/IntegrationsPage.tsx, \."]
|
|
114
|
-
// CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
|
|
115
|
-
// CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
|
|
116
|
-
// CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
|
|
273
|
+
// CIL[END stable_id="~/.framework/functions/invites.ts:generateaccountslug:function"]
|
|
274
|
+
// CIL[DOC-VECTOR type="context" content="purpose='Generates a URL-friendly slug from a given account name by normalizing and truncating it.'; No verified external callers" status="ready"]
|
|
275
|
+
// CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only" status="ready"]
|
|
276
|
+
// CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified" status="ready"]
|
|
277
|
+
// CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented" status="ready"]
|
|
117
278
|
// CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
|
|
118
279
|
// CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
|
|
280
|
+
// CIL[DOC-MACRO purpose="Role validation function for admin permissions."]
|
|
281
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Determines if a given role slug corresponds to an admin role. This function is used to validate user permissions based on their role."]
|
|
282
|
+
// CIL[DOC-CONTEXT module="invites" layer="app-specific" related_files=".framework/functions/_shared/middleware,.framework/functions/_shared/db,.framework/functions/_shared/audit,.framework/functions/_shared/rate-limit,.framework/functions/_shared/geo,.framework/functions/integration-outbound,.framework/functions/admin-data"]
|
|
283
|
+
// CIL[DOC-CONTRACT inputs="roleSlug: string | undefined (optional) — the role identifier to check for admin status." outputs="boolean — true if the role is an admin role, false otherwise." errors="none" side_effects="none"]
|
|
284
|
+
// CIL[DOC-MICRO logic_summary="Checks if the role slug is defined and matches specific admin criteria." notable_decisions="none"]
|
|
285
|
+
// CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
|
|
286
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
287
|
+
/**
|
|
288
|
+
* Determines if a given role slug corresponds to an admin role. This function is used to validate user permissions based on their role.
|
|
289
|
+
*
|
|
290
|
+
* @macro Role validation function for admin permissions.
|
|
291
|
+
* @micro Checks if the role slug is defined and matches specific admin criteria.
|
|
292
|
+
* @inputSpec
|
|
293
|
+
* roleSlug: string | undefined (optional) — the role identifier to check for admin status.
|
|
294
|
+
* @outputSpec boolean — true if the role is an admin role, false otherwise.
|
|
295
|
+
* @sideEffects none
|
|
296
|
+
* @stability stable
|
|
297
|
+
* @cil-generated
|
|
298
|
+
*/
|
|
299
|
+
// CIL[BEGIN stable_id="~/.framework/functions/invites.ts:isadminrole:function" type="function" identity="isAdminRole" parent="~/.framework/functions/invites.ts"]
|
|
300
|
+
// CIL[DOC-MACRO purpose="isAdminRole function"]
|
|
301
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Implements the isAdminRole function."]
|
|
302
|
+
// CIL[DOC-CONTEXT module=".framework/functions/invites.ts" layer="app-specific" related_files=""]
|
|
303
|
+
// CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
|
|
304
|
+
// CIL[DOC-MICRO logic_summary="Implements the isAdminRole function." notable_decisions="[]"]
|
|
305
|
+
// CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
|
|
306
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
119
307
|
function isAdminRole(roleSlug: string | undefined): boolean {
|
|
120
308
|
return !!roleSlug && (
|
|
121
309
|
roleSlug === 'system_admin' ||
|
|
122
310
|
roleSlug.endsWith('_admin')
|
|
123
311
|
)
|
|
124
312
|
}
|
|
125
|
-
// CIL[END
|
|
126
|
-
|
|
127
|
-
// CIL[
|
|
128
|
-
// CIL[DOC-
|
|
129
|
-
// CIL[DOC-
|
|
130
|
-
// CIL[DOC-
|
|
131
|
-
// CIL[DOC-
|
|
132
|
-
// CIL[DOC-
|
|
313
|
+
// CIL[END stable_id="~/.framework/functions/invites.ts:isadminrole:function"]
|
|
314
|
+
|
|
315
|
+
// CIL[BEGIN stable_id="~/.framework/functions/invites.ts:requireaccountadmin:function" type="function" identity="requireAccountAdmin" parent="~/.framework/functions/invites.ts"]
|
|
316
|
+
// CIL[DOC-MACRO purpose="Account-scoped admin gate for invite and member mutations"]
|
|
317
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Require system_admin, or an *_admin role on the principal bound to the SAME account being mutated — closes cross-account invite/member management."]
|
|
318
|
+
// CIL[DOC-CONTEXT module=".framework/functions/invites.ts" layer="app-specific" related_files=".framework/functions/_shared/middleware.ts"]
|
|
319
|
+
// CIL[DOC-CONTRACT inputs="ctx (server-resolved principal), accountId" outputs="void" errors="Error — Authentication required; Error — Insufficient permissions — *_admin role required for this account" side_effects="none"]
|
|
320
|
+
// CIL[DOC-MICRO logic_summary="Anonymous callers throw; system principals (permissions 'system' or roles 'system_admin') pass; otherwise the principal must carry an *_admin role AND principal.accountId must equal the target account_id" notable_decisions="principal.accountId is server-resolved from the people row, so it is trustworthy — no client input feeds the check"]
|
|
321
|
+
// CIL[DOC-CONFIDENCE pass="first" score="0.85" flags="none"]
|
|
322
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
323
|
+
// CIL[DOC-NOTE type="security" content="Added in the registration/invitation hardening batch — previously invite mutations only required a non-anonymous principal, so any authenticated user could revoke/approve/block invites in any account."]
|
|
324
|
+
function requireAccountAdmin(ctx: any, accountId: string | null | undefined): void {
|
|
325
|
+
const principal = ctx?.principal
|
|
326
|
+
if (!principal || principal.id === 'anonymous') {
|
|
327
|
+
throw new Error('Authentication required')
|
|
328
|
+
}
|
|
329
|
+
if (
|
|
330
|
+
principal.permissions?.includes('system') ||
|
|
331
|
+
(principal.roles || []).includes('system_admin')
|
|
332
|
+
) {
|
|
333
|
+
return
|
|
334
|
+
}
|
|
335
|
+
const isAdmin = (principal.roles || []).some((r: string) => isAdminRole(r))
|
|
336
|
+
if (isAdmin && accountId && principal.accountId === accountId) {
|
|
337
|
+
return
|
|
338
|
+
}
|
|
339
|
+
throw new Error('Insufficient permissions — *_admin role required for this account')
|
|
340
|
+
}
|
|
341
|
+
// CIL[END stable_id="~/.framework/functions/invites.ts:requireaccountadmin:function"]
|
|
342
|
+
|
|
343
|
+
// CIL[BEGIN stable_id="~/.framework/functions/invites.ts:approleallowlist:function" type="function" identity="appRoleAllowlist" parent="~/.framework/functions/invites.ts"]
|
|
344
|
+
// CIL[DOC-MACRO purpose="Extract the declared invitable-role allowlist from an app config"]
|
|
345
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Return config.invitation.allowed_roles when declared, else config.registration.roles, else null (no declared allowlist)."]
|
|
346
|
+
// CIL[DOC-CONTEXT module=".framework/functions/invites.ts" layer="app-specific" related_files=""]
|
|
347
|
+
// CIL[DOC-CONTRACT inputs="appConfig (apps.config jsonb)" outputs="string[] | null" errors="none" side_effects="none"]
|
|
348
|
+
// CIL[DOC-MICRO logic_summary="Reads invitation.allowed_roles first, falls back to registration.roles, filters non-strings, returns null when undeclared or empty" notable_decisions="null means no allowlist declared — callers still enforce the system_admin/is_system rejection"]
|
|
349
|
+
// CIL[DOC-CONFIDENCE pass="first" score="0.85" flags="none"]
|
|
350
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
351
|
+
function appRoleAllowlist(appConfig: any): string[] | null {
|
|
352
|
+
const declared = appConfig?.invitation?.allowed_roles ?? appConfig?.registration?.roles
|
|
353
|
+
if (!Array.isArray(declared)) return null
|
|
354
|
+
const slugs = declared.filter((r: any) => typeof r === 'string')
|
|
355
|
+
return slugs.length ? slugs : null
|
|
356
|
+
}
|
|
357
|
+
// CIL[END stable_id="~/.framework/functions/invites.ts:approleallowlist:function"]
|
|
358
|
+
|
|
359
|
+
// CIL[BEGIN stable_id="~/.framework/functions/invites.ts:resolveapproleallowlist:function" type="function" identity="resolveAppRoleAllowlist" parent="~/.framework/functions/invites.ts"]
|
|
360
|
+
// CIL[DOC-MACRO purpose="Resolve the calling app's invitable-role allowlist"]
|
|
361
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Look up apps.config by the request's app context (ctx.appId header, else body.app slug) and return its declared role allowlist."]
|
|
362
|
+
// CIL[DOC-CONTEXT module=".framework/functions/invites.ts" layer="app-specific" related_files=".framework/functions/_shared/db.ts"]
|
|
363
|
+
// CIL[DOC-CONTRACT inputs="ctx, body" outputs="Promise<string[] | null>" errors="none (unknown app resolves to null)" side_effects="one apps read"]
|
|
364
|
+
// CIL[DOC-MICRO logic_summary="ctx.appId (x-app-id header) wins over body.app; queries apps by slug; delegates to appRoleAllowlist" notable_decisions="an unresolved app context yields null — the non-system checks still apply, matching the pre-hardening default of no app allowlist"]
|
|
365
|
+
// CIL[DOC-CONFIDENCE pass="first" score="0.85" flags="none"]
|
|
366
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
367
|
+
async function resolveAppRoleAllowlist(ctx: any, body: any): Promise<string[] | null> {
|
|
368
|
+
const appSlug = ctx?.appId || body?.app
|
|
369
|
+
if (!appSlug || typeof appSlug !== 'string') return null
|
|
370
|
+
const { data: appRow } = await adminDb
|
|
371
|
+
.from('apps')
|
|
372
|
+
.select('config')
|
|
373
|
+
.eq('slug', appSlug)
|
|
374
|
+
.maybeSingle()
|
|
375
|
+
return appRoleAllowlist(appRow?.config)
|
|
376
|
+
}
|
|
377
|
+
// CIL[END stable_id="~/.framework/functions/invites.ts:resolveapproleallowlist:function"]
|
|
378
|
+
|
|
379
|
+
// CIL[BEGIN stable_id="~/.framework/functions/invites.ts:roleallowlistforaccount:function" type="function" identity="roleAllowlistForAccount" parent="~/.framework/functions/invites.ts"]
|
|
380
|
+
// CIL[DOC-MACRO purpose="Union of invitable-role allowlists declared by apps owned by an account"]
|
|
381
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Collect config.invitation.allowed_roles / config.registration.roles from every app whose owner_account_id is the account, returning the merged set or null when none is declared."]
|
|
382
|
+
// CIL[DOC-CONTEXT module=".framework/functions/invites.ts" layer="app-specific" related_files=".framework/functions/_shared/db.ts"]
|
|
383
|
+
// CIL[DOC-CONTRACT inputs="accountId" outputs="Promise<string[] | null>" errors="none" side_effects="one apps read"]
|
|
384
|
+
// CIL[DOC-MICRO logic_summary="Queries apps by owner_account_id and unions each app's declared allowlist" notable_decisions="used by updateMember to scope role_slug to the member's account apps"]
|
|
385
|
+
// CIL[DOC-CONFIDENCE pass="first" score="0.85" flags="none"]
|
|
386
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
387
|
+
async function roleAllowlistForAccount(accountId: string | null | undefined): Promise<string[] | null> {
|
|
388
|
+
if (!accountId) return null
|
|
389
|
+
const { data: appRows } = await adminDb
|
|
390
|
+
.from('apps')
|
|
391
|
+
.select('config')
|
|
392
|
+
.eq('owner_account_id', accountId)
|
|
393
|
+
const merged = new Set<string>()
|
|
394
|
+
for (const row of appRows || []) {
|
|
395
|
+
for (const slug of appRoleAllowlist(row?.config) || []) merged.add(slug)
|
|
396
|
+
}
|
|
397
|
+
return merged.size ? Array.from(merged) : null
|
|
398
|
+
}
|
|
399
|
+
// CIL[END stable_id="~/.framework/functions/invites.ts:roleallowlistforaccount:function"]
|
|
400
|
+
|
|
401
|
+
// CIL[BEGIN stable_id="~/.framework/functions/invites.ts:resolveinvitebaseurl:function" type="function" identity="resolveInviteBaseUrl" parent="~/.framework/functions/invites.ts"]
|
|
402
|
+
// CIL[DOC-MACRO purpose="Validate a client-supplied invite base URL against the origin allowlist"]
|
|
403
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Accept base_url only when its origin matches APP_BASE_URL, an ALLOWED_ORIGINS csv entry, or localhost/127.0.0.1; otherwise fall back to APP_BASE_URL or http://localhost:8888."]
|
|
404
|
+
// CIL[DOC-CONTEXT module=".framework/functions/invites.ts" layer="app-specific" related_files=""]
|
|
405
|
+
// CIL[DOC-CONTRACT inputs="baseUrl: string | undefined" outputs="string — validated origin or fallback base URL" errors="none (malformed input falls back)" side_effects="none"]
|
|
406
|
+
// CIL[DOC-MICRO logic_summary="Parses the origin; localhost/127.0.0.1 always allowed for dev; otherwise the origin must be in the APP_BASE_URL + ALLOWED_ORIGINS allowlist; any miss or parse failure returns the fallback" notable_decisions="drops the 'https://yourapp.com' placeholder; never returns the attacker-supplied URL when it is not allowlisted — invite links can no longer be pointed at phishing origins"]
|
|
407
|
+
// CIL[DOC-CONFIDENCE pass="first" score="0.85" flags="none"]
|
|
133
408
|
// CIL[DOC-STABILITY stability="stable"]
|
|
134
|
-
// CIL[DOC-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
409
|
+
// CIL[DOC-NOTE type="security" content="base_url is client-controlled; without this check an admin tricked into creating an invite could hand out a token link on an arbitrary origin."]
|
|
410
|
+
function resolveInviteBaseUrl(baseUrl: string | undefined): string {
|
|
411
|
+
const fallback = process.env.APP_BASE_URL || 'http://localhost:8888'
|
|
412
|
+
if (!baseUrl) return fallback
|
|
413
|
+
try {
|
|
414
|
+
const origin = new URL(baseUrl).origin
|
|
415
|
+
const hostname = new URL(origin).hostname
|
|
416
|
+
if (hostname === 'localhost' || hostname === '127.0.0.1') return origin
|
|
417
|
+
const allowed = new Set<string>()
|
|
418
|
+
if (process.env.APP_BASE_URL) {
|
|
419
|
+
try { allowed.add(new URL(process.env.APP_BASE_URL).origin) } catch { /* malformed env value */ }
|
|
420
|
+
}
|
|
421
|
+
for (const entry of (process.env.ALLOWED_ORIGINS || '').split(',')) {
|
|
422
|
+
const trimmed = entry.trim()
|
|
423
|
+
if (!trimmed) continue
|
|
424
|
+
try { allowed.add(new URL(trimmed).origin) } catch { /* skip malformed entry */ }
|
|
425
|
+
}
|
|
426
|
+
return allowed.has(origin) ? origin : fallback
|
|
427
|
+
} catch {
|
|
428
|
+
return fallback
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
// CIL[END stable_id="~/.framework/functions/invites.ts:resolveinvitebaseurl:function"]
|
|
432
|
+
// CIL[DOC-VECTOR type="context" content="purpose='Determines if a given role slug corresponds to an admin role.'; No verified external callers" status="ready"]
|
|
433
|
+
// CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only" status="ready"]
|
|
434
|
+
// CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified" status="ready"]
|
|
435
|
+
// CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented" status="ready"]
|
|
138
436
|
// CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
|
|
139
437
|
// CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
|
|
438
|
+
// CIL[DOC-MACRO purpose="This constant checks if there are any users in the database and determines if the current user is the first one."]
|
|
439
|
+
// CIL[DOC-RESPONSIBILITY responsibility="This constant checks if there are any users in the database and determines if the current user is the first one. It exists to facilitate user onboarding by identifying if the application is being used for the first time."]
|
|
440
|
+
// CIL[DOC-CONTEXT module="invites" layer="app-specific" related_files=".framework/functions/_shared/middleware,.framework/functions/_shared/db,.framework/functions/_shared/audit,.framework/functions/_shared/rate-limit,.framework/functions/_shared/geo,.framework/functions/integration-outbound,.framework/functions/admin-data"]
|
|
441
|
+
// CIL[DOC-CONTRACT inputs="none" outputs="none" errors="Error — failed to check first user: error message" side_effects="none dependencies="adminDb.from('people')"]
|
|
442
|
+
// CIL[DOC-MICRO logic_summary="This constant checks if there are any users in the database and determines if the current user is the first one. It exists to facilitate user onboarding by identifying if the application is being used for the first time." notable_decisions="none"]
|
|
443
|
+
// CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
|
|
444
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
445
|
+
/**
|
|
446
|
+
* This constant checks if there are any users in the database and determines if the current user is the first one. It exists to facilitate user onboarding by identifying if the application is being used for the first time.
|
|
447
|
+
*
|
|
448
|
+
* @throws Error — failed to check first user: error message
|
|
449
|
+
* @sideEffects none
|
|
450
|
+
* @stability stable
|
|
451
|
+
* @calls adminDb.from('people')
|
|
452
|
+
* @cil-generated
|
|
453
|
+
*/
|
|
454
|
+
// CIL[BEGIN stable_id="~/.framework/functions/invites.ts:checkfirstuser:constant" type="constant" identity="checkFirstUser" parent="~/.framework/functions/invites.ts"]
|
|
455
|
+
// CIL[DOC-MACRO purpose="checkFirstUser constant"]
|
|
456
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Implements the checkFirstUser constant."]
|
|
457
|
+
// CIL[DOC-CONTEXT module=".framework/functions/invites.ts" layer="app-specific" related_files=""]
|
|
458
|
+
// CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
|
|
459
|
+
// CIL[DOC-MICRO logic_summary="Implements the checkFirstUser constant." notable_decisions="[]"]
|
|
460
|
+
// CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
|
|
461
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
140
462
|
export const checkFirstUser = createPublicHandler(async (_ctx, _body) => {
|
|
141
463
|
const { data, error } = await adminDb
|
|
142
464
|
.from('people')
|
|
@@ -147,21 +469,37 @@ export const checkFirstUser = createPublicHandler(async (_ctx, _body) => {
|
|
|
147
469
|
|
|
148
470
|
return { is_first_user: (data?.length ?? 0) === 0 }
|
|
149
471
|
})
|
|
150
|
-
// CIL[END
|
|
151
|
-
// CIL[
|
|
152
|
-
// CIL[DOC-
|
|
153
|
-
// CIL[DOC-
|
|
154
|
-
// CIL[DOC-
|
|
155
|
-
// CIL[DOC-CONTRACT inputs="none" outputs="{match: boolean}" errors="Invalid email, DB errors" side_effects="DB read operation"]
|
|
156
|
-
// CIL[DOC-MICRO logic_summary="Excludes personal domains, checks for active accounts with same domain" notable_decisions="[]"]
|
|
157
|
-
// CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
|
|
158
|
-
// CIL[DOC-STABILITY stability="stable"]
|
|
159
|
-
// CIL[DOC-VECTOR type="context" content="Layer: api-handler. Direct callers: 0. Direct calls: 0. Related files: none."]
|
|
160
|
-
// CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
|
|
161
|
-
// CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
|
|
162
|
-
// CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
|
|
472
|
+
// CIL[END stable_id="~/.framework/functions/invites.ts:checkfirstuser:constant"]
|
|
473
|
+
// CIL[DOC-VECTOR type="context" content="purpose='This constant checks if there are any users in the database and determines if the current user is the first one.'; No verified external callers" status="ready"]
|
|
474
|
+
// CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only" status="ready"]
|
|
475
|
+
// CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified" status="ready"]
|
|
476
|
+
// CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented" status="ready"]
|
|
163
477
|
// CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
|
|
164
478
|
// CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
|
|
479
|
+
// CIL[DOC-MACRO purpose="Validates the provided email domain against a list of personal domains and checks for an active account associated with that domain."]
|
|
480
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Validates the provided email domain against a list of personal domains and checks for an active account associated with that domain. This function exists to ensure that only valid and non-personal email domains are processed for account matching."]
|
|
481
|
+
// CIL[DOC-CONTEXT module="invites" layer="app-specific" related_files=".framework/functions/_shared/middleware,.framework/functions/_shared/db,.framework/functions/_shared/audit,.framework/functions/_shared/rate-limit,.framework/functions/_shared/geo,.framework/functions/integration-outbound,.framework/functions/admin-data"]
|
|
482
|
+
// CIL[DOC-CONTRACT inputs="none" outputs="none" errors="Error — Valid email is required if the email is missing or invalid." side_effects="Database read operation to check for active accounts. dependencies="adminDb.from('people')"]
|
|
483
|
+
// CIL[DOC-MICRO logic_summary="Validates the provided email domain against a list of personal domains and checks for an active account associated with that domain. This function exists to ensure that only valid and non-personal email domains are processed for account matching." notable_decisions="none"]
|
|
484
|
+
// CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
|
|
485
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
486
|
+
/**
|
|
487
|
+
* Validates the provided email domain against a list of personal domains and checks for an active account associated with that domain. This function exists to ensure that only valid and non-personal email domains are processed for account matching.
|
|
488
|
+
*
|
|
489
|
+
* @throws Error — Valid email is required if the email is missing or invalid.
|
|
490
|
+
* @sideEffects Database read operation to check for active accounts.
|
|
491
|
+
* @stability stable
|
|
492
|
+
* @calls adminDb.from('people')
|
|
493
|
+
* @cil-generated
|
|
494
|
+
*/
|
|
495
|
+
// CIL[BEGIN stable_id="~/.framework/functions/invites.ts:checkdomain:constant" type="constant" identity="checkDomain" parent="~/.framework/functions/invites.ts"]
|
|
496
|
+
// CIL[DOC-MACRO purpose="checkDomain constant"]
|
|
497
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Implements the checkDomain constant."]
|
|
498
|
+
// CIL[DOC-CONTEXT module=".framework/functions/invites.ts" layer="app-specific" related_files=""]
|
|
499
|
+
// CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
|
|
500
|
+
// CIL[DOC-MICRO logic_summary="Implements the checkDomain constant." notable_decisions="[]"]
|
|
501
|
+
// CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
|
|
502
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
165
503
|
export const checkDomain = createPublicHandler(async (_ctx, _body) => {
|
|
166
504
|
const email = _ctx.query?.email as string | undefined
|
|
167
505
|
if (!email || !email.includes('@')) {
|
|
@@ -184,28 +522,45 @@ export const checkDomain = createPublicHandler(async (_ctx, _body) => {
|
|
|
184
522
|
const first = data?.[0]
|
|
185
523
|
return { match: !!first, account_id: first?.account_id ?? null }
|
|
186
524
|
})
|
|
187
|
-
// CIL[END
|
|
188
|
-
// CIL[
|
|
189
|
-
// CIL[DOC-
|
|
190
|
-
// CIL[DOC-
|
|
191
|
-
// CIL[DOC-
|
|
192
|
-
// CIL[DOC-CONTRACT inputs="none" outputs="Invite preview data" errors="Token invalid/expired/blocked" side_effects="Updates attempt count, blocks after max attempts"]
|
|
193
|
-
// CIL[DOC-MICRO logic_summary="Validates token, checks expiry/blocked status, increments attempts, blocks after max" notable_decisions="[]"]
|
|
194
|
-
// CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="complex_dependencies"]
|
|
195
|
-
// CIL[DOC-STABILITY stability="stable"]
|
|
196
|
-
// CIL[DOC-VECTOR type="context" content="Layer: api-handler. Direct callers: 0. Direct calls: 0. Related files: none."]
|
|
197
|
-
// CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
|
|
198
|
-
// CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
|
|
199
|
-
// CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
|
|
525
|
+
// CIL[END stable_id="~/.framework/functions/invites.ts:checkdomain:constant"]
|
|
526
|
+
// CIL[DOC-VECTOR type="context" content="purpose='Validates the provided email domain against a list of personal domains and checks for an active account associated with that domain.'; No verified external callers" status="ready"]
|
|
527
|
+
// CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only" status="ready"]
|
|
528
|
+
// CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified" status="ready"]
|
|
529
|
+
// CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented" status="ready"]
|
|
200
530
|
// CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
|
|
201
531
|
// CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
|
|
532
|
+
// CIL[DOC-MACRO purpose="Validates an invite token by checking its existence and status in the database — read-only, no invite-attempt writes."]
|
|
533
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Validates an invite token by checking its existence and status in the database. This function ensures that the invite link is valid, not expired, and not blocked before returning user details."]
|
|
534
|
+
// CIL[DOC-CONTEXT module="invites" layer="app-specific" related_files=".framework/functions/_shared/middleware,.framework/functions/_shared/db,.framework/functions/_shared/audit,.framework/functions/_shared/rate-limit,.framework/functions/_shared/geo,.framework/functions/integration-outbound,.framework/functions/admin-data"]
|
|
535
|
+
// CIL[DOC-CONTRACT inputs="none" outputs="none" errors="Error — Token is required, Error — Invite not found or already used, Error — This invite link has been blocked. Please contact the administrator., Error — This invite link has expired. Please request a new one., Error — Too many failed attempts. This invite has been blocked." side_effects="Read-only single people lookup. dependencies="adminDb.from('people')"]
|
|
536
|
+
// CIL[DOC-MICRO logic_summary="Validates an invite token by checking its existence and status in the database. This function ensures that the invite link is valid, not expired, and not blocked before returning user details." notable_decisions="attempts-counter writes removed so previews cannot self-block the invite"]
|
|
537
|
+
// CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
|
|
538
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
539
|
+
/**
|
|
540
|
+
* Validates an invite token by checking its existence and status in the database. This function ensures that the invite link is valid, not expired, and not blocked before returning user details. Read-only: performs no invite-attempt writes.
|
|
541
|
+
*
|
|
542
|
+
* @throws Error — Token is required, Error — Invite not found or already used, Error — This invite link has been blocked. Please contact the administrator., Error — This invite link has expired. Please request a new one., Error — Too many failed attempts. This invite has been blocked.
|
|
543
|
+
* @sideEffects none — read-only preview (the invite_attempts write was removed in the registration hardening batch)
|
|
544
|
+
* @stability stable
|
|
545
|
+
* @calls adminDb.from('people')
|
|
546
|
+
* @cil-generated
|
|
547
|
+
*/
|
|
548
|
+
// CIL[BEGIN stable_id="~/.framework/functions/invites.ts:validatetoken:constant" type="constant" identity="validateToken" parent="~/.framework/functions/invites.ts"]
|
|
549
|
+
// CIL[DOC-MACRO purpose="validateToken constant"]
|
|
550
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Implements the validateToken constant."]
|
|
551
|
+
// CIL[DOC-CONTEXT module=".framework/functions/invites.ts" layer="app-specific" related_files=""]
|
|
552
|
+
// CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
|
|
553
|
+
// CIL[DOC-MICRO logic_summary="Read-only invite preview: looks up the token on invited/pending people and reports blocked/expired/attempt-cap states without any writes." notable_decisions="attempts increment + auto-block write removed — previews no longer consume the attempts budget"]
|
|
554
|
+
// CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
|
|
555
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
556
|
+
// CIL[DOC-NOTE type="security" content="Registration/invitation hardening: the per-call invite_attempts write was removed entirely; MAX_ATTEMPTS is kept only to surface the blocked state on legacy rows."]
|
|
202
557
|
export const validateToken = createPublicHandler(async (_ctx, _body) => {
|
|
203
558
|
const token = _ctx.query?.token as string | undefined
|
|
204
559
|
if (!token) throw new Error('Token is required')
|
|
205
560
|
|
|
206
561
|
const { data: person } = await adminDb
|
|
207
562
|
.from('people')
|
|
208
|
-
.select('id, email, account_id, role_id, status, data, account:accounts!people_account_id_fkey(display_name), role:roles(slug, name)')
|
|
563
|
+
.select('id, email, account_id, role_id, status, data, account:accounts!people_account_id_fkey(display_name), role:roles!people_role_id_fkey(slug, name)')
|
|
209
564
|
.filter('data->>invite_token', 'eq', token)
|
|
210
565
|
.in('status', ['invited', 'pending'])
|
|
211
566
|
.single()
|
|
@@ -224,22 +579,11 @@ export const validateToken = createPublicHandler(async (_ctx, _body) => {
|
|
|
224
579
|
throw new Error('This invite link has expired. Please request a new one.')
|
|
225
580
|
}
|
|
226
581
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
.update({
|
|
233
|
-
data: {
|
|
234
|
-
...meta,
|
|
235
|
-
invite_attempts: attempts,
|
|
236
|
-
invite_blocked_at: blocked ? new Date().toISOString() : null,
|
|
237
|
-
},
|
|
238
|
-
updated_at: new Date().toISOString(),
|
|
239
|
-
})
|
|
240
|
-
.eq('id', person.id)
|
|
241
|
-
|
|
242
|
-
if (blocked) {
|
|
582
|
+
// Read-only preview: no invite_attempts increment and no auto-block write.
|
|
583
|
+
// Previously every preview call — including harmless page refreshes —
|
|
584
|
+
// counted toward MAX_ATTEMPTS and could self-block the invite. Legacy rows
|
|
585
|
+
// that already carry a counter at/above the cap still surface blocked.
|
|
586
|
+
if ((meta.invite_attempts || 0) >= MAX_ATTEMPTS) {
|
|
243
587
|
throw new Error('Too many failed attempts. This invite has been blocked.')
|
|
244
588
|
}
|
|
245
589
|
|
|
@@ -254,286 +598,180 @@ export const validateToken = createPublicHandler(async (_ctx, _body) => {
|
|
|
254
598
|
kind: meta.invite_kind,
|
|
255
599
|
}
|
|
256
600
|
})
|
|
257
|
-
// CIL[END
|
|
258
|
-
// CIL[
|
|
259
|
-
// CIL[DOC-
|
|
260
|
-
// CIL[DOC-
|
|
261
|
-
// CIL[DOC-
|
|
262
|
-
// CIL[DOC-RESPONSIBILITY responsibility="Handle walk-up registration flows and account assignment"]
|
|
263
|
-
// CIL[DOC-CONTEXT module="invites" layer="api-handler" related_files="[]"]
|
|
264
|
-
// CIL[DOC-CONTRACT inputs="none" outputs="Registration result" errors="Invalid path, missing params, DB errors" side_effects="DB updates, role resolution"]
|
|
265
|
-
// CIL[DOC-MICRO logic_summary="Handles new_account, existing_account, and join_request paths with role resolution" notable_decisions="[]"]
|
|
266
|
-
// CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="complex_dependencies"]
|
|
267
|
-
// CIL[DOC-STABILITY stability="stable"]
|
|
268
|
-
// CIL[DOC-VECTOR type="context" content="Layer: api-handler. Direct callers: 0. Direct calls: 0. Related files: none."]
|
|
269
|
-
// CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
|
|
270
|
-
// CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
|
|
271
|
-
// CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
|
|
601
|
+
// CIL[END stable_id="~/.framework/functions/invites.ts:validatetoken:constant"]
|
|
602
|
+
// CIL[DOC-VECTOR type="context" content="purpose='Validates an invite token by checking its existence and status in the database, and updates the invite attempts.'; No verified external callers" status="ready"]
|
|
603
|
+
// CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only" status="ready"]
|
|
604
|
+
// CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified" status="ready"]
|
|
605
|
+
// CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented" status="ready"]
|
|
272
606
|
// CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
|
|
273
607
|
// CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
|
|
608
|
+
// CIL[DOC-MACRO purpose="The completeRegistration function handles the registration process for users, either linking them to an existing account or creating a new account based on the provided path."]
|
|
609
|
+
// CIL[DOC-RESPONSIBILITY responsibility="The completeRegistration function handles the registration process for users, either linking them to an existing account or creating a new account based on the provided path. It ensures that all required fields are validated and manages role assignments and rate limiting."]
|
|
610
|
+
// CIL[DOC-CONTEXT module="invites" layer="app-specific" related_files=".framework/functions/_shared/middleware,.framework/functions/_shared/db,.framework/functions/_shared/audit,.framework/functions/_shared/rate-limit,.framework/functions/_shared/geo,.framework/functions/integration-outbound,.framework/functions/admin-data"]
|
|
611
|
+
// CIL[DOC-CONTRACT inputs="none" outputs="none" errors="Error — Authentication required; Error — app and path are required; Error (400) — any RPC error from complete_registration_placement." side_effects="Rate limit checks; placement writes happen inside the service-role-only complete_registration_placement RPC (migration 052); emits registration.completed audit. dependencies="adminDb.rpc('complete_registration_placement'), checkRateLimit, getClientGeo"]
|
|
612
|
+
// CIL[DOC-MICRO logic_summary="The completeRegistration function handles the registration process for users, either linking them to an existing account or creating a new account based on the provided path. It validates app+path, runs blocklist/rate-limit checks, and delegates all placement to the RPC bound by ctx.principal.id." notable_decisions="body email never steers placement; role/account/join-target are server-resolved in the RPC"]
|
|
613
|
+
// CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
|
|
614
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
615
|
+
/**
|
|
616
|
+
* The completeRegistration function handles the registration process for users, either linking them to an existing account or creating a new account based on the provided path. It ensures that all required fields are validated and manages role assignments and rate limiting.
|
|
617
|
+
*
|
|
618
|
+
* @throws Error — Authentication required; Error — app and path are required; Error (400) — RPC placement errors from complete_registration_placement.
|
|
619
|
+
* @sideEffects Rate limit checks; placement writes happen inside the complete_registration_placement RPC (migration 052); emits registration.completed audit.
|
|
620
|
+
* @stability stable
|
|
621
|
+
* @calls adminDb.rpc('complete_registration_placement'), checkRateLimit, getClientGeo
|
|
622
|
+
* @cil-generated
|
|
623
|
+
*/
|
|
624
|
+
// CIL[BEGIN stable_id="~/.framework/functions/invites.ts:completeregistration:constant" type="constant" identity="completeRegistration" parent="~/.framework/functions/invites.ts"]
|
|
625
|
+
// CIL[DOC-MACRO purpose="completeRegistration constant"]
|
|
626
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Implements the completeRegistration constant."]
|
|
627
|
+
// CIL[DOC-CONTEXT module=".framework/functions/invites.ts" layer="app-specific" related_files=""]
|
|
628
|
+
// CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
|
|
629
|
+
// CIL[DOC-MICRO logic_summary="Validates app+path, runs blocklist and person/ip-keyed rate limits, then delegates ALL placement work to the complete_registration_placement RPC bound by ctx.principal.id." notable_decisions="Body email is used ONLY for the blocklist check — the request body can no longer steer account placement, role assignment, or join targets; the RPC enforces app policy, role allowlists, and idempotency atomically"]
|
|
630
|
+
// CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
|
|
631
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
632
|
+
// CIL[DOC-NOTE type="security" content="Registration/invitation hardening: previously bound people rows by client-supplied email (identity spoofing), accepted client role_slug/target_account_id, and reported success on zero-row updates. Placement now lives in the service-role-only RPC from migration 052."]
|
|
274
633
|
export const completeRegistration = createHandler(async (ctx, body) => {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
if (!path || !email) {
|
|
278
|
-
throw new Error('path and email are required')
|
|
634
|
+
if (!ctx.principal || ctx.principal.id === 'anonymous') {
|
|
635
|
+
throw new Error('Authentication required')
|
|
279
636
|
}
|
|
637
|
+
const { path, account_name, email, app } = body ?? {}
|
|
280
638
|
|
|
281
|
-
if (
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
const { data: targetAccount } = await adminDb
|
|
285
|
-
.from('accounts')
|
|
286
|
-
.select('id, slug')
|
|
287
|
-
.eq('slug', target_account)
|
|
288
|
-
.single()
|
|
289
|
-
|
|
290
|
-
if (!targetAccount) {
|
|
291
|
-
throw new Error(`Target account '${target_account}' not found`)
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
const { data: personType } = await adminDb
|
|
295
|
-
.from('types')
|
|
296
|
-
.select('id, design_schema, validation_schema')
|
|
297
|
-
.eq('kind', 'person')
|
|
298
|
-
.eq('slug', 'person')
|
|
299
|
-
.single()
|
|
300
|
-
|
|
301
|
-
const targetRoleSlug = role_slug || 'member'
|
|
302
|
-
|
|
303
|
-
let { data: roles } = await adminDb
|
|
304
|
-
.from('roles')
|
|
305
|
-
.select('id, slug')
|
|
306
|
-
.eq('slug', targetRoleSlug)
|
|
307
|
-
.limit(1)
|
|
308
|
-
|
|
309
|
-
let role = roles?.[0]
|
|
310
|
-
|
|
311
|
-
if (!role) {
|
|
312
|
-
const { data: fallbackRoles } = await adminDb
|
|
313
|
-
.from('roles')
|
|
314
|
-
.select('id, slug')
|
|
315
|
-
.in('slug', ['member_admin', 'member'])
|
|
316
|
-
.order('slug')
|
|
317
|
-
.limit(1)
|
|
318
|
-
role = fallbackRoles?.[0]
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
const { error: updateErr } = await adminDb
|
|
322
|
-
.from('people')
|
|
323
|
-
.update({
|
|
324
|
-
account_id: targetAccount.id,
|
|
325
|
-
role_id: role?.id ?? null,
|
|
326
|
-
status: 'active',
|
|
327
|
-
type_id: personType.id,
|
|
328
|
-
data: {},
|
|
329
|
-
updated_at: new Date().toISOString(),
|
|
330
|
-
})
|
|
331
|
-
.eq('email', email)
|
|
332
|
-
.eq('status', 'active')
|
|
333
|
-
.is('role_id', null)
|
|
334
|
-
|
|
335
|
-
if (updateErr) throw new Error('Failed to assign user to existing account: ' + updateErr.message)
|
|
336
|
-
|
|
337
|
-
return {
|
|
338
|
-
status: 'active',
|
|
339
|
-
account_slug: targetAccount.slug
|
|
340
|
-
}
|
|
639
|
+
if (!app || !path) {
|
|
640
|
+
throw new Error('app and path are required')
|
|
341
641
|
}
|
|
342
642
|
|
|
643
|
+
// Registration chokepoint: deny blocklisted email / domain / client IP
|
|
644
|
+
// before any placement work. Fails open pre-049. The body email is used
|
|
645
|
+
// ONLY here — binding below is always ctx.principal.id.
|
|
646
|
+
const clientGeo = getClientGeo(ctx)
|
|
647
|
+
await checkRegistrationBlocklist(ctx, email || ctx.principal.email || '', clientGeo.ip)
|
|
648
|
+
|
|
649
|
+
// Rate limits keyed by the server-resolved person plus the client IP —
|
|
650
|
+
// a client-supplied email can no longer mint fresh limiter buckets.
|
|
651
|
+
await checkRateLimit(ctx, 'registration', { identifier: `p:${ctx.principal.id}` })
|
|
652
|
+
if (clientGeo.ip) {
|
|
653
|
+
await checkRateLimit(ctx, 'registration', { identifier: `ip:${clientGeo.ip}` })
|
|
654
|
+
}
|
|
343
655
|
if (path === 'new_account') {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
.select('id, design_schema, validation_schema')
|
|
349
|
-
.eq('kind', 'person')
|
|
350
|
-
.eq('slug', 'person')
|
|
351
|
-
.single()
|
|
352
|
-
|
|
353
|
-
const { data: accountType } = await adminDb
|
|
354
|
-
.from('types')
|
|
355
|
-
.select('id')
|
|
356
|
-
.eq('kind', 'account')
|
|
357
|
-
.eq('slug', 'account')
|
|
358
|
-
.single()
|
|
359
|
-
|
|
360
|
-
const targetRoleSlug = role_slug || 'member'
|
|
361
|
-
|
|
362
|
-
let { data: roles } = await adminDb
|
|
363
|
-
.from('roles')
|
|
364
|
-
.select('id, slug')
|
|
365
|
-
.eq('slug', targetRoleSlug)
|
|
366
|
-
.limit(1)
|
|
367
|
-
|
|
368
|
-
let role = roles?.[0]
|
|
369
|
-
|
|
370
|
-
if (!role) {
|
|
371
|
-
const { data: fallbackRoles } = await adminDb
|
|
372
|
-
.from('roles')
|
|
373
|
-
.select('id, slug')
|
|
374
|
-
.in('slug', ['member_admin', 'member'])
|
|
375
|
-
.order('slug')
|
|
376
|
-
.limit(1)
|
|
377
|
-
role = fallbackRoles?.[0]
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
if (!role && !role_slug) {
|
|
381
|
-
const { data: newRole, error: roleErr } = await adminDb
|
|
382
|
-
.from('roles')
|
|
383
|
-
.insert({
|
|
384
|
-
slug: 'member',
|
|
385
|
-
name: 'Member',
|
|
386
|
-
description: 'Default member role with basic permissions',
|
|
387
|
-
permissions: { items: ['read', 'create'], accounts: ['read'] },
|
|
388
|
-
is_active: true,
|
|
389
|
-
})
|
|
390
|
-
.select('id')
|
|
391
|
-
.single()
|
|
392
|
-
|
|
393
|
-
if (!roleErr && newRole) {
|
|
394
|
-
role = newRole
|
|
395
|
-
}
|
|
656
|
+
// Preserve the account-creation abuse cap (config: rate_limits.account_create).
|
|
657
|
+
await checkRateLimit(ctx, 'account_create', { identifier: `p:${ctx.principal.id}` })
|
|
658
|
+
if (clientGeo.ip) {
|
|
659
|
+
await checkRateLimit(ctx, 'account_create', { identifier: `ip:${clientGeo.ip}` })
|
|
396
660
|
}
|
|
661
|
+
}
|
|
397
662
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
.single()
|
|
409
|
-
|
|
410
|
-
if (accErr || !account) throw new Error('Failed to create account: ' + accErr?.message)
|
|
411
|
-
|
|
412
|
-
const { error: pErr } = await adminDb
|
|
413
|
-
.from('people')
|
|
414
|
-
.update({
|
|
415
|
-
account_id: account.id,
|
|
416
|
-
role_id: role?.id ?? null,
|
|
417
|
-
status: 'active',
|
|
418
|
-
updated_at: new Date().toISOString(),
|
|
419
|
-
})
|
|
420
|
-
.eq('email', email)
|
|
421
|
-
.eq('status', 'active')
|
|
422
|
-
.is('role_id', null)
|
|
423
|
-
|
|
424
|
-
if (pErr) throw new Error('Failed to update person: ' + pErr.message)
|
|
663
|
+
// All placement logic lives in the service-role-only RPC (migration 052):
|
|
664
|
+
// app policy resolution, role allowlist/non-system validation, account
|
|
665
|
+
// creation, domain-join targeting, and the role_id IS NULL idempotency
|
|
666
|
+
// precondition are enforced atomically there.
|
|
667
|
+
const { data: rpcResult, error: rpcError } = await adminDb.rpc('complete_registration_placement', {
|
|
668
|
+
p_person_id: ctx.principal.id,
|
|
669
|
+
p_app_slug: app,
|
|
670
|
+
p_path: path,
|
|
671
|
+
p_account_name: account_name ?? null,
|
|
672
|
+
})
|
|
425
673
|
|
|
426
|
-
|
|
674
|
+
if (rpcError || !rpcResult) {
|
|
675
|
+
const err: any = new Error(rpcError?.message || 'Registration placement failed')
|
|
676
|
+
err.statusCode = 400
|
|
677
|
+
throw err
|
|
427
678
|
}
|
|
428
679
|
|
|
429
|
-
|
|
430
|
-
const { target_account_id, role_slug } = body
|
|
431
|
-
if (!target_account_id) throw new Error('target_account_id is required for join_request')
|
|
680
|
+
const placement = typeof rpcResult === 'string' ? JSON.parse(rpcResult) : rpcResult
|
|
432
681
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
.
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
if (!role) {
|
|
443
|
-
const { data: fallbackRoles } = await adminDb
|
|
444
|
-
.from('roles')
|
|
445
|
-
.select('id')
|
|
446
|
-
.eq('slug', 'member')
|
|
447
|
-
.limit(1)
|
|
448
|
-
role = fallbackRoles?.[0]
|
|
682
|
+
void recordRegistrationGeo(ctx.principal.id, ctx)
|
|
683
|
+
await emitLog(ctx, 'registration.completed', { type: 'person', id: ctx.principal.id }, {
|
|
684
|
+
after: {
|
|
685
|
+
app,
|
|
686
|
+
path,
|
|
687
|
+
status: placement.status,
|
|
688
|
+
account_slug: placement.account_slug,
|
|
689
|
+
role_slug: placement.role_slug,
|
|
449
690
|
}
|
|
691
|
+
})
|
|
450
692
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
.single()
|
|
457
|
-
|
|
458
|
-
const { error: pErr } = await adminDb
|
|
459
|
-
.from('people')
|
|
460
|
-
.update({
|
|
461
|
-
account_id: target_account_id,
|
|
462
|
-
role_id: role?.id ?? null,
|
|
463
|
-
status: 'pending',
|
|
464
|
-
data: {
|
|
465
|
-
invite_kind: 'access_request',
|
|
466
|
-
invite_invited_by: null,
|
|
467
|
-
invite_token: null,
|
|
468
|
-
},
|
|
469
|
-
updated_at: new Date().toISOString(),
|
|
470
|
-
})
|
|
471
|
-
.eq('email', email)
|
|
472
|
-
.eq('status', 'active')
|
|
473
|
-
.is('role_id', null)
|
|
474
|
-
|
|
475
|
-
if (pErr) throw new Error('Failed to create access request: ' + pErr.message)
|
|
476
|
-
|
|
477
|
-
return { status: 'pending' }
|
|
693
|
+
// Response shape preserved: {status, account_slug} plus role_slug and a
|
|
694
|
+
// `next` passthrough when the RPC supplies them.
|
|
695
|
+
const result: Record<string, any> = {
|
|
696
|
+
status: placement.status,
|
|
697
|
+
account_slug: placement.account_slug ?? null,
|
|
478
698
|
}
|
|
479
|
-
|
|
480
|
-
|
|
699
|
+
if (placement.role_slug !== undefined) result.role_slug = placement.role_slug
|
|
700
|
+
if (placement.next !== undefined) result.next = placement.next
|
|
701
|
+
return result
|
|
481
702
|
})
|
|
482
|
-
// CIL[END
|
|
483
|
-
// CIL[
|
|
484
|
-
// CIL[DOC-
|
|
485
|
-
// CIL[DOC-
|
|
486
|
-
// CIL[DOC-
|
|
487
|
-
// CIL[DOC-CONTRACT inputs="none" outputs="{id, token, invite_url, expires_at}" errors="Auth required, permission denied, DB errors" side_effects="DB insert, audit logging"]
|
|
488
|
-
// CIL[DOC-MICRO logic_summary="Validates admin permissions, creates person stub with invite metadata, generates token" notable_decisions="[]"]
|
|
489
|
-
// CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="complex_dependencies"]
|
|
490
|
-
// CIL[DOC-STABILITY stability="stable"]
|
|
491
|
-
// CIL[DOC-VECTOR type="context" content="Layer: api-handler. Direct callers: 0. Direct calls: 0. Related files: none."]
|
|
492
|
-
// CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
|
|
493
|
-
// CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
|
|
494
|
-
// CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
|
|
703
|
+
// CIL[END stable_id="~/.framework/functions/invites.ts:completeregistration:constant"]
|
|
704
|
+
// CIL[DOC-VECTOR type="context" content="purpose='The completeRegistration function handles the registration process for users, either linking them to an existing account or creating a new account based on the provided path.'; No verified external callers" status="ready"]
|
|
705
|
+
// CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only" status="ready"]
|
|
706
|
+
// CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified" status="ready"]
|
|
707
|
+
// CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented" status="ready"]
|
|
495
708
|
// CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
|
|
496
709
|
// CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
|
|
710
|
+
// CIL[DOC-MACRO purpose="Creates an invite for a user to join an account, ensuring proper authentication and authorization."]
|
|
711
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Creates an invite for a user to join an account, ensuring proper authentication and authorization. It validates input and checks for existing users before generating an invite token."]
|
|
712
|
+
// CIL[DOC-CONTEXT module="invites" layer="app-specific" related_files=".framework/functions/_shared/middleware,.framework/functions/_shared/db,.framework/functions/_shared/audit,.framework/functions/_shared/rate-limit,.framework/functions/_shared/geo,.framework/functions/integration-outbound,.framework/functions/admin-data"]
|
|
713
|
+
// CIL[DOC-CONTRACT inputs="none" outputs="none" errors="Error — Authentication required, Error — email, account_id, and role_slug are required, Error — Insufficient permissions — *_admin role required to create invites, Error — Role '{role_slug}' is not permitted in this context, Error — Role '{role_slug}' not found, Error — A person with this email already exists or has a pending invite for this account, Error — Failed to create invite." side_effects="Writes to the database to create an invite and may read from multiple tables. dependencies="adminDb.from('people'), adminDb.from('roles'), adminDb.from('types'), adminDataCreate, checkRateLimit, generateToken"]
|
|
714
|
+
// CIL[DOC-MICRO logic_summary="Creates an invite for a user to join an account, ensuring proper authentication and authorization. It validates input and checks for existing users before generating an invite token." notable_decisions="none"]
|
|
715
|
+
// CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
|
|
716
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
717
|
+
/**
|
|
718
|
+
* Creates an invite for a user to join an account, ensuring proper authentication and authorization. It validates input and checks for existing users before generating an invite token.
|
|
719
|
+
*
|
|
720
|
+
* @throws Error — Authentication required, Error — email, account_id, and role_slug are required, Error — Insufficient permissions — *_admin role required to create invites, Error — Role '{role_slug}' is not permitted in this context, Error — Role '{role_slug}' not found, Error — A person with this email already exists or has a pending invite for this account, Error — Failed to create invite.
|
|
721
|
+
* @sideEffects Writes to the database to create an invite and may read from multiple tables.
|
|
722
|
+
* @stability stable
|
|
723
|
+
* @calls adminDb.from('people'), adminDb.from('roles'), adminDb.from('types'), adminDataCreate, checkRateLimit, generateToken
|
|
724
|
+
* @cil-generated
|
|
725
|
+
*/
|
|
726
|
+
// CIL[BEGIN stable_id="~/.framework/functions/invites.ts:createinvite:constant" type="constant" identity="createInvite" parent="~/.framework/functions/invites.ts"]
|
|
727
|
+
// CIL[DOC-MACRO purpose="createInvite constant"]
|
|
728
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Implements the createInvite constant."]
|
|
729
|
+
// CIL[DOC-CONTEXT module=".framework/functions/invites.ts" layer="app-specific" related_files=""]
|
|
730
|
+
// CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
|
|
731
|
+
// CIL[DOC-MICRO logic_summary="Requires an account-scoped admin principal, enforces the calling app's declared role allowlist, rejects system_admin/is_system roles, creates the invited-stub person, and builds the invite link on an origin-allowlisted base URL." notable_decisions="Client-supplied allowed_role_slugs and arbitrary base_url origins are no longer trusted; admin scope is ctx.principal.accountId vs the target account_id"]
|
|
732
|
+
// CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
|
|
733
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
734
|
+
// CIL[DOC-NOTE type="security" content="Registration/invitation hardening: caller admin check is now account-scoped via requireAccountAdmin; role_slug must be non-system and inside the app's server-resolved allowlist; base_url passes the origin allowlist instead of a yourapp.com fallback."]
|
|
497
735
|
export const createInvite = createHandler(async (ctx, body) => {
|
|
498
736
|
if (!ctx.principal || ctx.principal.id === 'anonymous') {
|
|
499
737
|
throw new Error('Authentication required')
|
|
500
738
|
}
|
|
501
739
|
|
|
502
|
-
const { full_name, email, account_id, role_slug,
|
|
740
|
+
const { full_name, email, account_id, role_slug, base_url } = body
|
|
503
741
|
if (!email || !account_id || !role_slug) {
|
|
504
742
|
throw new Error('email, account_id, and role_slug are required')
|
|
505
743
|
}
|
|
506
744
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
.select('role:roles(slug)')
|
|
510
|
-
.eq('id', ctx.principal.id)
|
|
511
|
-
.eq('account_id', account_id)
|
|
512
|
-
.single()
|
|
745
|
+
// Caller must be system_admin or an *_admin bound to THIS account.
|
|
746
|
+
requireAccountAdmin(ctx, account_id)
|
|
513
747
|
|
|
514
|
-
|
|
515
|
-
|
|
748
|
+
// Abuse control: cap invites per account (config: rate_limits.invite_create).
|
|
749
|
+
await checkRateLimit(ctx, 'invite_create', { identifier: `acct:${account_id}` })
|
|
516
750
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
if (!isSystemAdmin && !isAccountAdmin) {
|
|
522
|
-
throw new Error('Insufficient permissions — *_admin role required to create invites')
|
|
751
|
+
// Privileged roles can never be assigned through invites.
|
|
752
|
+
if (role_slug === 'system_admin') {
|
|
753
|
+
throw new Error(`Role 'system_admin' is not permitted in this context`)
|
|
523
754
|
}
|
|
524
755
|
|
|
525
|
-
|
|
756
|
+
// Server-side allowlist: the calling app's config.invitation.allowed_roles
|
|
757
|
+
// when declared, else config.registration.roles. Client-supplied
|
|
758
|
+
// allowed_role_slugs are no longer consulted.
|
|
759
|
+
const allowlist = await resolveAppRoleAllowlist(ctx, body)
|
|
760
|
+
if (allowlist && !allowlist.includes(role_slug)) {
|
|
526
761
|
throw new Error(`Role '${role_slug}' is not permitted in this context`)
|
|
527
762
|
}
|
|
528
763
|
|
|
529
764
|
const { data: roleRows } = await adminDb
|
|
530
765
|
.from('roles')
|
|
531
|
-
.select('id, slug')
|
|
766
|
+
.select('id, slug, is_system')
|
|
532
767
|
.eq('slug', role_slug)
|
|
533
768
|
.limit(1)
|
|
534
769
|
|
|
535
770
|
const resolvedRole = roleRows?.[0]
|
|
536
771
|
if (!resolvedRole) throw new Error(`Role '${role_slug}' not found`)
|
|
772
|
+
if (resolvedRole.is_system === true) {
|
|
773
|
+
throw new Error(`Role '${role_slug}' is not permitted in this context`)
|
|
774
|
+
}
|
|
537
775
|
|
|
538
776
|
const { data: existing } = await adminDb
|
|
539
777
|
.from('people')
|
|
@@ -580,30 +818,82 @@ export const createInvite = createHandler(async (ctx, body) => {
|
|
|
580
818
|
|
|
581
819
|
if (!stub) throw new Error('Failed to create invite')
|
|
582
820
|
|
|
583
|
-
const appBaseUrl = base_url
|
|
821
|
+
const appBaseUrl = resolveInviteBaseUrl(base_url)
|
|
584
822
|
const inviteUrl = `${appBaseUrl}/register?t=${token}`
|
|
585
823
|
|
|
824
|
+
// Email delivery: when an 'invite-email' integration is configured, send the
|
|
825
|
+
// invite through the outbound broker. Always return invite_url so the UI can
|
|
826
|
+
// fall back to a copyable link when no provider is configured.
|
|
827
|
+
let emailSent = false
|
|
828
|
+
try {
|
|
829
|
+
const { data: emailIntegration } = await adminDb
|
|
830
|
+
.from('integrations')
|
|
831
|
+
.select('id')
|
|
832
|
+
.eq('slug', 'invite-email')
|
|
833
|
+
.eq('is_active', true)
|
|
834
|
+
.maybeSingle()
|
|
835
|
+
|
|
836
|
+
if (emailIntegration) {
|
|
837
|
+
const { data: account } = await adminDb
|
|
838
|
+
.from('accounts')
|
|
839
|
+
.select('display_name')
|
|
840
|
+
.eq('id', account_id)
|
|
841
|
+
.maybeSingle()
|
|
842
|
+
await checkRateLimit(ctx, 'invite_email', { identifier: `acct:${account_id}` })
|
|
843
|
+
const result = await executeOutbound(ctx, {
|
|
844
|
+
integration_slug: 'invite-email',
|
|
845
|
+
action_config: {
|
|
846
|
+
to: email,
|
|
847
|
+
to_name: stubName,
|
|
848
|
+
invite_url: inviteUrl,
|
|
849
|
+
account_name: account?.display_name || '',
|
|
850
|
+
invited_by: ctx.principal.id,
|
|
851
|
+
expires_at: expiresAt,
|
|
852
|
+
}
|
|
853
|
+
})
|
|
854
|
+
emailSent = !!result?.success
|
|
855
|
+
}
|
|
856
|
+
} catch (err: any) {
|
|
857
|
+
console.warn('invite email send failed:', err?.message)
|
|
858
|
+
}
|
|
859
|
+
|
|
586
860
|
await emitLog(ctx, 'invite.created', { type: 'person', id: stub.id }, {
|
|
587
|
-
after: { email, full_name: stubName, account_id, role_slug, expires_at: expiresAt }
|
|
861
|
+
after: { email, full_name: stubName, account_id, role_slug, expires_at: expiresAt, email_sent: emailSent }
|
|
588
862
|
})
|
|
589
863
|
|
|
590
|
-
return { id: stub.id, token, invite_url: inviteUrl, expires_at: expiresAt }
|
|
864
|
+
return { id: stub.id, token, invite_url: inviteUrl, expires_at: expiresAt, email_sent: emailSent }
|
|
591
865
|
})
|
|
592
|
-
// CIL[END
|
|
593
|
-
// CIL[
|
|
594
|
-
// CIL[DOC-
|
|
595
|
-
// CIL[DOC-
|
|
596
|
-
// CIL[DOC-
|
|
597
|
-
// CIL[DOC-CONTRACT inputs="none" outputs="Array of invite records" errors="Auth required, DB errors" side_effects="DB read operations"]
|
|
598
|
-
// CIL[DOC-MICRO logic_summary="Filters by account scope, role slugs, returns mapped invite data" notable_decisions="[]"]
|
|
599
|
-
// CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="complex_dependencies"]
|
|
600
|
-
// CIL[DOC-STABILITY stability="stable"]
|
|
601
|
-
// CIL[DOC-VECTOR type="context" content="Layer: api-handler. Direct callers: 0. Direct calls: 0. Related files: none."]
|
|
602
|
-
// CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
|
|
603
|
-
// CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
|
|
604
|
-
// CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
|
|
866
|
+
// CIL[END stable_id="~/.framework/functions/invites.ts:createinvite:constant"]
|
|
867
|
+
// CIL[DOC-VECTOR type="context" content="purpose='Creates an invite for a user to join an account, ensuring proper authentication and authorization.'; No verified external callers" status="ready"]
|
|
868
|
+
// CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only" status="ready"]
|
|
869
|
+
// CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified" status="ready"]
|
|
870
|
+
// CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented" status="ready"]
|
|
605
871
|
// CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
|
|
606
872
|
// CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
|
|
873
|
+
// CIL[DOC-MACRO purpose="Lists invites for a user based on their account and roles, ensuring authentication and appropriate permissions."]
|
|
874
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Lists invites for a user based on their account and roles, ensuring authentication and appropriate permissions. It filters invites based on role slugs and returns relevant invite details."]
|
|
875
|
+
// CIL[DOC-CONTEXT module="invites" layer="app-specific" related_files=".framework/functions/_shared/middleware,.framework/functions/_shared/db,.framework/functions/_shared/audit,.framework/functions/_shared/rate-limit,.framework/functions/_shared/geo,.framework/functions/integration-outbound,.framework/functions/admin-data"]
|
|
876
|
+
// CIL[DOC-CONTRACT inputs="none" outputs="none" errors="Error — Authentication required, Error — Account scope required, Error — Database error" side_effects="Database read operations dependencies="adminDb.from('roles'), adminDb.from('people')"]
|
|
877
|
+
// CIL[DOC-MICRO logic_summary="Lists invites for a user based on their account and roles, ensuring authentication and appropriate permissions. It filters invites based on role slugs and returns relevant invite details." notable_decisions="none"]
|
|
878
|
+
// CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
|
|
879
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
880
|
+
/**
|
|
881
|
+
* Lists invites for a user based on their account and roles, ensuring authentication and appropriate permissions. It filters invites based on role slugs and returns relevant invite details.
|
|
882
|
+
*
|
|
883
|
+
* @throws Error — Authentication required, Error — Account scope required, Error — Database error
|
|
884
|
+
* @sideEffects Database read operations
|
|
885
|
+
* @stability stable
|
|
886
|
+
* @calls adminDb.from('roles'), adminDb.from('people')
|
|
887
|
+
* @cil-generated
|
|
888
|
+
*/
|
|
889
|
+
// CIL[BEGIN stable_id="~/.framework/functions/invites.ts:listinvites:constant" type="constant" identity="listInvites" parent="~/.framework/functions/invites.ts"]
|
|
890
|
+
// CIL[DOC-MACRO purpose="listInvites constant"]
|
|
891
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Implements the listInvites constant."]
|
|
892
|
+
// CIL[DOC-CONTEXT module=".framework/functions/invites.ts" layer="app-specific" related_files=""]
|
|
893
|
+
// CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
|
|
894
|
+
// CIL[DOC-MICRO logic_summary="Implements the listInvites constant." notable_decisions="[]"]
|
|
895
|
+
// CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
|
|
896
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
607
897
|
export const listInvites = createHandler(async (ctx, _body) => {
|
|
608
898
|
if (!ctx.principal || ctx.principal.id === 'anonymous') {
|
|
609
899
|
throw new Error('Authentication required')
|
|
@@ -635,7 +925,7 @@ export const listInvites = createHandler(async (ctx, _body) => {
|
|
|
635
925
|
id, email, full_name, status, created_at, updated_at,
|
|
636
926
|
account_id, role_id,
|
|
637
927
|
account:accounts!people_account_id_fkey(id, slug, display_name),
|
|
638
|
-
role:roles(id, slug, name),
|
|
928
|
+
role:roles!people_role_id_fkey(id, slug, name),
|
|
639
929
|
data
|
|
640
930
|
`)
|
|
641
931
|
.not('data->>invite_kind', 'is', null)
|
|
@@ -669,21 +959,38 @@ export const listInvites = createHandler(async (ctx, _body) => {
|
|
|
669
959
|
role: Array.isArray(row.role) ? row.role[0] : row.role,
|
|
670
960
|
}))
|
|
671
961
|
})
|
|
672
|
-
// CIL[END
|
|
673
|
-
// CIL[
|
|
674
|
-
// CIL[DOC-
|
|
675
|
-
// CIL[DOC-
|
|
676
|
-
// CIL[DOC-
|
|
677
|
-
// CIL[DOC-CONTRACT inputs="none" outputs="{success: boolean}" errors="Auth required, person not found, DB errors" side_effects="DB update, audit logging"]
|
|
678
|
-
// CIL[DOC-MICRO logic_summary="Sets person status to inactive, clears invite token, logs revocation" notable_decisions="[]"]
|
|
679
|
-
// CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
|
|
680
|
-
// CIL[DOC-STABILITY stability="stable"]
|
|
681
|
-
// CIL[DOC-VECTOR type="context" content="Layer: api-handler. Direct callers: 0. Direct calls: 0. Related files: none."]
|
|
682
|
-
// CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
|
|
683
|
-
// CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
|
|
684
|
-
// CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
|
|
962
|
+
// CIL[END stable_id="~/.framework/functions/invites.ts:listinvites:constant"]
|
|
963
|
+
// CIL[DOC-VECTOR type="context" content="purpose='Lists invites for a user based on their account and roles, ensuring authentication and appropriate permissions.'; No verified external callers" status="ready"]
|
|
964
|
+
// CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only" status="ready"]
|
|
965
|
+
// CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified" status="ready"]
|
|
966
|
+
// CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented" status="ready"]
|
|
685
967
|
// CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
|
|
686
968
|
// CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
|
|
969
|
+
// CIL[DOC-MACRO purpose="The revokeInvite function revokes an invitation for a person by updating their status in the database."]
|
|
970
|
+
// CIL[DOC-RESPONSIBILITY responsibility="The revokeInvite function revokes an invitation for a person by updating their status in the database. It ensures that only authenticated users can perform this action and that the person exists in a valid state before revocation."]
|
|
971
|
+
// CIL[DOC-CONTEXT module="invites" layer="app-specific" related_files=".framework/functions/_shared/middleware,.framework/functions/_shared/db,.framework/functions/_shared/audit,.framework/functions/_shared/rate-limit,.framework/functions/_shared/geo,.framework/functions/integration-outbound,.framework/functions/admin-data"]
|
|
972
|
+
// CIL[DOC-CONTRACT inputs="none" outputs="none" errors="Error — Authentication required, person_id is required, Invite not found or already processed, error from database update" side_effects="Updates the status of a person in the database, emits a log event dependencies="adminDb.from('people'), emitLog"]
|
|
973
|
+
// CIL[DOC-MICRO logic_summary="The revokeInvite function revokes an invitation for a person by updating their status in the database. It ensures that only authenticated users can perform this action and that the person exists in a valid state before revocation." notable_decisions="none"]
|
|
974
|
+
// CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
|
|
975
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
976
|
+
/**
|
|
977
|
+
* The revokeInvite function revokes an invitation for a person by updating their status in the database. It ensures that only authenticated users can perform this action and that the person exists in a valid state before revocation.
|
|
978
|
+
*
|
|
979
|
+
* @throws Error — Authentication required, person_id is required, Invite not found or already processed, error from database update
|
|
980
|
+
* @sideEffects Updates the status of a person in the database, emits a log event
|
|
981
|
+
* @stability stable
|
|
982
|
+
* @calls adminDb.from('people'), emitLog
|
|
983
|
+
* @cil-generated
|
|
984
|
+
*/
|
|
985
|
+
// CIL[BEGIN stable_id="~/.framework/functions/invites.ts:revokeinvite:constant" type="constant" identity="revokeInvite" parent="~/.framework/functions/invites.ts"]
|
|
986
|
+
// CIL[DOC-MACRO purpose="revokeInvite constant"]
|
|
987
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Implements the revokeInvite constant."]
|
|
988
|
+
// CIL[DOC-CONTEXT module=".framework/functions/invites.ts" layer="app-specific" related_files=""]
|
|
989
|
+
// CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
|
|
990
|
+
// CIL[DOC-MICRO logic_summary="Implements the revokeInvite constant." notable_decisions="[]"]
|
|
991
|
+
// CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
|
|
992
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
993
|
+
// CIL[DOC-NOTE type="security" content="Registration/invitation hardening: now requires an admin of the target person's account (or system_admin) via requireAccountAdmin — previously any authenticated principal could revoke."]
|
|
687
994
|
export const revokeInvite = createHandler(async (ctx, body) => {
|
|
688
995
|
if (!ctx.principal || ctx.principal.id === 'anonymous') throw new Error('Authentication required')
|
|
689
996
|
|
|
@@ -699,6 +1006,9 @@ export const revokeInvite = createHandler(async (ctx, body) => {
|
|
|
699
1006
|
|
|
700
1007
|
if (!person) throw new Error('Invite not found or already processed')
|
|
701
1008
|
|
|
1009
|
+
// Scope: only an admin of the invite's own account (or system_admin).
|
|
1010
|
+
requireAccountAdmin(ctx, person.account_id)
|
|
1011
|
+
|
|
702
1012
|
const { error } = await adminDb
|
|
703
1013
|
.from('people')
|
|
704
1014
|
.update({
|
|
@@ -715,21 +1025,38 @@ export const revokeInvite = createHandler(async (ctx, body) => {
|
|
|
715
1025
|
|
|
716
1026
|
return { success: true }
|
|
717
1027
|
})
|
|
718
|
-
// CIL[END
|
|
719
|
-
// CIL[
|
|
720
|
-
// CIL[DOC-
|
|
721
|
-
// CIL[DOC-
|
|
722
|
-
// CIL[DOC-
|
|
723
|
-
// CIL[DOC-CONTRACT inputs="none" outputs="{token, invite_url, expires_at}" errors="Auth required, person not found, DB errors" side_effects="DB update, audit logging"]
|
|
724
|
-
// CIL[DOC-MICRO logic_summary="Generates new token, resets attempts, updates expiry, returns new URL" notable_decisions="[]"]
|
|
725
|
-
// CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
|
|
726
|
-
// CIL[DOC-STABILITY stability="stable"]
|
|
727
|
-
// CIL[DOC-VECTOR type="context" content="Layer: api-handler. Direct callers: 0. Direct calls: 0. Related files: none."]
|
|
728
|
-
// CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
|
|
729
|
-
// CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
|
|
730
|
-
// CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
|
|
1028
|
+
// CIL[END stable_id="~/.framework/functions/invites.ts:revokeinvite:constant"]
|
|
1029
|
+
// CIL[DOC-VECTOR type="context" content="purpose='The revokeInvite function revokes an invitation for a person by updating their status in the database.'; No verified external callers" status="ready"]
|
|
1030
|
+
// CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only" status="ready"]
|
|
1031
|
+
// CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified" status="ready"]
|
|
1032
|
+
// CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented" status="ready"]
|
|
731
1033
|
// CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
|
|
732
1034
|
// CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
|
|
1035
|
+
// CIL[DOC-MACRO purpose="The reinvite constant handles the process of inviting a person by generating a new invite token and updating their status in the database."]
|
|
1036
|
+
// CIL[DOC-RESPONSIBILITY responsibility="The reinvite constant handles the process of inviting a person by generating a new invite token and updating their status in the database. It ensures that only authenticated users can perform this action."]
|
|
1037
|
+
// CIL[DOC-CONTEXT module="invites" layer="app-specific" related_files=".framework/functions/_shared/middleware,.framework/functions/_shared/db,.framework/functions/_shared/audit,.framework/functions/_shared/rate-limit,.framework/functions/_shared/geo,.framework/functions/integration-outbound,.framework/functions/admin-data"]
|
|
1038
|
+
// CIL[DOC-CONTRACT inputs="none" outputs="none" errors="Error — Authentication required, person_id is required, Person not found, error from database update." side_effects="Writes to the 'people' table in the database and emits a log entry. dependencies="adminDb.from('people'), generateToken, emitLog"]
|
|
1039
|
+
// CIL[DOC-MICRO logic_summary="The reinvite constant handles the process of inviting a person by generating a new invite token and updating their status in the database. It ensures that only authenticated users can perform this action." notable_decisions="none"]
|
|
1040
|
+
// CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
|
|
1041
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
1042
|
+
/**
|
|
1043
|
+
* The reinvite constant handles the process of inviting a person by generating a new invite token and updating their status in the database. It ensures that only authenticated users can perform this action.
|
|
1044
|
+
*
|
|
1045
|
+
* @throws Error — Authentication required, person_id is required, Person not found, error from database update.
|
|
1046
|
+
* @sideEffects Writes to the 'people' table in the database and emits a log entry.
|
|
1047
|
+
* @stability stable
|
|
1048
|
+
* @calls adminDb.from('people'), generateToken, emitLog
|
|
1049
|
+
* @cil-generated
|
|
1050
|
+
*/
|
|
1051
|
+
// CIL[BEGIN stable_id="~/.framework/functions/invites.ts:reinvite:constant" type="constant" identity="reinvite" parent="~/.framework/functions/invites.ts"]
|
|
1052
|
+
// CIL[DOC-MACRO purpose="reinvite constant"]
|
|
1053
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Implements the reinvite constant."]
|
|
1054
|
+
// CIL[DOC-CONTEXT module=".framework/functions/invites.ts" layer="app-specific" related_files=""]
|
|
1055
|
+
// CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
|
|
1056
|
+
// CIL[DOC-MICRO logic_summary="Implements the reinvite constant." notable_decisions="[]"]
|
|
1057
|
+
// CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
|
|
1058
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
1059
|
+
// CIL[DOC-NOTE type="security" content="Registration/invitation hardening: requires an admin of the target person's account via requireAccountAdmin, restricts reinvite to invited/inactive people, and passes base_url through the origin allowlist."]
|
|
733
1060
|
export const reinvite = createHandler(async (ctx, body) => {
|
|
734
1061
|
if (!ctx.principal || ctx.principal.id === 'anonymous') throw new Error('Authentication required')
|
|
735
1062
|
|
|
@@ -744,6 +1071,13 @@ export const reinvite = createHandler(async (ctx, body) => {
|
|
|
744
1071
|
|
|
745
1072
|
if (!person) throw new Error('Person not found')
|
|
746
1073
|
|
|
1074
|
+
// Scope: only an admin of the person's account (or system_admin), and only
|
|
1075
|
+
// invited/inactive people may be re-invited — never an active member.
|
|
1076
|
+
requireAccountAdmin(ctx, person.account_id)
|
|
1077
|
+
if (person.status !== 'invited' && person.status !== 'inactive') {
|
|
1078
|
+
throw new Error('Person is not in a reinvitable state')
|
|
1079
|
+
}
|
|
1080
|
+
|
|
747
1081
|
const newToken = generateToken()
|
|
748
1082
|
const expiresAt = new Date(Date.now() + INVITE_EXPIRY_DAYS * 86400 * 1000).toISOString()
|
|
749
1083
|
|
|
@@ -767,7 +1101,7 @@ export const reinvite = createHandler(async (ctx, body) => {
|
|
|
767
1101
|
|
|
768
1102
|
if (error) throw error
|
|
769
1103
|
|
|
770
|
-
const appBaseUrl = base_url
|
|
1104
|
+
const appBaseUrl = resolveInviteBaseUrl(base_url)
|
|
771
1105
|
const inviteUrl = `${appBaseUrl}/register?t=${newToken}`
|
|
772
1106
|
|
|
773
1107
|
await emitLog(ctx, 'invite.reinvited', { type: 'person', id: person_id }, {
|
|
@@ -776,21 +1110,38 @@ export const reinvite = createHandler(async (ctx, body) => {
|
|
|
776
1110
|
|
|
777
1111
|
return { token: newToken, invite_url: inviteUrl, expires_at: expiresAt }
|
|
778
1112
|
})
|
|
779
|
-
// CIL[END
|
|
780
|
-
// CIL[
|
|
781
|
-
// CIL[DOC-
|
|
782
|
-
// CIL[DOC-
|
|
783
|
-
// CIL[DOC-
|
|
784
|
-
// CIL[DOC-CONTRACT inputs="none" outputs="{success: boolean}" errors="Auth required, request not found, DB errors" side_effects="DB update, audit logging"]
|
|
785
|
-
// CIL[DOC-MICRO logic_summary="Sets status to active, strips invite metadata, logs approval" notable_decisions="[]"]
|
|
786
|
-
// CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
|
|
787
|
-
// CIL[DOC-STABILITY stability="stable"]
|
|
788
|
-
// CIL[DOC-VECTOR type="context" content="Layer: api-handler. Direct callers: 0. Direct calls: 0. Related files: none."]
|
|
789
|
-
// CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
|
|
790
|
-
// CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
|
|
791
|
-
// CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
|
|
1113
|
+
// CIL[END stable_id="~/.framework/functions/invites.ts:reinvite:constant"]
|
|
1114
|
+
// CIL[DOC-VECTOR type="context" content="purpose='The reinvite constant handles the process of inviting a person by generating a new invite token and updating their status in the database.'; No verified external callers" status="ready"]
|
|
1115
|
+
// CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only" status="ready"]
|
|
1116
|
+
// CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified" status="ready"]
|
|
1117
|
+
// CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented" status="ready"]
|
|
792
1118
|
// CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
|
|
793
1119
|
// CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
|
|
1120
|
+
// CIL[DOC-MACRO purpose="Approves an access request for a person by updating their status to active."]
|
|
1121
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Approves an access request for a person by updating their status to active. It ensures that the request is authenticated and that the person exists in a pending state before proceeding."]
|
|
1122
|
+
// CIL[DOC-CONTEXT module="invites" layer="app-specific" related_files=".framework/functions/_shared/middleware,.framework/functions/_shared/db,.framework/functions/_shared/audit,.framework/functions/_shared/rate-limit,.framework/functions/_shared/geo,.framework/functions/integration-outbound,.framework/functions/admin-data"]
|
|
1123
|
+
// CIL[DOC-CONTRACT inputs="none" outputs="none" errors="Error — Authentication required, person_id is required, Access request not found or already processed, error from database update" side_effects="Writes to the 'people' table in the database and emits a log event. dependencies="adminDb.from('people'), emitLog"]
|
|
1124
|
+
// CIL[DOC-MICRO logic_summary="Approves an access request for a person by updating their status to active. It ensures that the request is authenticated and that the person exists in a pending state before proceeding." notable_decisions="none"]
|
|
1125
|
+
// CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
|
|
1126
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
1127
|
+
/**
|
|
1128
|
+
* Approves an access request for a person by updating their status to active. It ensures that the request is authenticated and that the person exists in a pending state before proceeding.
|
|
1129
|
+
*
|
|
1130
|
+
* @throws Error — Authentication required, person_id is required, Access request not found or already processed, error from database update
|
|
1131
|
+
* @sideEffects Writes to the 'people' table in the database and emits a log event.
|
|
1132
|
+
* @stability stable
|
|
1133
|
+
* @calls adminDb.from('people'), emitLog
|
|
1134
|
+
* @cil-generated
|
|
1135
|
+
*/
|
|
1136
|
+
// CIL[BEGIN stable_id="~/.framework/functions/invites.ts:approve:constant" type="constant" identity="approve" parent="~/.framework/functions/invites.ts"]
|
|
1137
|
+
// CIL[DOC-MACRO purpose="approve constant"]
|
|
1138
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Implements the approve constant."]
|
|
1139
|
+
// CIL[DOC-CONTEXT module=".framework/functions/invites.ts" layer="app-specific" related_files=""]
|
|
1140
|
+
// CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
|
|
1141
|
+
// CIL[DOC-MICRO logic_summary="Implements the approve constant." notable_decisions="[]"]
|
|
1142
|
+
// CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
|
|
1143
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
1144
|
+
// CIL[DOC-NOTE type="security" content="Registration/invitation hardening: now requires an admin of the target person's account (or system_admin) via requireAccountAdmin — previously any authenticated principal could approve."]
|
|
794
1145
|
export const approve = createHandler(async (ctx, body) => {
|
|
795
1146
|
if (!ctx.principal || ctx.principal.id === 'anonymous') throw new Error('Authentication required')
|
|
796
1147
|
|
|
@@ -806,6 +1157,9 @@ export const approve = createHandler(async (ctx, body) => {
|
|
|
806
1157
|
|
|
807
1158
|
if (!person) throw new Error('Access request not found or already processed')
|
|
808
1159
|
|
|
1160
|
+
// Scope: only an admin of the requesting account (or system_admin).
|
|
1161
|
+
requireAccountAdmin(ctx, person.account_id)
|
|
1162
|
+
|
|
809
1163
|
const { invite_token, invite_kind, invite_expires_at, invite_attempts, invite_blocked_at, invite_invited_by, ...remainingData } =
|
|
810
1164
|
(person.data as any) || {}
|
|
811
1165
|
|
|
@@ -827,21 +1181,38 @@ export const approve = createHandler(async (ctx, body) => {
|
|
|
827
1181
|
|
|
828
1182
|
return { success: true }
|
|
829
1183
|
})
|
|
830
|
-
// CIL[END
|
|
831
|
-
// CIL[
|
|
832
|
-
// CIL[DOC-
|
|
833
|
-
// CIL[DOC-
|
|
834
|
-
// CIL[DOC-
|
|
835
|
-
// CIL[DOC-CONTRACT inputs="none" outputs="{success: boolean}" errors="Auth required, person not found, DB errors" side_effects="DB update, audit logging"]
|
|
836
|
-
// CIL[DOC-MICRO logic_summary="Sets invite_blocked_at timestamp to prevent token validation" notable_decisions="[]"]
|
|
837
|
-
// CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
|
|
838
|
-
// CIL[DOC-STABILITY stability="stable"]
|
|
839
|
-
// CIL[DOC-VECTOR type="context" content="Layer: api-handler. Direct callers: 0. Direct calls: 0. Related files: none."]
|
|
840
|
-
// CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
|
|
841
|
-
// CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
|
|
842
|
-
// CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
|
|
1184
|
+
// CIL[END stable_id="~/.framework/functions/invites.ts:approve:constant"]
|
|
1185
|
+
// CIL[DOC-VECTOR type="context" content="purpose='Approves an access request for a person by updating their status to active.'; No verified external callers" status="ready"]
|
|
1186
|
+
// CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only" status="ready"]
|
|
1187
|
+
// CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified" status="ready"]
|
|
1188
|
+
// CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented" status="ready"]
|
|
843
1189
|
// CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
|
|
844
1190
|
// CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
|
|
1191
|
+
// CIL[DOC-MACRO purpose="Blocks an invite for a specified person by updating their record in the database."]
|
|
1192
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Blocks an invite for a specified person by updating their record in the database. This function ensures that only authenticated users can perform this action."]
|
|
1193
|
+
// CIL[DOC-CONTEXT module="invites" layer="app-specific" related_files=".framework/functions/_shared/middleware,.framework/functions/_shared/db,.framework/functions/_shared/audit,.framework/functions/_shared/rate-limit,.framework/functions/_shared/geo,.framework/functions/integration-outbound,.framework/functions/admin-data"]
|
|
1194
|
+
// CIL[DOC-CONTRACT inputs="none" outputs="none" errors="Error — Authentication required, person_id is required, Person not found, or database update error." side_effects="Writes to the 'people' table in the database and emits a log event. dependencies="adminDb.from('people'), emitLog"]
|
|
1195
|
+
// CIL[DOC-MICRO logic_summary="Blocks an invite for a specified person by updating their record in the database. This function ensures that only authenticated users can perform this action." notable_decisions="none"]
|
|
1196
|
+
// CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
|
|
1197
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
1198
|
+
/**
|
|
1199
|
+
* Blocks an invite for a specified person by updating their record in the database. This function ensures that only authenticated users can perform this action.
|
|
1200
|
+
*
|
|
1201
|
+
* @throws Error — Authentication required, person_id is required, Person not found, or database update error.
|
|
1202
|
+
* @sideEffects Writes to the 'people' table in the database and emits a log event.
|
|
1203
|
+
* @stability stable
|
|
1204
|
+
* @calls adminDb.from('people'), emitLog
|
|
1205
|
+
* @cil-generated
|
|
1206
|
+
*/
|
|
1207
|
+
// CIL[BEGIN stable_id="~/.framework/functions/invites.ts:blockinvite:constant" type="constant" identity="blockInvite" parent="~/.framework/functions/invites.ts"]
|
|
1208
|
+
// CIL[DOC-MACRO purpose="blockInvite constant"]
|
|
1209
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Implements the blockInvite constant."]
|
|
1210
|
+
// CIL[DOC-CONTEXT module=".framework/functions/invites.ts" layer="app-specific" related_files=""]
|
|
1211
|
+
// CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
|
|
1212
|
+
// CIL[DOC-MICRO logic_summary="Implements the blockInvite constant." notable_decisions="[]"]
|
|
1213
|
+
// CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
|
|
1214
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
1215
|
+
// CIL[DOC-NOTE type="security" content="Registration/invitation hardening: now requires an admin of the target person's account (or system_admin) via requireAccountAdmin — previously any authenticated principal could block."]
|
|
845
1216
|
export const blockInvite = createHandler(async (ctx, body) => {
|
|
846
1217
|
if (!ctx.principal || ctx.principal.id === 'anonymous') throw new Error('Authentication required')
|
|
847
1218
|
|
|
@@ -856,6 +1227,9 @@ export const blockInvite = createHandler(async (ctx, body) => {
|
|
|
856
1227
|
|
|
857
1228
|
if (!person) throw new Error('Person not found')
|
|
858
1229
|
|
|
1230
|
+
// Scope: only an admin of the person's account (or system_admin).
|
|
1231
|
+
requireAccountAdmin(ctx, person.account_id)
|
|
1232
|
+
|
|
859
1233
|
const meta = (person.data as any) || {}
|
|
860
1234
|
|
|
861
1235
|
const { error } = await adminDb
|
|
@@ -877,21 +1251,38 @@ export const blockInvite = createHandler(async (ctx, body) => {
|
|
|
877
1251
|
|
|
878
1252
|
return { success: true }
|
|
879
1253
|
})
|
|
880
|
-
// CIL[END
|
|
881
|
-
// CIL[
|
|
882
|
-
// CIL[DOC-
|
|
883
|
-
// CIL[DOC-
|
|
884
|
-
// CIL[DOC-
|
|
885
|
-
// CIL[DOC-CONTRACT inputs="none" outputs="{success: boolean}" errors="Auth required, request not found, DB errors" side_effects="DB update, audit logging"]
|
|
886
|
-
// CIL[DOC-MICRO logic_summary="Sets status to inactive and is_active false, logs denial" notable_decisions="[]"]
|
|
887
|
-
// CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
|
|
888
|
-
// CIL[DOC-STABILITY stability="stable"]
|
|
889
|
-
// CIL[DOC-VECTOR type="context" content="Layer: api-handler. Direct callers: 0. Direct calls: 0. Related files: none."]
|
|
890
|
-
// CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
|
|
891
|
-
// CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
|
|
892
|
-
// CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
|
|
1254
|
+
// CIL[END stable_id="~/.framework/functions/invites.ts:blockinvite:constant"]
|
|
1255
|
+
// CIL[DOC-VECTOR type="context" content="purpose='Blocks an invite for a specified person by updating their record in the database.'; No verified external callers" status="ready"]
|
|
1256
|
+
// CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only" status="ready"]
|
|
1257
|
+
// CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified" status="ready"]
|
|
1258
|
+
// CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented" status="ready"]
|
|
893
1259
|
// CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
|
|
894
1260
|
// CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
|
|
1261
|
+
// CIL[DOC-MACRO purpose="The deny function processes an access request by marking a person as inactive and logging the action."]
|
|
1262
|
+
// CIL[DOC-RESPONSIBILITY responsibility="The deny function processes an access request by marking a person as inactive and logging the action. It ensures that only authenticated users can deny requests and that the specified person exists and is pending."]
|
|
1263
|
+
// CIL[DOC-CONTEXT module="invites" layer="app-specific" related_files=".framework/functions/_shared/middleware,.framework/functions/_shared/db,.framework/functions/_shared/audit,.framework/functions/_shared/rate-limit,.framework/functions/_shared/geo,.framework/functions/integration-outbound,.framework/functions/admin-data"]
|
|
1264
|
+
// CIL[DOC-CONTRACT inputs="none" outputs="none" errors="Error — Authentication required, person_id is required, Access request not found or already processed, error from database update" side_effects="Updates the database record for the person and emits a log event. dependencies="adminDb.from('people'), emitLog"]
|
|
1265
|
+
// CIL[DOC-MICRO logic_summary="The deny function processes an access request by marking a person as inactive and logging the action. It ensures that only authenticated users can deny requests and that the specified person exists and is pending." notable_decisions="none"]
|
|
1266
|
+
// CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
|
|
1267
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
1268
|
+
/**
|
|
1269
|
+
* The deny function processes an access request by marking a person as inactive and logging the action. It ensures that only authenticated users can deny requests and that the specified person exists and is pending.
|
|
1270
|
+
*
|
|
1271
|
+
* @throws Error — Authentication required, person_id is required, Access request not found or already processed, error from database update
|
|
1272
|
+
* @sideEffects Updates the database record for the person and emits a log event.
|
|
1273
|
+
* @stability stable
|
|
1274
|
+
* @calls adminDb.from('people'), emitLog
|
|
1275
|
+
* @cil-generated
|
|
1276
|
+
*/
|
|
1277
|
+
// CIL[BEGIN stable_id="~/.framework/functions/invites.ts:deny:constant" type="constant" identity="deny" parent="~/.framework/functions/invites.ts"]
|
|
1278
|
+
// CIL[DOC-MACRO purpose="deny constant"]
|
|
1279
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Implements the deny constant."]
|
|
1280
|
+
// CIL[DOC-CONTEXT module=".framework/functions/invites.ts" layer="app-specific" related_files=""]
|
|
1281
|
+
// CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
|
|
1282
|
+
// CIL[DOC-MICRO logic_summary="Implements the deny constant." notable_decisions="[]"]
|
|
1283
|
+
// CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
|
|
1284
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
1285
|
+
// CIL[DOC-NOTE type="security" content="Registration/invitation hardening: now requires an admin of the target person's account (or system_admin) via requireAccountAdmin — previously any authenticated principal could deny."]
|
|
895
1286
|
export const deny = createHandler(async (ctx, body) => {
|
|
896
1287
|
if (!ctx.principal || ctx.principal.id === 'anonymous') throw new Error('Authentication required')
|
|
897
1288
|
|
|
@@ -907,6 +1298,9 @@ export const deny = createHandler(async (ctx, body) => {
|
|
|
907
1298
|
|
|
908
1299
|
if (!person) throw new Error('Access request not found or already processed')
|
|
909
1300
|
|
|
1301
|
+
// Scope: only an admin of the requesting account (or system_admin).
|
|
1302
|
+
requireAccountAdmin(ctx, person.account_id)
|
|
1303
|
+
|
|
910
1304
|
const { error } = await adminDb
|
|
911
1305
|
.from('people')
|
|
912
1306
|
.update({
|
|
@@ -924,21 +1318,37 @@ export const deny = createHandler(async (ctx, body) => {
|
|
|
924
1318
|
|
|
925
1319
|
return { success: true }
|
|
926
1320
|
})
|
|
927
|
-
// CIL[END
|
|
928
|
-
// CIL[
|
|
929
|
-
// CIL[DOC-
|
|
930
|
-
// CIL[DOC-
|
|
931
|
-
// CIL[DOC-
|
|
932
|
-
// CIL[DOC-CONTRACT inputs="none" outputs="Varies by action" errors="Unknown action" side_effects="Delegates to handlers"]
|
|
933
|
-
// CIL[DOC-MICRO logic_summary="Switch statement routes action parameter to appropriate handler function" notable_decisions="[]"]
|
|
934
|
-
// CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
|
|
935
|
-
// CIL[DOC-STABILITY stability="stable"]
|
|
936
|
-
// CIL[DOC-VECTOR type="context" content="Layer: api-handler. Direct callers: 0. Direct calls: 0. Related files: none."]
|
|
937
|
-
// CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
|
|
938
|
-
// CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
|
|
939
|
-
// CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
|
|
1321
|
+
// CIL[END stable_id="~/.framework/functions/invites.ts:deny:constant"]
|
|
1322
|
+
// CIL[DOC-VECTOR type="context" content="purpose='The deny function processes an access request by marking a person as inactive and logging the action.'; No verified external callers" status="ready"]
|
|
1323
|
+
// CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only" status="ready"]
|
|
1324
|
+
// CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified" status="ready"]
|
|
1325
|
+
// CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented" status="ready"]
|
|
940
1326
|
// CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
|
|
941
1327
|
// CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
|
|
1328
|
+
// CIL[DOC-MACRO purpose="The handler processes various actions related to user invites and member management based on the provided context and body."]
|
|
1329
|
+
// CIL[DOC-RESPONSIBILITY responsibility="The handler processes various actions related to user invites and member management based on the provided context and body. It serves as a centralized entry point for handling invite-related operations in the application."]
|
|
1330
|
+
// CIL[DOC-CONTEXT module="invites" layer="app-specific" related_files=".framework/functions/_shared/middleware,.framework/functions/_shared/db,.framework/functions/_shared/audit,.framework/functions/_shared/rate-limit,.framework/functions/_shared/geo,.framework/functions/integration-outbound,.framework/functions/admin-data"]
|
|
1331
|
+
// CIL[DOC-CONTRACT inputs="none" outputs="none" errors="Error — Unknown action: {action}" side_effects="none dependencies="checkFirstUser, checkDomain, validateToken, completeRegistration, createInvite, listInvites, revokeInvite, reinvite, blockInvite, approve, deny, listMembers, updateMember, deactivateMember"]
|
|
1332
|
+
// CIL[DOC-MICRO logic_summary="The handler processes various actions related to user invites and member management based on the provided context and body. It serves as a centralized entry point for handling invite-related operations in the application." notable_decisions="none"]
|
|
1333
|
+
// CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
|
|
1334
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
1335
|
+
/**
|
|
1336
|
+
* The handler processes various actions related to user invites and member management based on the provided context and body. It serves as a centralized entry point for handling invite-related operations in the application.
|
|
1337
|
+
*
|
|
1338
|
+
* @throws Error — Unknown action: {action}
|
|
1339
|
+
* @sideEffects none
|
|
1340
|
+
* @stability stable
|
|
1341
|
+
* @calls checkFirstUser, checkDomain, validateToken, completeRegistration, createInvite, listInvites, revokeInvite, reinvite, blockInvite, approve, deny, listMembers, updateMember, deactivateMember
|
|
1342
|
+
* @cil-generated
|
|
1343
|
+
*/
|
|
1344
|
+
// CIL[BEGIN stable_id="~/.framework/functions/invites.ts:handler:constant" type="constant" identity="handler" parent="~/.framework/functions/invites.ts"]
|
|
1345
|
+
// CIL[DOC-MACRO purpose="handler constant"]
|
|
1346
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Implements the handler constant."]
|
|
1347
|
+
// CIL[DOC-CONTEXT module=".framework/functions/invites.ts" layer="app-specific" related_files=""]
|
|
1348
|
+
// CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
|
|
1349
|
+
// CIL[DOC-MICRO logic_summary="Implements the handler constant." notable_decisions="[]"]
|
|
1350
|
+
// CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
|
|
1351
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
942
1352
|
export const handler = createPublicHandler(async (ctx, body) => {
|
|
943
1353
|
const action = ctx.query?.action
|
|
944
1354
|
|
|
@@ -961,16 +1371,86 @@ export const handler = createPublicHandler(async (ctx, body) => {
|
|
|
961
1371
|
throw new Error(`Unknown action: ${action}`)
|
|
962
1372
|
}
|
|
963
1373
|
})
|
|
964
|
-
// CIL[END
|
|
1374
|
+
// CIL[END stable_id="~/.framework/functions/invites.ts:handler:constant"]
|
|
1375
|
+
// CIL[DOC-VECTOR type="context" content="purpose='The handler processes various actions related to user invites and member management based on the provided context and body.'; No verified external callers" status="ready"]
|
|
1376
|
+
// CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only" status="ready"]
|
|
1377
|
+
// CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified" status="ready"]
|
|
1378
|
+
// CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented" status="ready"]
|
|
1379
|
+
// CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
|
|
1380
|
+
// CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
|
|
965
1381
|
|
|
966
1382
|
// ─── Member management actions (runtime CRUD via admin-data) ─────────────────
|
|
967
1383
|
|
|
1384
|
+
// CIL[DOC-MACRO purpose="Determines if a user has the necessary permissions to manage members based on their principal context."]
|
|
1385
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Determines if a user has the necessary permissions to manage members based on their principal context. This function is essential for enforcing access control in member management operations."]
|
|
1386
|
+
// CIL[DOC-CONTEXT module="invites" layer="app-specific" related_files=".framework/functions/_shared/middleware,.framework/functions/_shared/db,.framework/functions/_shared/audit,.framework/functions/_shared/rate-limit,.framework/functions/_shared/geo,.framework/functions/integration-outbound,.framework/functions/admin-data"]
|
|
1387
|
+
// CIL[DOC-CONTRACT inputs="ctx: any (required) — the context object containing user principal information." outputs="boolean — returns true if the user can manage members, otherwise false." errors="none" side_effects="none dependencies="isAdminRole"]
|
|
1388
|
+
// CIL[DOC-MICRO logic_summary="Determines if a user has the necessary permissions to manage members based on their principal context. This function is essential for enforcing access control in member management operations." notable_decisions="none"]
|
|
1389
|
+
// CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
|
|
1390
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
1391
|
+
/**
|
|
1392
|
+
* Determines if a user has the necessary permissions to manage members based on their principal context. This function is essential for enforcing access control in member management operations.
|
|
1393
|
+
*
|
|
1394
|
+
* @inputSpec
|
|
1395
|
+
* ctx: any (required) — the context object containing user principal information.
|
|
1396
|
+
* @outputSpec boolean — returns true if the user can manage members, otherwise false.
|
|
1397
|
+
* @sideEffects none
|
|
1398
|
+
* @stability stable
|
|
1399
|
+
* @calls isAdminRole
|
|
1400
|
+
* @cil-generated
|
|
1401
|
+
*/
|
|
1402
|
+
// CIL[BEGIN stable_id="~/.framework/functions/invites.ts:canmanagemembers:function" type="function" identity="canManageMembers" parent="~/.framework/functions/invites.ts"]
|
|
1403
|
+
// CIL[DOC-MACRO purpose="canManageMembers function"]
|
|
1404
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Implements the canManageMembers function."]
|
|
1405
|
+
// CIL[DOC-CONTEXT module=".framework/functions/invites.ts" layer="app-specific" related_files=""]
|
|
1406
|
+
// CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
|
|
1407
|
+
// CIL[DOC-MICRO logic_summary="Implements the canManageMembers function." notable_decisions="[]"]
|
|
1408
|
+
// CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
|
|
1409
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
968
1410
|
function canManageMembers(ctx: any) {
|
|
969
1411
|
if (!ctx.principal || ctx.principal.id === 'anonymous') return false
|
|
970
1412
|
if (ctx.principal.permissions?.includes('system')) return true
|
|
971
1413
|
return (ctx.principal.roles || []).some((r: string) => isAdminRole(r))
|
|
972
1414
|
}
|
|
1415
|
+
// CIL[END stable_id="~/.framework/functions/invites.ts:canmanagemembers:function"]
|
|
1416
|
+
// CIL[DOC-VECTOR type="context" content="purpose='Determines if a user has the necessary permissions to manage members based on their principal context.'; No verified external callers" status="ready"]
|
|
1417
|
+
// CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only" status="ready"]
|
|
1418
|
+
// CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified" status="ready"]
|
|
1419
|
+
// CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented" status="ready"]
|
|
1420
|
+
// CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
|
|
1421
|
+
// CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
|
|
973
1422
|
|
|
1423
|
+
// CIL[DOC-MACRO purpose="Async function that retrieves and formats member data for a given account."]
|
|
1424
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Lists members associated with a specific account, requiring admin permissions. It retrieves member data and their roles from the database."]
|
|
1425
|
+
// CIL[DOC-CONTEXT module="invites" layer="app-specific" related_files=".framework/functions/_shared/middleware,.framework/functions/_shared/db,.framework/functions/_shared/audit,.framework/functions/_shared/rate-limit,.framework/functions/_shared/geo,.framework/functions/integration-outbound,.framework/functions/admin-data"]
|
|
1426
|
+
// CIL[DOC-CONTRACT inputs="ctx: any (required) — the context object containing query and principal information.\\nbody: any (required) — the request body containing account_id." outputs="Promise<{ data: Array<any> }> — an object containing an array of member records with their roles." errors="Error — Insufficient permissions — *_admin role required to list members, Error — account_id is required" side_effects="Calls to the database to retrieve member and role information. dependencies="canManageMembers, adminDataList, adminDb.from"]
|
|
1427
|
+
// CIL[DOC-MICRO logic_summary="Utilizes an admin context to fetch member records and their associated roles from the database." notable_decisions="none"]
|
|
1428
|
+
// CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
|
|
1429
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
1430
|
+
/**
|
|
1431
|
+
* Lists members associated with a specific account, requiring admin permissions. It retrieves member data and their roles from the database.
|
|
1432
|
+
*
|
|
1433
|
+
* @macro Async function that retrieves and formats member data for a given account.
|
|
1434
|
+
* @micro Utilizes an admin context to fetch member records and their associated roles from the database.
|
|
1435
|
+
* @inputSpec
|
|
1436
|
+
* ctx: any (required) — the context object containing query and principal information.
|
|
1437
|
+
* body: any (required) — the request body containing account_id.
|
|
1438
|
+
* @outputSpec Promise<{ data: Array<any> }> — an object containing an array of member records with their roles.
|
|
1439
|
+
* @throws Error — Insufficient permissions — *_admin role required to list members, Error — account_id is required
|
|
1440
|
+
* @sideEffects Calls to the database to retrieve member and role information.
|
|
1441
|
+
* @stability stable
|
|
1442
|
+
* @calls canManageMembers, adminDataList, adminDb.from
|
|
1443
|
+
* @cil-generated
|
|
1444
|
+
*/
|
|
1445
|
+
// CIL[BEGIN stable_id="~/.framework/functions/invites.ts:listmembers:function" type="function" identity="listMembers" parent="~/.framework/functions/invites.ts"]
|
|
1446
|
+
// CIL[DOC-MACRO purpose="listMembers function"]
|
|
1447
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Implements the listMembers function."]
|
|
1448
|
+
// CIL[DOC-CONTEXT module=".framework/functions/invites.ts" layer="app-specific" related_files=""]
|
|
1449
|
+
// CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
|
|
1450
|
+
// CIL[DOC-MICRO logic_summary="Implements the listMembers function." notable_decisions="[]"]
|
|
1451
|
+
// CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
|
|
1452
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
1453
|
+
// CIL[DOC-NOTE type="security" content="Registration/invitation hardening: the queried account_id is now bound to the caller via requireAccountAdmin — previously any *_admin could list members of any account."]
|
|
974
1454
|
async function listMembers(ctx: any, body: any) {
|
|
975
1455
|
if (!canManageMembers(ctx)) {
|
|
976
1456
|
throw new Error('Insufficient permissions — *_admin role required to list members')
|
|
@@ -979,6 +1459,9 @@ async function listMembers(ctx: any, body: any) {
|
|
|
979
1459
|
const accountId = ctx.query?.account_id || body?.account_id || ctx.principal?.accountId
|
|
980
1460
|
if (!accountId) throw new Error('account_id is required')
|
|
981
1461
|
|
|
1462
|
+
// Scope: only an admin of the queried account (or system_admin).
|
|
1463
|
+
requireAccountAdmin(ctx, accountId)
|
|
1464
|
+
|
|
982
1465
|
const adminCtx = { ...ctx, query: { ...ctx.query, entity: 'people', type_slug: 'person', account_id: accountId } }
|
|
983
1466
|
const records = await adminDataList(adminCtx, {})
|
|
984
1467
|
|
|
@@ -1003,18 +1486,100 @@ async function listMembers(ctx: any, body: any) {
|
|
|
1003
1486
|
|
|
1004
1487
|
return { data }
|
|
1005
1488
|
}
|
|
1489
|
+
// CIL[END stable_id="~/.framework/functions/invites.ts:listmembers:function"]
|
|
1490
|
+
// CIL[DOC-VECTOR type="context" content="purpose='Lists members associated with a specific account, requiring admin permissions.'; No verified external callers" status="ready"]
|
|
1491
|
+
// CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only" status="ready"]
|
|
1492
|
+
// CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified" status="ready"]
|
|
1493
|
+
// CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented" status="ready"]
|
|
1494
|
+
// CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
|
|
1495
|
+
// CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
|
|
1006
1496
|
|
|
1007
|
-
|
|
1497
|
+
// CIL[DOC-MACRO purpose="Resolves the ID of a role based on its slug."]
|
|
1498
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Resolves the ID of a role based on its slug. This function is essential for retrieving role identifiers in the application."]
|
|
1499
|
+
// CIL[DOC-CONTEXT module="invites" layer="app-specific" related_files=".framework/functions/_shared/middleware,.framework/functions/_shared/db,.framework/functions/_shared/audit,.framework/functions/_shared/rate-limit,.framework/functions/_shared/geo,.framework/functions/integration-outbound,.framework/functions/admin-data"]
|
|
1500
|
+
// CIL[DOC-CONTRACT inputs="slug: string (required) — the unique identifier for the role." outputs="Promise<string> — the ID of the resolved role." errors="Error — Role 'slug' not found" side_effects="none dependencies="adminDb.from('roles')"]
|
|
1501
|
+
// CIL[DOC-MICRO logic_summary="Resolves the ID of a role based on its slug. This function is essential for retrieving role identifiers in the application." notable_decisions="none"]
|
|
1502
|
+
// CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
|
|
1503
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
1504
|
+
/**
|
|
1505
|
+
* Resolves the ID of a role based on its slug. This function is essential for retrieving role identifiers in the application.
|
|
1506
|
+
*
|
|
1507
|
+
* @inputSpec
|
|
1508
|
+
* slug: string (required) — the unique identifier for the role.
|
|
1509
|
+
* @outputSpec Promise<string> — the ID of the resolved role.
|
|
1510
|
+
* @throws Error — Role 'slug' not found
|
|
1511
|
+
* @sideEffects none
|
|
1512
|
+
* @stability stable
|
|
1513
|
+
* @calls adminDb.from('roles')
|
|
1514
|
+
* @cil-generated
|
|
1515
|
+
*/
|
|
1516
|
+
// CIL[BEGIN stable_id="~/.framework/functions/invites.ts:resolveroleid:function" type="function" identity="resolveRoleId" parent="~/.framework/functions/invites.ts"]
|
|
1517
|
+
// CIL[DOC-MACRO purpose="resolveRoleId function"]
|
|
1518
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Implements the resolveRoleId function."]
|
|
1519
|
+
// CIL[DOC-CONTEXT module=".framework/functions/invites.ts" layer="app-specific" related_files=""]
|
|
1520
|
+
// CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
|
|
1521
|
+
// CIL[DOC-MICRO logic_summary="Implements the resolveRoleId function." notable_decisions="[]"]
|
|
1522
|
+
// CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
|
|
1523
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
1524
|
+
// CIL[DOC-NOTE type="security" content="Registration/invitation hardening: rejects system_admin and is_system roles and enforces an optional server-resolved allowlist — arbitrary role_slug (incl. system_admin) could previously be assigned to any member."]
|
|
1525
|
+
async function resolveRoleId(slug: string, allowlist?: string[] | null) {
|
|
1526
|
+
if (slug === 'system_admin') {
|
|
1527
|
+
throw new Error(`Role 'system_admin' is not permitted in this context`)
|
|
1528
|
+
}
|
|
1008
1529
|
const { data: roleRows } = await adminDb
|
|
1009
1530
|
.from('roles')
|
|
1010
|
-
.select('id, slug')
|
|
1531
|
+
.select('id, slug, is_system')
|
|
1011
1532
|
.eq('slug', slug)
|
|
1012
1533
|
.limit(1)
|
|
1013
1534
|
const resolved = roleRows?.[0]
|
|
1014
1535
|
if (!resolved) throw new Error(`Role '${slug}' not found`)
|
|
1536
|
+
if (resolved.is_system === true) {
|
|
1537
|
+
throw new Error(`Role '${slug}' is not permitted in this context`)
|
|
1538
|
+
}
|
|
1539
|
+
if (allowlist && !allowlist.includes(slug)) {
|
|
1540
|
+
throw new Error(`Role '${slug}' is not permitted in this context`)
|
|
1541
|
+
}
|
|
1015
1542
|
return resolved.id
|
|
1016
1543
|
}
|
|
1544
|
+
// CIL[END stable_id="~/.framework/functions/invites.ts:resolveroleid:function"]
|
|
1545
|
+
// CIL[DOC-VECTOR type="context" content="purpose='Resolves the ID of a role based on its slug.'; No verified external callers" status="ready"]
|
|
1546
|
+
// CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only" status="ready"]
|
|
1547
|
+
// CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified" status="ready"]
|
|
1548
|
+
// CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented" status="ready"]
|
|
1549
|
+
// CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
|
|
1550
|
+
// CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
|
|
1017
1551
|
|
|
1552
|
+
// CIL[DOC-MACRO purpose="Function responsible for updating member information in the system."]
|
|
1553
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Updates the member's details such as full name and role based on the provided input. It ensures that the caller has the necessary permissions before making any changes."]
|
|
1554
|
+
// CIL[DOC-CONTEXT module="invites" layer="app-specific" related_files=".framework/functions/_shared/middleware,.framework/functions/_shared/db,.framework/functions/_shared/audit,.framework/functions/_shared/rate-limit,.framework/functions/_shared/geo,.framework/functions/integration-outbound,.framework/functions/admin-data"]
|
|
1555
|
+
// CIL[DOC-CONTRACT inputs="ctx: any (required) — the context containing user permissions and request details.\\nbody: any (required) — the object containing member update details including person_id, full_name, and role_slug." outputs="Promise<{ success: boolean, data: any }> — indicates success and returns the updated member data." errors="Error — Insufficient permissions, person_id is required, No update fields provided, Failed to update member" side_effects="Database update operation, logging of the update event dependencies="canManageMembers, resolveRoleId, adminDataUpdate, emitLog"]
|
|
1556
|
+
// CIL[DOC-MICRO logic_summary="Validates input fields and constructs an update record before making a database call." notable_decisions="none"]
|
|
1557
|
+
// CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
|
|
1558
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
1559
|
+
/**
|
|
1560
|
+
* Updates the member's details such as full name and role based on the provided input. It ensures that the caller has the necessary permissions before making any changes.
|
|
1561
|
+
*
|
|
1562
|
+
* @macro Function responsible for updating member information in the system.
|
|
1563
|
+
* @micro Validates input fields and constructs an update record before making a database call.
|
|
1564
|
+
* @inputSpec
|
|
1565
|
+
* ctx: any (required) — the context containing user permissions and request details.
|
|
1566
|
+
* body: any (required) — the object containing member update details including person_id, full_name, and role_slug.
|
|
1567
|
+
* @outputSpec Promise<{ success: boolean, data: any }> — indicates success and returns the updated member data.
|
|
1568
|
+
* @throws Error — Insufficient permissions, person_id is required, No update fields provided, Failed to update member
|
|
1569
|
+
* @sideEffects Database update operation, logging of the update event
|
|
1570
|
+
* @stability stable
|
|
1571
|
+
* @calls canManageMembers, resolveRoleId, adminDataUpdate, emitLog
|
|
1572
|
+
* @cil-generated
|
|
1573
|
+
*/
|
|
1574
|
+
// CIL[BEGIN stable_id="~/.framework/functions/invites.ts:updatemember:function" type="function" identity="updateMember" parent="~/.framework/functions/invites.ts"]
|
|
1575
|
+
// CIL[DOC-MACRO purpose="updateMember function"]
|
|
1576
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Implements the updateMember function."]
|
|
1577
|
+
// CIL[DOC-CONTEXT module=".framework/functions/invites.ts" layer="app-specific" related_files=""]
|
|
1578
|
+
// CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
|
|
1579
|
+
// CIL[DOC-MICRO logic_summary="Implements the updateMember function." notable_decisions="[]"]
|
|
1580
|
+
// CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
|
|
1581
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
1582
|
+
// CIL[DOC-NOTE type="security" content="Registration/invitation hardening: resolves the target person's account first and requires an admin of THAT account (or system_admin) via requireAccountAdmin — previously any *_admin could update members cross-account and assign any role incl. system_admin."]
|
|
1018
1583
|
async function updateMember(ctx: any, body: any) {
|
|
1019
1584
|
if (!canManageMembers(ctx)) {
|
|
1020
1585
|
throw new Error('Insufficient permissions — *_admin role required to update members')
|
|
@@ -1023,9 +1588,25 @@ async function updateMember(ctx: any, body: any) {
|
|
|
1023
1588
|
const { person_id, full_name, role_slug } = body
|
|
1024
1589
|
if (!person_id) throw new Error('person_id is required')
|
|
1025
1590
|
|
|
1591
|
+
const { data: target } = await adminDb
|
|
1592
|
+
.from('people')
|
|
1593
|
+
.select('id, account_id')
|
|
1594
|
+
.eq('id', person_id)
|
|
1595
|
+
.single()
|
|
1596
|
+
|
|
1597
|
+
if (!target) throw new Error('Person not found')
|
|
1598
|
+
|
|
1599
|
+
// Scope: only an admin of the member's own account (or system_admin).
|
|
1600
|
+
requireAccountAdmin(ctx, target.account_id)
|
|
1601
|
+
|
|
1026
1602
|
const recordData: any = {}
|
|
1027
1603
|
if (full_name !== undefined) recordData.full_name = full_name
|
|
1028
|
-
if (role_slug !== undefined)
|
|
1604
|
+
if (role_slug !== undefined) {
|
|
1605
|
+
// Non-system roles only; additionally constrained to the member's
|
|
1606
|
+
// account-app allowlist when one is declared.
|
|
1607
|
+
const allowlist = await roleAllowlistForAccount(target.account_id)
|
|
1608
|
+
recordData.role_id = await resolveRoleId(role_slug, allowlist)
|
|
1609
|
+
}
|
|
1029
1610
|
|
|
1030
1611
|
if (Object.keys(recordData).length === 0) {
|
|
1031
1612
|
throw new Error('No update fields provided')
|
|
@@ -1042,7 +1623,43 @@ async function updateMember(ctx: any, body: any) {
|
|
|
1042
1623
|
await emitLog(ctx, 'member.updated', { type: 'person', id: person_id }, { after: recordData })
|
|
1043
1624
|
return { success: true, data: updated }
|
|
1044
1625
|
}
|
|
1626
|
+
// CIL[END stable_id="~/.framework/functions/invites.ts:updatemember:function"]
|
|
1627
|
+
// CIL[DOC-VECTOR type="context" content="purpose='Updates the member's details such as full name and role based on the provided input.'; No verified external callers" status="ready"]
|
|
1628
|
+
// CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only" status="ready"]
|
|
1629
|
+
// CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified" status="ready"]
|
|
1630
|
+
// CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented" status="ready"]
|
|
1631
|
+
// CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
|
|
1632
|
+
// CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
|
|
1045
1633
|
|
|
1634
|
+
// CIL[DOC-MACRO purpose="Deactivates a member by updating their status to inactive."]
|
|
1635
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Deactivates a member by updating their status to inactive. This function ensures that only users with the appropriate permissions can perform this action."]
|
|
1636
|
+
// CIL[DOC-CONTEXT module="invites" layer="app-specific" related_files=".framework/functions/_shared/middleware,.framework/functions/_shared/db,.framework/functions/_shared/audit,.framework/functions/_shared/rate-limit,.framework/functions/_shared/geo,.framework/functions/integration-outbound,.framework/functions/admin-data"]
|
|
1637
|
+
// CIL[DOC-CONTRACT inputs="ctx: any (required) — the context object containing user permissions and query parameters.\\nbody: any (required) — the request body containing the person_id of the member to deactivate." outputs="Promise<{ success: boolean, data: any }> — indicates success and returns the updated member data." errors="Error — Insufficient permissions — *_admin role required to deactivate members, Error — person_id is required, Error — Failed to deactivate member" side_effects="Updates the member's status in the database, emits a log event for the deactivation. dependencies="canManageMembers, adminDataUpdate, emitLog"]
|
|
1638
|
+
// CIL[DOC-MICRO logic_summary="Deactivates a member by updating their status to inactive. This function ensures that only users with the appropriate permissions can perform this action." notable_decisions="none"]
|
|
1639
|
+
// CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
|
|
1640
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
1641
|
+
/**
|
|
1642
|
+
* Deactivates a member by updating their status to inactive. This function ensures that only users with the appropriate permissions can perform this action.
|
|
1643
|
+
*
|
|
1644
|
+
* @inputSpec
|
|
1645
|
+
* ctx: any (required) — the context object containing user permissions and query parameters.
|
|
1646
|
+
* body: any (required) — the request body containing the person_id of the member to deactivate.
|
|
1647
|
+
* @outputSpec Promise<{ success: boolean, data: any }> — indicates success and returns the updated member data.
|
|
1648
|
+
* @throws Error — Insufficient permissions — *_admin role required to deactivate members, Error — person_id is required, Error — Failed to deactivate member
|
|
1649
|
+
* @sideEffects Updates the member's status in the database, emits a log event for the deactivation.
|
|
1650
|
+
* @stability stable
|
|
1651
|
+
* @calls canManageMembers, adminDataUpdate, emitLog
|
|
1652
|
+
* @cil-generated
|
|
1653
|
+
*/
|
|
1654
|
+
// CIL[BEGIN stable_id="~/.framework/functions/invites.ts:deactivatemember:function" type="function" identity="deactivateMember" parent="~/.framework/functions/invites.ts"]
|
|
1655
|
+
// CIL[DOC-MACRO purpose="deactivateMember function"]
|
|
1656
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Implements the deactivateMember function."]
|
|
1657
|
+
// CIL[DOC-CONTEXT module=".framework/functions/invites.ts" layer="app-specific" related_files=""]
|
|
1658
|
+
// CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
|
|
1659
|
+
// CIL[DOC-MICRO logic_summary="Implements the deactivateMember function." notable_decisions="[]"]
|
|
1660
|
+
// CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
|
|
1661
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
1662
|
+
// CIL[DOC-NOTE type="security" content="Registration/invitation hardening: resolves the target person's account first and requires an admin of THAT account (or system_admin) via requireAccountAdmin — previously any *_admin could deactivate members cross-account."]
|
|
1046
1663
|
async function deactivateMember(ctx: any, body: any) {
|
|
1047
1664
|
if (!canManageMembers(ctx)) {
|
|
1048
1665
|
throw new Error('Insufficient permissions — *_admin role required to deactivate members')
|
|
@@ -1051,6 +1668,17 @@ async function deactivateMember(ctx: any, body: any) {
|
|
|
1051
1668
|
const { person_id } = body
|
|
1052
1669
|
if (!person_id) throw new Error('person_id is required')
|
|
1053
1670
|
|
|
1671
|
+
const { data: target } = await adminDb
|
|
1672
|
+
.from('people')
|
|
1673
|
+
.select('id, account_id')
|
|
1674
|
+
.eq('id', person_id)
|
|
1675
|
+
.single()
|
|
1676
|
+
|
|
1677
|
+
if (!target) throw new Error('Person not found')
|
|
1678
|
+
|
|
1679
|
+
// Scope: only an admin of the member's own account (or system_admin).
|
|
1680
|
+
requireAccountAdmin(ctx, target.account_id)
|
|
1681
|
+
|
|
1054
1682
|
const adminCtx = {
|
|
1055
1683
|
...ctx,
|
|
1056
1684
|
query: { ...ctx.query, entity: 'people', id: person_id }
|
|
@@ -1064,4 +1692,11 @@ async function deactivateMember(ctx: any, body: any) {
|
|
|
1064
1692
|
|
|
1065
1693
|
await emitLog(ctx, 'member.deactivated', { type: 'person', id: person_id }, {})
|
|
1066
1694
|
return { success: true, data: updated }
|
|
1067
|
-
}
|
|
1695
|
+
}
|
|
1696
|
+
// CIL[END stable_id="~/.framework/functions/invites.ts:deactivatemember:function"]
|
|
1697
|
+
// CIL[DOC-VECTOR type="context" content="purpose='Deactivates a member by updating their status to inactive.'; No verified external callers" status="ready"]
|
|
1698
|
+
// CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only" status="ready"]
|
|
1699
|
+
// CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified" status="ready"]
|
|
1700
|
+
// CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented" status="ready"]
|
|
1701
|
+
// CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
|
|
1702
|
+
// CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
|