spine-framework 0.3.145 → 0.4.1
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 +5067 -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
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module security
|
|
3
|
+
* @audience internal
|
|
4
|
+
* @layer security
|
|
5
|
+
* @stability experimental
|
|
6
|
+
*
|
|
7
|
+
* System-admin security operations: flagged-actor report, three-layer
|
|
8
|
+
* ban (internal blocklist + Supabase Auth ban + Netlify edge deny rule),
|
|
9
|
+
* unban, and blocklist CRUD. INVARIANT: every action requires a
|
|
10
|
+
* system-admin principal; the Netlify edge layer degrades gracefully
|
|
11
|
+
* when NETLIFY_ACCESS_TOKEN / NETLIFY_SITE_ID are unset (local dev).
|
|
12
|
+
*
|
|
13
|
+
* @seeAlso .framework/functions/_shared/security.ts (enforcement helpers)
|
|
14
|
+
* @seeAlso .framework/migrations/049_security_blocklist.sql (schema)
|
|
15
|
+
*
|
|
16
|
+
* @cil-generated
|
|
17
|
+
*/
|
|
18
|
+
// CIL[BEGIN stable_id="~/.framework/functions/security.ts:module_header:module_header" type="module_header" identity="module_header" parent="~/.framework/functions/security.ts"]
|
|
19
|
+
// CIL[DOC-MACRO purpose="System-admin security operations: flagged-actor report, ban/unban, blocklist CRUD, Netlify edge sync"]
|
|
20
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Aggregate security.* audit events into a flagged-accounts report; apply three-layer bans (security_blocklist + auth.admin ban_duration + Netlify firewall rule); expose blocklist list/add/revoke"]
|
|
21
|
+
// CIL[DOC-CONTEXT module="functions" layer="security" related_files=".framework/functions/_shared/security.ts,.framework/migrations/049_security_blocklist.sql,.framework/src/pages/admin/SecurityPage.tsx"]
|
|
22
|
+
// CIL[DOC-CONTRACT inputs="?action= flagged_accounts|ban_user|unban_user|blocklist|blocklist_add|blocklist_revoke + body" outputs="JSON report / action result" errors="403 non-system-admin; 400 bad action" side_effects="security_blocklist writes, auth.admin updates, Netlify firewall PUT, audit logs"]
|
|
23
|
+
// CIL[DOC-MICRO logic_summary="Report groups security.* log events by principal id then enriches with people/auth data; ban writes person+email+ip blocklist rows, bans auth user (876600h), and syncs one managed Netlify ip-deny rule labelled spine-security:blocklist" notable_decisions="Edge rule is a single shared deny rule whose data.addresses array is mutated — preserves user-managed geo rules and stays within tight free-tier rule limits; all edge calls skip silently without NETLIFY_ACCESS_TOKEN/SITE_ID"]
|
|
24
|
+
// CIL[DOC-STABILITY stability="experimental"]
|
|
25
|
+
|
|
26
|
+
import { createHandler, requireSystemContextWithAudit } from './_shared/middleware'
|
|
27
|
+
import type { RequestContext } from './_shared/middleware'
|
|
28
|
+
import { adminDb } from './_shared/db'
|
|
29
|
+
import { emitAudit } from './_shared/audit'
|
|
30
|
+
// CIL[END stable_id="~/.framework/functions/security.ts:module_header:module_header"]
|
|
31
|
+
|
|
32
|
+
// CIL[BEGIN stable_id="~/.framework/functions/security.ts:netlifyedgesync:comment" type="comment" identity="netlifyEdgeSync" parent="~/.framework/functions/security.ts"]
|
|
33
|
+
// CIL[DOC-MACRO purpose="Netlify Firewall Traffic Rules sync for banned IPs"]
|
|
34
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Keep a single managed ip-deny rule (description spine-security:blocklist) whose data.addresses mirrors active ip blocklist entries"]
|
|
35
|
+
// CIL[DOC-CONTRACT inputs="ips to add/remove" outputs="{ synced: boolean, skipped?: boolean, error?: string }" errors="never throws — returns error field" side_effects="Netlify API GET/PUT of full ruleset"]
|
|
36
|
+
// CIL[DOC-MICRO logic_summary="GET firewall_rule_set → mutate managed rule's addresses (or create rule) → PUT whole published ruleset back; user-managed rules are preserved verbatim" notable_decisions="PUT replaces the entire ruleset, so the full published rule list is round-tripped; concurrent edits could still race — acceptable at admin-action frequency"]
|
|
37
|
+
// CIL[DOC-STABILITY stability="experimental"]
|
|
38
|
+
const MANAGED_RULE_DESC = 'spine-security:blocklist'
|
|
39
|
+
const NETLIFY_API = 'https://api.netlify.com'
|
|
40
|
+
|
|
41
|
+
async function netlifyEdgeSync(ipsToAdd: string[], ipsToRemove: string[] = []): Promise<{ synced: boolean; skipped?: boolean; error?: string }> {
|
|
42
|
+
const token = process.env.NETLIFY_ACCESS_TOKEN
|
|
43
|
+
const siteId = process.env.NETLIFY_SITE_ID
|
|
44
|
+
if (!token || !siteId) return { synced: false, skipped: true }
|
|
45
|
+
const ips = [...new Set([...ipsToAdd, ...ipsToRemove].filter(Boolean))]
|
|
46
|
+
if (!ips.length) return { synced: false, skipped: true }
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const base = `${NETLIFY_API}/api/v1/sites/${siteId}/firewall_rule_set`
|
|
50
|
+
const getRes = await fetch(base, { headers: { Authorization: `Bearer ${token}` } })
|
|
51
|
+
if (!getRes.ok) return { synced: false, error: `Netlify GET ${getRes.status}` }
|
|
52
|
+
const payload = await getRes.json().catch(() => ({}))
|
|
53
|
+
const ruleset = payload?.published ?? payload ?? { default: 'allow', rules: [] }
|
|
54
|
+
const rules: any[] = Array.isArray(ruleset.rules) ? ruleset.rules : []
|
|
55
|
+
|
|
56
|
+
const managedIdx = rules.findIndex(
|
|
57
|
+
(r) => r?.type === 'ip' && r?.action === 'deny' && String(r?.description || '').startsWith(MANAGED_RULE_DESC)
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
let addresses: string[] = managedIdx >= 0
|
|
61
|
+
? [...(rules[managedIdx]?.data?.addresses || [])]
|
|
62
|
+
: []
|
|
63
|
+
|
|
64
|
+
for (const ip of ipsToAdd) if (ip && !addresses.includes(ip)) addresses.push(ip)
|
|
65
|
+
addresses = addresses.filter((a) => !ipsToRemove.includes(a))
|
|
66
|
+
|
|
67
|
+
if (managedIdx >= 0) {
|
|
68
|
+
if (addresses.length) {
|
|
69
|
+
rules[managedIdx] = { ...rules[managedIdx], data: { ...rules[managedIdx].data, addresses } }
|
|
70
|
+
} else {
|
|
71
|
+
rules.splice(managedIdx, 1)
|
|
72
|
+
}
|
|
73
|
+
} else if (addresses.length) {
|
|
74
|
+
rules.push({
|
|
75
|
+
type: 'ip',
|
|
76
|
+
action: 'deny',
|
|
77
|
+
data: { addresses },
|
|
78
|
+
description: `${MANAGED_RULE_DESC} — managed by Spine security admin`,
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const putRes = await fetch(base, {
|
|
83
|
+
method: 'PUT',
|
|
84
|
+
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
|
|
85
|
+
body: JSON.stringify({ default: ruleset.default || 'allow', rules }),
|
|
86
|
+
})
|
|
87
|
+
if (!putRes.ok) return { synced: false, error: `Netlify PUT ${putRes.status}` }
|
|
88
|
+
return { synced: true }
|
|
89
|
+
} catch (err: any) {
|
|
90
|
+
return { synced: false, error: err?.message || 'Netlify sync failed' }
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// CIL[END stable_id="~/.framework/functions/security.ts:netlifyedgesync:comment"]
|
|
94
|
+
|
|
95
|
+
// CIL[BEGIN stable_id="~/.framework/functions/security.ts:flaggedaccounts:function" type="function" identity="flaggedAccounts" parent="~/.framework/functions/security.ts"]
|
|
96
|
+
// CIL[DOC-MACRO purpose="Aggregate security.* audit events into a flagged-actors report"]
|
|
97
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Group recent security.rate_limit_exceeded / injection_detected / registration_blocked log rows by acting person or account, enrich with people + account + blocklist state"]
|
|
98
|
+
// CIL[DOC-CONTRACT inputs="ctx" outputs="{ rows: FlaggedRow[] }" errors="returns empty rows on missing tables" side_effects="logs + people + accounts + security_blocklist reads"]
|
|
99
|
+
// CIL[DOC-MICRO logic_summary="Fetch last 500 security.* events → bucket by person id (fallback account id) → count signals + collect kinds/patterns/ips → join people(email, geo, status), accounts(display_name), active blocklist entries → sort by signal count desc" notable_decisions="IP shown comes from the person's registration_ip/last_ip (trusted edge capture), not spoofable request data"]
|
|
100
|
+
// CIL[DOC-STABILITY stability="experimental"]
|
|
101
|
+
const SECURITY_SIGNALS = [
|
|
102
|
+
'security.rate_limit_exceeded',
|
|
103
|
+
'security.injection_detected',
|
|
104
|
+
'security.registration_blocked',
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
async function flaggedAccounts(ctx: RequestContext) {
|
|
108
|
+
const { data: events, error } = await adminDb
|
|
109
|
+
.from('logs')
|
|
110
|
+
.select('account_id, started_at, metadata')
|
|
111
|
+
.in('span_name', SECURITY_SIGNALS)
|
|
112
|
+
.order('started_at', { ascending: false })
|
|
113
|
+
.limit(500)
|
|
114
|
+
if (error) {
|
|
115
|
+
console.warn('flaggedAccounts query failed (missing logs?):', error.message)
|
|
116
|
+
return { rows: [] }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Bucket by acting principal id, falling back to account id for
|
|
120
|
+
// anonymous signals (e.g. registration_blocked carries no person).
|
|
121
|
+
const buckets = new Map<string, {
|
|
122
|
+
person_id: string | null
|
|
123
|
+
account_id: string | null
|
|
124
|
+
signals: number
|
|
125
|
+
kinds: Set<string>
|
|
126
|
+
patterns: Set<string>
|
|
127
|
+
last_seen_at: string | null
|
|
128
|
+
}>()
|
|
129
|
+
|
|
130
|
+
for (const ev of events || []) {
|
|
131
|
+
const meta = ev.metadata || {}
|
|
132
|
+
const pid = meta.principal?.id && meta.principal.id !== 'anonymous' ? meta.principal.id : null
|
|
133
|
+
const key = pid || `acct:${ev.account_id || meta.target?.account_id || 'unknown'}`
|
|
134
|
+
const b = buckets.get(key) || {
|
|
135
|
+
person_id: pid,
|
|
136
|
+
account_id: ev.account_id || meta.target?.account_id || null,
|
|
137
|
+
signals: 0,
|
|
138
|
+
kinds: new Set<string>(),
|
|
139
|
+
patterns: new Set<string>(),
|
|
140
|
+
last_seen_at: null,
|
|
141
|
+
}
|
|
142
|
+
b.signals += 1
|
|
143
|
+
if (meta.action) b.kinds.add(String(meta.action).replace('security.', ''))
|
|
144
|
+
if (meta.pattern) b.patterns.add(String(meta.pattern))
|
|
145
|
+
if (!b.last_seen_at || ev.started_at > b.last_seen_at) b.last_seen_at = ev.started_at
|
|
146
|
+
buckets.set(key, b)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const personIds = [...buckets.values()].map(b => b.person_id).filter(Boolean) as string[]
|
|
150
|
+
const accountIds = [...new Set([...buckets.values()].map(b => b.account_id).filter(Boolean))] as string[]
|
|
151
|
+
|
|
152
|
+
const [{ data: people }, { data: accounts }, { data: bl }] = await Promise.all([
|
|
153
|
+
personIds.length
|
|
154
|
+
? adminDb.from('people').select('id, email, account_id, auth_uid, status, data').in('id', personIds)
|
|
155
|
+
: Promise.resolve({ data: [] as any[] }),
|
|
156
|
+
accountIds.length
|
|
157
|
+
? adminDb.from('accounts').select('id, slug, display_name').in('id', accountIds)
|
|
158
|
+
: Promise.resolve({ data: [] as any[] }),
|
|
159
|
+
adminDb.from('security_blocklist').select('type, value').is('revoked_at', null).or(`expires_at.is.null,expires_at.gt.${new Date().toISOString()}`),
|
|
160
|
+
])
|
|
161
|
+
|
|
162
|
+
const personById = new Map((people || []).map((p: any) => [p.id, p]))
|
|
163
|
+
const accountById = new Map((accounts || []).map((a: any) => [a.id, a]))
|
|
164
|
+
const activeBlocks = new Set((bl || []).map((r: any) => `${r.type}:${r.value}`))
|
|
165
|
+
|
|
166
|
+
const rows = [...buckets.values()].map((b) => {
|
|
167
|
+
const p = b.person_id ? personById.get(b.person_id) : null
|
|
168
|
+
const acctId = b.account_id || p?.account_id || null
|
|
169
|
+
const acct = acctId ? accountById.get(acctId) : null
|
|
170
|
+
const email = p?.email || null
|
|
171
|
+
const ip = p?.data?.last_ip || p?.data?.registration_ip || null
|
|
172
|
+
const banned = !!(
|
|
173
|
+
(p && activeBlocks.has(`person_id:${p.id}`)) ||
|
|
174
|
+
(email && activeBlocks.has(`email:${String(email).toLowerCase()}`)) ||
|
|
175
|
+
(ip && activeBlocks.has(`ip:${ip}`)) ||
|
|
176
|
+
(acctId && activeBlocks.has(`account_id:${acctId}`))
|
|
177
|
+
)
|
|
178
|
+
return {
|
|
179
|
+
person_id: b.person_id,
|
|
180
|
+
email,
|
|
181
|
+
account_id: acctId,
|
|
182
|
+
account_name: acct?.display_name || acct?.slug || null,
|
|
183
|
+
last_ip: ip,
|
|
184
|
+
last_country: p?.data?.last_country || p?.data?.registration_country || null,
|
|
185
|
+
person_status: p?.status || null,
|
|
186
|
+
signals: b.signals,
|
|
187
|
+
kinds: [...b.kinds],
|
|
188
|
+
patterns: [...b.patterns],
|
|
189
|
+
last_seen_at: b.last_seen_at,
|
|
190
|
+
banned,
|
|
191
|
+
}
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
rows.sort((a, b) => b.signals - a.signals)
|
|
195
|
+
return { rows }
|
|
196
|
+
}
|
|
197
|
+
// CIL[END stable_id="~/.framework/functions/security.ts:flaggedaccounts:function"]
|
|
198
|
+
|
|
199
|
+
// CIL[BEGIN stable_id="~/.framework/functions/security.ts:addblockrows:function" type="function" identity="addBlockRows" parent="~/.framework/functions/security.ts"]
|
|
200
|
+
// CIL[DOC-MACRO purpose="Upsert active hard-block rows into security_blocklist"]
|
|
201
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Insert rows with revoked_at cleared on (type,value) conflict so a re-ban re-activates a revoked row"]
|
|
202
|
+
// CIL[DOC-CONTRACT inputs="rows[], bannedBy, reason" outputs="void" errors="throws on DB error" side_effects="security_blocklist upsert"]
|
|
203
|
+
// CIL[DOC-MICRO logic_summary="map rows → upsert onConflict type,value with revoked_at:null" notable_decisions="[]"]
|
|
204
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
205
|
+
async function addBlockRows(
|
|
206
|
+
rows: Array<{ type: string; value: string }>,
|
|
207
|
+
bannedBy: string | undefined,
|
|
208
|
+
reason?: string,
|
|
209
|
+
): Promise<void> {
|
|
210
|
+
const clean = rows.filter(r => r.value)
|
|
211
|
+
if (!clean.length) return
|
|
212
|
+
const { error } = await adminDb
|
|
213
|
+
.from('security_blocklist')
|
|
214
|
+
.upsert(
|
|
215
|
+
clean.map(r => ({
|
|
216
|
+
type: r.type,
|
|
217
|
+
value: r.value,
|
|
218
|
+
severity: 'hard',
|
|
219
|
+
reason: reason || null,
|
|
220
|
+
banned_by: bannedBy || null,
|
|
221
|
+
revoked_at: null,
|
|
222
|
+
expires_at: null,
|
|
223
|
+
})),
|
|
224
|
+
{ onConflict: 'type,value' }
|
|
225
|
+
)
|
|
226
|
+
if (error) throw new Error('security_blocklist upsert failed: ' + error.message)
|
|
227
|
+
}
|
|
228
|
+
// CIL[END stable_id="~/.framework/functions/security.ts:addblockrows:function"]
|
|
229
|
+
|
|
230
|
+
// CIL[BEGIN stable_id="~/.framework/functions/security.ts:banuser:function" type="function" identity="banUser" parent="~/.framework/functions/security.ts"]
|
|
231
|
+
// CIL[DOC-MACRO purpose="Three-layer ban: internal blocklist + Supabase Auth ban + Netlify edge deny"]
|
|
232
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Given a person_id (or bare email/ip), persist blocklist rows for person+email+account+IP, ban the auth user, and push the IP into the managed Netlify deny rule"]
|
|
233
|
+
// CIL[DOC-CONTRACT inputs="ctx, body {person_id?|email?|ip?, reason?, block_ip?, block_email?}" outputs="{ ok, person_id, layers }" errors="400 missing target; 404 person not found; 500 blocklist write failure" side_effects="security_blocklist upserts, auth.admin.updateUserById, Netlify ruleset PUT, audit events"]
|
|
234
|
+
// CIL[DOC-MICRO logic_summary="Resolve person → write blocklist rows (fail = abort) → auth ban via ban_duration 876600h → edge sync for last/registration IP → audit security.user_banned with per-layer results" notable_decisions="Internal blocklist is authoritative and must succeed; auth/edge failures are recorded in the audit event and returned, not thrown — a ban must never half-apply silently"]
|
|
235
|
+
// CIL[DOC-STABILITY stability="experimental"]
|
|
236
|
+
async function banUser(ctx: RequestContext, body: any) {
|
|
237
|
+
const { person_id, email: rawEmail, ip: rawIp, reason, block_ip = true, block_email = true } = body || {}
|
|
238
|
+
if (!person_id && !rawEmail && !rawIp) {
|
|
239
|
+
throw new Error('person_id, email, or ip is required')
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
let person: any = null
|
|
243
|
+
if (person_id) {
|
|
244
|
+
const { data } = await adminDb
|
|
245
|
+
.from('people')
|
|
246
|
+
.select('id, email, account_id, auth_uid, status, data')
|
|
247
|
+
.eq('id', person_id)
|
|
248
|
+
.maybeSingle()
|
|
249
|
+
if (!data) throw new Error(`Person not found: ${person_id}`)
|
|
250
|
+
person = data
|
|
251
|
+
} else if (rawEmail) {
|
|
252
|
+
const { data } = await adminDb
|
|
253
|
+
.from('people')
|
|
254
|
+
.select('id, email, account_id, auth_uid, status, data')
|
|
255
|
+
.eq('email', String(rawEmail).toLowerCase())
|
|
256
|
+
.maybeSingle()
|
|
257
|
+
person = data // may be null — banning a bare email is still valid
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const email = (person?.email || rawEmail || '').toLowerCase() || null
|
|
261
|
+
const ip = person?.data?.last_ip || person?.data?.registration_ip || rawIp || null
|
|
262
|
+
|
|
263
|
+
const layers: Record<string, any> = { blocklist: 'pending', auth: 'skipped', edge: 'skipped' }
|
|
264
|
+
|
|
265
|
+
// Layer 1 (authoritative): internal blocklist rows.
|
|
266
|
+
const rows: Array<{ type: string; value: string }> = []
|
|
267
|
+
if (person?.id) rows.push({ type: 'person_id', value: person.id })
|
|
268
|
+
if (person?.account_id) rows.push({ type: 'account_id', value: person.account_id })
|
|
269
|
+
if (email && block_email) rows.push({ type: 'email', value: email })
|
|
270
|
+
if (ip && block_ip) rows.push({ type: 'ip', value: ip })
|
|
271
|
+
await addBlockRows(rows, ctx.principal?.id, reason)
|
|
272
|
+
layers.blocklist = 'applied'
|
|
273
|
+
|
|
274
|
+
// Layer 2: Supabase Auth ban — ~100y duration is effectively permanent.
|
|
275
|
+
if (person?.auth_uid) {
|
|
276
|
+
const { error: authErr } = await adminDb.auth.admin.updateUserById(person.auth_uid, {
|
|
277
|
+
ban_duration: '876600h',
|
|
278
|
+
})
|
|
279
|
+
layers.auth = authErr ? `error: ${authErr.message}` : 'applied'
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Layer 3: Netlify edge IP deny rule.
|
|
283
|
+
if (ip && block_ip) {
|
|
284
|
+
const edge = await netlifyEdgeSync([ip])
|
|
285
|
+
layers.edge = edge.synced ? 'applied' : edge.skipped ? 'skipped' : `error: ${edge.error}`
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
await emitAudit(ctx, 'security.user_banned', { type: 'person', id: person?.id, account_id: person?.account_id }, {
|
|
289
|
+
result: 'success',
|
|
290
|
+
target_email: email,
|
|
291
|
+
target_ip: ip,
|
|
292
|
+
reason: reason || null,
|
|
293
|
+
layers,
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
return { ok: true, person_id: person?.id || null, email, ip, layers }
|
|
297
|
+
}
|
|
298
|
+
// CIL[END stable_id="~/.framework/functions/security.ts:banuser:function"]
|
|
299
|
+
|
|
300
|
+
// CIL[BEGIN stable_id="~/.framework/functions/security.ts:unbanuser:function" type="function" identity="unbanUser" parent="~/.framework/functions/security.ts"]
|
|
301
|
+
// CIL[DOC-MACRO purpose="Reverse a ban: revoke blocklist rows, lift auth ban, remove edge IP"]
|
|
302
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Revoke active security_blocklist rows for the person's identifiers, lift the Supabase ban, and pull the IP out of the managed Netlify rule"]
|
|
303
|
+
// CIL[DOC-CONTRACT inputs="ctx, body {person_id}" outputs="{ ok, revoked, layers }" errors="404 person not found" side_effects="blocklist updates, auth admin call, Netlify PUT, audit"]
|
|
304
|
+
// CIL[DOC-MICRO logic_summary="Load person → revoke matching (type,value) rows → ban_duration 'none' → edge remove → audit security.user_unbanned" notable_decisions="account_id rows are intentionally left alone on unban — an org-level block is a separate decision"]
|
|
305
|
+
// CIL[DOC-STABILITY stability="experimental"]
|
|
306
|
+
async function unbanUser(ctx: RequestContext, body: any) {
|
|
307
|
+
const { person_id } = body || {}
|
|
308
|
+
if (!person_id) throw new Error('person_id is required')
|
|
309
|
+
|
|
310
|
+
const { data: person } = await adminDb
|
|
311
|
+
.from('people')
|
|
312
|
+
.select('id, email, account_id, auth_uid, data')
|
|
313
|
+
.eq('id', person_id)
|
|
314
|
+
.maybeSingle()
|
|
315
|
+
if (!person) throw new Error(`Person not found: ${person_id}`)
|
|
316
|
+
|
|
317
|
+
const email = (person.email || '').toLowerCase()
|
|
318
|
+
const ip = person.data?.last_ip || person.data?.registration_ip || null
|
|
319
|
+
|
|
320
|
+
const pairs: Array<{ type: string; value: string }> = [
|
|
321
|
+
{ type: 'person_id', value: person.id },
|
|
322
|
+
]
|
|
323
|
+
if (email) pairs.push({ type: 'email', value: email })
|
|
324
|
+
if (ip) pairs.push({ type: 'ip', value: ip })
|
|
325
|
+
|
|
326
|
+
const or = pairs.map(p => `and(type.eq.${p.type},value.eq.${p.value})`).join(',')
|
|
327
|
+
const { data: revoked } = await adminDb
|
|
328
|
+
.from('security_blocklist')
|
|
329
|
+
.update({ revoked_at: new Date().toISOString() })
|
|
330
|
+
.is('revoked_at', null)
|
|
331
|
+
.or(or)
|
|
332
|
+
.select('id')
|
|
333
|
+
|
|
334
|
+
const layers: Record<string, any> = { blocklist: `revoked ${revoked?.length || 0}`, auth: 'skipped', edge: 'skipped' }
|
|
335
|
+
|
|
336
|
+
if (person.auth_uid) {
|
|
337
|
+
const { error: authErr } = await adminDb.auth.admin.updateUserById(person.auth_uid, {
|
|
338
|
+
ban_duration: 'none',
|
|
339
|
+
})
|
|
340
|
+
layers.auth = authErr ? `error: ${authErr.message}` : 'applied'
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (ip) {
|
|
344
|
+
const edge = await netlifyEdgeSync([], [ip])
|
|
345
|
+
layers.edge = edge.synced ? 'applied' : edge.skipped ? 'skipped' : `error: ${edge.error}`
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
await emitAudit(ctx, 'security.user_unbanned', { type: 'person', id: person.id, account_id: person.account_id }, {
|
|
349
|
+
result: 'success',
|
|
350
|
+
target_email: email,
|
|
351
|
+
layers,
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
return { ok: true, person_id: person.id, layers }
|
|
355
|
+
}
|
|
356
|
+
// CIL[END stable_id="~/.framework/functions/security.ts:unbanuser:function"]
|
|
357
|
+
|
|
358
|
+
// CIL[BEGIN stable_id="~/.framework/functions/security.ts:blocklistcrud:comment" type="comment" identity="blocklistCrud" parent="~/.framework/functions/security.ts"]
|
|
359
|
+
// CIL[DOC-MACRO purpose="Blocklist list/add/revoke for the admin UI"]
|
|
360
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Expose the security_blocklist table to system admins; ip entries sync to the Netlify edge on add/revoke"]
|
|
361
|
+
// CIL[DOC-MICRO logic_summary="list returns active+recent rows; add upserts (type,value,severity,reason,expires_at); revoke stamps revoked_at. Both edge-sync ip values" notable_decisions="[]"]
|
|
362
|
+
// CIL[DOC-STABILITY stability="experimental"]
|
|
363
|
+
async function listBlocklist() {
|
|
364
|
+
const { data, error } = await adminDb
|
|
365
|
+
.from('security_blocklist')
|
|
366
|
+
.select('id, type, value, severity, reason, banned_by, expires_at, revoked_at, created_at')
|
|
367
|
+
.order('created_at', { ascending: false })
|
|
368
|
+
.limit(500)
|
|
369
|
+
if (error) throw new Error('blocklist read failed: ' + error.message)
|
|
370
|
+
return { rows: data || [] }
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
async function blocklistAdd(ctx: RequestContext, body: any) {
|
|
374
|
+
const { type, value, severity = 'hard', reason, expires_at } = body || {}
|
|
375
|
+
if (!type || !value) throw new Error('type and value are required')
|
|
376
|
+
if (!['ip', 'email', 'email_domain', 'person_id', 'account_id'].includes(type)) {
|
|
377
|
+
throw new Error(`invalid type: ${type}`)
|
|
378
|
+
}
|
|
379
|
+
const normalized = type === 'email' || type === 'email_domain' ? String(value).toLowerCase() : String(value)
|
|
380
|
+
|
|
381
|
+
const { error } = await adminDb
|
|
382
|
+
.from('security_blocklist')
|
|
383
|
+
.upsert({
|
|
384
|
+
type, value: normalized, severity, reason: reason || null,
|
|
385
|
+
banned_by: ctx.principal?.id || null,
|
|
386
|
+
expires_at: expires_at || null,
|
|
387
|
+
revoked_at: null,
|
|
388
|
+
}, { onConflict: 'type,value' })
|
|
389
|
+
if (error) throw new Error('blocklist write failed: ' + error.message)
|
|
390
|
+
|
|
391
|
+
const edge = type === 'ip' ? await netlifyEdgeSync([normalized]) : { synced: false, skipped: true }
|
|
392
|
+
await emitAudit(ctx, 'security.blocklist_add', { type: 'security_blocklist' }, {
|
|
393
|
+
result: 'success', block_type: type, value: normalized, severity,
|
|
394
|
+
edge: edge.synced ? 'applied' : edge.skipped ? 'skipped' : `error: ${edge.error}`,
|
|
395
|
+
})
|
|
396
|
+
return { ok: true }
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
async function blocklistRevoke(ctx: RequestContext, body: any) {
|
|
400
|
+
const { id, type, value } = body || {}
|
|
401
|
+
if (!id && !(type && value)) throw new Error('id or (type, value) is required')
|
|
402
|
+
|
|
403
|
+
let query = adminDb
|
|
404
|
+
.from('security_blocklist')
|
|
405
|
+
.update({ revoked_at: new Date().toISOString() })
|
|
406
|
+
.is('revoked_at', null)
|
|
407
|
+
.select('id, type, value')
|
|
408
|
+
query = id ? query.eq('id', id) : query.eq('type', type).eq('value', String(value).toLowerCase())
|
|
409
|
+
const { data: revoked, error } = await query
|
|
410
|
+
if (error) throw new Error('blocklist revoke failed: ' + error.message)
|
|
411
|
+
|
|
412
|
+
const ip = (revoked || []).find((r: any) => r.type === 'ip')?.value
|
|
413
|
+
const edge = ip ? await netlifyEdgeSync([], [ip]) : { synced: false, skipped: true }
|
|
414
|
+
await emitAudit(ctx, 'security.blocklist_revoked', { type: 'security_blocklist', id }, {
|
|
415
|
+
result: 'success', revoked: revoked?.length || 0,
|
|
416
|
+
edge: edge.synced ? 'applied' : edge.skipped ? 'skipped' : `error: ${edge.error}`,
|
|
417
|
+
})
|
|
418
|
+
return { ok: true, revoked: revoked?.length || 0 }
|
|
419
|
+
}
|
|
420
|
+
// CIL[END stable_id="~/.framework/functions/security.ts:blocklistcrud:comment"]
|
|
421
|
+
|
|
422
|
+
// CIL[BEGIN stable_id="~/.framework/functions/security.ts:handler:constant" type="constant" identity="handler" parent="~/.framework/functions/security.ts"]
|
|
423
|
+
// CIL[DOC-MACRO purpose="Action dispatcher — system-admin only"]
|
|
424
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Route ?action= to the security operations; every action is gated by requireSystemContextWithAudit"]
|
|
425
|
+
// CIL[DOC-CONTRACT inputs="ctx, body" outputs="action result JSON" errors="401/403 unauthenticated/non-admin; 400 unknown action" side_effects="delegated"]
|
|
426
|
+
// CIL[DOC-MICRO logic_summary="Gate once, then switch on ctx.query.action || body.action" notable_decisions="[]"]
|
|
427
|
+
// CIL[DOC-STABILITY stability="experimental"]
|
|
428
|
+
export const handler = createHandler(async (ctx, body) => {
|
|
429
|
+
const deny = requireSystemContextWithAudit(ctx, 'security_admin')
|
|
430
|
+
if (deny) return deny
|
|
431
|
+
|
|
432
|
+
const action = (ctx.query?.action as string) || (body?.action as string) || ''
|
|
433
|
+
switch (action) {
|
|
434
|
+
case 'flagged_accounts': return flaggedAccounts(ctx)
|
|
435
|
+
case 'ban_user': return banUser(ctx, body)
|
|
436
|
+
case 'unban_user': return unbanUser(ctx, body)
|
|
437
|
+
case 'blocklist': return listBlocklist()
|
|
438
|
+
case 'blocklist_add': return blocklistAdd(ctx, body)
|
|
439
|
+
case 'blocklist_revoke': return blocklistRevoke(ctx, body)
|
|
440
|
+
default: {
|
|
441
|
+
const e: any = new Error(`Unknown security action: ${action}`)
|
|
442
|
+
e.statusCode = 400
|
|
443
|
+
throw e
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
})
|
|
447
|
+
// CIL[END stable_id="~/.framework/functions/security.ts:handler:constant"]
|
|
448
|
+
|
|
449
|
+
// CIL[DOC-VECTOR type="context" content="purpose='System-admin security operations endpoint'; exposes flagged_accounts report, three-layer ban/unban, blocklist CRUD, Netlify edge IP sync" status="ready"]
|
|
450
|
+
// CIL[DOC-VECTOR type="blast_radius" content="middleware.ts enforcement reads security_blocklist; invites.ts registration gate; admin SecurityPage consumes this API" status="ready"]
|