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,345 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module functions/_shared
|
|
3
|
+
* @audience internal
|
|
4
|
+
* @layer security
|
|
5
|
+
* @stability stable
|
|
6
|
+
*
|
|
7
|
+
* Shared security enforcement: denylist lookups (security_blocklist),
|
|
8
|
+
* client geo stamping, prompt-injection detection, and app security
|
|
9
|
+
* config resolution. INVARIANT: every check fails OPEN when the backing
|
|
10
|
+
* table/RPC is absent so installs without migration 049 keep working.
|
|
11
|
+
*
|
|
12
|
+
* @seeAlso .framework/functions/_shared/middleware.ts (enforcement point)
|
|
13
|
+
* @seeAlso .framework/functions/_shared/rate-limit.ts (violation signal)
|
|
14
|
+
* @seeAlso .framework/migrations/049_security_blocklist.sql (schema)
|
|
15
|
+
* @seeAlso .framework/functions/security.ts (admin actions)
|
|
16
|
+
*
|
|
17
|
+
* @cil-generated
|
|
18
|
+
*/
|
|
19
|
+
// CIL[BEGIN stable_id="~/.framework/functions/_shared/security.ts:module_header:module_header" type="module_header" identity="module_header" parent="~/.framework/functions/_shared/security.ts"]
|
|
20
|
+
// CIL[DOC-MACRO purpose="Shared security enforcement: denylist lookups, geo stamping, injection detection, app security config"]
|
|
21
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Provide fail-open helpers for blocklist enforcement (request + registration), people.data geo stamping via RPC, prompt-injection pattern detection, and cached apps.config reads"]
|
|
22
|
+
// CIL[DOC-CONTEXT module="functions/_shared" layer="security" related_files=".framework/functions/_shared/middleware.ts,.framework/functions/_shared/rate-limit.ts,.framework/functions/_shared/geo.ts,.framework/migrations/049_security_blocklist.sql,.framework/functions/security.ts"]
|
|
23
|
+
// CIL[DOC-CONTRACT inputs="RequestContext / person id / email / ip / appId" outputs="boolean blocked, matched pattern, void RPC stamps, app config" errors="never throws on infra failure — fail-open" side_effects="security_blocklist reads, people.data writes, logs writes via emitAudit"]
|
|
24
|
+
// CIL[DOC-MICRO logic_summary="Blocklist queries filter type+value with revoked/expired exclusion in JS; geo stamping uses SECURITY DEFINER RPCs to merge people.data keys; app config cached 60s per warm instance" notable_decisions="Fail-open everywhere: a missing 049 table must never deny legitimate traffic; IP block entries are also enforced at the Netlify edge but the app-layer check covers direct function hits"]
|
|
25
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
26
|
+
// CIL[DOC-AUDIENCE audience="internal"]
|
|
27
|
+
|
|
28
|
+
import { adminDb } from './db'
|
|
29
|
+
import { getClientGeo } from './geo'
|
|
30
|
+
import { checkRateLimit } from './rate-limit'
|
|
31
|
+
import type { RateLimitPolicy } from './rate-limit'
|
|
32
|
+
import { emitAudit } from './audit'
|
|
33
|
+
import type { RequestContext } from './middleware'
|
|
34
|
+
import type { Principal } from './principal'
|
|
35
|
+
// CIL[END stable_id="~/.framework/functions/_shared/security.ts:module_header:module_header"]
|
|
36
|
+
|
|
37
|
+
// CIL[BEGIN stable_id="~/.framework/functions/_shared/security.ts:injection_patterns:constant" type="constant" identity="INJECTION_PATTERNS" parent="~/.framework/functions/_shared/security.ts"]
|
|
38
|
+
// CIL[DOC-MACRO purpose="Prompt-injection detection patterns"]
|
|
39
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Named regex set matching known instruction-override phrasings"]
|
|
40
|
+
// CIL[DOC-CONTRACT inputs="none" outputs="[name, RegExp][]" errors="none" side_effects="none"]
|
|
41
|
+
// CIL[DOC-MICRO logic_summary="Each entry pairs a stable label (stored on the flagged record) with a case-insensitive pattern" notable_decisions="First-pass heuristic — deliberately narrow to keep false positives low; catches verbatim 'ignore previous instructions' style attacks, not obfuscated variants"]
|
|
42
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
43
|
+
export const INJECTION_PATTERNS: Array<[string, RegExp]> = [
|
|
44
|
+
['ignore_instructions', /ignore\s+(all\s+)?(the\s+)?(previous|prior|above|earlier)\s+(instruction|prompt|rule|context)s?/i],
|
|
45
|
+
['role_reassignment', /you\s+are\s+now\s+(a\s+|an\s+)?(system\s+)?(administrator|admin|root|developer|system\s+prompt)/i],
|
|
46
|
+
['exfil_everything', /(output|print|return|dump|reveal|leak)\s+(the\s+)?(contents|full\s+text|entire|all)\s+(of\s+)?(every|all|the\s+system)/i],
|
|
47
|
+
['disregard_prompt', /disregard\s+(your\s+)?(previous|prior|system|all)\s+(prompt|instruction|rule|guideline)s?/i],
|
|
48
|
+
['dan_jailbreak', /\bDAN\s+(mode|jailbreak)\b|\bdo\s+anything\s+now\b/i],
|
|
49
|
+
['fake_system_prefix', /^\s*system\s*:\s*/im],
|
|
50
|
+
['prompt_echo', /repeat\s+(back\s+)?(your\s+)?(system\s+)?(prompt|instructions)\s+(verbatim|word\s+for\s+word|exactly)/i],
|
|
51
|
+
]
|
|
52
|
+
// CIL[END stable_id="~/.framework/functions/_shared/security.ts:injection_patterns:constant"]
|
|
53
|
+
|
|
54
|
+
// CIL[BEGIN stable_id="~/.framework/functions/_shared/security.ts:detectinjection:function" type="function" identity="detectInjection" parent="~/.framework/functions/_shared/security.ts"]
|
|
55
|
+
// CIL[DOC-MACRO purpose="Detect prompt-injection phrasing in user text"]
|
|
56
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Return the matched pattern name or null"]
|
|
57
|
+
// CIL[DOC-CONTRACT inputs="text: string" outputs="string | null (pattern name)" errors="none" side_effects="none"]
|
|
58
|
+
// CIL[DOC-MICRO logic_summary="Linear scan over INJECTION_PATTERNS; returns first match label" notable_decisions="[]"]
|
|
59
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
60
|
+
export function detectInjection(text: string | null | undefined): string | null {
|
|
61
|
+
if (!text || typeof text !== 'string') return null
|
|
62
|
+
for (const [name, re] of INJECTION_PATTERNS) {
|
|
63
|
+
if (re.test(text)) return name
|
|
64
|
+
}
|
|
65
|
+
return null
|
|
66
|
+
}
|
|
67
|
+
// CIL[END stable_id="~/.framework/functions/_shared/security.ts:detectinjection:function"]
|
|
68
|
+
|
|
69
|
+
// CIL[BEGIN stable_id="~/.framework/functions/_shared/security.ts:isactiverule:function" type="function" identity="isActiveRule" parent="~/.framework/functions/_shared/security.ts"]
|
|
70
|
+
// CIL[DOC-MACRO purpose="Filter revoked/expired blocklist rows in JS"]
|
|
71
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Evaluate revoked_at and expires_at client-side"]
|
|
72
|
+
// CIL[DOC-CONTRACT inputs="row: {revoked_at, expires_at}" outputs="boolean" errors="none" side_effects="none"]
|
|
73
|
+
// CIL[DOC-MICRO logic_summary="Active = not revoked and (no expiry or expiry in future)" notable_decisions="[]"]
|
|
74
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
75
|
+
function isActiveRule(row: { revoked_at?: string | null; expires_at?: string | null }): boolean {
|
|
76
|
+
if (row.revoked_at) return false
|
|
77
|
+
if (row.expires_at && Date.parse(row.expires_at) <= Date.now()) return false
|
|
78
|
+
return true
|
|
79
|
+
}
|
|
80
|
+
// CIL[END stable_id="~/.framework/functions/_shared/security.ts:isactiverule:function"]
|
|
81
|
+
|
|
82
|
+
// Blocklist decision cache — every authenticated request hits lookupBlocklist,
|
|
83
|
+
// which is a DB round-trip even when the table is empty. Cache the decision per
|
|
84
|
+
// candidate set for a bounded window: a newly-added hard block takes effect
|
|
85
|
+
// within BLOCKLIST_CACHE_TTL_MS, and a revoked block releases within the same
|
|
86
|
+
// window. Promise memoization means concurrent requests share one query.
|
|
87
|
+
const BLOCKLIST_CACHE_TTL_MS = 15_000
|
|
88
|
+
const BLOCKLIST_CACHE_MAX = 1000
|
|
89
|
+
const blocklistCache = new Map<string, { value: Promise<any>; expiresAt: number }>()
|
|
90
|
+
|
|
91
|
+
// CIL[BEGIN stable_id="~/.framework/functions/_shared/security.ts:lookupblocklist:function" type="function" identity="lookupBlocklist" parent="~/.framework/functions/_shared/security.ts"]
|
|
92
|
+
// CIL[DOC-MACRO purpose="Query security_blocklist for a set of (type,value) candidates"]
|
|
93
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Single .or() query across candidate identities; returns the matching active row or null"]
|
|
94
|
+
// CIL[DOC-CONTRACT inputs="checks: {type,value}[], severity filter implicit ('hard')" outputs="blocklist row | null" errors="returns null on any query failure — fail-open" side_effects="DB read"]
|
|
95
|
+
// CIL[DOC-MICRO logic_summary="Builds a PostgREST or() of and(type.eq.,value.eq.) clauses; filters revoked/expired in JS" notable_decisions="severity 'hard' only — 'soft' entries are advisory signals for the report, not request blocks"]
|
|
96
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
97
|
+
async function lookupBlocklist(
|
|
98
|
+
checks: Array<{ type: string; value: string }>,
|
|
99
|
+
): Promise<{ type: string; value: string; reason: string | null } | null> {
|
|
100
|
+
if (!checks.length) return null
|
|
101
|
+
try {
|
|
102
|
+
const or = checks
|
|
103
|
+
.map(c => `and(type.eq.${c.type},value.eq.${c.value})`)
|
|
104
|
+
.join(',')
|
|
105
|
+
const { data, error } = await adminDb
|
|
106
|
+
.from('security_blocklist')
|
|
107
|
+
.select('type, value, reason, severity, revoked_at, expires_at')
|
|
108
|
+
.eq('severity', 'hard')
|
|
109
|
+
.or(or)
|
|
110
|
+
.limit(5)
|
|
111
|
+
if (error) {
|
|
112
|
+
console.warn('blocklist lookup failed (fail-open):', error.message)
|
|
113
|
+
return null
|
|
114
|
+
}
|
|
115
|
+
return (data || []).find(isActiveRule) ?? null
|
|
116
|
+
} catch (err: any) {
|
|
117
|
+
console.warn('blocklist lookup error (fail-open):', err?.message)
|
|
118
|
+
return null
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// CIL[END stable_id="~/.framework/functions/_shared/security.ts:lookupblocklist:function"]
|
|
122
|
+
|
|
123
|
+
// CIL[BEGIN stable_id="~/.framework/functions/_shared/security.ts:isblockedrequest:function" type="function" identity="isBlockedRequest" parent="~/.framework/functions/_shared/security.ts"]
|
|
124
|
+
// CIL[DOC-MACRO purpose="Check whether the current request identity is hard-blocked"]
|
|
125
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Match person_id / account_id / email / client IP against security_blocklist"]
|
|
126
|
+
// CIL[DOC-CONTRACT inputs="ctx: RequestContext (headers required for IP)" outputs="blocking row | null" errors="never throws" side_effects="DB read"]
|
|
127
|
+
// CIL[DOC-MICRO logic_summary="Skips machine principals and anonymous callers; candidates assembled then one lookupBlocklist call" notable_decisions="Machine principals (cron/api-key/system) are never blocklist-denied — they are infrastructure, not users"]
|
|
128
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
129
|
+
export async function isBlockedRequest(ctx: RequestContext): Promise<{ type: string; value: string; reason: string | null } | null> {
|
|
130
|
+
const p: Principal | null = ctx.principal as any
|
|
131
|
+
const { ip } = getClientGeo(ctx)
|
|
132
|
+
|
|
133
|
+
const checks: Array<{ type: string; value: string }> = []
|
|
134
|
+
if (p && p.type === 'human' && p.id && p.id !== 'anonymous') {
|
|
135
|
+
checks.push({ type: 'person_id', value: p.id })
|
|
136
|
+
if (p.accountId) checks.push({ type: 'account_id', value: p.accountId })
|
|
137
|
+
if (p.email) checks.push({ type: 'email', value: p.email.toLowerCase() })
|
|
138
|
+
}
|
|
139
|
+
if (ip) checks.push({ type: 'ip', value: ip })
|
|
140
|
+
if (!checks.length) return null
|
|
141
|
+
|
|
142
|
+
const key = checks.map(c => `${c.type}=${c.value}`).sort().join('|')
|
|
143
|
+
const hit = blocklistCache.get(key)
|
|
144
|
+
if (hit && hit.expiresAt > Date.now()) return hit.value
|
|
145
|
+
blocklistCache.delete(key)
|
|
146
|
+
const pending = lookupBlocklist(checks)
|
|
147
|
+
if (blocklistCache.size >= BLOCKLIST_CACHE_MAX) {
|
|
148
|
+
const oldest = blocklistCache.keys().next().value
|
|
149
|
+
if (oldest !== undefined) blocklistCache.delete(oldest)
|
|
150
|
+
}
|
|
151
|
+
blocklistCache.set(key, { value: pending, expiresAt: Date.now() + BLOCKLIST_CACHE_TTL_MS })
|
|
152
|
+
pending.catch(() => blocklistCache.delete(key))
|
|
153
|
+
return pending
|
|
154
|
+
}
|
|
155
|
+
// CIL[END stable_id="~/.framework/functions/_shared/security.ts:isblockedrequest:function"]
|
|
156
|
+
|
|
157
|
+
// CIL[BEGIN stable_id="~/.framework/functions/_shared/security.ts:isblockedip:function" type="function" identity="isBlockedIp" parent="~/.framework/functions/_shared/security.ts"]
|
|
158
|
+
// CIL[DOC-MACRO purpose="Check whether a raw client IP is hard-blocked"]
|
|
159
|
+
// CIL[DOC-RESPONSIBILITY responsibility="IP-only variant for public/unauthenticated paths"]
|
|
160
|
+
// CIL[DOC-CONTRACT inputs="ip: string | undefined" outputs="blocking row | null" errors="never throws" side_effects="DB read"]
|
|
161
|
+
// CIL[DOC-MICRO logic_summary="Wraps lookupBlocklist with a single ip candidate" notable_decisions="[]"]
|
|
162
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
163
|
+
export async function isBlockedIp(ip: string | undefined): Promise<{ type: string; value: string; reason: string | null } | null> {
|
|
164
|
+
if (!ip) return null
|
|
165
|
+
return lookupBlocklist([{ type: 'ip', value: ip }])
|
|
166
|
+
}
|
|
167
|
+
// CIL[END stable_id="~/.framework/functions/_shared/security.ts:isblockedip:function"]
|
|
168
|
+
|
|
169
|
+
// CIL[BEGIN stable_id="~/.framework/functions/_shared/security.ts:checkregistrationblocklist:function" type="function" identity="checkRegistrationBlocklist" parent="~/.framework/functions/_shared/security.ts"]
|
|
170
|
+
// CIL[DOC-MACRO purpose="Deny registration for blocklisted email, domain, or IP"]
|
|
171
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Throw a 403 when the signup email, its domain, or the client IP is on the denylist; emit a security audit event on hit"]
|
|
172
|
+
// CIL[DOC-CONTRACT inputs="ctx, email, ip" outputs="void" errors="Error (403) when blocked" side_effects="DB read; audit log on hit"]
|
|
173
|
+
// CIL[DOC-MICRO logic_summary="Candidates: email (lowercased), email_domain, ip — one lookup; emits security.registration_blocked audit on match" notable_decisions="Error messages deliberately generic — do not tell an attacker which signal matched"]
|
|
174
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
175
|
+
export async function checkRegistrationBlocklist(
|
|
176
|
+
ctx: RequestContext,
|
|
177
|
+
email: string,
|
|
178
|
+
ip?: string,
|
|
179
|
+
): Promise<void> {
|
|
180
|
+
const normalized = (email || '').toLowerCase()
|
|
181
|
+
const domain = normalized.split('@')[1]
|
|
182
|
+
const checks: Array<{ type: string; value: string }> = [
|
|
183
|
+
{ type: 'email', value: normalized },
|
|
184
|
+
]
|
|
185
|
+
if (domain) checks.push({ type: 'email_domain', value: domain })
|
|
186
|
+
if (ip) checks.push({ type: 'ip', value: ip })
|
|
187
|
+
|
|
188
|
+
const hit = await lookupBlocklist(checks)
|
|
189
|
+
if (!hit) return
|
|
190
|
+
|
|
191
|
+
void emitAudit(ctx, 'security.registration_blocked', { type: 'registration' }, {
|
|
192
|
+
result: 'denied',
|
|
193
|
+
blocked_type: hit.type,
|
|
194
|
+
reason: hit.reason,
|
|
195
|
+
})
|
|
196
|
+
const err: any = new Error('This registration is not eligible. Contact support if you believe this is an error.')
|
|
197
|
+
err.statusCode = 403
|
|
198
|
+
throw err
|
|
199
|
+
}
|
|
200
|
+
// CIL[END stable_id="~/.framework/functions/_shared/security.ts:checkregistrationblocklist:function"]
|
|
201
|
+
|
|
202
|
+
// CIL[BEGIN stable_id="~/.framework/functions/_shared/security.ts:geo_touch_throttle:comment" type="comment" identity="geoTouchThrottle" parent="~/.framework/functions/_shared/security.ts"]
|
|
203
|
+
// CIL[DOC-MACRO purpose="Per-person throttle state for geo last-seen writes"]
|
|
204
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Bound person_touch_geo RPC writes to at most once per GEO_TOUCH_TTL_MS per person per warm instance"]
|
|
205
|
+
// CIL[DOC-CONTEXT module=".framework/functions/_shared/security.ts" layer="shared-core" related_files=".framework/functions/_shared/middleware.ts"]
|
|
206
|
+
// CIL[DOC-CONTRACT inputs="none" outputs="module-level Map<personId, epochMs>" errors="none" side_effects="in-memory throttle state; resets on cold start, which is acceptable"]
|
|
207
|
+
// CIL[DOC-MICRO logic_summary="Chatty clients (polling UIs) would otherwise trigger one RPC write per request; last_seen_at does not need sub-minute granularity" notable_decisions="per warm instance so no extra reads are needed; entries are small so growth is bounded by unique persons per instance"]
|
|
208
|
+
// CIL[DOC-AUDIENCE audience="framework"]
|
|
209
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
210
|
+
const GEO_TOUCH_TTL_MS = 5 * 60 * 1000
|
|
211
|
+
const geoTouchCache = new Map<string, number>()
|
|
212
|
+
// CIL[END stable_id="~/.framework/functions/_shared/security.ts:geo_touch_throttle:comment"]
|
|
213
|
+
|
|
214
|
+
// CIL[BEGIN stable_id="~/.framework/functions/_shared/security.ts:touchpersongeo:function" type="function" identity="touchPersonGeo" parent="~/.framework/functions/_shared/security.ts"]
|
|
215
|
+
// CIL[DOC-MACRO purpose="Stamp last-seen IP/country onto a person row"]
|
|
216
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Fire-and-forget RPC merge of last_ip/last_country/last_seen_at into people.data, throttled per person per warm instance"]
|
|
217
|
+
// CIL[DOC-CONTRACT inputs="personId, ctx (headers for geo)" outputs="void" errors="never throws — swallowed with warn" side_effects="people.data update at most once per GEO_TOUCH_TTL_MS per person per instance"]
|
|
218
|
+
// CIL[DOC-MICRO logic_summary="Calls person_touch_geo RPC only when geo headers resolved; no-op in local dev without Netlify geo headers; in-memory Map throttles repeat touches inside GEO_TOUCH_TTL_MS" notable_decisions="Missing RPC (pre-049 install) is swallowed — callers must not await for correctness; throttle is per warm instance so it needs no extra reads"]
|
|
219
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
220
|
+
export async function touchPersonGeo(personId: string, ctx: RequestContext): Promise<void> {
|
|
221
|
+
try {
|
|
222
|
+
if (!personId || personId === 'anonymous') return
|
|
223
|
+
const { ip, country } = getClientGeo(ctx)
|
|
224
|
+
if (!ip && !country) return
|
|
225
|
+
// Chatty clients (polling UIs) otherwise trigger one RPC write per
|
|
226
|
+
// request. last_seen_at doesn't need sub-minute granularity — skip the
|
|
227
|
+
// write if this warm instance stamped the person recently.
|
|
228
|
+
const last = geoTouchCache.get(personId) || 0
|
|
229
|
+
if (Date.now() - last < GEO_TOUCH_TTL_MS) return
|
|
230
|
+
geoTouchCache.set(personId, Date.now())
|
|
231
|
+
await adminDb.rpc('person_touch_geo', {
|
|
232
|
+
p_person_id: personId,
|
|
233
|
+
p_ip: ip ?? null,
|
|
234
|
+
p_country: country ?? null,
|
|
235
|
+
})
|
|
236
|
+
} catch (err: any) {
|
|
237
|
+
console.warn('touchPersonGeo failed (non-fatal):', err?.message)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// CIL[END stable_id="~/.framework/functions/_shared/security.ts:touchpersongeo:function"]
|
|
241
|
+
|
|
242
|
+
// CIL[BEGIN stable_id="~/.framework/functions/_shared/security.ts:recordregistrationgeo:function" type="function" identity="recordRegistrationGeo" parent="~/.framework/functions/_shared/security.ts"]
|
|
243
|
+
// CIL[DOC-MACRO purpose="Stamp first-seen registration IP/country onto a person row"]
|
|
244
|
+
// CIL[DOC-RESPONSIBILITY responsibility="RPC merge of registration_ip/registration_country that never overwrites existing values"]
|
|
245
|
+
// CIL[DOC-CONTRACT inputs="personId, ctx" outputs="void" errors="never throws" side_effects="people.data update"]
|
|
246
|
+
// CIL[DOC-MICRO logic_summary="Calls person_record_registration_geo (COALESCE keeps first values)" notable_decisions="[]"]
|
|
247
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
248
|
+
export async function recordRegistrationGeo(personId: string, ctx: RequestContext): Promise<void> {
|
|
249
|
+
try {
|
|
250
|
+
if (!personId || personId === 'anonymous') return
|
|
251
|
+
const { ip, country } = getClientGeo(ctx)
|
|
252
|
+
if (!ip && !country) return
|
|
253
|
+
await adminDb.rpc('person_record_registration_geo', {
|
|
254
|
+
p_person_id: personId,
|
|
255
|
+
p_ip: ip ?? null,
|
|
256
|
+
p_country: country ?? null,
|
|
257
|
+
})
|
|
258
|
+
} catch (err: any) {
|
|
259
|
+
console.warn('recordRegistrationGeo failed (non-fatal):', err?.message)
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
// CIL[END stable_id="~/.framework/functions/_shared/security.ts:recordregistrationgeo:function"]
|
|
263
|
+
|
|
264
|
+
// CIL[BEGIN stable_id="~/.framework/functions/_shared/security.ts:appconfigcache:comment" type="comment" identity="appConfigCache" parent="~/.framework/functions/_shared/security.ts"]
|
|
265
|
+
// CIL[DOC-MACRO purpose="60s per-instance cache of apps.config to keep hot paths cheap"]
|
|
266
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Avoid an apps-table read on every admin-data create when no rate limits are configured"]
|
|
267
|
+
// CIL[DOC-MICRO logic_summary="Map<slug,{config,at}> with TTL; stale entries refreshed lazily" notable_decisions="Cache lives only as long as the warm lambda; config changes propagate within 60s"]
|
|
268
|
+
// CIL[DOC-CONTRACT inputs="appId (getAppConfig)" outputs="apps.config JSON or null" errors="returns null on lookup failure" side_effects="one apps-table read per slug per TTL window"]
|
|
269
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
270
|
+
const APP_CONFIG_TTL_MS = 60_000
|
|
271
|
+
const appConfigCache = new Map<string, { config: any; at: number }>()
|
|
272
|
+
|
|
273
|
+
async function getAppConfig(appId: string | null | undefined): Promise<any | null> {
|
|
274
|
+
if (!appId) return null
|
|
275
|
+
const hit = appConfigCache.get(appId)
|
|
276
|
+
if (hit && Date.now() - hit.at < APP_CONFIG_TTL_MS) return hit.config
|
|
277
|
+
try {
|
|
278
|
+
const { data } = await adminDb
|
|
279
|
+
.from('apps')
|
|
280
|
+
.select('config')
|
|
281
|
+
.eq('slug', appId)
|
|
282
|
+
.maybeSingle()
|
|
283
|
+
const config = data?.config ?? null
|
|
284
|
+
appConfigCache.set(appId, { config, at: Date.now() })
|
|
285
|
+
return config
|
|
286
|
+
} catch {
|
|
287
|
+
return null
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
// CIL[END stable_id="~/.framework/functions/_shared/security.ts:appconfigcache:comment"]
|
|
291
|
+
|
|
292
|
+
// CIL[BEGIN stable_id="~/.framework/functions/_shared/security.ts:getappsecurityconfig:function" type="function" identity="getAppSecurityConfig" parent="~/.framework/functions/_shared/security.ts"]
|
|
293
|
+
// CIL[DOC-MACRO purpose="Resolve an app's security config block"]
|
|
294
|
+
// CIL[DOC-CONTRACT inputs="appId" outputs="config.security object | null" errors="never throws" side_effects="cached apps read"]
|
|
295
|
+
// CIL[DOC-MICRO logic_summary="Returns apps.config.security — prompt_injection mode ('flag'|'block'|'off'), etc." notable_decisions="[]"]
|
|
296
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
297
|
+
export async function getAppSecurityConfig(appId: string | null | undefined): Promise<{
|
|
298
|
+
prompt_injection?: 'flag' | 'block' | 'off'
|
|
299
|
+
[key: string]: any
|
|
300
|
+
} | null> {
|
|
301
|
+
const config = await getAppConfig(appId)
|
|
302
|
+
return config?.security ?? null
|
|
303
|
+
}
|
|
304
|
+
// CIL[END stable_id="~/.framework/functions/_shared/security.ts:getappsecurityconfig:function"]
|
|
305
|
+
|
|
306
|
+
// CIL[BEGIN stable_id="~/.framework/functions/_shared/security.ts:checktypeslugestatelimit:function" type="function" identity="checkTypeSlugRateLimit" parent="~/.framework/functions/_shared/security.ts"]
|
|
307
|
+
// CIL[DOC-MACRO purpose="Rate-limit an admin-data create by type slug when the calling app declares a policy"]
|
|
308
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Zero-cost pass-through unless config.rate_limits has a '<type_slug>_create' policy for the calling app"]
|
|
309
|
+
// CIL[DOC-CONTRACT inputs="ctx, typeSlug" outputs="void" errors="RateLimitError (429) when over a configured policy" side_effects="rate_limits upsert when policy exists"]
|
|
310
|
+
// CIL[DOC-MICRO logic_summary="Cached app config → policies lookup; only invokes checkRateLimit when a policy is actually declared so unconfigured creates pay no extra DB reads" notable_decisions="Convention over config: '<type_slug>_create' action names keep admin-data generic — apps opt in via manifest config.rate_limits"]
|
|
311
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
312
|
+
export async function checkTypeSlugRateLimit(ctx: RequestContext, typeSlug: string | null | undefined): Promise<void> {
|
|
313
|
+
if (!typeSlug) return
|
|
314
|
+
const action = `${typeSlug}_create`
|
|
315
|
+
const config = await getAppConfig(ctx.appId)
|
|
316
|
+
const raw = config?.rate_limits?.[action]
|
|
317
|
+
const policies: RateLimitPolicy[] = (Array.isArray(raw) ? raw : raw ? [raw] : [])
|
|
318
|
+
.filter((p: any) => typeof p?.limit === 'number' && typeof p?.window_seconds === 'number')
|
|
319
|
+
if (!policies.length) return
|
|
320
|
+
await checkRateLimit(ctx, action, { policies })
|
|
321
|
+
}
|
|
322
|
+
// CIL[END stable_id="~/.framework/functions/_shared/security.ts:checktypeslugestatelimit:function"]
|
|
323
|
+
|
|
324
|
+
// CIL[BEGIN stable_id="~/.framework/functions/_shared/security.ts:emitsecurityevent:function" type="function" identity="emitSecurityEvent" parent="~/.framework/functions/_shared/security.ts"]
|
|
325
|
+
// CIL[DOC-MACRO purpose="Emit a security.* audit event"]
|
|
326
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Write a logs row via emitAudit with adminDb so the security report can aggregate signals"]
|
|
327
|
+
// CIL[DOC-CONTRACT inputs="ctx, action ('security.x'), target, metadata" outputs="void" errors="never throws" side_effects="logs insert"]
|
|
328
|
+
// CIL[DOC-MICRO logic_summary="Thin wrapper forcing ctx.db=adminDb — security events must be written even when the caller's RLS db lacks logs insert" notable_decisions="[]"]
|
|
329
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
330
|
+
export async function emitSecurityEvent(
|
|
331
|
+
ctx: RequestContext,
|
|
332
|
+
action: string,
|
|
333
|
+
target?: { type: string; id?: string; account_id?: string },
|
|
334
|
+
metadata?: Record<string, any>,
|
|
335
|
+
): Promise<void> {
|
|
336
|
+
try {
|
|
337
|
+
await emitAudit({ ...ctx, db: adminDb } as any, action, target || { type: 'security' }, {
|
|
338
|
+
result: 'denied',
|
|
339
|
+
...metadata,
|
|
340
|
+
})
|
|
341
|
+
} catch (err: any) {
|
|
342
|
+
console.warn('emitSecurityEvent failed:', err?.message)
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
// CIL[END stable_id="~/.framework/functions/_shared/security.ts:emitsecurityevent:function"]
|