spine-framework 0.4.3 → 0.5.0-rc.4
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 +13 -3
- package/.framework/cli/commands/bootstrap.ts +392 -0
- package/.framework/cli/commands/dev.ts +91 -218
- package/.framework/cli/commands/doctor.ts +132 -26
- package/.framework/cli/commands/generate.ts +38 -298
- package/.framework/cli/commands/init.ts +20 -1
- package/.framework/cli/commands/migrate.ts +36 -30
- package/.framework/cli/commands/migrations.ts +16 -56
- package/.framework/cli/commands/upgrade.ts +35 -0
- package/.framework/cli/commands/validate-schema.ts +147 -0
- package/.framework/cli/context.ts +223 -110
- package/.framework/cli/index.ts +6 -0
- package/.framework/cli/lib/project-files.ts +241 -1
- package/.framework/functions/_shared/agent-runner.ts +1000 -81
- package/.framework/functions/_shared/cil.ts +2 -1
- package/.framework/functions/_shared/db.ts +3 -6
- package/.framework/functions/_shared/hash-api-key.ts +183 -79
- package/.framework/functions/_shared/kb.ts +61 -7
- package/.framework/functions/_shared/logging.ts +562 -224
- package/.framework/functions/_shared/message-sequence.ts +53 -0
- package/.framework/functions/_shared/middleware.ts +47 -44
- package/.framework/functions/_shared/pipeline-runner.ts +150 -37
- package/.framework/functions/_shared/principal.ts +9 -6
- package/.framework/functions/_shared/trigger-engine.ts +149 -40
- package/.framework/functions/admin-data.ts +48 -31
- package/.framework/functions/ai-agents.ts +103 -33
- package/.framework/functions/api-keys.ts +301 -122
- package/.framework/functions/apps.ts +43 -26
- package/.framework/functions/cil-ingest.ts +48 -15
- package/.framework/functions/debug-auth.ts +14 -8
- package/.framework/functions/deliveries.ts +39 -12
- package/.framework/functions/embeddings.ts +145 -76
- package/.framework/functions/integration-outbound.ts +67 -27
- package/.framework/functions/integrations.ts +254 -127
- package/.framework/functions/logs.ts +273 -133
- package/.framework/functions/observability.ts +277 -147
- package/.framework/functions/pipeline-executions.ts +1 -1
- package/.framework/functions/pipeline-runner-background.ts +120 -0
- package/.framework/functions/pipelines.ts +14 -9
- package/.framework/functions/prompt-configs.ts +7 -4
- package/.framework/functions/roles.ts +8 -5
- package/.framework/functions/security.ts +30 -17
- package/.framework/functions/system-cron.ts +250 -128
- package/.framework/functions/tests.ts +7 -6
- package/.framework/functions/timers.ts +310 -107
- package/.framework/functions/trigger-scheduler.ts +65 -5
- package/.framework/functions/triggers.ts +28 -13
- package/.framework/functions/types.ts +13 -10
- package/.framework/migrations/064_episode_leases.sql +196 -0
- package/.framework/migrations/065_execution_tables_updated_at.sql +20 -0
- package/.framework/migrations/066_episode_steal_thread_merge.sql +148 -0
- package/.framework/migrations/067_episode_lease_stale_steal.sql +93 -0
- package/.framework/migrations/068_cron_schedule_timer_rpcs.sql +251 -0
- package/.framework/migrations/069_log_stats_window.sql +54 -0
- package/.framework/migrations/070_app_type_schema_rpcs.sql +166 -0
- package/.framework/migrations/071_timer_interval_keys.sql +96 -0
- package/.framework/migrations/072_slug_autofill.sql +77 -0
- package/.framework/migrations/073_seed_member_roles.sql +26 -0
- package/.framework/schemas/spine.config.schema.json +62 -0
- package/.framework/src/App.tsx +3 -1
- package/.framework/src/apps/admin/index.tsx +1141 -775
- package/.framework/src/components/ErrorBoundary.tsx +136 -0
- package/.framework/src/components/admin/AdminListPage.tsx +81 -73
- package/.framework/src/components/admin/AdminSidebar.tsx +92 -81
- package/.framework/src/components/admin/EntitySelect.tsx +181 -0
- package/.framework/src/components/admin/PageInstructions.tsx +86 -127
- package/.framework/src/components/app-shell/GenericAppShell.tsx +3 -1
- package/.framework/src/components/app-shell/GenericDetailPage.tsx +37 -13
- package/.framework/src/components/app-shell/GenericListPage.tsx +28 -32
- package/.framework/src/components/auth/TurnstileWidget.tsx +9 -6
- package/.framework/src/components/kb/KBArticleEditor.tsx +49 -6
- package/.framework/src/components/layout/Layout.tsx +4 -2
- package/.framework/src/components/layout/Sidebar.tsx +304 -179
- package/.framework/src/components/runtime/DataDetailPage.tsx +67 -35
- package/.framework/src/components/runtime/DataHeader.tsx +96 -48
- package/.framework/src/components/runtime/DataListPage.tsx +3 -26
- package/.framework/src/components/runtime/DataStats.tsx +10 -10
- package/.framework/src/components/runtime/DataTable.tsx +12 -10
- package/.framework/src/components/ui/sheet.tsx +24 -26
- package/.framework/src/components/ui/textarea.tsx +74 -48
- package/.framework/src/hooks/useBranding.ts +47 -0
- package/.framework/src/hooks/useEntityList.ts +12 -9
- package/.framework/src/hooks/useListSchema.ts +108 -58
- package/.framework/src/instructions/README.md +57 -0
- package/.framework/src/instructions/ai-agents.md +69 -0
- package/.framework/src/instructions/alerts.md +80 -0
- package/.framework/src/instructions/api-keys.md +87 -0
- package/.framework/src/instructions/apps.md +77 -0
- package/.framework/src/instructions/concepts/ai-and-prompts.md +150 -0
- package/.framework/src/instructions/concepts/apps-and-renderers.md +162 -0
- package/.framework/src/instructions/concepts/auth-and-api-keys.md +133 -0
- package/.framework/src/instructions/concepts/automation.md +186 -0
- package/.framework/src/instructions/concepts/concurrency-and-idempotency.md +125 -0
- package/.framework/src/instructions/concepts/core-vs-custom.md +124 -0
- package/.framework/src/instructions/concepts/design-schema.md +152 -0
- package/.framework/src/instructions/concepts/integrations-and-webhooks.md +127 -0
- package/.framework/src/instructions/concepts/observability.md +116 -0
- package/.framework/src/instructions/concepts/permissions.md +150 -0
- package/.framework/src/instructions/concepts/runtime-records.md +111 -0
- package/.framework/src/instructions/concepts/scope-and-accounts.md +112 -0
- package/.framework/src/instructions/concepts/spine-way.md +172 -0
- package/.framework/src/instructions/deliveries.md +59 -0
- package/.framework/src/instructions/guide/00-orientation.md +106 -0
- package/.framework/src/instructions/guide/01-install-and-first-run.md +253 -0
- package/.framework/src/instructions/guide/02-project-map.md +147 -0
- package/.framework/src/instructions/guide/03-domain-model.md +223 -0
- package/.framework/src/instructions/guide/04-first-app.md +227 -0
- package/.framework/src/instructions/guide/05-glossary.md +183 -0
- package/.framework/src/instructions/integrations.md +96 -0
- package/.framework/src/instructions/internal/cil-markers.md +83 -0
- package/.framework/src/instructions/invites.md +60 -0
- package/.framework/src/instructions/kb.md +80 -0
- package/.framework/src/instructions/logs.md +77 -0
- package/.framework/src/instructions/observability.md +61 -0
- package/.framework/src/instructions/operations/deployment.md +115 -0
- package/.framework/src/instructions/operations/migrations.md +125 -0
- package/.framework/src/instructions/operations/security-checklist.md +148 -0
- package/.framework/src/instructions/operations/troubleshooting.md +170 -0
- package/.framework/src/instructions/operations/upgrade.md +116 -0
- package/.framework/src/instructions/pipeline-executions.md +62 -0
- package/.framework/src/instructions/pipelines.md +78 -0
- package/.framework/src/instructions/prompts.md +95 -0
- package/.framework/src/instructions/reference/api/admin-data.md +162 -0
- package/.framework/src/instructions/reference/api/auth.md +113 -0
- package/.framework/src/instructions/reference/api/errors-and-envelopes.md +113 -0
- package/.framework/src/instructions/reference/app-manifest.md +150 -0
- package/.framework/src/instructions/reference/cli.md +124 -0
- package/.framework/src/instructions/reference/database.md +132 -0
- package/.framework/src/instructions/reference/design-schema.md +280 -0
- package/.framework/src/instructions/reference/env-vars.md +133 -0
- package/.framework/src/instructions/reference/functions/account-nodes.md +75 -0
- package/.framework/src/instructions/reference/functions/admin-data.md +195 -0
- package/.framework/src/instructions/reference/functions/admin-funnel.md +90 -0
- package/.framework/src/instructions/reference/functions/admin-install-funnel.md +107 -0
- package/.framework/src/instructions/reference/functions/admin-signals.md +102 -0
- package/.framework/src/instructions/reference/functions/ai-agents.md +148 -0
- package/.framework/src/instructions/reference/functions/ai-interactions.md +156 -0
- package/.framework/src/instructions/reference/functions/api-keys.md +130 -0
- package/.framework/src/instructions/reference/functions/apps.md +140 -0
- package/.framework/src/instructions/reference/functions/auth.md +99 -0
- package/.framework/src/instructions/reference/functions/debug-auth.md +91 -0
- package/.framework/src/instructions/reference/functions/deliveries.md +110 -0
- package/.framework/src/instructions/reference/functions/embeddings.md +166 -0
- package/.framework/src/instructions/reference/functions/eval-harness.md +149 -0
- package/.framework/src/instructions/reference/functions/index.md +103 -0
- package/.framework/src/instructions/reference/functions/integration-outbound.md +200 -0
- package/.framework/src/instructions/reference/functions/integration-routes.md +151 -0
- package/.framework/src/instructions/reference/functions/integrations.md +130 -0
- package/.framework/src/instructions/reference/functions/invites.md +182 -0
- package/.framework/src/instructions/reference/functions/item-progress.md +107 -0
- package/.framework/src/instructions/reference/functions/kb.md +166 -0
- package/.framework/src/instructions/reference/functions/logs.md +141 -0
- package/.framework/src/instructions/reference/functions/observability.md +105 -0
- package/.framework/src/instructions/reference/functions/pipeline-executions.md +132 -0
- package/.framework/src/instructions/reference/functions/pipeline-runner-background.md +121 -0
- package/.framework/src/instructions/reference/functions/pipelines.md +136 -0
- package/.framework/src/instructions/reference/functions/prompt-configs.md +121 -0
- package/.framework/src/instructions/reference/functions/roles.md +121 -0
- package/.framework/src/instructions/reference/functions/security.md +140 -0
- package/.framework/src/instructions/reference/functions/system-cron.md +136 -0
- package/.framework/src/instructions/reference/functions/system.md +106 -0
- package/.framework/src/instructions/reference/functions/tests.md +97 -0
- package/.framework/src/instructions/reference/functions/timers.md +117 -0
- package/.framework/src/instructions/reference/functions/trigger-scheduler.md +102 -0
- package/.framework/src/instructions/reference/functions/triggers.md +140 -0
- package/.framework/src/instructions/reference/functions/types.md +146 -0
- package/.framework/src/instructions/reference/seeds.md +126 -0
- package/.framework/src/instructions/reference/shared-modules.md +127 -0
- package/.framework/src/instructions/reference/spine-config.md +114 -0
- package/.framework/src/instructions/roles.md +80 -0
- package/.framework/src/instructions/runtime-detail.md +70 -0
- package/.framework/src/instructions/runtime-list.md +77 -0
- package/.framework/src/instructions/security.md +67 -0
- package/.framework/src/instructions/tasks/add-a-pipeline.md +110 -0
- package/.framework/src/instructions/tasks/add-a-role.md +96 -0
- package/.framework/src/instructions/tasks/add-a-timer.md +114 -0
- package/.framework/src/instructions/tasks/add-a-trigger.md +106 -0
- package/.framework/src/instructions/tasks/add-a-type.md +151 -0
- package/.framework/src/instructions/tasks/add-a-webhook.md +108 -0
- package/.framework/src/instructions/tasks/add-an-ai-agent.md +108 -0
- package/.framework/src/instructions/tasks/add-an-integration.md +124 -0
- package/.framework/src/instructions/tasks/build-a-custom-page.md +118 -0
- package/.framework/src/instructions/tasks/package-and-install-an-app.md +127 -0
- package/.framework/src/instructions/tasks/test-your-code.md +108 -0
- package/.framework/src/instructions/tasks/write-a-function.md +132 -0
- package/.framework/src/instructions/tasks/write-a-migration.md +110 -0
- package/.framework/src/instructions/testing.md +59 -0
- package/.framework/src/instructions/timers.md +82 -0
- package/.framework/src/instructions/triggers.md +69 -0
- package/.framework/src/instructions/types.md +93 -0
- package/.framework/src/lib/branding.ts +128 -0
- package/.framework/src/pages/NoAppPage.tsx +3 -1
- package/.framework/src/pages/NotFoundPage.tsx +62 -31
- package/.framework/src/pages/SetupPage.tsx +5 -2
- package/.framework/src/pages/admin/AIAgentDetailPage.tsx +117 -106
- package/.framework/src/pages/admin/AIAgentsPage.tsx +86 -48
- package/.framework/src/pages/admin/APIKeyDetailPage.tsx +153 -152
- package/.framework/src/pages/admin/APIKeysPage.tsx +91 -90
- package/.framework/src/pages/admin/AlertsConfigPage.tsx +29 -18
- package/.framework/src/pages/admin/AppDetailPage.tsx +6 -60
- package/.framework/src/pages/admin/AppsPage.tsx +104 -66
- package/.framework/src/pages/admin/DeliveriesPage.tsx +5 -5
- package/.framework/src/pages/admin/EmbeddingDetailPage.tsx +5 -23
- package/.framework/src/pages/admin/EmbeddingsPage.tsx +5 -26
- package/.framework/src/pages/admin/IntegrationDetailPage.tsx +7 -7
- package/.framework/src/pages/admin/IntegrationsPage.tsx +85 -43
- package/.framework/src/pages/admin/LogsPage.tsx +144 -76
- package/.framework/src/pages/admin/ObservabilityDashboard.tsx +223 -140
- package/.framework/src/pages/admin/PipelineDetailPage.tsx +134 -113
- package/.framework/src/pages/admin/PipelineExecutionsPage.tsx +108 -45
- package/.framework/src/pages/admin/PipelinesPage.tsx +160 -115
- package/.framework/src/pages/admin/PromptConfigDetailPage.tsx +96 -98
- package/.framework/src/pages/admin/PromptConfigsPage.tsx +101 -59
- package/.framework/src/pages/admin/RoleDetailPage.tsx +90 -79
- package/.framework/src/pages/admin/RolesPage.tsx +86 -43
- package/.framework/src/pages/admin/SecurityPage.tsx +1 -1
- package/.framework/src/pages/admin/TestingDashboard.tsx +16 -28
- package/.framework/src/pages/admin/TimerDetailPage.tsx +117 -100
- package/.framework/src/pages/admin/TimersPage.tsx +90 -62
- package/.framework/src/pages/admin/TriggerDetailPage.tsx +90 -83
- package/.framework/src/pages/admin/TriggersPage.tsx +124 -99
- package/.framework/src/pages/admin/TypeDetailPage.tsx +152 -138
- package/.framework/src/pages/admin/TypesPage.tsx +90 -48
- package/.framework/src/pages/admin/cil/CILAgentPage.tsx +17 -25
- package/.framework/src/pages/admin/cil/CILChunkDetailPage.tsx +17 -27
- package/.framework/src/pages/admin/cil/CILDashboardPage.tsx +72 -40
- package/.framework/src/pages/admin/cil/CILFileListPage.tsx +321 -129
- package/.framework/src/pages/admin/cil/CILFileSelectPage.tsx +18 -29
- package/.framework/src/pages/admin/cil/CILGraphPage.tsx +26 -38
- package/.framework/src/pages/admin/cil/CILIngestionPage.tsx +34 -46
- package/.framework/src/pages/admin/cil/CILProjectsPage.tsx +189 -103
- package/.framework/src/pages/admin/cil/CILSearchPage.tsx +246 -136
- package/.framework/src/pages/admin/cil/CILSuggestionsPage.tsx +11 -22
- package/.framework/src/pages/admin/cil/CILVersionsPage.tsx +9 -20
- package/.framework/src/pages/admin/cil/FlexiblePipelinePage.tsx +26 -38
- package/.framework/src/pages/admin/kb/KBAdminPage.tsx +7 -26
- package/.framework/src/pages/auth/AuthConfirmPage.tsx +3 -1
- package/.framework/src/pages/auth/ForgotPasswordPage.tsx +3 -1
- package/.framework/src/pages/auth/LoginPage.tsx +4 -2
- package/.framework/src/pages/auth/RegisterPage.tsx +3 -1
- package/.framework/templates/agent/AGENTS.md +150 -0
- package/.framework/templates/agent/skills/spine-add-ai-agent/SKILL.md +58 -0
- package/.framework/templates/agent/skills/spine-add-automation/SKILL.md +62 -0
- package/.framework/templates/agent/skills/spine-add-integration/SKILL.md +63 -0
- package/.framework/templates/agent/skills/spine-architect/SKILL.md +86 -0
- package/.framework/templates/agent/skills/spine-install-app/SKILL.md +72 -0
- package/.framework/templates/agent/skills/spine-migration/SKILL.md +75 -0
- package/.framework/templates/agent/skills/spine-new-app/SKILL.md +67 -0
- package/.framework/templates/agent/skills/spine-new-function/SKILL.md +65 -0
- package/.framework/templates/agent/skills/spine-new-type/SKILL.md +65 -0
- package/.framework/templates/agent/skills/spine-schema/SKILL.md +77 -0
- package/.framework/templates/agent/skills/spine-test/SKILL.md +67 -0
- package/.framework/templates/agent/skills/spine-troubleshoot/SKILL.md +74 -0
- package/.framework/templates/agent/skills/spine-upgrade/SKILL.md +69 -0
- package/.framework/templates/project.json +5 -1
- package/.framework/tests/unit/account-scoping-gates.test.ts +392 -0
- package/.framework/tests/unit/agent-files.test.ts +355 -0
- package/.framework/tests/unit/agent-runner.test.ts +496 -47
- package/.framework/tests/unit/api-keys-create-scope-gate.test.ts +160 -0
- package/.framework/tests/unit/api-keys-omit-secret-fields.test.ts +38 -0
- package/.framework/tests/unit/bootstrap.test.ts +242 -0
- package/.framework/tests/unit/cli-context-api-key.test.ts +114 -0
- package/.framework/tests/unit/debug-auth.test.ts +38 -0
- package/.framework/tests/unit/deliveries-account-scoping.test.ts +185 -0
- package/.framework/tests/unit/episode-lease.test.ts +695 -0
- package/.framework/tests/unit/integration-outbound-credentials.test.ts +167 -0
- package/.framework/tests/unit/integrations-audit-redaction.test.ts +130 -0
- package/.framework/tests/unit/kb-ingest-lease.test.ts +19 -0
- package/.framework/tests/unit/logging-app-id.test.ts +75 -0
- package/.framework/tests/unit/pipeline-runner-background.test.ts +120 -0
- package/.framework/tests/unit/pipeline-runner.test.ts +153 -1
- package/.framework/tests/unit/principal-cache.test.ts +15 -0
- package/.framework/tests/unit/system-context-denial.test.ts +145 -0
- package/.framework/tests/unit/system-cron.test.ts +240 -0
- package/.framework/tests/unit/timers.test.ts +159 -0
- package/.framework/tests/unit/trigger-engine.test.ts +278 -91
- package/.framework/tests/unit/trigger-scheduler.test.ts +78 -0
- package/README.md +125 -166
- package/config/tsconfig.cli.json +2 -2
- package/netlify.toml +5 -0
- package/package.json +15 -4
- package/templates/project/package-lock.json +2 -2
- package/.framework/docs/admin-routes.md +0 -76
- package/.framework/docs/api-docs-progress.md +0 -38
- package/.framework/docs/api-governance.md +0 -146
- package/.framework/docs/api-testing-results.md +0 -212
- package/.framework/docs/assembly-launch-guide.md +0 -138
- package/.framework/docs/audit-results.md +0 -590
- package/.framework/docs/db-api-inventory.md +0 -95
- package/.framework/docs/gap-remediation-backlog.md +0 -103
- package/.framework/docs/ui-adoption-verification.md +0 -111
- package/.framework/docs/ui-api-coverage.md +0 -84
- package/.framework/docs/v2-compatibility-audit.md +0 -228
- package/.framework/mcp/index.ts +0 -133
- package/.framework/mcp/load-env.ts +0 -60
- package/.framework/mcp/shared/app-manifest.ts +0 -275
- package/.framework/mcp/shared/db.ts +0 -477
- package/.framework/mcp/shared/kb.ts +0 -118
- package/.framework/mcp/tools/architect.ts +0 -431
- package/.framework/mcp/tools/cil/context.ts +0 -345
- package/.framework/mcp/tools/cil/graph.ts +0 -242
- package/.framework/mcp/tools/cil/ingest.ts +0 -1217
- package/.framework/mcp/tools/cil/pipeline-orchestrator.ts +0 -693
- package/.framework/mcp/tools/cil/pipeline.ts +0 -767
- package/.framework/mcp/tools/cil/project.ts +0 -791
- package/.framework/mcp/tools/cil/retrieval.ts +0 -336
- package/.framework/mcp/tools/cil/search-core.ts +0 -454
- package/.framework/mcp/tools/cil/search.ts +0 -97
- package/.framework/mcp/tools/cil/suggest.ts +0 -186
- package/.framework/mcp/tools/cil/validate.ts +0 -1369
- package/.framework/mcp/tools/kb/delete.ts +0 -139
- package/.framework/mcp/tools/kb/index.ts +0 -52
- package/.framework/mcp/tools/kb/ingest.ts +0 -84
- package/.framework/mcp/tools/kb/list-types.ts +0 -102
- package/.framework/mcp/tools/kb/publish.ts +0 -100
- package/.framework/mcp/tools/kb/reindex.ts +0 -86
- package/.framework/mcp/tools/kb/revoke.ts +0 -85
- package/.framework/mcp/tools/kb/search.ts +0 -101
- package/.framework/mcp/tools/kb/upsert.ts +0 -125
- package/.framework/mcp/tools/observability.ts +0 -510
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// CIL[BEGIN stable_id="~/.framework/functions/pipeline-runner-background.ts:module_header:module_header" type="module_header" identity="module_header" parent="~/.framework/functions/pipeline-runner-background.ts"]
|
|
2
|
+
// CIL[DOC-MACRO purpose="Background entry point for event-triggered pipeline executions — receives dispatched payloads from checkAndFireTriggers and runs the pipeline in an isolated process with its own connection budget"]
|
|
3
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Authenticate internal dispatch calls via INTERNAL_INVOKE_SECRET, reconstruct a machine RequestContext, and invoke runPipeline with the pre-created execution_id so the queued row transitions to running/completed rather than being duplicated"]
|
|
4
|
+
// CIL[DOC-CONTEXT module="pipeline-runner-background" layer="functions" related_files=".framework/functions/_shared/trigger-engine.ts,.framework/functions/_shared/pipeline-runner.ts,.framework/functions/_shared/principal.ts,.framework/functions/_shared/middleware.ts"]
|
|
5
|
+
// CIL[DOC-CONTRACT inputs="POST body { pipeline_id, trigger_data, account_id, execution_id, trigger_id } with X-Internal-Secret header" outputs="{ ok: true, execution_id } | { error }" errors="403 on missing/wrong secret, 400 on missing pipeline_id, 500 on pipeline failure" side_effects="Runs the named pipeline; updates pipeline_executions row from queued→running→terminal"]
|
|
6
|
+
// CIL[DOC-MICRO logic_summary="Validates the shared internal secret (timing-safe, same as principal.isInternalInvokeAuthorized), builds a SYSTEM_PRINCIPAL RequestContext carrying the original account scope, then calls runPipeline with the caller's pre-created execution_id. Status 'queued' → 'running' is handled inside runPipeline via the new opts.executionId parameter." notable_decisions="Does NOT use createHandler — it would resolve the secret to anonymous and 401 before reaching the handler. Auth is checked inline and SYSTEM_PRINCIPAL provides the machine context directly."]
|
|
7
|
+
// CIL[DOC-CONFIDENCE pass="first" score="0.9" flags="none"]
|
|
8
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
9
|
+
// CIL[DOC-AUDIENCE audience="framework"]
|
|
10
|
+
|
|
11
|
+
import { createHash, timingSafeEqual } from 'node:crypto'
|
|
12
|
+
import { runPipeline, sweepStaleExecutions } from './_shared/pipeline-runner'
|
|
13
|
+
import { SYSTEM_PRINCIPAL } from './_shared/principal'
|
|
14
|
+
import { adminDb } from './_shared/db'
|
|
15
|
+
// CIL[END stable_id="~/.framework/functions/pipeline-runner-background.ts:module_header:module_header"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
// CIL[BEGIN stable_id="~/.framework/functions/pipeline-runner-background.ts:isdispatchauthorized:function" type="function" identity="isDispatchAuthorized" parent="~/.framework/functions/pipeline-runner-background.ts"]
|
|
19
|
+
// CIL[DOC-MACRO purpose="Verify the X-Internal-Secret header using timing-safe comparison (mirrors principal.isInternalInvokeAuthorized)"]
|
|
20
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Return true only when the request carries a valid INTERNAL_INVOKE_SECRET"]
|
|
21
|
+
// CIL[DOC-CONTEXT module="pipeline-runner-background" layer="functions" related_files=".framework/functions/_shared/principal.ts"]
|
|
22
|
+
// CIL[DOC-CONTRACT inputs="event headers" outputs="boolean" errors="none" side_effects="none"]
|
|
23
|
+
// CIL[DOC-MICRO logic_summary="SHA-256 both sides before timingSafeEqual so early-exit on length mismatch cannot leak the expected secret length via response timing — identical pattern to principal.isInternalInvokeAuthorized" notable_decisions="duplicated here rather than imported because isInternalInvokeAuthorized is unexported from principal.ts"]
|
|
24
|
+
// CIL[DOC-CONFIDENCE pass="first" score="0.9" flags="none"]
|
|
25
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
26
|
+
function isDispatchAuthorized(event: any): boolean {
|
|
27
|
+
const expected = (globalThis as any).process?.env?.INTERNAL_INVOKE_SECRET
|
|
28
|
+
if (!expected) return false
|
|
29
|
+
|
|
30
|
+
const provided = event.headers?.['x-internal-secret'] || event.headers?.['X-Internal-Secret']
|
|
31
|
+
if (!provided || typeof provided !== 'string') return false
|
|
32
|
+
|
|
33
|
+
const expectedHash = createHash('sha256').update(expected).digest()
|
|
34
|
+
const providedHash = createHash('sha256').update(provided).digest()
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
return timingSafeEqual(expectedHash, providedHash)
|
|
38
|
+
} catch {
|
|
39
|
+
return false
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// CIL[END stable_id="~/.framework/functions/pipeline-runner-background.ts:isdispatchauthorized:function"]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
// CIL[BEGIN stable_id="~/.framework/functions/pipeline-runner-background.ts:handler:constant" type="constant" identity="handler" parent="~/.framework/functions/pipeline-runner-background.ts"]
|
|
46
|
+
// CIL[DOC-MACRO purpose="Netlify function handler for dispatched pipeline executions"]
|
|
47
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Authenticate, sweep stale executions, then runPipeline with the caller's pre-created execution_id"]
|
|
48
|
+
// CIL[DOC-CONTEXT module="pipeline-runner-background" layer="functions" related_files=".framework/functions/_shared/pipeline-runner.ts"]
|
|
49
|
+
// CIL[DOC-CONTRACT inputs="POST body { pipeline_id, trigger_data, account_id, execution_id, trigger_id }" outputs="{ ok, execution_id, status } | { error }" errors="403 unauthorized, 400 missing pipeline_id" side_effects="Pipeline runs; pipeline_executions transitions queued→running→terminal"]
|
|
50
|
+
// CIL[DOC-MICRO logic_summary="Reject without INTERNAL_INVOKE_SECRET; build SYSTEM_PRINCIPAL ctx scoped to the dispatching account; runPipeline with opts.executionId so the pre-created row is claimed rather than duplicated; return the final ExecutionResult" notable_decisions="sweepStaleExecutions is called opportunistically on entry — same fire-and-forget pattern as runPipeline itself, but on a fresh process it costs nothing"]
|
|
51
|
+
// CIL[DOC-CONFIDENCE pass="first" score="0.9" flags="none"]
|
|
52
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
53
|
+
export const handler = async (event: any, _context: any) => {
|
|
54
|
+
const requestId = crypto.randomUUID()
|
|
55
|
+
|
|
56
|
+
if (!isDispatchAuthorized(event)) {
|
|
57
|
+
return {
|
|
58
|
+
statusCode: 403,
|
|
59
|
+
body: JSON.stringify({ error: 'Forbidden — invalid internal secret' }),
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let body: any = {}
|
|
64
|
+
try {
|
|
65
|
+
body = typeof event.body === 'string' ? JSON.parse(event.body || '{}') : (event.body || {})
|
|
66
|
+
} catch {
|
|
67
|
+
return { statusCode: 400, body: JSON.stringify({ error: 'Invalid JSON body' }) }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const { pipeline_id, trigger_data, account_id, execution_id, trigger_id } = body
|
|
71
|
+
|
|
72
|
+
if (!pipeline_id) {
|
|
73
|
+
return { statusCode: 400, body: JSON.stringify({ error: 'pipeline_id is required' }) }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Opportunistic sweep — picks up any executions orphaned by a prior dead
|
|
77
|
+
// invocation before this one starts its own work.
|
|
78
|
+
void sweepStaleExecutions().catch(() => {})
|
|
79
|
+
|
|
80
|
+
const ctx = {
|
|
81
|
+
requestId,
|
|
82
|
+
principal: {
|
|
83
|
+
...SYSTEM_PRINCIPAL,
|
|
84
|
+
provenance: {
|
|
85
|
+
...SYSTEM_PRINCIPAL.provenance,
|
|
86
|
+
sourceType: 'trigger' as const,
|
|
87
|
+
invokedAt: new Date().toISOString(),
|
|
88
|
+
triggerId: trigger_id || null,
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
db: adminDb,
|
|
92
|
+
accountId: account_id || null,
|
|
93
|
+
appId: null,
|
|
94
|
+
query: {},
|
|
95
|
+
requestPath: '/.netlify/functions/pipeline-runner-background',
|
|
96
|
+
headers: event.headers || {},
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const result = await runPipeline(pipeline_id, trigger_data || {}, ctx as any, {
|
|
101
|
+
executionId: execution_id,
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
statusCode: 200,
|
|
106
|
+
body: JSON.stringify({
|
|
107
|
+
ok: result.status === 'completed',
|
|
108
|
+
execution_id: result.executionId,
|
|
109
|
+
status: result.status,
|
|
110
|
+
}),
|
|
111
|
+
}
|
|
112
|
+
} catch (err: any) {
|
|
113
|
+
console.error(`[${requestId}] pipeline-runner-background failed for pipeline ${pipeline_id}:`, err)
|
|
114
|
+
return {
|
|
115
|
+
statusCode: 500,
|
|
116
|
+
body: JSON.stringify({ error: err.message || 'Pipeline execution failed', execution_id }),
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// CIL[END stable_id="~/.framework/functions/pipeline-runner-background.ts:handler:constant"]
|
|
@@ -176,7 +176,8 @@ export const create = createHandler(async (ctx, body) => {
|
|
|
176
176
|
throw new Error('User context (person and account) required')
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
-
const
|
|
179
|
+
const db = isSystemAdmin(ctx.principal) ? adminDb : ctx.db
|
|
180
|
+
const { data, error: err } = await db
|
|
180
181
|
.from('pipelines')
|
|
181
182
|
.insert({
|
|
182
183
|
app_id: app_id || null,
|
|
@@ -187,7 +188,7 @@ export const create = createHandler(async (ctx, body) => {
|
|
|
187
188
|
config: config || {},
|
|
188
189
|
stages,
|
|
189
190
|
metadata: metadata || {},
|
|
190
|
-
created_by: ctx.principal.id
|
|
191
|
+
created_by: ctx.principal.type === 'human' ? ctx.principal.id : null
|
|
191
192
|
})
|
|
192
193
|
.select()
|
|
193
194
|
.single()
|
|
@@ -228,7 +229,8 @@ export const update = createHandler(async (ctx, body) => {
|
|
|
228
229
|
throw new Error('User context (person and account) required')
|
|
229
230
|
}
|
|
230
231
|
|
|
231
|
-
const
|
|
232
|
+
const db = isSystemAdmin(ctx.principal) ? adminDb : ctx.db
|
|
233
|
+
const { data: current } = await db
|
|
232
234
|
.from('pipelines')
|
|
233
235
|
.select('*')
|
|
234
236
|
.eq('id', id)
|
|
@@ -248,7 +250,7 @@ export const update = createHandler(async (ctx, body) => {
|
|
|
248
250
|
if (is_active !== undefined) updateData.is_active = is_active
|
|
249
251
|
if (app_id !== undefined) updateData.app_id = app_id
|
|
250
252
|
|
|
251
|
-
const { data, error: err } = await
|
|
253
|
+
const { data, error: err } = await db
|
|
252
254
|
.from('pipelines')
|
|
253
255
|
.update(updateData)
|
|
254
256
|
.eq('id', id)
|
|
@@ -290,7 +292,8 @@ export const toggle = createHandler(async (ctx, body) => {
|
|
|
290
292
|
throw new Error('User context (person and account) required')
|
|
291
293
|
}
|
|
292
294
|
|
|
293
|
-
const
|
|
295
|
+
const db = isSystemAdmin(ctx.principal) ? adminDb : ctx.db
|
|
296
|
+
const { data: current } = await db
|
|
294
297
|
.from('pipelines')
|
|
295
298
|
.select('*')
|
|
296
299
|
.eq('id', id)
|
|
@@ -300,7 +303,7 @@ export const toggle = createHandler(async (ctx, body) => {
|
|
|
300
303
|
throw new Error('Pipeline not found')
|
|
301
304
|
}
|
|
302
305
|
|
|
303
|
-
const { data, error: err } = await
|
|
306
|
+
const { data, error: err } = await db
|
|
304
307
|
.from('pipelines')
|
|
305
308
|
.update({ is_active, updated_at: new Date().toISOString() })
|
|
306
309
|
.eq('id', id)
|
|
@@ -338,7 +341,8 @@ export const getExecutions = createHandler(async (ctx, _body) => {
|
|
|
338
341
|
throw new Error('Pipeline ID is required')
|
|
339
342
|
}
|
|
340
343
|
|
|
341
|
-
const
|
|
344
|
+
const db = isSystemAdmin(ctx.principal) ? adminDb : ctx.db
|
|
345
|
+
const { data, error: err } = await db
|
|
342
346
|
.from('pipeline_executions')
|
|
343
347
|
.select('*')
|
|
344
348
|
.eq('pipeline_id', pipeline_id)
|
|
@@ -374,7 +378,8 @@ export const remove = createHandler(async (ctx, _body) => {
|
|
|
374
378
|
throw new Error('Pipeline ID is required')
|
|
375
379
|
}
|
|
376
380
|
|
|
377
|
-
const
|
|
381
|
+
const db = isSystemAdmin(ctx.principal) ? adminDb : ctx.db
|
|
382
|
+
const { data: current } = await db
|
|
378
383
|
.from('pipelines')
|
|
379
384
|
.select('id, name')
|
|
380
385
|
.eq('id', id)
|
|
@@ -382,7 +387,7 @@ export const remove = createHandler(async (ctx, _body) => {
|
|
|
382
387
|
|
|
383
388
|
if (!current) throw new Error('Pipeline not found')
|
|
384
389
|
|
|
385
|
-
const { error: err } = await
|
|
390
|
+
const { error: err } = await db
|
|
386
391
|
.from('pipelines')
|
|
387
392
|
.delete()
|
|
388
393
|
.eq('id', id)
|
|
@@ -131,7 +131,8 @@ export const create = createHandler(async (ctx, body) => {
|
|
|
131
131
|
throw new Error('Account context required')
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
-
const
|
|
134
|
+
const db = isSystemAdmin(ctx.principal) ? adminDb : ctx.db
|
|
135
|
+
const { data, error: err } = await db
|
|
135
136
|
.from('prompt_configs')
|
|
136
137
|
.insert({
|
|
137
138
|
app_id: app_id || null,
|
|
@@ -201,7 +202,8 @@ export const update = createHandler(async (ctx, body) => {
|
|
|
201
202
|
if (updates[key] !== undefined) updateData[key] = updates[key]
|
|
202
203
|
}
|
|
203
204
|
|
|
204
|
-
const
|
|
205
|
+
const db = isSystemAdmin(ctx.principal) ? adminDb : ctx.db
|
|
206
|
+
const { data, error: err } = await db
|
|
205
207
|
.from('prompt_configs')
|
|
206
208
|
.update(updateData)
|
|
207
209
|
.eq('id', id)
|
|
@@ -239,7 +241,8 @@ export const remove = createHandler(async (ctx, _body) => {
|
|
|
239
241
|
throw new Error('Config ID is required')
|
|
240
242
|
}
|
|
241
243
|
|
|
242
|
-
const
|
|
244
|
+
const db = isSystemAdmin(ctx.principal) ? adminDb : ctx.db
|
|
245
|
+
const { data: current } = await db
|
|
243
246
|
.from('prompt_configs')
|
|
244
247
|
.select('id, name, slug')
|
|
245
248
|
.eq('id', id)
|
|
@@ -247,7 +250,7 @@ export const remove = createHandler(async (ctx, _body) => {
|
|
|
247
250
|
|
|
248
251
|
if (!current) throw new Error('Prompt config not found')
|
|
249
252
|
|
|
250
|
-
const { error: err } = await
|
|
253
|
+
const { error: err } = await db
|
|
251
254
|
.from('prompt_configs')
|
|
252
255
|
.delete()
|
|
253
256
|
.eq('id', id)
|
|
@@ -188,7 +188,8 @@ export const create = createHandler(async (ctx, body) => {
|
|
|
188
188
|
throw new Error('Role slug already exists for this app')
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
-
const
|
|
191
|
+
const db = permissions.isSystemAdmin(ctx) ? adminDb : ctx.db
|
|
192
|
+
const { data, error: err } = await db
|
|
192
193
|
.from('roles')
|
|
193
194
|
.insert({
|
|
194
195
|
app_id,
|
|
@@ -234,7 +235,8 @@ export const update = createHandler(async (ctx, body) => {
|
|
|
234
235
|
throw new Error('User context (person and account) required')
|
|
235
236
|
}
|
|
236
237
|
|
|
237
|
-
const
|
|
238
|
+
const db = permissions.isSystemAdmin(ctx) ? adminDb : ctx.db
|
|
239
|
+
const { data: current, error: fetchErr } = await db
|
|
238
240
|
.from('roles')
|
|
239
241
|
.select('*')
|
|
240
242
|
.eq('id', id)
|
|
@@ -255,7 +257,7 @@ export const update = createHandler(async (ctx, body) => {
|
|
|
255
257
|
throw new Error(fieldValidation.error)
|
|
256
258
|
}
|
|
257
259
|
|
|
258
|
-
const { data, error: err } = await
|
|
260
|
+
const { data, error: err } = await db
|
|
259
261
|
.from('roles')
|
|
260
262
|
.update({
|
|
261
263
|
...updates,
|
|
@@ -297,7 +299,8 @@ export const remove = createHandler(async (ctx, body) => {
|
|
|
297
299
|
throw new Error('User context (person and account) required')
|
|
298
300
|
}
|
|
299
301
|
|
|
300
|
-
const
|
|
302
|
+
const db = permissions.isSystemAdmin(ctx) ? adminDb : ctx.db
|
|
303
|
+
const { data: current, error: fetchErr } = await db
|
|
301
304
|
.from('roles')
|
|
302
305
|
.select('*')
|
|
303
306
|
.eq('id', id)
|
|
@@ -307,7 +310,7 @@ export const remove = createHandler(async (ctx, body) => {
|
|
|
307
310
|
throw new Error('Role not found or access denied')
|
|
308
311
|
}
|
|
309
312
|
|
|
310
|
-
const { data, error: err } = await
|
|
313
|
+
const { data, error: err } = await db
|
|
311
314
|
.from('roles')
|
|
312
315
|
.update({
|
|
313
316
|
is_active: false,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// CIL[BEGIN stable_id="~/.framework/functions/security.ts:module_header:module_header" type="module_header" identity="module_header" parent="~/.framework/functions/security.ts"]
|
|
1
2
|
/**
|
|
2
3
|
* @module security
|
|
3
4
|
* @audience internal
|
|
@@ -15,7 +16,6 @@
|
|
|
15
16
|
*
|
|
16
17
|
* @cil-generated
|
|
17
18
|
*/
|
|
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
19
|
// CIL[DOC-MACRO purpose="System-admin security operations: flagged-actor report, ban/unban, blocklist CRUD, Netlify edge sync"]
|
|
20
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
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"]
|
|
@@ -29,15 +29,19 @@ import { adminDb } from './_shared/db'
|
|
|
29
29
|
import { emitAudit } from './_shared/audit'
|
|
30
30
|
// CIL[END stable_id="~/.framework/functions/security.ts:module_header:module_header"]
|
|
31
31
|
|
|
32
|
-
// CIL[BEGIN stable_id="~/.framework/functions/security.ts:netlifyedgesync:comment" type="comment" identity="netlifyEdgeSync" parent="~/.framework/functions/security.ts"]
|
|
33
32
|
// CIL[DOC-MACRO purpose="Netlify Firewall Traffic Rules sync for banned IPs"]
|
|
34
33
|
// CIL[DOC-RESPONSIBILITY responsibility="Keep a single managed ip-deny rule (description spine-security:blocklist) whose data.addresses mirrors active ip blocklist entries"]
|
|
35
34
|
// 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
35
|
// 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
36
|
// CIL[DOC-STABILITY stability="experimental"]
|
|
37
|
+
// CIL[BEGIN stable_id="~/.framework/functions/security.ts:MANAGED_RULE_DESC:constant" type="constant" identity="MANAGED_RULE_DESC" parent="~/.framework/functions/security.ts"]
|
|
38
38
|
const MANAGED_RULE_DESC = 'spine-security:blocklist'
|
|
39
|
+
// CIL[END stable_id="~/.framework/functions/security.ts:MANAGED_RULE_DESC:constant"]
|
|
40
|
+
// CIL[BEGIN stable_id="~/.framework/functions/security.ts:NETLIFY_API:constant" type="constant" identity="NETLIFY_API" parent="~/.framework/functions/security.ts"]
|
|
39
41
|
const NETLIFY_API = 'https://api.netlify.com'
|
|
42
|
+
// CIL[END stable_id="~/.framework/functions/security.ts:NETLIFY_API:constant"]
|
|
40
43
|
|
|
44
|
+
// CIL[BEGIN stable_id="~/.framework/functions/security.ts:netlifyEdgeSync:function" type="function" identity="netlifyEdgeSync" parent="~/.framework/functions/security.ts"]
|
|
41
45
|
async function netlifyEdgeSync(ipsToAdd: string[], ipsToRemove: string[] = []): Promise<{ synced: boolean; skipped?: boolean; error?: string }> {
|
|
42
46
|
const token = process.env.NETLIFY_ACCESS_TOKEN
|
|
43
47
|
const siteId = process.env.NETLIFY_SITE_ID
|
|
@@ -90,25 +94,29 @@ async function netlifyEdgeSync(ipsToAdd: string[], ipsToRemove: string[] = []):
|
|
|
90
94
|
return { synced: false, error: err?.message || 'Netlify sync failed' }
|
|
91
95
|
}
|
|
92
96
|
}
|
|
93
|
-
// CIL[END stable_id="~/.framework/functions/security.ts:
|
|
97
|
+
// CIL[END stable_id="~/.framework/functions/security.ts:netlifyEdgeSync:function"]
|
|
94
98
|
|
|
95
|
-
// CIL[BEGIN stable_id="~/.framework/functions/security.ts:flaggedaccounts:function" type="function" identity="flaggedAccounts" parent="~/.framework/functions/security.ts"]
|
|
96
99
|
// CIL[DOC-MACRO purpose="Aggregate security.* audit events into a flagged-actors report"]
|
|
97
100
|
// 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
101
|
// 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"]
|
|
102
|
+
// CIL[DOC-MICRO logic_summary="Fetch last 500 security.* events within 30 days → 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; started_at lower bound keeps the span_name scan index-bounded (no span_name index, and zero-signal scans would otherwise walk the whole logs table)"]
|
|
100
103
|
// CIL[DOC-STABILITY stability="experimental"]
|
|
104
|
+
// CIL[BEGIN stable_id="~/.framework/functions/security.ts:SECURITY_SIGNALS:constant" type="constant" identity="SECURITY_SIGNALS" parent="~/.framework/functions/security.ts"]
|
|
101
105
|
const SECURITY_SIGNALS = [
|
|
102
106
|
'security.rate_limit_exceeded',
|
|
103
107
|
'security.injection_detected',
|
|
104
108
|
'security.registration_blocked',
|
|
105
109
|
]
|
|
110
|
+
// CIL[END stable_id="~/.framework/functions/security.ts:SECURITY_SIGNALS:constant"]
|
|
106
111
|
|
|
112
|
+
// CIL[BEGIN stable_id="~/.framework/functions/security.ts:flaggedAccounts:function" type="function" identity="flaggedAccounts" parent="~/.framework/functions/security.ts"]
|
|
107
113
|
async function flaggedAccounts(ctx: RequestContext) {
|
|
114
|
+
const since = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString()
|
|
108
115
|
const { data: events, error } = await adminDb
|
|
109
116
|
.from('logs')
|
|
110
117
|
.select('account_id, started_at, metadata')
|
|
111
118
|
.in('span_name', SECURITY_SIGNALS)
|
|
119
|
+
.gte('started_at', since)
|
|
112
120
|
.order('started_at', { ascending: false })
|
|
113
121
|
.limit(500)
|
|
114
122
|
if (error) {
|
|
@@ -194,14 +202,14 @@ async function flaggedAccounts(ctx: RequestContext) {
|
|
|
194
202
|
rows.sort((a, b) => b.signals - a.signals)
|
|
195
203
|
return { rows }
|
|
196
204
|
}
|
|
197
|
-
// CIL[END stable_id="~/.framework/functions/security.ts:
|
|
205
|
+
// CIL[END stable_id="~/.framework/functions/security.ts:flaggedAccounts:function"]
|
|
198
206
|
|
|
199
|
-
// CIL[BEGIN stable_id="~/.framework/functions/security.ts:addblockrows:function" type="function" identity="addBlockRows" parent="~/.framework/functions/security.ts"]
|
|
200
207
|
// CIL[DOC-MACRO purpose="Upsert active hard-block rows into security_blocklist"]
|
|
201
208
|
// CIL[DOC-RESPONSIBILITY responsibility="Insert rows with revoked_at cleared on (type,value) conflict so a re-ban re-activates a revoked row"]
|
|
202
209
|
// CIL[DOC-CONTRACT inputs="rows[], bannedBy, reason" outputs="void" errors="throws on DB error" side_effects="security_blocklist upsert"]
|
|
203
210
|
// CIL[DOC-MICRO logic_summary="map rows → upsert onConflict type,value with revoked_at:null" notable_decisions="[]"]
|
|
204
211
|
// CIL[DOC-STABILITY stability="stable"]
|
|
212
|
+
// CIL[BEGIN stable_id="~/.framework/functions/security.ts:addBlockRows:function" type="function" identity="addBlockRows" parent="~/.framework/functions/security.ts"]
|
|
205
213
|
async function addBlockRows(
|
|
206
214
|
rows: Array<{ type: string; value: string }>,
|
|
207
215
|
bannedBy: string | undefined,
|
|
@@ -225,14 +233,14 @@ async function addBlockRows(
|
|
|
225
233
|
)
|
|
226
234
|
if (error) throw new Error('security_blocklist upsert failed: ' + error.message)
|
|
227
235
|
}
|
|
228
|
-
// CIL[END stable_id="~/.framework/functions/security.ts:
|
|
236
|
+
// CIL[END stable_id="~/.framework/functions/security.ts:addBlockRows:function"]
|
|
229
237
|
|
|
230
|
-
// CIL[BEGIN stable_id="~/.framework/functions/security.ts:banuser:function" type="function" identity="banUser" parent="~/.framework/functions/security.ts"]
|
|
231
238
|
// CIL[DOC-MACRO purpose="Three-layer ban: internal blocklist + Supabase Auth ban + Netlify edge deny"]
|
|
232
239
|
// 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
240
|
// 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
241
|
// 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
242
|
// CIL[DOC-STABILITY stability="experimental"]
|
|
243
|
+
// CIL[BEGIN stable_id="~/.framework/functions/security.ts:banUser:function" type="function" identity="banUser" parent="~/.framework/functions/security.ts"]
|
|
236
244
|
async function banUser(ctx: RequestContext, body: any) {
|
|
237
245
|
const { person_id, email: rawEmail, ip: rawIp, reason, block_ip = true, block_email = true } = body || {}
|
|
238
246
|
if (!person_id && !rawEmail && !rawIp) {
|
|
@@ -295,14 +303,14 @@ async function banUser(ctx: RequestContext, body: any) {
|
|
|
295
303
|
|
|
296
304
|
return { ok: true, person_id: person?.id || null, email, ip, layers }
|
|
297
305
|
}
|
|
298
|
-
// CIL[END stable_id="~/.framework/functions/security.ts:
|
|
306
|
+
// CIL[END stable_id="~/.framework/functions/security.ts:banUser:function"]
|
|
299
307
|
|
|
300
|
-
// CIL[BEGIN stable_id="~/.framework/functions/security.ts:unbanuser:function" type="function" identity="unbanUser" parent="~/.framework/functions/security.ts"]
|
|
301
308
|
// CIL[DOC-MACRO purpose="Reverse a ban: revoke blocklist rows, lift auth ban, remove edge IP"]
|
|
302
309
|
// 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
310
|
// 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
311
|
// 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
312
|
// CIL[DOC-STABILITY stability="experimental"]
|
|
313
|
+
// CIL[BEGIN stable_id="~/.framework/functions/security.ts:unbanUser:function" type="function" identity="unbanUser" parent="~/.framework/functions/security.ts"]
|
|
306
314
|
async function unbanUser(ctx: RequestContext, body: any) {
|
|
307
315
|
const { person_id } = body || {}
|
|
308
316
|
if (!person_id) throw new Error('person_id is required')
|
|
@@ -353,13 +361,13 @@ async function unbanUser(ctx: RequestContext, body: any) {
|
|
|
353
361
|
|
|
354
362
|
return { ok: true, person_id: person.id, layers }
|
|
355
363
|
}
|
|
356
|
-
// CIL[END stable_id="~/.framework/functions/security.ts:
|
|
364
|
+
// CIL[END stable_id="~/.framework/functions/security.ts:unbanUser:function"]
|
|
357
365
|
|
|
358
|
-
// CIL[BEGIN stable_id="~/.framework/functions/security.ts:blocklistcrud:comment" type="comment" identity="blocklistCrud" parent="~/.framework/functions/security.ts"]
|
|
359
366
|
// CIL[DOC-MACRO purpose="Blocklist list/add/revoke for the admin UI"]
|
|
360
367
|
// CIL[DOC-RESPONSIBILITY responsibility="Expose the security_blocklist table to system admins; ip entries sync to the Netlify edge on add/revoke"]
|
|
361
368
|
// 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
369
|
// CIL[DOC-STABILITY stability="experimental"]
|
|
370
|
+
// CIL[BEGIN stable_id="~/.framework/functions/security.ts:listBlocklist:function" type="function" identity="listBlocklist" parent="~/.framework/functions/security.ts"]
|
|
363
371
|
async function listBlocklist() {
|
|
364
372
|
const { data, error } = await adminDb
|
|
365
373
|
.from('security_blocklist')
|
|
@@ -369,7 +377,9 @@ async function listBlocklist() {
|
|
|
369
377
|
if (error) throw new Error('blocklist read failed: ' + error.message)
|
|
370
378
|
return { rows: data || [] }
|
|
371
379
|
}
|
|
380
|
+
// CIL[END stable_id="~/.framework/functions/security.ts:listBlocklist:function"]
|
|
372
381
|
|
|
382
|
+
// CIL[BEGIN stable_id="~/.framework/functions/security.ts:blocklistAdd:function" type="function" identity="blocklistAdd" parent="~/.framework/functions/security.ts"]
|
|
373
383
|
async function blocklistAdd(ctx: RequestContext, body: any) {
|
|
374
384
|
const { type, value, severity = 'hard', reason, expires_at } = body || {}
|
|
375
385
|
if (!type || !value) throw new Error('type and value are required')
|
|
@@ -395,7 +405,9 @@ async function blocklistAdd(ctx: RequestContext, body: any) {
|
|
|
395
405
|
})
|
|
396
406
|
return { ok: true }
|
|
397
407
|
}
|
|
408
|
+
// CIL[END stable_id="~/.framework/functions/security.ts:blocklistAdd:function"]
|
|
398
409
|
|
|
410
|
+
// CIL[BEGIN stable_id="~/.framework/functions/security.ts:blocklistRevoke:function" type="function" identity="blocklistRevoke" parent="~/.framework/functions/security.ts"]
|
|
399
411
|
async function blocklistRevoke(ctx: RequestContext, body: any) {
|
|
400
412
|
const { id, type, value } = body || {}
|
|
401
413
|
if (!id && !(type && value)) throw new Error('id or (type, value) is required')
|
|
@@ -417,17 +429,18 @@ async function blocklistRevoke(ctx: RequestContext, body: any) {
|
|
|
417
429
|
})
|
|
418
430
|
return { ok: true, revoked: revoked?.length || 0 }
|
|
419
431
|
}
|
|
420
|
-
// CIL[END stable_id="~/.framework/functions/security.ts:
|
|
432
|
+
// CIL[END stable_id="~/.framework/functions/security.ts:blocklistRevoke:function"]
|
|
421
433
|
|
|
422
|
-
// CIL[BEGIN stable_id="~/.framework/functions/security.ts:handler:constant" type="constant" identity="handler" parent="~/.framework/functions/security.ts"]
|
|
423
434
|
// CIL[DOC-MACRO purpose="Action dispatcher — system-admin only"]
|
|
424
435
|
// CIL[DOC-RESPONSIBILITY responsibility="Route ?action= to the security operations; every action is gated by requireSystemContextWithAudit"]
|
|
425
436
|
// CIL[DOC-CONTRACT inputs="ctx, body" outputs="action result JSON" errors="401/403 unauthenticated/non-admin; 400 unknown action" side_effects="delegated"]
|
|
426
437
|
// CIL[DOC-MICRO logic_summary="Gate once, then switch on ctx.query.action || body.action" notable_decisions="[]"]
|
|
427
438
|
// CIL[DOC-STABILITY stability="experimental"]
|
|
439
|
+
// CIL[BEGIN stable_id="~/.framework/functions/security.ts:handler:constant" type="constant" identity="handler" parent="~/.framework/functions/security.ts"]
|
|
428
440
|
export const handler = createHandler(async (ctx, body) => {
|
|
429
|
-
|
|
430
|
-
|
|
441
|
+
// Throws a statusCode-carrying error on denial; createHandler maps it to a
|
|
442
|
+
// real HTTP 401/403 (F-051).
|
|
443
|
+
requireSystemContextWithAudit(ctx, 'security_admin')
|
|
431
444
|
|
|
432
445
|
const action = (ctx.query?.action as string) || (body?.action as string) || ''
|
|
433
446
|
switch (action) {
|