spine-framework 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.framework/README.md +129 -0
- package/.framework/cli/bin.cjs +14 -0
- package/.framework/cli/commands/agents.ts +153 -0
- package/.framework/cli/commands/auth.ts +94 -0
- package/.framework/cli/commands/create-app.ts +185 -0
- package/.framework/cli/commands/dev.ts +295 -0
- package/.framework/cli/commands/doctor.ts +442 -0
- package/.framework/cli/commands/generate.ts +332 -0
- package/.framework/cli/commands/init.ts +272 -0
- package/.framework/cli/commands/install-app.ts +391 -0
- package/.framework/cli/commands/items.ts +253 -0
- package/.framework/cli/commands/migrations.ts +141 -0
- package/.framework/cli/commands/pipelines.ts +166 -0
- package/.framework/cli/commands/status.ts +197 -0
- package/.framework/cli/commands/system.ts +184 -0
- package/.framework/cli/commands/test.ts +227 -0
- package/.framework/cli/commands/uninstall-app.ts +166 -0
- package/.framework/cli/context.ts +268 -0
- package/.framework/cli/env-loader.ts +36 -0
- package/.framework/cli/index.ts +106 -0
- package/.framework/cli/welcome.cjs +45 -0
- package/.framework/docs/API.md +384 -0
- package/.framework/docs/STABILITY.md +52 -0
- package/.framework/docs/admin-routes.md +76 -0
- package/.framework/docs/api-docs-progress.md +38 -0
- package/.framework/docs/api-governance.md +146 -0
- package/.framework/docs/api-testing-results.md +212 -0
- package/.framework/docs/apis/admin-configs.md +567 -0
- package/.framework/docs/apis/admin-data.md +272 -0
- package/.framework/docs/apis/index.md +231 -0
- package/.framework/docs/apis/internal.md +295 -0
- package/.framework/docs/apis/runtime.md +537 -0
- package/.framework/docs/assembly-launch-guide.md +138 -0
- package/.framework/docs/audit-results.md +590 -0
- package/.framework/docs/authorization-model.md +170 -0
- package/.framework/docs/db-api-inventory.md +95 -0
- package/.framework/docs/examples/custom-app/README.md +77 -0
- package/.framework/docs/examples/custom-function/README.md +27 -0
- package/.framework/docs/examples/custom-function/handler.ts +48 -0
- package/.framework/docs/examples/custom-webhook/README.md +68 -0
- package/.framework/docs/gap-remediation-backlog.md +103 -0
- package/.framework/docs/guides/cli-guide.md +224 -0
- package/.framework/docs/guides/getting-started.md +103 -0
- package/.framework/docs/guides/import-guide.md +193 -0
- package/.framework/docs/guides/testing-guide.md +229 -0
- package/.framework/docs/permission-examples.md +326 -0
- package/.framework/docs/ui-adoption-verification.md +111 -0
- package/.framework/docs/ui-api-coverage.md +84 -0
- package/.framework/docs/v2-compatibility-audit.md +228 -0
- package/.framework/functions/.gitkeep +1 -0
- package/.framework/functions/_shared/agent-runner.ts +1097 -0
- package/.framework/functions/_shared/app-manifest.ts +184 -0
- package/.framework/functions/_shared/audit.ts +150 -0
- package/.framework/functions/_shared/db.ts +174 -0
- package/.framework/functions/_shared/index.ts +382 -0
- package/.framework/functions/_shared/middleware.ts +490 -0
- package/.framework/functions/_shared/permissions.ts +1325 -0
- package/.framework/functions/_shared/pipeline-runner.ts +731 -0
- package/.framework/functions/_shared/principal.ts +760 -0
- package/.framework/functions/_shared/schema-utils.ts +967 -0
- package/.framework/functions/_shared/testing.ts +258 -0
- package/.framework/functions/_shared/trigger-engine.ts +425 -0
- package/.framework/functions/_shared/webhook-registration.ts +168 -0
- package/.framework/functions/_shared/webhook-registry.ts +129 -0
- package/.framework/functions/account-nodes.ts +111 -0
- package/.framework/functions/admin-data.ts +606 -0
- package/.framework/functions/ai-agents.ts +323 -0
- package/.framework/functions/api-keys.ts +376 -0
- package/.framework/functions/apps.ts +483 -0
- package/.framework/functions/auth.ts +196 -0
- package/.framework/functions/debug-auth.ts +107 -0
- package/.framework/functions/embeddings.ts +556 -0
- package/.framework/functions/integration-routes.ts +523 -0
- package/.framework/functions/integrations.ts +319 -0
- package/.framework/functions/item-progress.ts +272 -0
- package/.framework/functions/logs.ts +438 -0
- package/.framework/functions/observability.ts +275 -0
- package/.framework/functions/pipeline-executions.ts +494 -0
- package/.framework/functions/pipelines.ts +485 -0
- package/.framework/functions/prompt-configs.ts +339 -0
- package/.framework/functions/roles.ts +387 -0
- package/.framework/functions/system-cron.ts +742 -0
- package/.framework/functions/system.ts +323 -0
- package/.framework/functions/tests.ts +119 -0
- package/.framework/functions/timers.ts +357 -0
- package/.framework/functions/triggers.ts +563 -0
- package/.framework/functions/types.ts +604 -0
- package/.framework/migrations/000_foundation.sql +1256 -0
- package/.framework/migrations/001_seed.sql +92 -0
- package/.framework/migrations/002_seed_constraints.sql +13 -0
- package/.framework/migrations/003_auth_user_trigger.sql +59 -0
- package/.framework/src/App.tsx +126 -0
- package/.framework/src/apps/admin/index.tsx +173 -0
- package/.framework/src/components/AppWrapper.tsx +56 -0
- package/.framework/src/components/CustomAppLoader.tsx +116 -0
- package/.framework/src/components/admin/AdminListPage.tsx +151 -0
- package/.framework/src/components/admin/AdminSidebar.tsx +166 -0
- package/.framework/src/components/admin/AdminStatsCard.tsx +62 -0
- package/.framework/src/components/admin/SortableTableHeader.tsx +42 -0
- package/.framework/src/components/app-shell/GenericAppShell.tsx +181 -0
- package/.framework/src/components/app-shell/GenericDetailPage.tsx +200 -0
- package/.framework/src/components/app-shell/GenericListPage.tsx +116 -0
- package/.framework/src/components/app-sidebar.tsx +228 -0
- package/.framework/src/components/auth/ProtectedRoute.tsx +88 -0
- package/.framework/src/components/layout/AppShell.tsx +91 -0
- package/.framework/src/components/layout/Header.tsx +88 -0
- package/.framework/src/components/layout/Layout.tsx +95 -0
- package/.framework/src/components/layout/Sidebar.tsx +329 -0
- package/.framework/src/components/runtime/DataDetailHeader.tsx +77 -0
- package/.framework/src/components/runtime/DataDetailPage.tsx +171 -0
- package/.framework/src/components/runtime/DataFilters.tsx +91 -0
- package/.framework/src/components/runtime/DataHeader.tsx +68 -0
- package/.framework/src/components/runtime/DataListPage.tsx +124 -0
- package/.framework/src/components/runtime/DataStats.tsx +70 -0
- package/.framework/src/components/runtime/DataTable.tsx +174 -0
- package/.framework/src/components/runtime/SchemaDetailForm.tsx +134 -0
- package/.framework/src/components/runtime/index.ts +18 -0
- package/.framework/src/components/search-form.tsx +29 -0
- package/.framework/src/components/shared/AgentView.tsx +213 -0
- package/.framework/src/components/shared/FieldRenderer.tsx +478 -0
- package/.framework/src/components/shared/SchemaFields.tsx +226 -0
- package/.framework/src/components/ui/DataTable.tsx +343 -0
- package/.framework/src/components/ui/Form.tsx +281 -0
- package/.framework/src/components/ui/ItemCard.tsx +296 -0
- package/.framework/src/components/ui/ItemListView.tsx +308 -0
- package/.framework/src/components/ui/LoadingSpinner.tsx +52 -0
- package/.framework/src/components/ui/Modal.tsx +61 -0
- package/.framework/src/components/ui/RichTextEditor.tsx +210 -0
- package/.framework/src/components/ui/accordion.tsx +82 -0
- package/.framework/src/components/ui/alert-dialog.tsx +197 -0
- package/.framework/src/components/ui/alert.tsx +76 -0
- package/.framework/src/components/ui/aspect-ratio.tsx +11 -0
- package/.framework/src/components/ui/avatar.tsx +110 -0
- package/.framework/src/components/ui/badge.tsx +49 -0
- package/.framework/src/components/ui/breadcrumb.tsx +122 -0
- package/.framework/src/components/ui/button-group.tsx +83 -0
- package/.framework/src/components/ui/button.tsx +65 -0
- package/.framework/src/components/ui/calendar.tsx +222 -0
- package/.framework/src/components/ui/card.tsx +100 -0
- package/.framework/src/components/ui/carousel.tsx +240 -0
- package/.framework/src/components/ui/chart.tsx +373 -0
- package/.framework/src/components/ui/checkbox.tsx +31 -0
- package/.framework/src/components/ui/collapsible.tsx +33 -0
- package/.framework/src/components/ui/combobox.tsx +299 -0
- package/.framework/src/components/ui/command.tsx +193 -0
- package/.framework/src/components/ui/context-menu.tsx +261 -0
- package/.framework/src/components/ui/dialog.tsx +165 -0
- package/.framework/src/components/ui/direction.tsx +22 -0
- package/.framework/src/components/ui/drawer.tsx +132 -0
- package/.framework/src/components/ui/dropdown-menu.tsx +269 -0
- package/.framework/src/components/ui/empty.tsx +104 -0
- package/.framework/src/components/ui/field.tsx +238 -0
- package/.framework/src/components/ui/hover-card.tsx +42 -0
- package/.framework/src/components/ui/input-group.tsx +153 -0
- package/.framework/src/components/ui/input-otp.tsx +87 -0
- package/.framework/src/components/ui/input.tsx +19 -0
- package/.framework/src/components/ui/item.tsx +196 -0
- package/.framework/src/components/ui/kbd.tsx +26 -0
- package/.framework/src/components/ui/label.tsx +22 -0
- package/.framework/src/components/ui/menubar.tsx +277 -0
- package/.framework/src/components/ui/native-select.tsx +61 -0
- package/.framework/src/components/ui/navigation-menu.tsx +164 -0
- package/.framework/src/components/ui/pagination.tsx +129 -0
- package/.framework/src/components/ui/popover.tsx +87 -0
- package/.framework/src/components/ui/progress.tsx +31 -0
- package/.framework/src/components/ui/radio-group.tsx +42 -0
- package/.framework/src/components/ui/resizable.tsx +50 -0
- package/.framework/src/components/ui/scroll-area.tsx +53 -0
- package/.framework/src/components/ui/select.tsx +195 -0
- package/.framework/src/components/ui/separator.tsx +26 -0
- package/.framework/src/components/ui/sheet.tsx +145 -0
- package/.framework/src/components/ui/sidebar.tsx +706 -0
- package/.framework/src/components/ui/skeleton.tsx +13 -0
- package/.framework/src/components/ui/slider.tsx +59 -0
- package/.framework/src/components/ui/sonner.tsx +47 -0
- package/.framework/src/components/ui/spinner.tsx +10 -0
- package/.framework/src/components/ui/switch.tsx +33 -0
- package/.framework/src/components/ui/table-primitives.tsx +141 -0
- package/.framework/src/components/ui/table.tsx +114 -0
- package/.framework/src/components/ui/tabs.tsx +90 -0
- package/.framework/src/components/ui/textarea.tsx +18 -0
- package/.framework/src/components/ui/toggle-group.tsx +89 -0
- package/.framework/src/components/ui/toggle.tsx +45 -0
- package/.framework/src/components/ui/tooltip.tsx +57 -0
- package/.framework/src/contexts/AppContext.tsx +133 -0
- package/.framework/src/contexts/AuthContext.tsx +371 -0
- package/.framework/src/hooks/use-mobile.ts +19 -0
- package/.framework/src/hooks/useApi.ts +526 -0
- package/.framework/src/hooks/useApps.ts +114 -0
- package/.framework/src/hooks/useEntityList.ts +190 -0
- package/.framework/src/hooks/useEntityRecord.ts +308 -0
- package/.framework/src/hooks/useForm.ts +307 -0
- package/.framework/src/hooks/useListSchema.ts +264 -0
- package/.framework/src/hooks/useSchemaRecord.ts +223 -0
- package/.framework/src/index.css +128 -0
- package/.framework/src/lib/api.ts +156 -0
- package/.framework/src/lib/supabase.ts +94 -0
- package/.framework/src/lib/utils.ts +317 -0
- package/.framework/src/main.tsx +27 -0
- package/.framework/src/pages/DashboardPage.tsx +181 -0
- package/.framework/src/pages/NotFoundPage.tsx +39 -0
- package/.framework/src/pages/admin/AIAgentDetailPage.tsx +161 -0
- package/.framework/src/pages/admin/AIAgentsPage.tsx +318 -0
- package/.framework/src/pages/admin/APIKeyDetailPage.tsx +199 -0
- package/.framework/src/pages/admin/APIKeysPage.tsx +303 -0
- package/.framework/src/pages/admin/AlertsConfigPage.tsx +523 -0
- package/.framework/src/pages/admin/AppDetailPage.tsx +493 -0
- package/.framework/src/pages/admin/AppsPage.tsx +355 -0
- package/.framework/src/pages/admin/DesignedPage.tsx +491 -0
- package/.framework/src/pages/admin/EmbeddingDetailPage.tsx +534 -0
- package/.framework/src/pages/admin/EmbeddingsPage.tsx +424 -0
- package/.framework/src/pages/admin/ExtendedShadcnTestPage.tsx +176 -0
- package/.framework/src/pages/admin/IncrementalShadcnTestPage.tsx +109 -0
- package/.framework/src/pages/admin/IntegratedDashboard.tsx +402 -0
- package/.framework/src/pages/admin/IntegrationDetailPage.tsx +187 -0
- package/.framework/src/pages/admin/IntegrationsPage.tsx +301 -0
- package/.framework/src/pages/admin/LogsPage.tsx +283 -0
- package/.framework/src/pages/admin/MinimalShadcnTestPage.tsx +85 -0
- package/.framework/src/pages/admin/ObservabilityDashboard.tsx +470 -0
- package/.framework/src/pages/admin/PipelineDetailPage.tsx +183 -0
- package/.framework/src/pages/admin/PipelineExecutionsPage.tsx +279 -0
- package/.framework/src/pages/admin/PipelinesPage.tsx +390 -0
- package/.framework/src/pages/admin/PromptConfigDetailPage.tsx +299 -0
- package/.framework/src/pages/admin/PromptConfigsPage.tsx +292 -0
- package/.framework/src/pages/admin/ProperlyDesignedPage.tsx +434 -0
- package/.framework/src/pages/admin/RoleDetailPage.tsx +273 -0
- package/.framework/src/pages/admin/RolesPage.tsx +292 -0
- package/.framework/src/pages/admin/SelectTestPage.tsx +61 -0
- package/.framework/src/pages/admin/ShadcnTestPage.tsx +588 -0
- package/.framework/src/pages/admin/SimpleDashboard.tsx +387 -0
- package/.framework/src/pages/admin/TestRunDetailPage.tsx +172 -0
- package/.framework/src/pages/admin/TestingDashboard.tsx +257 -0
- package/.framework/src/pages/admin/TimerDetailPage.tsx +151 -0
- package/.framework/src/pages/admin/TimersPage.tsx +376 -0
- package/.framework/src/pages/admin/TriggerDetailPage.tsx +149 -0
- package/.framework/src/pages/admin/TriggersPage.tsx +381 -0
- package/.framework/src/pages/admin/TypeDetailPage.tsx +694 -0
- package/.framework/src/pages/admin/TypesPage.tsx +295 -0
- package/.framework/src/pages/auth/LoginPage.tsx +188 -0
- package/.framework/src/pages/auth/RegisterPage.tsx +163 -0
- package/.framework/src/pages/spine-framework/APIPage.tsx +17 -0
- package/.framework/src/pages/spine-framework/CLIPage.tsx +25 -0
- package/.framework/src/types/auth.ts +125 -0
- package/.framework/src/types/types.ts +407 -0
- package/STRUCTURE.md +150 -0
- package/config/components.json +25 -0
- package/config/deno.lock +108 -0
- package/config/package-lock.json +17183 -0
- package/config/postcss.config.cjs +10 -0
- package/config/tailwind.config.cjs +78 -0
- package/config/tsconfig.build.json +32 -0
- package/config/tsconfig.cli.json +18 -0
- package/config/tsconfig.json +41 -0
- package/config/tsconfig.node.json +17 -0
- package/config/tsconfig.node.tsbuildinfo +1 -0
- package/config/tsconfig.tsbuildinfo +1 -0
- package/config/typedoc.json +16 -0
- package/config/vite.config.d.ts +2 -0
- package/config/vite.config.ts +72 -0
- package/dist/cli/commands/agents.d.ts +39 -0
- package/dist/cli/commands/agents.d.ts.map +1 -0
- package/dist/cli/commands/auth.d.ts +36 -0
- package/dist/cli/commands/auth.d.ts.map +1 -0
- package/dist/cli/commands/create-app.d.ts +23 -0
- package/dist/cli/commands/create-app.d.ts.map +1 -0
- package/dist/cli/commands/dev.d.ts +39 -0
- package/dist/cli/commands/dev.d.ts.map +1 -0
- package/dist/cli/commands/doctor.d.ts +42 -0
- package/dist/cli/commands/doctor.d.ts.map +1 -0
- package/dist/cli/commands/generate.d.ts +36 -0
- package/dist/cli/commands/generate.d.ts.map +1 -0
- package/dist/cli/commands/init.d.ts +30 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/install-app.d.ts +30 -0
- package/dist/cli/commands/install-app.d.ts.map +1 -0
- package/dist/cli/commands/items.d.ts +45 -0
- package/dist/cli/commands/items.d.ts.map +1 -0
- package/dist/cli/commands/migrations.d.ts +41 -0
- package/dist/cli/commands/migrations.d.ts.map +1 -0
- package/dist/cli/commands/pipelines.d.ts +40 -0
- package/dist/cli/commands/pipelines.d.ts.map +1 -0
- package/dist/cli/commands/status.d.ts +23 -0
- package/dist/cli/commands/status.d.ts.map +1 -0
- package/dist/cli/commands/system.d.ts +29 -0
- package/dist/cli/commands/system.d.ts.map +1 -0
- package/dist/cli/commands/test.d.ts +46 -0
- package/dist/cli/commands/test.d.ts.map +1 -0
- package/dist/cli/commands/uninstall-app.d.ts +23 -0
- package/dist/cli/commands/uninstall-app.d.ts.map +1 -0
- package/dist/cli/context.d.ts +88 -0
- package/dist/cli/context.d.ts.map +1 -0
- package/dist/cli/env-loader.d.ts +14 -0
- package/dist/cli/env-loader.d.ts.map +1 -0
- package/dist/cli/index.d.ts +41 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/functions/_shared/agent-runner.d.ts +156 -0
- package/dist/functions/_shared/agent-runner.d.ts.map +1 -0
- package/dist/functions/_shared/app-manifest.d.ts +68 -0
- package/dist/functions/_shared/app-manifest.d.ts.map +1 -0
- package/dist/functions/_shared/audit.d.ts +91 -0
- package/dist/functions/_shared/audit.d.ts.map +1 -0
- package/dist/functions/_shared/db.d.ts +125 -0
- package/dist/functions/_shared/db.d.ts.map +1 -0
- package/dist/functions/_shared/index.d.ts +298 -0
- package/dist/functions/_shared/index.d.ts.map +1 -0
- package/dist/functions/_shared/middleware.d.ts +315 -0
- package/dist/functions/_shared/middleware.d.ts.map +1 -0
- package/dist/functions/_shared/permissions.d.ts +626 -0
- package/dist/functions/_shared/permissions.d.ts.map +1 -0
- package/dist/functions/_shared/pipeline-runner.d.ts +124 -0
- package/dist/functions/_shared/pipeline-runner.d.ts.map +1 -0
- package/dist/functions/_shared/principal.d.ts +284 -0
- package/dist/functions/_shared/principal.d.ts.map +1 -0
- package/dist/functions/_shared/schema-utils.d.ts +181 -0
- package/dist/functions/_shared/schema-utils.d.ts.map +1 -0
- package/dist/functions/_shared/testing.d.ts +172 -0
- package/dist/functions/_shared/testing.d.ts.map +1 -0
- package/dist/functions/_shared/trigger-engine.d.ts +140 -0
- package/dist/functions/_shared/trigger-engine.d.ts.map +1 -0
- package/dist/functions/_shared/webhook-registration.d.ts +81 -0
- package/dist/functions/_shared/webhook-registration.d.ts.map +1 -0
- package/dist/functions/_shared/webhook-registry.d.ts +57 -0
- package/dist/functions/_shared/webhook-registry.d.ts.map +1 -0
- package/dist/functions/account-nodes.d.ts +48 -0
- package/dist/functions/account-nodes.d.ts.map +1 -0
- package/dist/functions/admin-data.d.ts +178 -0
- package/dist/functions/admin-data.d.ts.map +1 -0
- package/dist/functions/ai-agents.d.ts +125 -0
- package/dist/functions/ai-agents.d.ts.map +1 -0
- package/dist/functions/api-keys.d.ts +140 -0
- package/dist/functions/api-keys.d.ts.map +1 -0
- package/dist/functions/apps.d.ts +163 -0
- package/dist/functions/apps.d.ts.map +1 -0
- package/dist/functions/auth.d.ts +74 -0
- package/dist/functions/auth.d.ts.map +1 -0
- package/dist/functions/debug-auth.d.ts +33 -0
- package/dist/functions/debug-auth.d.ts.map +1 -0
- package/dist/functions/embeddings.d.ts +205 -0
- package/dist/functions/embeddings.d.ts.map +1 -0
- package/dist/functions/integration-routes.d.ts +45 -0
- package/dist/functions/integration-routes.d.ts.map +1 -0
- package/dist/functions/integrations.d.ts +124 -0
- package/dist/functions/integrations.d.ts.map +1 -0
- package/dist/functions/item-progress.d.ts +41 -0
- package/dist/functions/item-progress.d.ts.map +1 -0
- package/dist/functions/logs.d.ts +162 -0
- package/dist/functions/logs.d.ts.map +1 -0
- package/dist/functions/observability.d.ts +123 -0
- package/dist/functions/observability.d.ts.map +1 -0
- package/dist/functions/pipeline-executions.d.ts +190 -0
- package/dist/functions/pipeline-executions.d.ts.map +1 -0
- package/dist/functions/pipelines.d.ts +171 -0
- package/dist/functions/pipelines.d.ts.map +1 -0
- package/dist/functions/prompt-configs.d.ts +125 -0
- package/dist/functions/prompt-configs.d.ts.map +1 -0
- package/dist/functions/roles.d.ts +118 -0
- package/dist/functions/roles.d.ts.map +1 -0
- package/dist/functions/system-cron.d.ts +65 -0
- package/dist/functions/system-cron.d.ts.map +1 -0
- package/dist/functions/system.d.ts +29 -0
- package/dist/functions/system.d.ts.map +1 -0
- package/dist/functions/tests.d.ts +28 -0
- package/dist/functions/tests.d.ts.map +1 -0
- package/dist/functions/timers.d.ts +139 -0
- package/dist/functions/timers.d.ts.map +1 -0
- package/dist/functions/triggers.d.ts +203 -0
- package/dist/functions/triggers.d.ts.map +1 -0
- package/dist/functions/types.d.ts +151 -0
- package/dist/functions/types.d.ts.map +1 -0
- package/dist/src/types/types.d.ts +364 -0
- package/dist/src/types/types.d.ts.map +1 -0
- package/package.json +192 -0
- package/scripts/app-install-cli.ts +286 -0
- package/scripts/assemble-frontend.sh +79 -0
- package/scripts/assemble-functions.sh +62 -0
- package/scripts/assemble.sh +35 -0
- package/scripts/boundary-check.sh +106 -0
- package/scripts/build-manifest.sh +80 -0
- package/scripts/check-core-integrity.sh +82 -0
- package/scripts/ingest-chunks.cjs +202 -0
- package/scripts/kb-chunk-parser.cjs +312 -0
- package/scripts/kb-chunk-parser.ts +330 -0
- package/scripts/load-test-app-install.ts +484 -0
- package/scripts/netlify-dev-wrapper.sh +22 -0
- package/scripts/verify-integrity.sh +69 -0
|
@@ -0,0 +1,742 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module system-cron
|
|
3
|
+
* @audience core-contributor
|
|
4
|
+
* @layer api-handler
|
|
5
|
+
* @stability stable
|
|
6
|
+
*
|
|
7
|
+
* Scheduled job runner invoked by an external cron service (e.g., AWS
|
|
8
|
+
* EventBridge, Google Cloud Scheduler). On each tick it:
|
|
9
|
+
*
|
|
10
|
+
* 1. Authenticates the request via `SCHEDULER_API_KEY` or machine principal
|
|
11
|
+
* 2. Fetches all schedules due via `get_due_schedules` RPC
|
|
12
|
+
* 3. For each schedule: validates creator, loads action + machine principal,
|
|
13
|
+
* checks scope, executes action, records outcome
|
|
14
|
+
* 4. Fetches all timers due via `get_due_timers` RPC and runs each pipeline
|
|
15
|
+
* 5. Evaluates threshold alerts via `evaluateThresholds`
|
|
16
|
+
* 6. Runs daily log cleanup at 00:00 UTC via `cleanupOldLogs`
|
|
17
|
+
*
|
|
18
|
+
* **Routed by:** `POST /.netlify/functions/system-cron`
|
|
19
|
+
*
|
|
20
|
+
* **Authentication:** Request must supply `SCHEDULER_API_KEY` via `?api_key`
|
|
21
|
+
* or `?scheduler_key`, OR originate from an internal machine principal
|
|
22
|
+
* (type='machine', machineType='internal'). Unauthorized requests receive 403.
|
|
23
|
+
*
|
|
24
|
+
* **Response shape:**
|
|
25
|
+
* ```ts
|
|
26
|
+
* {
|
|
27
|
+
* executed: number
|
|
28
|
+
* success: number
|
|
29
|
+
* failed: number
|
|
30
|
+
* skipped: number
|
|
31
|
+
* thresholds_evaluated: number
|
|
32
|
+
* thresholds_breached: number
|
|
33
|
+
* logs_cleaned: number
|
|
34
|
+
* results: Array<{ scheduleId, actionId, status, error?, durationMs }>
|
|
35
|
+
* }
|
|
36
|
+
* ```
|
|
37
|
+
*
|
|
38
|
+
* INVARIANT: Uses `adminDb` (service-role client, bypasses RLS) because
|
|
39
|
+
* machine principals must access cross-account data.
|
|
40
|
+
* INVARIANT: Log cleanup only runs when the current UTC time is 00:00.
|
|
41
|
+
*
|
|
42
|
+
* @seeAlso timers.ts (timer configuration CRUD)
|
|
43
|
+
* @seeAlso pipelines.ts (pipeline configuration CRUD)
|
|
44
|
+
* @seeAlso pipeline-runner.ts (runPipeline — timer/schedule execution)
|
|
45
|
+
* @seeAlso observability.ts (analytics RPCs used by evaluateThresholds)
|
|
46
|
+
* @seeAlso audit.ts (emitAudit for cron lifecycle events)
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
import { createHandler, RequestContext } from './_shared/middleware'
|
|
50
|
+
import { adminDb } from './_shared/db'
|
|
51
|
+
import { emitAudit } from './_shared/audit'
|
|
52
|
+
import { runPipeline } from './_shared/pipeline-runner'
|
|
53
|
+
|
|
54
|
+
const SCHEDULER_API_KEY: string | undefined = (globalThis as any).process?.env?.SCHEDULER_API_KEY || (globalThis as any).Deno?.env?.get?.('SCHEDULER_API_KEY')
|
|
55
|
+
|
|
56
|
+
// ─── MAIN HANDLER ────────────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Netlify function entry point — the entire cron tick logic lives here.
|
|
60
|
+
* Returns HTTP 200 with a results summary on success, or HTTP 403/500
|
|
61
|
+
* on auth/runtime failure.
|
|
62
|
+
*
|
|
63
|
+
* @throws Returns 403 JSON if scheduler key is missing or invalid
|
|
64
|
+
* @throws Returns 500 JSON on unhandled top-level error
|
|
65
|
+
* @sideEffects DB read: get_due_schedules, validate_schedule_creator,
|
|
66
|
+
* actions, api_keys, get_due_timers
|
|
67
|
+
* @sideEffects DB write: schedule_executions (INSERT), update_schedule_after_run
|
|
68
|
+
* RPC, update_timer_after_run RPC, cleanup_old_logs RPC
|
|
69
|
+
* @sideEffects pipeline: runPipeline (timers + threshold responses)
|
|
70
|
+
* @sideEffects audit: emitAudit for unauthorized, error, schedule.execute,
|
|
71
|
+
* threshold.breached events
|
|
72
|
+
* @calledBy External cron scheduler (AWS EventBridge / Google Cloud Scheduler)
|
|
73
|
+
*/
|
|
74
|
+
export const handler = createHandler(async (ctx: RequestContext) => {
|
|
75
|
+
// ============================================
|
|
76
|
+
// SECURITY: Validate this is an internal request
|
|
77
|
+
// ============================================
|
|
78
|
+
|
|
79
|
+
// Check if request has scheduler authentication
|
|
80
|
+
const requestApiKey = ctx.query?.api_key || ctx.query?.scheduler_key
|
|
81
|
+
|
|
82
|
+
if (requestApiKey !== SCHEDULER_API_KEY) {
|
|
83
|
+
// Also allow if the principal is a system machine (for internal invocations)
|
|
84
|
+
if (ctx.principal?.type !== 'machine' || ctx.principal?.machineType !== 'internal') {
|
|
85
|
+
await emitAudit(ctx, 'system_cron.unauthorized_access', {
|
|
86
|
+
type: 'system',
|
|
87
|
+
account_id: ctx.accountId || undefined
|
|
88
|
+
}, { result: 'denied', error: 'Invalid or missing scheduler authentication' })
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
statusCode: 403,
|
|
92
|
+
body: JSON.stringify({ error: 'Forbidden - Invalid scheduler authentication' })
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ============================================
|
|
98
|
+
// Find and execute due schedules
|
|
99
|
+
// ============================================
|
|
100
|
+
|
|
101
|
+
const results: Array<{
|
|
102
|
+
scheduleId: string
|
|
103
|
+
actionId: string
|
|
104
|
+
status: 'success' | 'failed' | 'skipped'
|
|
105
|
+
error?: string
|
|
106
|
+
durationMs: number
|
|
107
|
+
}> = []
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
// Get all schedules due for execution
|
|
111
|
+
const { data: dueSchedules, error: schedulesError } = await adminDb.rpc('get_due_schedules', {
|
|
112
|
+
p_now: new Date().toISOString()
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
if (schedulesError) {
|
|
116
|
+
throw new Error(`Failed to fetch due schedules: ${schedulesError.message}`)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!dueSchedules || dueSchedules.length === 0) {
|
|
120
|
+
return {
|
|
121
|
+
statusCode: 200,
|
|
122
|
+
body: JSON.stringify({
|
|
123
|
+
message: 'No schedules due for execution',
|
|
124
|
+
executed: 0,
|
|
125
|
+
results: []
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Execute each due schedule
|
|
131
|
+
for (const schedule of dueSchedules) {
|
|
132
|
+
const startTime = Date.now()
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
// Validate schedule can run (creator still active)
|
|
136
|
+
const { data: validation, error: validationError } = await adminDb.rpc('validate_schedule_creator', {
|
|
137
|
+
p_schedule_id: schedule.schedule_id
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
if (validationError || !validation?.is_valid) {
|
|
141
|
+
// Schedule was auto-paused by validation function
|
|
142
|
+
results.push({
|
|
143
|
+
scheduleId: schedule.schedule_id,
|
|
144
|
+
actionId: schedule.action_id,
|
|
145
|
+
status: 'skipped',
|
|
146
|
+
error: validation?.error_message || 'Schedule validation failed',
|
|
147
|
+
durationMs: Date.now() - startTime
|
|
148
|
+
})
|
|
149
|
+
continue
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Load the action
|
|
153
|
+
const { data: action, error: actionError } = await adminDb
|
|
154
|
+
.from('actions')
|
|
155
|
+
.select('*')
|
|
156
|
+
.eq('id', schedule.action_id)
|
|
157
|
+
.single()
|
|
158
|
+
|
|
159
|
+
if (actionError || !action) {
|
|
160
|
+
throw new Error(`Action not found: ${schedule.action_id}`)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Load the machine principal
|
|
164
|
+
const { data: machine, error: machineError } = await adminDb
|
|
165
|
+
.from('api_keys')
|
|
166
|
+
.select('*')
|
|
167
|
+
.eq('id', schedule.machine_principal_id)
|
|
168
|
+
.single()
|
|
169
|
+
|
|
170
|
+
if (machineError || !machine) {
|
|
171
|
+
throw new Error(`Machine principal not found: ${schedule.machine_principal_id}`)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Create execution context with machine principal
|
|
175
|
+
const executionCtx: RequestContext = {
|
|
176
|
+
requestId: ctx.requestId,
|
|
177
|
+
principal: {
|
|
178
|
+
id: machine.id,
|
|
179
|
+
type: 'machine',
|
|
180
|
+
accountId: machine.account_id,
|
|
181
|
+
scopes: schedule.delegated_scopes || machine.scopes || [],
|
|
182
|
+
machineType: machine.machine_type,
|
|
183
|
+
isInternal: machine.is_internal,
|
|
184
|
+
provenance: {
|
|
185
|
+
sourceType: 'cron',
|
|
186
|
+
createdBy: machine.created_by,
|
|
187
|
+
invokedAt: new Date().toISOString(),
|
|
188
|
+
cronId: schedule.schedule_id
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
db: adminDb, // Machines use adminDb (RLS checks their ID)
|
|
192
|
+
accountId: machine.account_id,
|
|
193
|
+
appId: null,
|
|
194
|
+
requestPath: '/.netlify/functions/system-cron',
|
|
195
|
+
query: {}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Check machine has required scope for this action
|
|
199
|
+
const requiredScope = action.required_scopes?.[0] || `${action.handler}:execute`
|
|
200
|
+
const hasScope = executionCtx.principal.scopes?.includes(requiredScope) ||
|
|
201
|
+
executionCtx.principal.scopes?.includes('*:*')
|
|
202
|
+
|
|
203
|
+
if (!hasScope) {
|
|
204
|
+
throw new Error(`Machine lacks required scope: ${requiredScope}`)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Execute the action
|
|
208
|
+
const executionResult = await executeAction(executionCtx, action, schedule.config)
|
|
209
|
+
|
|
210
|
+
// Record execution success
|
|
211
|
+
await adminDb.from('schedule_executions').insert({
|
|
212
|
+
schedule_id: schedule.schedule_id,
|
|
213
|
+
account_id: schedule.account_id,
|
|
214
|
+
machine_principal_id: machine.id,
|
|
215
|
+
status: 'success',
|
|
216
|
+
input_params: schedule.config,
|
|
217
|
+
output_result: executionResult,
|
|
218
|
+
duration_ms: Date.now() - startTime
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
// Update schedule state
|
|
222
|
+
await adminDb.rpc('update_schedule_after_run', {
|
|
223
|
+
p_schedule_id: schedule.schedule_id,
|
|
224
|
+
p_success: true,
|
|
225
|
+
p_error_message: null
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
// Emit audit log
|
|
229
|
+
await emitAudit(executionCtx, 'schedule.execute', {
|
|
230
|
+
type: 'schedule',
|
|
231
|
+
id: schedule.schedule_id,
|
|
232
|
+
account_id: schedule.account_id
|
|
233
|
+
}, {
|
|
234
|
+
action_id: action.id,
|
|
235
|
+
action_handler: action.handler,
|
|
236
|
+
result: 'success'
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
results.push({
|
|
240
|
+
scheduleId: schedule.schedule_id,
|
|
241
|
+
actionId: schedule.action_id,
|
|
242
|
+
status: 'success',
|
|
243
|
+
durationMs: Date.now() - startTime
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
} catch (execError: any) {
|
|
247
|
+
const errorMessage = execError.message || 'Execution failed'
|
|
248
|
+
|
|
249
|
+
// Record execution failure
|
|
250
|
+
await adminDb.from('schedule_executions').insert({
|
|
251
|
+
schedule_id: schedule.schedule_id,
|
|
252
|
+
account_id: schedule.account_id,
|
|
253
|
+
machine_principal_id: schedule.machine_principal_id,
|
|
254
|
+
status: 'failed',
|
|
255
|
+
input_params: schedule.config,
|
|
256
|
+
error_message: errorMessage,
|
|
257
|
+
duration_ms: Date.now() - startTime
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
// Update schedule state
|
|
261
|
+
await adminDb.rpc('update_schedule_after_run', {
|
|
262
|
+
p_schedule_id: schedule.schedule_id,
|
|
263
|
+
p_success: false,
|
|
264
|
+
p_error_message: errorMessage
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
results.push({
|
|
268
|
+
scheduleId: schedule.schedule_id,
|
|
269
|
+
actionId: schedule.action_id,
|
|
270
|
+
status: 'failed',
|
|
271
|
+
error: errorMessage,
|
|
272
|
+
durationMs: Date.now() - startTime
|
|
273
|
+
})
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ============================================
|
|
278
|
+
// Execute due timers (pipeline-based timers)
|
|
279
|
+
// ============================================
|
|
280
|
+
|
|
281
|
+
try {
|
|
282
|
+
const { data: dueTimers, error: timersError } = await adminDb.rpc('get_due_timers', {
|
|
283
|
+
p_now: new Date().toISOString()
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
if (!timersError && dueTimers && dueTimers.length > 0) {
|
|
287
|
+
for (const timer of dueTimers) {
|
|
288
|
+
const timerStartTime = Date.now()
|
|
289
|
+
|
|
290
|
+
try {
|
|
291
|
+
// Validate timer can run
|
|
292
|
+
if (!timer.pipeline_id) {
|
|
293
|
+
throw new Error(`Timer ${timer.timer_id} has no pipeline_id`)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Create execution context for timer
|
|
297
|
+
const timerCtx: RequestContext = {
|
|
298
|
+
requestId: ctx.requestId,
|
|
299
|
+
principal: {
|
|
300
|
+
id: 'timer:' + timer.timer_id,
|
|
301
|
+
type: 'machine' as const,
|
|
302
|
+
accountId: timer.account_id,
|
|
303
|
+
scopes: ['pipelines:execute'],
|
|
304
|
+
machineType: 'timer',
|
|
305
|
+
isInternal: true,
|
|
306
|
+
provenance: {
|
|
307
|
+
sourceType: 'timer',
|
|
308
|
+
createdBy: timer.created_by,
|
|
309
|
+
invokedAt: new Date().toISOString(),
|
|
310
|
+
timerId: timer.timer_id
|
|
311
|
+
}
|
|
312
|
+
},
|
|
313
|
+
db: adminDb,
|
|
314
|
+
accountId: timer.account_id,
|
|
315
|
+
appId: timer.app_id || null,
|
|
316
|
+
requestPath: '/.netlify/functions/system-cron',
|
|
317
|
+
query: {}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Run the pipeline
|
|
321
|
+
const result = await runPipeline(timer.pipeline_id, {
|
|
322
|
+
timer_id: timer.timer_id,
|
|
323
|
+
timer_name: timer.name,
|
|
324
|
+
execution_count: timer.execution_count || 0
|
|
325
|
+
}, timerCtx)
|
|
326
|
+
|
|
327
|
+
// Update timer state
|
|
328
|
+
await adminDb.rpc('update_timer_after_run', {
|
|
329
|
+
p_timer_id: timer.timer_id,
|
|
330
|
+
p_success: result.status === 'completed',
|
|
331
|
+
p_error_message: result.error || null
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
results.push({
|
|
335
|
+
scheduleId: timer.timer_id,
|
|
336
|
+
actionId: timer.pipeline_id,
|
|
337
|
+
status: result.status === 'completed' ? 'success' : 'failed',
|
|
338
|
+
error: result.error,
|
|
339
|
+
durationMs: Date.now() - timerStartTime
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
} catch (timerError: any) {
|
|
343
|
+
// Update timer with failure
|
|
344
|
+
await adminDb.rpc('update_timer_after_run', {
|
|
345
|
+
p_timer_id: timer.timer_id,
|
|
346
|
+
p_success: false,
|
|
347
|
+
p_error_message: timerError.message
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
results.push({
|
|
351
|
+
scheduleId: timer.timer_id,
|
|
352
|
+
actionId: timer.pipeline_id,
|
|
353
|
+
status: 'failed',
|
|
354
|
+
error: timerError.message,
|
|
355
|
+
durationMs: Date.now() - timerStartTime
|
|
356
|
+
})
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
} catch (timerLoopError) {
|
|
361
|
+
console.error('Timer execution error:', timerLoopError)
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// ============================================
|
|
365
|
+
// Evaluate threshold alerts (every minute)
|
|
366
|
+
// ============================================
|
|
367
|
+
let thresholdResults: any[] = []
|
|
368
|
+
try {
|
|
369
|
+
thresholdResults = await evaluateThresholds(ctx)
|
|
370
|
+
const breachedCount = thresholdResults.filter(r => r.breached).length
|
|
371
|
+
const firedCount = thresholdResults.filter(r => r.fired).length
|
|
372
|
+
|
|
373
|
+
if (breachedCount > 0) {
|
|
374
|
+
console.log(`Thresholds: ${breachedCount} breached, ${firedCount} pipelines fired`)
|
|
375
|
+
}
|
|
376
|
+
} catch (thresholdError) {
|
|
377
|
+
console.error('Threshold evaluation error:', thresholdError)
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// ============================================
|
|
381
|
+
// Daily log cleanup (check if it's time)
|
|
382
|
+
// ============================================
|
|
383
|
+
let logsCleaned = 0
|
|
384
|
+
const now = new Date()
|
|
385
|
+
const currentHour = now.getUTCHours()
|
|
386
|
+
const currentMinute = now.getUTCMinutes()
|
|
387
|
+
|
|
388
|
+
// Run cleanup once per day at 00:00 UTC
|
|
389
|
+
if (currentHour === 0 && currentMinute === 0) {
|
|
390
|
+
try {
|
|
391
|
+
logsCleaned = await cleanupOldLogs()
|
|
392
|
+
} catch (cleanupError) {
|
|
393
|
+
console.error('Log cleanup error:', cleanupError)
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Return summary
|
|
398
|
+
const successCount = results.filter(r => r.status === 'success').length
|
|
399
|
+
const failedCount = results.filter(r => r.status === 'failed').length
|
|
400
|
+
const skippedCount = results.filter(r => r.status === 'skipped').length
|
|
401
|
+
|
|
402
|
+
return {
|
|
403
|
+
statusCode: 200,
|
|
404
|
+
body: JSON.stringify({
|
|
405
|
+
message: `Executed ${results.length} total (schedules + timers)`,
|
|
406
|
+
executed: results.length,
|
|
407
|
+
success: successCount,
|
|
408
|
+
failed: failedCount,
|
|
409
|
+
skipped: skippedCount,
|
|
410
|
+
thresholds_evaluated: thresholdResults.length,
|
|
411
|
+
thresholds_breached: thresholdResults.filter(r => r.breached).length,
|
|
412
|
+
logs_cleaned: logsCleaned,
|
|
413
|
+
results
|
|
414
|
+
})
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
} catch (error: any) {
|
|
418
|
+
console.error('System cron error:', error)
|
|
419
|
+
|
|
420
|
+
await emitAudit(ctx, 'system_cron.error', {
|
|
421
|
+
type: 'system',
|
|
422
|
+
account_id: ctx.accountId || undefined
|
|
423
|
+
}, {
|
|
424
|
+
result: 'failure',
|
|
425
|
+
error: error.message
|
|
426
|
+
})
|
|
427
|
+
|
|
428
|
+
return {
|
|
429
|
+
statusCode: 500,
|
|
430
|
+
body: JSON.stringify({
|
|
431
|
+
error: 'System cron execution failed',
|
|
432
|
+
message: error.message
|
|
433
|
+
})
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
})
|
|
437
|
+
|
|
438
|
+
// ─── PRIVATE HELPERS ───────────────────────────────────────────────────────────
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Dispatches to the appropriate handler module (`functions` | `integrations`
|
|
442
|
+
* | `custom`) based on `action.handler_module`. Merges action-level config
|
|
443
|
+
* with schedule-specific config before invoking.
|
|
444
|
+
*
|
|
445
|
+
* @throws Error('Unknown handler module: <module>') on unrecognized module
|
|
446
|
+
* @throws Error('Custom handlers not yet implemented: <name>')
|
|
447
|
+
*/
|
|
448
|
+
async function executeAction(
|
|
449
|
+
ctx: RequestContext,
|
|
450
|
+
action: any,
|
|
451
|
+
config: any
|
|
452
|
+
): Promise<any> {
|
|
453
|
+
const handlerModule = action.handler_module || 'functions'
|
|
454
|
+
const handlerName = action.handler
|
|
455
|
+
|
|
456
|
+
// Merge action config with schedule-specific config
|
|
457
|
+
const mergedConfig = {
|
|
458
|
+
...action.config,
|
|
459
|
+
...config
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
switch (handlerModule) {
|
|
463
|
+
case 'functions':
|
|
464
|
+
return await executeFunctionHandler(ctx, handlerName, mergedConfig)
|
|
465
|
+
|
|
466
|
+
case 'integrations':
|
|
467
|
+
return await executeIntegrationHandler(ctx, handlerName, mergedConfig)
|
|
468
|
+
|
|
469
|
+
case 'custom':
|
|
470
|
+
// Custom handlers would be loaded from v2-custom
|
|
471
|
+
throw new Error(`Custom handlers not yet implemented: ${handlerName}`)
|
|
472
|
+
|
|
473
|
+
default:
|
|
474
|
+
throw new Error(`Unknown handler module: ${handlerModule}`)
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Routes to one of the built-in named handlers: `send_email`,
|
|
480
|
+
* `generate_report`, `notify_watchers`, `run_pipeline`.
|
|
481
|
+
* `run_pipeline` delegates to `runPipeline` from `_shared/pipeline-runner.ts`.
|
|
482
|
+
*
|
|
483
|
+
* @throws Error('Unknown function handler: <name>') on unrecognized name
|
|
484
|
+
* @throws Error('pipeline_id is required for run_pipeline handler')
|
|
485
|
+
*/
|
|
486
|
+
async function executeFunctionHandler(
|
|
487
|
+
ctx: RequestContext,
|
|
488
|
+
handlerName: string,
|
|
489
|
+
config: any
|
|
490
|
+
): Promise<any> {
|
|
491
|
+
// Built-in handlers
|
|
492
|
+
const handlers: Record<string, Function> = {
|
|
493
|
+
'send_email': async (ctx: RequestContext, config: any) => {
|
|
494
|
+
// Implementation would integrate with email service
|
|
495
|
+
console.log(`[${ctx.requestId}] Sending email:`, config)
|
|
496
|
+
return { sent: true, recipients: config.recipients }
|
|
497
|
+
},
|
|
498
|
+
|
|
499
|
+
'generate_report': async (ctx: RequestContext, config: any) => {
|
|
500
|
+
// Implementation would generate and deliver report
|
|
501
|
+
console.log(`[${ctx.requestId}] Generating report:`, config)
|
|
502
|
+
return { generated: true, format: config.output_format }
|
|
503
|
+
},
|
|
504
|
+
|
|
505
|
+
'notify_watchers': async (ctx: RequestContext, config: any) => {
|
|
506
|
+
// Implementation would notify item watchers
|
|
507
|
+
console.log(`[${ctx.requestId}] Notifying watchers:`, config)
|
|
508
|
+
return { notified: true }
|
|
509
|
+
},
|
|
510
|
+
|
|
511
|
+
'run_pipeline': async (ctx: RequestContext, config: any) => {
|
|
512
|
+
// Execute a pipeline as part of scheduled action
|
|
513
|
+
const { pipeline_id, trigger_data = {} } = config
|
|
514
|
+
|
|
515
|
+
if (!pipeline_id) {
|
|
516
|
+
throw new Error('pipeline_id is required for run_pipeline handler')
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const result = await runPipeline(pipeline_id, trigger_data, ctx)
|
|
520
|
+
return {
|
|
521
|
+
success: result.status === 'completed',
|
|
522
|
+
execution_id: result.executionId,
|
|
523
|
+
stages_completed: result.stages?.length || 0,
|
|
524
|
+
duration_ms: result.durationMs
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const handler = handlers[handlerName]
|
|
530
|
+
if (!handler) {
|
|
531
|
+
throw new Error(`Unknown function handler: ${handlerName}`)
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
return await handler(ctx, config)
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Placeholder for integration-based action handlers (external service calls).
|
|
539
|
+
* Currently logs and returns `{ executed: true }` for all handler names.
|
|
540
|
+
*/
|
|
541
|
+
async function executeIntegrationHandler(
|
|
542
|
+
ctx: RequestContext,
|
|
543
|
+
handlerName: string,
|
|
544
|
+
config: any
|
|
545
|
+
): Promise<any> {
|
|
546
|
+
// Integration handlers would call external services
|
|
547
|
+
// This is a placeholder for future implementation
|
|
548
|
+
console.log(`[${ctx.requestId}] Integration handler: ${handlerName}`, config)
|
|
549
|
+
return { executed: true, handler: handlerName }
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Evaluates all active `threshold_alert` items across all accounts.
|
|
554
|
+
* For each threshold, queries the appropriate observability RPC
|
|
555
|
+
* (`get_error_rate`, `get_latency_percentiles`, or `get_pipeline_stats`)
|
|
556
|
+
* and fires the configured pipeline if the threshold is breached.
|
|
557
|
+
*
|
|
558
|
+
* Supported metrics: `error_rate`, `latency_p95`, `pipeline_failure_rate`
|
|
559
|
+
*
|
|
560
|
+
* @returns Array of `{ thresholdId, breached, fired }` results
|
|
561
|
+
* @sideEffects DB read: items (threshold_alert), observability RPCs
|
|
562
|
+
* @sideEffects pipeline: runPipeline when threshold breached and pipeline_id set
|
|
563
|
+
* @sideEffects audit: emitAudit('threshold.breached') for each breached threshold
|
|
564
|
+
* @calledBy handler (every tick)
|
|
565
|
+
*/
|
|
566
|
+
async function evaluateThresholds(ctx: RequestContext): Promise<Array<{ thresholdId: string; breached: boolean; fired: boolean }>> {
|
|
567
|
+
const results: Array<{ thresholdId: string; breached: boolean; fired: boolean }> = []
|
|
568
|
+
|
|
569
|
+
try {
|
|
570
|
+
// Load all active threshold alerts from items table
|
|
571
|
+
const { data: thresholds, error } = await adminDb
|
|
572
|
+
.from('items')
|
|
573
|
+
.select('*')
|
|
574
|
+
.eq('type', 'threshold_alert')
|
|
575
|
+
.eq('data->>is_active', 'true')
|
|
576
|
+
|
|
577
|
+
if (error) {
|
|
578
|
+
console.error('Failed to load thresholds:', error)
|
|
579
|
+
return results
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
if (!thresholds || thresholds.length === 0) {
|
|
583
|
+
return results
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Evaluate each threshold
|
|
587
|
+
for (const threshold of thresholds) {
|
|
588
|
+
try {
|
|
589
|
+
const config = threshold.data || {}
|
|
590
|
+
const { metric, operator, value, window_minutes, pipeline_id } = config
|
|
591
|
+
|
|
592
|
+
if (!metric || !operator || value === undefined || !window_minutes) {
|
|
593
|
+
console.warn(`Threshold ${threshold.id} missing required fields`)
|
|
594
|
+
continue
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Calculate time window
|
|
598
|
+
const now = new Date()
|
|
599
|
+
const from = new Date(now.getTime() - window_minutes * 60 * 1000)
|
|
600
|
+
|
|
601
|
+
let breached = false
|
|
602
|
+
let actualValue: number = 0
|
|
603
|
+
|
|
604
|
+
// Query appropriate RPC based on metric
|
|
605
|
+
switch (metric) {
|
|
606
|
+
case 'error_rate': {
|
|
607
|
+
const { data } = await adminDb.rpc('get_error_rate', {
|
|
608
|
+
p_account_id: threshold.account_id,
|
|
609
|
+
p_from: from.toISOString(),
|
|
610
|
+
p_to: now.toISOString()
|
|
611
|
+
})
|
|
612
|
+
if (data && data.length > 0) {
|
|
613
|
+
actualValue = data[0].rate
|
|
614
|
+
breached = operator === 'gt' ? actualValue > value : actualValue < value
|
|
615
|
+
}
|
|
616
|
+
break
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
case 'latency_p95': {
|
|
620
|
+
const { data } = await adminDb.rpc('get_latency_percentiles', {
|
|
621
|
+
p_account_id: threshold.account_id,
|
|
622
|
+
p_from: from.toISOString(),
|
|
623
|
+
p_to: now.toISOString()
|
|
624
|
+
})
|
|
625
|
+
if (data && data.length > 0) {
|
|
626
|
+
actualValue = data[0].p95
|
|
627
|
+
breached = operator === 'gt' ? actualValue > value : actualValue < value
|
|
628
|
+
}
|
|
629
|
+
break
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
case 'pipeline_failure_rate': {
|
|
633
|
+
const { data } = await adminDb.rpc('get_pipeline_stats', {
|
|
634
|
+
p_account_id: threshold.account_id,
|
|
635
|
+
p_from: from.toISOString(),
|
|
636
|
+
p_to: now.toISOString()
|
|
637
|
+
})
|
|
638
|
+
if (data && data.length > 0) {
|
|
639
|
+
// Calculate overall failure rate across all pipelines
|
|
640
|
+
const totalSuccess = data.reduce((sum: number, p: any) => sum + (parseInt(p.success_count) || 0), 0)
|
|
641
|
+
const totalFailure = data.reduce((sum: number, p: any) => sum + (parseInt(p.failure_count) || 0), 0)
|
|
642
|
+
const total = totalSuccess + totalFailure
|
|
643
|
+
actualValue = total > 0 ? (totalFailure / total) * 100 : 0
|
|
644
|
+
breached = operator === 'gt' ? actualValue > value : actualValue < value
|
|
645
|
+
}
|
|
646
|
+
break
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
default:
|
|
650
|
+
console.warn(`Unknown metric: ${metric}`)
|
|
651
|
+
continue
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// Fire pipeline if breached and pipeline_id configured
|
|
655
|
+
let fired = false
|
|
656
|
+
if (breached && pipeline_id) {
|
|
657
|
+
try {
|
|
658
|
+
await runPipeline(pipeline_id, {
|
|
659
|
+
threshold_id: threshold.id,
|
|
660
|
+
metric,
|
|
661
|
+
threshold_value: value,
|
|
662
|
+
actual_value: actualValue,
|
|
663
|
+
window_minutes,
|
|
664
|
+
triggered_at: now.toISOString()
|
|
665
|
+
}, ctx)
|
|
666
|
+
fired = true
|
|
667
|
+
} catch (pipelineError: any) {
|
|
668
|
+
console.error(`Failed to fire threshold pipeline ${pipeline_id}:`, pipelineError)
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// Log threshold event if breached
|
|
673
|
+
if (breached) {
|
|
674
|
+
await emitAudit(ctx, 'threshold.breached', {
|
|
675
|
+
type: 'threshold_alert',
|
|
676
|
+
id: threshold.id,
|
|
677
|
+
account_id: threshold.account_id
|
|
678
|
+
}, {
|
|
679
|
+
metric,
|
|
680
|
+
operator,
|
|
681
|
+
threshold_value: value,
|
|
682
|
+
actual_value: actualValue,
|
|
683
|
+
window_minutes,
|
|
684
|
+
pipeline_fired: fired,
|
|
685
|
+
pipeline_id
|
|
686
|
+
})
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
results.push({
|
|
690
|
+
thresholdId: threshold.id,
|
|
691
|
+
breached,
|
|
692
|
+
fired
|
|
693
|
+
})
|
|
694
|
+
|
|
695
|
+
} catch (thresholdError: any) {
|
|
696
|
+
console.error(`Error evaluating threshold ${threshold.id}:`, thresholdError)
|
|
697
|
+
results.push({
|
|
698
|
+
thresholdId: threshold.id,
|
|
699
|
+
breached: false,
|
|
700
|
+
fired: false
|
|
701
|
+
})
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
return results
|
|
706
|
+
|
|
707
|
+
} catch (error: any) {
|
|
708
|
+
console.error('Threshold evaluation error:', error)
|
|
709
|
+
return results
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* Runs the `cleanup_old_logs` RPC with 90-day retention. Called once daily
|
|
715
|
+
* by the handler at 00:00 UTC.
|
|
716
|
+
*
|
|
717
|
+
* @returns Number of log records deleted
|
|
718
|
+
* @sideEffects DB write: cleanup_old_logs RPC (cross-account DELETE)
|
|
719
|
+
* @calledBy handler (daily, 00:00 UTC)
|
|
720
|
+
*/
|
|
721
|
+
async function cleanupOldLogs(): Promise<number> {
|
|
722
|
+
try {
|
|
723
|
+
const { data, error } = await adminDb.rpc('cleanup_old_logs', {
|
|
724
|
+
p_retention_days: 90
|
|
725
|
+
})
|
|
726
|
+
|
|
727
|
+
if (error) {
|
|
728
|
+
console.error('Log cleanup failed:', error)
|
|
729
|
+
return 0
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
const deletedCount = data?.[0]?.deleted_count || 0
|
|
733
|
+
if (deletedCount > 0) {
|
|
734
|
+
console.log(`Cleaned up ${deletedCount} old log records`)
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
return deletedCount
|
|
738
|
+
} catch (error: any) {
|
|
739
|
+
console.error('Log cleanup error:', error)
|
|
740
|
+
return 0
|
|
741
|
+
}
|
|
742
|
+
}
|