@useatlas/create 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +231 -0
- package/index.ts +829 -0
- package/package.json +38 -0
- package/templates/docker/.env.example +67 -0
- package/templates/docker/Dockerfile +52 -0
- package/templates/docker/bin/__tests__/benchmark.test.ts +598 -0
- package/templates/docker/bin/__tests__/duckdb-ingest.test.ts +171 -0
- package/templates/docker/bin/__tests__/eval.test.ts +434 -0
- package/templates/docker/bin/__tests__/matview-partition.test.ts +615 -0
- package/templates/docker/bin/__tests__/multi-source.test.ts +113 -0
- package/templates/docker/bin/__tests__/plugin-cli.test.ts +322 -0
- package/templates/docker/bin/__tests__/profiler-heuristics.test.ts +608 -0
- package/templates/docker/bin/__tests__/query.test.ts +240 -0
- package/templates/docker/bin/__tests__/schema-drift.test.ts +542 -0
- package/templates/docker/bin/__tests__/view-yaml-generation.test.ts +146 -0
- package/templates/docker/bin/atlas.ts +5044 -0
- package/templates/docker/bin/benchmark.ts +695 -0
- package/templates/docker/bin/enrich.ts +559 -0
- package/templates/docker/bin/eval.ts +770 -0
- package/templates/docker/bin/smoke.ts +438 -0
- package/templates/docker/data/.gitkeep +0 -0
- package/templates/docker/data/cybersec.sql +1961 -0
- package/templates/docker/data/demo-semantic/catalog.yml +40 -0
- package/templates/docker/data/demo-semantic/entities/accounts.yml +170 -0
- package/templates/docker/data/demo-semantic/entities/companies.yml +207 -0
- package/templates/docker/data/demo-semantic/entities/people.yml +145 -0
- package/templates/docker/data/demo-semantic/glossary.yml +22 -0
- package/templates/docker/data/demo-semantic/metrics/accounts.yml +38 -0
- package/templates/docker/data/demo-semantic/metrics/companies.yml +89 -0
- package/templates/docker/data/demo.sql +373 -0
- package/templates/docker/data/ecommerce.sql +1690 -0
- package/templates/docker/data/init-demo-db.sql +8 -0
- package/templates/docker/docker-compose.yml +34 -0
- package/templates/docker/docs/deploy.md +390 -0
- package/templates/docker/eslint.config.mjs +18 -0
- package/templates/docker/gitignore +5 -0
- package/templates/docker/next.config.ts +9 -0
- package/templates/docker/package.json +59 -0
- package/templates/docker/postcss.config.mjs +8 -0
- package/templates/docker/public/.gitkeep +0 -0
- package/templates/docker/public/favicon.svg +4 -0
- package/templates/docker/railway.json +13 -0
- package/templates/docker/render.yaml +34 -0
- package/templates/docker/semantic/catalog.yml +5 -0
- package/templates/docker/semantic/entities/.gitkeep +0 -0
- package/templates/docker/semantic/glossary.yml +6 -0
- package/templates/docker/semantic/metrics/.gitkeep +0 -0
- package/templates/docker/sidecar/Dockerfile +28 -0
- package/templates/docker/sidecar/railway.json +14 -0
- package/templates/docker/sidecar/server.ts +188 -0
- package/templates/docker/src/api/__tests__/actions.test.ts +683 -0
- package/templates/docker/src/api/__tests__/admin.test.ts +820 -0
- package/templates/docker/src/api/__tests__/auth.test.ts +165 -0
- package/templates/docker/src/api/__tests__/chat.test.ts +376 -0
- package/templates/docker/src/api/__tests__/conversations.test.ts +555 -0
- package/templates/docker/src/api/__tests__/cors.test.ts +135 -0
- package/templates/docker/src/api/__tests__/health-plugin.test.ts +169 -0
- package/templates/docker/src/api/__tests__/health.test.ts +261 -0
- package/templates/docker/src/api/__tests__/query.test.ts +891 -0
- package/templates/docker/src/api/__tests__/scheduled-tasks.test.ts +601 -0
- package/templates/docker/src/api/__tests__/slack.test.ts +847 -0
- package/templates/docker/src/api/index.ts +117 -0
- package/templates/docker/src/api/routes/actions.ts +274 -0
- package/templates/docker/src/api/routes/admin.ts +757 -0
- package/templates/docker/src/api/routes/auth.ts +48 -0
- package/templates/docker/src/api/routes/chat.ts +465 -0
- package/templates/docker/src/api/routes/conversations.ts +266 -0
- package/templates/docker/src/api/routes/health.ts +287 -0
- package/templates/docker/src/api/routes/openapi.ts +390 -0
- package/templates/docker/src/api/routes/query.ts +318 -0
- package/templates/docker/src/api/routes/scheduled-tasks.ts +467 -0
- package/templates/docker/src/api/routes/slack.ts +611 -0
- package/templates/docker/src/api/server.ts +226 -0
- package/templates/docker/src/app/api/[...route]/route.ts +33 -0
- package/templates/docker/src/app/error.tsx +24 -0
- package/templates/docker/src/app/globals.css +126 -0
- package/templates/docker/src/app/layout.tsx +19 -0
- package/templates/docker/src/app/page.tsx +14 -0
- package/templates/docker/src/global.d.ts +1 -0
- package/templates/docker/src/lib/__tests__/agent-cache.test.ts +437 -0
- package/templates/docker/src/lib/__tests__/agent-dialect.test.ts +114 -0
- package/templates/docker/src/lib/__tests__/agent-health-annotations.test.ts +164 -0
- package/templates/docker/src/lib/__tests__/agent-integration.test.ts +514 -0
- package/templates/docker/src/lib/__tests__/config-actions.test.ts +166 -0
- package/templates/docker/src/lib/__tests__/config.test.ts +1063 -0
- package/templates/docker/src/lib/__tests__/conversations.test.ts +589 -0
- package/templates/docker/src/lib/__tests__/errors.test.ts +256 -0
- package/templates/docker/src/lib/__tests__/logger.test.ts +200 -0
- package/templates/docker/src/lib/__tests__/providers.test.ts +99 -0
- package/templates/docker/src/lib/__tests__/rls.test.ts +435 -0
- package/templates/docker/src/lib/__tests__/scheduled-task-types.test.ts +124 -0
- package/templates/docker/src/lib/__tests__/scheduled-tasks.test.ts +550 -0
- package/templates/docker/src/lib/__tests__/semantic-index.test.ts +547 -0
- package/templates/docker/src/lib/__tests__/semantic-multisource.test.ts +544 -0
- package/templates/docker/src/lib/__tests__/semantic.test.ts +363 -0
- package/templates/docker/src/lib/__tests__/startup-actions.test.ts +452 -0
- package/templates/docker/src/lib/__tests__/startup.test.ts +465 -0
- package/templates/docker/src/lib/__tests__/tracing.test.ts +28 -0
- package/templates/docker/src/lib/action-types.ts +95 -0
- package/templates/docker/src/lib/agent-query.ts +178 -0
- package/templates/docker/src/lib/agent.ts +505 -0
- package/templates/docker/src/lib/api-url.ts +2 -0
- package/templates/docker/src/lib/auth/__tests__/audit.test.ts +418 -0
- package/templates/docker/src/lib/auth/__tests__/byot-integration.test.ts +222 -0
- package/templates/docker/src/lib/auth/__tests__/byot.test.ts +366 -0
- package/templates/docker/src/lib/auth/__tests__/detect.test.ts +190 -0
- package/templates/docker/src/lib/auth/__tests__/managed.test.ts +173 -0
- package/templates/docker/src/lib/auth/__tests__/middleware.test.ts +456 -0
- package/templates/docker/src/lib/auth/__tests__/migrate.test.ts +201 -0
- package/templates/docker/src/lib/auth/__tests__/permissions.test.ts +225 -0
- package/templates/docker/src/lib/auth/__tests__/server.test.ts +34 -0
- package/templates/docker/src/lib/auth/__tests__/simple-key.test.ts +176 -0
- package/templates/docker/src/lib/auth/__tests__/types.test.ts +44 -0
- package/templates/docker/src/lib/auth/audit.ts +89 -0
- package/templates/docker/src/lib/auth/byot.ts +158 -0
- package/templates/docker/src/lib/auth/client.ts +35 -0
- package/templates/docker/src/lib/auth/detect.ts +83 -0
- package/templates/docker/src/lib/auth/managed.ts +73 -0
- package/templates/docker/src/lib/auth/middleware.ts +208 -0
- package/templates/docker/src/lib/auth/migrate.ts +111 -0
- package/templates/docker/src/lib/auth/permissions.ts +156 -0
- package/templates/docker/src/lib/auth/server.ts +142 -0
- package/templates/docker/src/lib/auth/simple-key.ts +92 -0
- package/templates/docker/src/lib/auth/types.ts +49 -0
- package/templates/docker/src/lib/config.ts +704 -0
- package/templates/docker/src/lib/conversation-types.ts +29 -0
- package/templates/docker/src/lib/conversations.ts +270 -0
- package/templates/docker/src/lib/db/__tests__/connection.test.ts +69 -0
- package/templates/docker/src/lib/db/__tests__/duckdb.test.ts +141 -0
- package/templates/docker/src/lib/db/__tests__/internal.test.ts +387 -0
- package/templates/docker/src/lib/db/__tests__/registry-health.test.ts +207 -0
- package/templates/docker/src/lib/db/__tests__/registry-pool-limits.test.ts +156 -0
- package/templates/docker/src/lib/db/__tests__/registry.test.ts +595 -0
- package/templates/docker/src/lib/db/__tests__/salesforce.test.ts +339 -0
- package/templates/docker/src/lib/db/__tests__/snowflake.test.ts +217 -0
- package/templates/docker/src/lib/db/__tests__/source-rate-limit.test.ts +130 -0
- package/templates/docker/src/lib/db/connection.ts +753 -0
- package/templates/docker/src/lib/db/duckdb.ts +122 -0
- package/templates/docker/src/lib/db/internal.ts +273 -0
- package/templates/docker/src/lib/db/salesforce.ts +342 -0
- package/templates/docker/src/lib/db/source-rate-limit.ts +191 -0
- package/templates/docker/src/lib/errors.ts +154 -0
- package/templates/docker/src/lib/logger.ts +98 -0
- package/templates/docker/src/lib/plugins/__tests__/hooks-integration.test.ts +202 -0
- package/templates/docker/src/lib/plugins/__tests__/hooks.test.ts +529 -0
- package/templates/docker/src/lib/plugins/__tests__/migrate.test.ts +521 -0
- package/templates/docker/src/lib/plugins/__tests__/registry.test.ts +346 -0
- package/templates/docker/src/lib/plugins/__tests__/tools.test.ts +49 -0
- package/templates/docker/src/lib/plugins/__tests__/wiring.test.ts +585 -0
- package/templates/docker/src/lib/plugins/hooks.ts +162 -0
- package/templates/docker/src/lib/plugins/index.ts +9 -0
- package/templates/docker/src/lib/plugins/migrate.ts +309 -0
- package/templates/docker/src/lib/plugins/registry.ts +231 -0
- package/templates/docker/src/lib/plugins/tools.ts +39 -0
- package/templates/docker/src/lib/plugins/wiring.ts +291 -0
- package/templates/docker/src/lib/providers.ts +102 -0
- package/templates/docker/src/lib/rls.ts +321 -0
- package/templates/docker/src/lib/scheduled-task-types.ts +132 -0
- package/templates/docker/src/lib/scheduled-tasks.ts +475 -0
- package/templates/docker/src/lib/scheduler/__tests__/delivery.test.ts +192 -0
- package/templates/docker/src/lib/scheduler/__tests__/engine.test.ts +248 -0
- package/templates/docker/src/lib/scheduler/__tests__/format-email.test.ts +96 -0
- package/templates/docker/src/lib/scheduler/__tests__/format-slack.test.ts +78 -0
- package/templates/docker/src/lib/scheduler/__tests__/format-webhook.test.ts +78 -0
- package/templates/docker/src/lib/scheduler/delivery.ts +248 -0
- package/templates/docker/src/lib/scheduler/engine.ts +317 -0
- package/templates/docker/src/lib/scheduler/executor.ts +73 -0
- package/templates/docker/src/lib/scheduler/format-email.ts +109 -0
- package/templates/docker/src/lib/scheduler/format-slack.ts +35 -0
- package/templates/docker/src/lib/scheduler/format-webhook.ts +37 -0
- package/templates/docker/src/lib/scheduler/index.ts +7 -0
- package/templates/docker/src/lib/security.ts +11 -0
- package/templates/docker/src/lib/semantic-index.ts +503 -0
- package/templates/docker/src/lib/semantic.ts +387 -0
- package/templates/docker/src/lib/sidecar-types.ts +16 -0
- package/templates/docker/src/lib/slack/__tests__/api.test.ts +160 -0
- package/templates/docker/src/lib/slack/__tests__/format.test.ts +237 -0
- package/templates/docker/src/lib/slack/__tests__/store.test.ts +188 -0
- package/templates/docker/src/lib/slack/__tests__/threads.test.ts +112 -0
- package/templates/docker/src/lib/slack/__tests__/verify.test.ts +111 -0
- package/templates/docker/src/lib/slack/api.ts +102 -0
- package/templates/docker/src/lib/slack/format.ts +209 -0
- package/templates/docker/src/lib/slack/store.ts +107 -0
- package/templates/docker/src/lib/slack/threads.ts +64 -0
- package/templates/docker/src/lib/slack/verify.ts +71 -0
- package/templates/docker/src/lib/startup.ts +730 -0
- package/templates/docker/src/lib/tools/__tests__/action-permissions.test.ts +594 -0
- package/templates/docker/src/lib/tools/__tests__/custom-validation.test.ts +238 -0
- package/templates/docker/src/lib/tools/__tests__/explore-backend.test.ts +267 -0
- package/templates/docker/src/lib/tools/__tests__/explore-nsjail.test.ts +492 -0
- package/templates/docker/src/lib/tools/__tests__/explore-plugin.test.ts +374 -0
- package/templates/docker/src/lib/tools/__tests__/explore-sdk-compat.test.ts +82 -0
- package/templates/docker/src/lib/tools/__tests__/explore-sidecar.test.ts +208 -0
- package/templates/docker/src/lib/tools/__tests__/registry-actions.test.ts +144 -0
- package/templates/docker/src/lib/tools/__tests__/registry.test.ts +235 -0
- package/templates/docker/src/lib/tools/__tests__/salesforce-tool.test.ts +154 -0
- package/templates/docker/src/lib/tools/__tests__/soql-validation.test.ts +303 -0
- package/templates/docker/src/lib/tools/__tests__/sql-audit.test.ts +225 -0
- package/templates/docker/src/lib/tools/__tests__/sql-connection-whitelist.test.ts +98 -0
- package/templates/docker/src/lib/tools/__tests__/sql-duckdb.test.ts +233 -0
- package/templates/docker/src/lib/tools/__tests__/sql-ratelimit.test.ts +225 -0
- package/templates/docker/src/lib/tools/__tests__/sql.test.ts +1012 -0
- package/templates/docker/src/lib/tools/actions/__tests__/audit.test.ts +211 -0
- package/templates/docker/src/lib/tools/actions/__tests__/email.test.ts +378 -0
- package/templates/docker/src/lib/tools/actions/__tests__/handler.test.ts +681 -0
- package/templates/docker/src/lib/tools/actions/__tests__/jira.test.ts +427 -0
- package/templates/docker/src/lib/tools/actions/audit.ts +47 -0
- package/templates/docker/src/lib/tools/actions/email.ts +191 -0
- package/templates/docker/src/lib/tools/actions/handler.ts +591 -0
- package/templates/docker/src/lib/tools/actions/index.ts +23 -0
- package/templates/docker/src/lib/tools/actions/jira.ts +220 -0
- package/templates/docker/src/lib/tools/explore-nsjail.ts +343 -0
- package/templates/docker/src/lib/tools/explore-sandbox.ts +264 -0
- package/templates/docker/src/lib/tools/explore-sidecar.ts +163 -0
- package/templates/docker/src/lib/tools/explore.ts +379 -0
- package/templates/docker/src/lib/tools/registry.ts +221 -0
- package/templates/docker/src/lib/tools/salesforce.ts +138 -0
- package/templates/docker/src/lib/tools/soql-validation.ts +172 -0
- package/templates/docker/src/lib/tools/sql.ts +680 -0
- package/templates/docker/src/lib/tracing.ts +40 -0
- package/templates/docker/src/lib/utils.ts +6 -0
- package/templates/docker/src/test-setup.ts +38 -0
- package/templates/docker/src/types/vercel-sandbox.d.ts +54 -0
- package/templates/docker/src/ui/components/actions/action-approval-card.tsx +295 -0
- package/templates/docker/src/ui/components/actions/action-status-badge.tsx +50 -0
- package/templates/docker/src/ui/components/admin/admin-layout.tsx +26 -0
- package/templates/docker/src/ui/components/admin/admin-sidebar.tsx +96 -0
- package/templates/docker/src/ui/components/admin/empty-state.tsx +24 -0
- package/templates/docker/src/ui/components/admin/entity-detail.tsx +233 -0
- package/templates/docker/src/ui/components/admin/entity-list.tsx +96 -0
- package/templates/docker/src/ui/components/admin/error-banner.tsx +22 -0
- package/templates/docker/src/ui/components/admin/feature-disabled.tsx +44 -0
- package/templates/docker/src/ui/components/admin/health-badge.tsx +30 -0
- package/templates/docker/src/ui/components/admin/loading-state.tsx +14 -0
- package/templates/docker/src/ui/components/admin/stat-card.tsx +32 -0
- package/templates/docker/src/ui/components/atlas-chat.tsx +370 -0
- package/templates/docker/src/ui/components/chart/chart-detection.ts +261 -0
- package/templates/docker/src/ui/components/chart/result-chart.tsx +375 -0
- package/templates/docker/src/ui/components/chat/api-key-bar.tsx +66 -0
- package/templates/docker/src/ui/components/chat/copy-button.tsx +25 -0
- package/templates/docker/src/ui/components/chat/data-table.tsx +102 -0
- package/templates/docker/src/ui/components/chat/error-banner.tsx +32 -0
- package/templates/docker/src/ui/components/chat/explore-card.tsx +41 -0
- package/templates/docker/src/ui/components/chat/loading-card.tsx +10 -0
- package/templates/docker/src/ui/components/chat/managed-auth-card.tsx +116 -0
- package/templates/docker/src/ui/components/chat/markdown.tsx +72 -0
- package/templates/docker/src/ui/components/chat/sql-block.tsx +30 -0
- package/templates/docker/src/ui/components/chat/sql-result-card.tsx +144 -0
- package/templates/docker/src/ui/components/chat/starter-prompts.ts +6 -0
- package/templates/docker/src/ui/components/chat/tool-part.tsx +40 -0
- package/templates/docker/src/ui/components/chat/typing-indicator.tsx +19 -0
- package/templates/docker/src/ui/components/conversations/conversation-item.tsx +120 -0
- package/templates/docker/src/ui/components/conversations/conversation-list.tsx +66 -0
- package/templates/docker/src/ui/components/conversations/conversation-sidebar.tsx +78 -0
- package/templates/docker/src/ui/components/conversations/delete-confirmation.tsx +27 -0
- package/templates/docker/src/ui/context.tsx +78 -0
- package/templates/docker/src/ui/hooks/use-admin-fetch.ts +104 -0
- package/templates/docker/src/ui/hooks/use-conversations.ts +184 -0
- package/templates/docker/src/ui/hooks/use-dark-mode.ts +17 -0
- package/templates/docker/src/ui/lib/action-types.ts +63 -0
- package/templates/docker/src/ui/lib/helpers.ts +104 -0
- package/templates/docker/src/ui/lib/types.ts +145 -0
- package/templates/docker/tsconfig.json +41 -0
- package/templates/docker/vercel.json +3 -0
- package/templates/nextjs-standalone/.env.example +68 -0
- package/templates/nextjs-standalone/bin/__tests__/benchmark.test.ts +598 -0
- package/templates/nextjs-standalone/bin/__tests__/duckdb-ingest.test.ts +171 -0
- package/templates/nextjs-standalone/bin/__tests__/eval.test.ts +434 -0
- package/templates/nextjs-standalone/bin/__tests__/matview-partition.test.ts +615 -0
- package/templates/nextjs-standalone/bin/__tests__/multi-source.test.ts +113 -0
- package/templates/nextjs-standalone/bin/__tests__/plugin-cli.test.ts +322 -0
- package/templates/nextjs-standalone/bin/__tests__/profiler-heuristics.test.ts +608 -0
- package/templates/nextjs-standalone/bin/__tests__/query.test.ts +240 -0
- package/templates/nextjs-standalone/bin/__tests__/schema-drift.test.ts +542 -0
- package/templates/nextjs-standalone/bin/__tests__/view-yaml-generation.test.ts +146 -0
- package/templates/nextjs-standalone/bin/atlas.ts +5044 -0
- package/templates/nextjs-standalone/bin/benchmark.ts +695 -0
- package/templates/nextjs-standalone/bin/enrich.ts +559 -0
- package/templates/nextjs-standalone/bin/eval.ts +770 -0
- package/templates/nextjs-standalone/bin/smoke.ts +438 -0
- package/templates/nextjs-standalone/data/.gitkeep +0 -0
- package/templates/nextjs-standalone/data/cybersec.sql +1961 -0
- package/templates/nextjs-standalone/data/demo-semantic/catalog.yml +40 -0
- package/templates/nextjs-standalone/data/demo-semantic/entities/accounts.yml +170 -0
- package/templates/nextjs-standalone/data/demo-semantic/entities/companies.yml +207 -0
- package/templates/nextjs-standalone/data/demo-semantic/entities/people.yml +145 -0
- package/templates/nextjs-standalone/data/demo-semantic/glossary.yml +22 -0
- package/templates/nextjs-standalone/data/demo-semantic/metrics/accounts.yml +38 -0
- package/templates/nextjs-standalone/data/demo-semantic/metrics/companies.yml +89 -0
- package/templates/nextjs-standalone/data/demo.sql +373 -0
- package/templates/nextjs-standalone/data/ecommerce.sql +1690 -0
- package/templates/nextjs-standalone/data/init-demo-db.sql +8 -0
- package/templates/nextjs-standalone/docs/deploy.md +390 -0
- package/templates/nextjs-standalone/eslint.config.mjs +18 -0
- package/templates/nextjs-standalone/gitignore +5 -0
- package/templates/nextjs-standalone/next.config.ts +10 -0
- package/templates/nextjs-standalone/package.json +63 -0
- package/templates/nextjs-standalone/postcss.config.mjs +8 -0
- package/templates/nextjs-standalone/semantic/catalog.yml +5 -0
- package/templates/nextjs-standalone/semantic/entities/.gitkeep +0 -0
- package/templates/nextjs-standalone/semantic/glossary.yml +6 -0
- package/templates/nextjs-standalone/semantic/metrics/.gitkeep +0 -0
- package/templates/nextjs-standalone/src/api/__tests__/actions.test.ts +683 -0
- package/templates/nextjs-standalone/src/api/__tests__/admin.test.ts +820 -0
- package/templates/nextjs-standalone/src/api/__tests__/auth.test.ts +165 -0
- package/templates/nextjs-standalone/src/api/__tests__/chat.test.ts +376 -0
- package/templates/nextjs-standalone/src/api/__tests__/conversations.test.ts +555 -0
- package/templates/nextjs-standalone/src/api/__tests__/cors.test.ts +135 -0
- package/templates/nextjs-standalone/src/api/__tests__/health-plugin.test.ts +169 -0
- package/templates/nextjs-standalone/src/api/__tests__/health.test.ts +261 -0
- package/templates/nextjs-standalone/src/api/__tests__/query.test.ts +891 -0
- package/templates/nextjs-standalone/src/api/__tests__/scheduled-tasks.test.ts +601 -0
- package/templates/nextjs-standalone/src/api/__tests__/slack.test.ts +847 -0
- package/templates/nextjs-standalone/src/api/index.ts +117 -0
- package/templates/nextjs-standalone/src/api/routes/actions.ts +274 -0
- package/templates/nextjs-standalone/src/api/routes/admin.ts +757 -0
- package/templates/nextjs-standalone/src/api/routes/auth.ts +48 -0
- package/templates/nextjs-standalone/src/api/routes/chat.ts +465 -0
- package/templates/nextjs-standalone/src/api/routes/conversations.ts +266 -0
- package/templates/nextjs-standalone/src/api/routes/health.ts +287 -0
- package/templates/nextjs-standalone/src/api/routes/openapi.ts +390 -0
- package/templates/nextjs-standalone/src/api/routes/query.ts +318 -0
- package/templates/nextjs-standalone/src/api/routes/scheduled-tasks.ts +467 -0
- package/templates/nextjs-standalone/src/api/routes/slack.ts +611 -0
- package/templates/nextjs-standalone/src/api/server.ts +226 -0
- package/templates/nextjs-standalone/src/app/api/[...route]/route.ts +33 -0
- package/templates/nextjs-standalone/src/app/error.tsx +24 -0
- package/templates/nextjs-standalone/src/app/global-error.tsx +68 -0
- package/templates/nextjs-standalone/src/app/globals.css +126 -0
- package/templates/nextjs-standalone/src/app/layout.tsx +19 -0
- package/templates/nextjs-standalone/src/app/page.tsx +14 -0
- package/templates/nextjs-standalone/src/lib/__tests__/agent-cache.test.ts +437 -0
- package/templates/nextjs-standalone/src/lib/__tests__/agent-dialect.test.ts +114 -0
- package/templates/nextjs-standalone/src/lib/__tests__/agent-health-annotations.test.ts +164 -0
- package/templates/nextjs-standalone/src/lib/__tests__/agent-integration.test.ts +514 -0
- package/templates/nextjs-standalone/src/lib/__tests__/config-actions.test.ts +166 -0
- package/templates/nextjs-standalone/src/lib/__tests__/config.test.ts +1063 -0
- package/templates/nextjs-standalone/src/lib/__tests__/conversations.test.ts +589 -0
- package/templates/nextjs-standalone/src/lib/__tests__/errors.test.ts +256 -0
- package/templates/nextjs-standalone/src/lib/__tests__/logger.test.ts +200 -0
- package/templates/nextjs-standalone/src/lib/__tests__/providers.test.ts +99 -0
- package/templates/nextjs-standalone/src/lib/__tests__/rls.test.ts +435 -0
- package/templates/nextjs-standalone/src/lib/__tests__/scheduled-task-types.test.ts +124 -0
- package/templates/nextjs-standalone/src/lib/__tests__/scheduled-tasks.test.ts +550 -0
- package/templates/nextjs-standalone/src/lib/__tests__/semantic-index.test.ts +547 -0
- package/templates/nextjs-standalone/src/lib/__tests__/semantic-multisource.test.ts +544 -0
- package/templates/nextjs-standalone/src/lib/__tests__/semantic.test.ts +363 -0
- package/templates/nextjs-standalone/src/lib/__tests__/startup-actions.test.ts +452 -0
- package/templates/nextjs-standalone/src/lib/__tests__/startup.test.ts +465 -0
- package/templates/nextjs-standalone/src/lib/__tests__/tracing.test.ts +28 -0
- package/templates/nextjs-standalone/src/lib/action-types.ts +95 -0
- package/templates/nextjs-standalone/src/lib/agent-query.ts +178 -0
- package/templates/nextjs-standalone/src/lib/agent.ts +505 -0
- package/templates/nextjs-standalone/src/lib/api-url.ts +3 -0
- package/templates/nextjs-standalone/src/lib/auth/__tests__/audit.test.ts +418 -0
- package/templates/nextjs-standalone/src/lib/auth/__tests__/byot-integration.test.ts +222 -0
- package/templates/nextjs-standalone/src/lib/auth/__tests__/byot.test.ts +366 -0
- package/templates/nextjs-standalone/src/lib/auth/__tests__/detect.test.ts +190 -0
- package/templates/nextjs-standalone/src/lib/auth/__tests__/managed.test.ts +173 -0
- package/templates/nextjs-standalone/src/lib/auth/__tests__/middleware.test.ts +456 -0
- package/templates/nextjs-standalone/src/lib/auth/__tests__/migrate.test.ts +201 -0
- package/templates/nextjs-standalone/src/lib/auth/__tests__/permissions.test.ts +225 -0
- package/templates/nextjs-standalone/src/lib/auth/__tests__/server.test.ts +34 -0
- package/templates/nextjs-standalone/src/lib/auth/__tests__/simple-key.test.ts +176 -0
- package/templates/nextjs-standalone/src/lib/auth/__tests__/types.test.ts +44 -0
- package/templates/nextjs-standalone/src/lib/auth/audit.ts +89 -0
- package/templates/nextjs-standalone/src/lib/auth/byot.ts +158 -0
- package/templates/nextjs-standalone/src/lib/auth/client.ts +23 -0
- package/templates/nextjs-standalone/src/lib/auth/detect.ts +83 -0
- package/templates/nextjs-standalone/src/lib/auth/managed.ts +73 -0
- package/templates/nextjs-standalone/src/lib/auth/middleware.ts +208 -0
- package/templates/nextjs-standalone/src/lib/auth/migrate.ts +111 -0
- package/templates/nextjs-standalone/src/lib/auth/permissions.ts +156 -0
- package/templates/nextjs-standalone/src/lib/auth/server.ts +142 -0
- package/templates/nextjs-standalone/src/lib/auth/simple-key.ts +92 -0
- package/templates/nextjs-standalone/src/lib/auth/types.ts +49 -0
- package/templates/nextjs-standalone/src/lib/config.ts +704 -0
- package/templates/nextjs-standalone/src/lib/conversation-types.ts +29 -0
- package/templates/nextjs-standalone/src/lib/conversations.ts +270 -0
- package/templates/nextjs-standalone/src/lib/db/__tests__/connection.test.ts +69 -0
- package/templates/nextjs-standalone/src/lib/db/__tests__/duckdb.test.ts +141 -0
- package/templates/nextjs-standalone/src/lib/db/__tests__/internal.test.ts +387 -0
- package/templates/nextjs-standalone/src/lib/db/__tests__/registry-health.test.ts +207 -0
- package/templates/nextjs-standalone/src/lib/db/__tests__/registry-pool-limits.test.ts +156 -0
- package/templates/nextjs-standalone/src/lib/db/__tests__/registry.test.ts +595 -0
- package/templates/nextjs-standalone/src/lib/db/__tests__/salesforce.test.ts +339 -0
- package/templates/nextjs-standalone/src/lib/db/__tests__/snowflake.test.ts +217 -0
- package/templates/nextjs-standalone/src/lib/db/__tests__/source-rate-limit.test.ts +130 -0
- package/templates/nextjs-standalone/src/lib/db/connection.ts +753 -0
- package/templates/nextjs-standalone/src/lib/db/duckdb.ts +122 -0
- package/templates/nextjs-standalone/src/lib/db/internal.ts +273 -0
- package/templates/nextjs-standalone/src/lib/db/salesforce.ts +342 -0
- package/templates/nextjs-standalone/src/lib/db/source-rate-limit.ts +191 -0
- package/templates/nextjs-standalone/src/lib/errors.ts +154 -0
- package/templates/nextjs-standalone/src/lib/logger.ts +98 -0
- package/templates/nextjs-standalone/src/lib/plugins/__tests__/hooks-integration.test.ts +202 -0
- package/templates/nextjs-standalone/src/lib/plugins/__tests__/hooks.test.ts +529 -0
- package/templates/nextjs-standalone/src/lib/plugins/__tests__/migrate.test.ts +521 -0
- package/templates/nextjs-standalone/src/lib/plugins/__tests__/registry.test.ts +346 -0
- package/templates/nextjs-standalone/src/lib/plugins/__tests__/tools.test.ts +49 -0
- package/templates/nextjs-standalone/src/lib/plugins/__tests__/wiring.test.ts +585 -0
- package/templates/nextjs-standalone/src/lib/plugins/hooks.ts +162 -0
- package/templates/nextjs-standalone/src/lib/plugins/index.ts +9 -0
- package/templates/nextjs-standalone/src/lib/plugins/migrate.ts +309 -0
- package/templates/nextjs-standalone/src/lib/plugins/registry.ts +231 -0
- package/templates/nextjs-standalone/src/lib/plugins/tools.ts +39 -0
- package/templates/nextjs-standalone/src/lib/plugins/wiring.ts +291 -0
- package/templates/nextjs-standalone/src/lib/providers.ts +102 -0
- package/templates/nextjs-standalone/src/lib/rls.ts +321 -0
- package/templates/nextjs-standalone/src/lib/scheduled-task-types.ts +132 -0
- package/templates/nextjs-standalone/src/lib/scheduled-tasks.ts +475 -0
- package/templates/nextjs-standalone/src/lib/scheduler/__tests__/delivery.test.ts +192 -0
- package/templates/nextjs-standalone/src/lib/scheduler/__tests__/engine.test.ts +248 -0
- package/templates/nextjs-standalone/src/lib/scheduler/__tests__/format-email.test.ts +96 -0
- package/templates/nextjs-standalone/src/lib/scheduler/__tests__/format-slack.test.ts +78 -0
- package/templates/nextjs-standalone/src/lib/scheduler/__tests__/format-webhook.test.ts +78 -0
- package/templates/nextjs-standalone/src/lib/scheduler/delivery.ts +248 -0
- package/templates/nextjs-standalone/src/lib/scheduler/engine.ts +317 -0
- package/templates/nextjs-standalone/src/lib/scheduler/executor.ts +73 -0
- package/templates/nextjs-standalone/src/lib/scheduler/format-email.ts +109 -0
- package/templates/nextjs-standalone/src/lib/scheduler/format-slack.ts +35 -0
- package/templates/nextjs-standalone/src/lib/scheduler/format-webhook.ts +37 -0
- package/templates/nextjs-standalone/src/lib/scheduler/index.ts +7 -0
- package/templates/nextjs-standalone/src/lib/security.ts +11 -0
- package/templates/nextjs-standalone/src/lib/semantic-index.ts +503 -0
- package/templates/nextjs-standalone/src/lib/semantic.ts +387 -0
- package/templates/nextjs-standalone/src/lib/sidecar-types.ts +16 -0
- package/templates/nextjs-standalone/src/lib/slack/__tests__/api.test.ts +160 -0
- package/templates/nextjs-standalone/src/lib/slack/__tests__/format.test.ts +237 -0
- package/templates/nextjs-standalone/src/lib/slack/__tests__/store.test.ts +188 -0
- package/templates/nextjs-standalone/src/lib/slack/__tests__/threads.test.ts +112 -0
- package/templates/nextjs-standalone/src/lib/slack/__tests__/verify.test.ts +111 -0
- package/templates/nextjs-standalone/src/lib/slack/api.ts +102 -0
- package/templates/nextjs-standalone/src/lib/slack/format.ts +209 -0
- package/templates/nextjs-standalone/src/lib/slack/store.ts +107 -0
- package/templates/nextjs-standalone/src/lib/slack/threads.ts +64 -0
- package/templates/nextjs-standalone/src/lib/slack/verify.ts +71 -0
- package/templates/nextjs-standalone/src/lib/startup.ts +730 -0
- package/templates/nextjs-standalone/src/lib/tools/__tests__/action-permissions.test.ts +594 -0
- package/templates/nextjs-standalone/src/lib/tools/__tests__/custom-validation.test.ts +238 -0
- package/templates/nextjs-standalone/src/lib/tools/__tests__/explore-backend.test.ts +267 -0
- package/templates/nextjs-standalone/src/lib/tools/__tests__/explore-nsjail.test.ts +492 -0
- package/templates/nextjs-standalone/src/lib/tools/__tests__/explore-plugin.test.ts +374 -0
- package/templates/nextjs-standalone/src/lib/tools/__tests__/explore-sdk-compat.test.ts +82 -0
- package/templates/nextjs-standalone/src/lib/tools/__tests__/explore-sidecar.test.ts +208 -0
- package/templates/nextjs-standalone/src/lib/tools/__tests__/registry-actions.test.ts +144 -0
- package/templates/nextjs-standalone/src/lib/tools/__tests__/registry.test.ts +235 -0
- package/templates/nextjs-standalone/src/lib/tools/__tests__/salesforce-tool.test.ts +154 -0
- package/templates/nextjs-standalone/src/lib/tools/__tests__/soql-validation.test.ts +303 -0
- package/templates/nextjs-standalone/src/lib/tools/__tests__/sql-audit.test.ts +225 -0
- package/templates/nextjs-standalone/src/lib/tools/__tests__/sql-connection-whitelist.test.ts +98 -0
- package/templates/nextjs-standalone/src/lib/tools/__tests__/sql-duckdb.test.ts +233 -0
- package/templates/nextjs-standalone/src/lib/tools/__tests__/sql-ratelimit.test.ts +225 -0
- package/templates/nextjs-standalone/src/lib/tools/__tests__/sql.test.ts +1012 -0
- package/templates/nextjs-standalone/src/lib/tools/actions/__tests__/audit.test.ts +211 -0
- package/templates/nextjs-standalone/src/lib/tools/actions/__tests__/email.test.ts +378 -0
- package/templates/nextjs-standalone/src/lib/tools/actions/__tests__/handler.test.ts +681 -0
- package/templates/nextjs-standalone/src/lib/tools/actions/__tests__/jira.test.ts +427 -0
- package/templates/nextjs-standalone/src/lib/tools/actions/audit.ts +47 -0
- package/templates/nextjs-standalone/src/lib/tools/actions/email.ts +191 -0
- package/templates/nextjs-standalone/src/lib/tools/actions/handler.ts +591 -0
- package/templates/nextjs-standalone/src/lib/tools/actions/index.ts +23 -0
- package/templates/nextjs-standalone/src/lib/tools/actions/jira.ts +220 -0
- package/templates/nextjs-standalone/src/lib/tools/explore-nsjail.ts +343 -0
- package/templates/nextjs-standalone/src/lib/tools/explore-sandbox.ts +264 -0
- package/templates/nextjs-standalone/src/lib/tools/explore-sidecar.ts +163 -0
- package/templates/nextjs-standalone/src/lib/tools/explore.ts +379 -0
- package/templates/nextjs-standalone/src/lib/tools/registry.ts +221 -0
- package/templates/nextjs-standalone/src/lib/tools/salesforce.ts +138 -0
- package/templates/nextjs-standalone/src/lib/tools/soql-validation.ts +172 -0
- package/templates/nextjs-standalone/src/lib/tools/sql.ts +680 -0
- package/templates/nextjs-standalone/src/lib/tracing.ts +40 -0
- package/templates/nextjs-standalone/src/lib/utils.ts +6 -0
- package/templates/nextjs-standalone/src/test-setup.ts +38 -0
- package/templates/nextjs-standalone/src/ui/components/actions/action-approval-card.tsx +295 -0
- package/templates/nextjs-standalone/src/ui/components/actions/action-status-badge.tsx +50 -0
- package/templates/nextjs-standalone/src/ui/components/admin/admin-layout.tsx +26 -0
- package/templates/nextjs-standalone/src/ui/components/admin/admin-sidebar.tsx +96 -0
- package/templates/nextjs-standalone/src/ui/components/admin/empty-state.tsx +24 -0
- package/templates/nextjs-standalone/src/ui/components/admin/entity-detail.tsx +233 -0
- package/templates/nextjs-standalone/src/ui/components/admin/entity-list.tsx +96 -0
- package/templates/nextjs-standalone/src/ui/components/admin/error-banner.tsx +22 -0
- package/templates/nextjs-standalone/src/ui/components/admin/feature-disabled.tsx +44 -0
- package/templates/nextjs-standalone/src/ui/components/admin/health-badge.tsx +30 -0
- package/templates/nextjs-standalone/src/ui/components/admin/loading-state.tsx +14 -0
- package/templates/nextjs-standalone/src/ui/components/admin/stat-card.tsx +32 -0
- package/templates/nextjs-standalone/src/ui/components/atlas-chat.tsx +370 -0
- package/templates/nextjs-standalone/src/ui/components/chart/chart-detection.ts +261 -0
- package/templates/nextjs-standalone/src/ui/components/chart/result-chart.tsx +375 -0
- package/templates/nextjs-standalone/src/ui/components/chat/api-key-bar.tsx +66 -0
- package/templates/nextjs-standalone/src/ui/components/chat/copy-button.tsx +25 -0
- package/templates/nextjs-standalone/src/ui/components/chat/data-table.tsx +102 -0
- package/templates/nextjs-standalone/src/ui/components/chat/error-banner.tsx +32 -0
- package/templates/nextjs-standalone/src/ui/components/chat/explore-card.tsx +41 -0
- package/templates/nextjs-standalone/src/ui/components/chat/loading-card.tsx +10 -0
- package/templates/nextjs-standalone/src/ui/components/chat/managed-auth-card.tsx +116 -0
- package/templates/nextjs-standalone/src/ui/components/chat/markdown.tsx +72 -0
- package/templates/nextjs-standalone/src/ui/components/chat/sql-block.tsx +30 -0
- package/templates/nextjs-standalone/src/ui/components/chat/sql-result-card.tsx +144 -0
- package/templates/nextjs-standalone/src/ui/components/chat/starter-prompts.ts +6 -0
- package/templates/nextjs-standalone/src/ui/components/chat/tool-part.tsx +40 -0
- package/templates/nextjs-standalone/src/ui/components/chat/typing-indicator.tsx +19 -0
- package/templates/nextjs-standalone/src/ui/components/conversations/conversation-item.tsx +120 -0
- package/templates/nextjs-standalone/src/ui/components/conversations/conversation-list.tsx +66 -0
- package/templates/nextjs-standalone/src/ui/components/conversations/conversation-sidebar.tsx +78 -0
- package/templates/nextjs-standalone/src/ui/components/conversations/delete-confirmation.tsx +27 -0
- package/templates/nextjs-standalone/src/ui/context.tsx +78 -0
- package/templates/nextjs-standalone/src/ui/hooks/use-admin-fetch.ts +104 -0
- package/templates/nextjs-standalone/src/ui/hooks/use-conversations.ts +184 -0
- package/templates/nextjs-standalone/src/ui/hooks/use-dark-mode.ts +17 -0
- package/templates/nextjs-standalone/src/ui/lib/action-types.ts +63 -0
- package/templates/nextjs-standalone/src/ui/lib/helpers.ts +104 -0
- package/templates/nextjs-standalone/src/ui/lib/types.ts +145 -0
- package/templates/nextjs-standalone/tsconfig.json +32 -0
- package/templates/nextjs-standalone/vercel.json +4 -0
|
@@ -0,0 +1,757 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Admin console API routes.
|
|
3
|
+
*
|
|
4
|
+
* Mounted at /api/v1/admin. All routes require admin role.
|
|
5
|
+
* Browsing endpoints are read-only; health-check routes (POST) trigger
|
|
6
|
+
* live probes and update cached health status.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import * as fs from "fs";
|
|
10
|
+
import * as path from "path";
|
|
11
|
+
import * as yaml from "js-yaml";
|
|
12
|
+
import { Hono } from "hono";
|
|
13
|
+
import { createLogger, withRequestContext } from "@atlas/api/lib/logger";
|
|
14
|
+
import type { AuthResult } from "@atlas/api/lib/auth/types";
|
|
15
|
+
import {
|
|
16
|
+
authenticateRequest,
|
|
17
|
+
checkRateLimit,
|
|
18
|
+
getClientIP,
|
|
19
|
+
} from "@atlas/api/lib/auth/middleware";
|
|
20
|
+
import { connections } from "@atlas/api/lib/db/connection";
|
|
21
|
+
import { hasInternalDB, internalQuery } from "@atlas/api/lib/db/internal";
|
|
22
|
+
import { plugins } from "@atlas/api/lib/plugins/registry";
|
|
23
|
+
|
|
24
|
+
const log = createLogger("admin-routes");
|
|
25
|
+
|
|
26
|
+
const admin = new Hono();
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Semantic layer root — resolves the semantic/ directory at cwd.
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @internal Exported for testing only. ATLAS_SEMANTIC_ROOT is a test-only
|
|
34
|
+
* env var; in production the semantic root is always resolved from cwd.
|
|
35
|
+
*/
|
|
36
|
+
export function getSemanticRoot(): string {
|
|
37
|
+
return process.env.ATLAS_SEMANTIC_ROOT ?? path.resolve(process.cwd(), "semantic");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Admin auth preamble — reuses existing auth then enforces admin role.
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Authenticate the request and enforce admin role. Returns either:
|
|
46
|
+
* - `{ error, status, headers? }` on failure (401/403/429/500)
|
|
47
|
+
* - `{ authResult }` on success (authenticated admin user)
|
|
48
|
+
*
|
|
49
|
+
* The `headers` field is only present for 429 rate-limit responses.
|
|
50
|
+
*/
|
|
51
|
+
async function adminAuthPreamble(req: Request, requestId: string) {
|
|
52
|
+
let authResult: AuthResult;
|
|
53
|
+
try {
|
|
54
|
+
authResult = await authenticateRequest(req);
|
|
55
|
+
} catch (err) {
|
|
56
|
+
log.error(
|
|
57
|
+
{ err: err instanceof Error ? err : new Error(String(err)), requestId },
|
|
58
|
+
"Auth dispatch failed",
|
|
59
|
+
);
|
|
60
|
+
return { error: { error: "auth_error", message: "Authentication system error" }, status: 500 as const };
|
|
61
|
+
}
|
|
62
|
+
if (!authResult.authenticated) {
|
|
63
|
+
log.warn({ requestId, status: authResult.status }, "Authentication failed");
|
|
64
|
+
return { error: { error: "auth_error", message: authResult.error }, status: authResult.status as 401 | 403 | 500 };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Enforce admin role
|
|
68
|
+
if (!authResult.user || authResult.user.role !== "admin") {
|
|
69
|
+
return { error: { error: "forbidden", message: "Admin role required." }, status: 403 as const };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const ip = getClientIP(req);
|
|
73
|
+
const rateLimitKey = authResult.user.id ?? (ip ? `ip:${ip}` : "anon");
|
|
74
|
+
const rateCheck = checkRateLimit(rateLimitKey);
|
|
75
|
+
if (!rateCheck.allowed) {
|
|
76
|
+
const retryAfterSeconds = Math.ceil((rateCheck.retryAfterMs ?? 60000) / 1000);
|
|
77
|
+
return {
|
|
78
|
+
error: { error: "rate_limited", message: "Too many requests. Please wait before trying again.", retryAfterSeconds },
|
|
79
|
+
status: 429 as const,
|
|
80
|
+
headers: { "Retry-After": String(retryAfterSeconds) },
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return { authResult };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
// Path traversal guard
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
/** Reject entity names that could escape the semantic root. */
|
|
92
|
+
function isValidEntityName(name: string): boolean {
|
|
93
|
+
return !!(
|
|
94
|
+
name &&
|
|
95
|
+
!name.includes("/") &&
|
|
96
|
+
!name.includes("\\") &&
|
|
97
|
+
!name.includes("..") &&
|
|
98
|
+
!name.includes("\0")
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
// YAML reading helpers
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
interface EntitySummary {
|
|
107
|
+
table: string;
|
|
108
|
+
description: string;
|
|
109
|
+
columnCount: number;
|
|
110
|
+
joinCount: number;
|
|
111
|
+
measureCount: number;
|
|
112
|
+
connection: string | null;
|
|
113
|
+
type: string | null;
|
|
114
|
+
source: string;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function readYamlFile(filePath: string): unknown {
|
|
118
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
119
|
+
return yaml.load(content);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Discover all entity YAML files from semantic/entities/ and
|
|
124
|
+
* semantic/{source}/entities/. Entities in the top-level entities/
|
|
125
|
+
* directory are tagged with source "default"; those under
|
|
126
|
+
* semantic/{name}/entities/ use the subdirectory name as source.
|
|
127
|
+
*/
|
|
128
|
+
function discoverEntities(root: string): EntitySummary[] {
|
|
129
|
+
const entities: EntitySummary[] = [];
|
|
130
|
+
|
|
131
|
+
const defaultDir = path.join(root, "entities");
|
|
132
|
+
if (fs.existsSync(defaultDir)) {
|
|
133
|
+
loadEntitiesFromDir(defaultDir, "default", entities);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Per-source subdirectories
|
|
137
|
+
const RESERVED_DIRS = new Set(["entities", "metrics"]);
|
|
138
|
+
if (fs.existsSync(root)) {
|
|
139
|
+
try {
|
|
140
|
+
const entries = fs.readdirSync(root, { withFileTypes: true });
|
|
141
|
+
for (const entry of entries) {
|
|
142
|
+
if (!entry.isDirectory() || RESERVED_DIRS.has(entry.name)) continue;
|
|
143
|
+
const subEntities = path.join(root, entry.name, "entities");
|
|
144
|
+
if (fs.existsSync(subEntities)) {
|
|
145
|
+
loadEntitiesFromDir(subEntities, entry.name, entities);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
} catch (err) {
|
|
149
|
+
log.warn({ err: err instanceof Error ? err : new Error(String(err)), root }, "Failed to scan semantic root for per-source directories");
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return entities;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function loadEntitiesFromDir(dir: string, source: string, out: EntitySummary[]): void {
|
|
157
|
+
let files: string[];
|
|
158
|
+
try {
|
|
159
|
+
files = fs.readdirSync(dir).filter((f) => f.endsWith(".yml"));
|
|
160
|
+
} catch (err) {
|
|
161
|
+
log.warn({ err: err instanceof Error ? err : new Error(String(err)), dir, source }, "Failed to read entities directory");
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
for (const file of files) {
|
|
166
|
+
try {
|
|
167
|
+
const raw = readYamlFile(path.join(dir, file)) as Record<string, unknown>;
|
|
168
|
+
if (!raw || typeof raw !== "object" || !raw.table) continue;
|
|
169
|
+
|
|
170
|
+
const dimensions = raw.dimensions && typeof raw.dimensions === "object"
|
|
171
|
+
? Object.keys(raw.dimensions)
|
|
172
|
+
: [];
|
|
173
|
+
const joins = Array.isArray(raw.joins) ? raw.joins : (raw.joins && typeof raw.joins === "object" ? Object.keys(raw.joins) : []);
|
|
174
|
+
const measures = Array.isArray(raw.measures) ? raw.measures : (raw.measures && typeof raw.measures === "object" ? Object.keys(raw.measures) : []);
|
|
175
|
+
|
|
176
|
+
out.push({
|
|
177
|
+
table: String(raw.table),
|
|
178
|
+
description: typeof raw.description === "string" ? raw.description : "",
|
|
179
|
+
columnCount: dimensions.length,
|
|
180
|
+
joinCount: Array.isArray(joins) ? joins.length : 0,
|
|
181
|
+
measureCount: Array.isArray(measures) ? measures.length : 0,
|
|
182
|
+
connection: typeof raw.connection === "string" ? raw.connection : null,
|
|
183
|
+
type: typeof raw.type === "string" ? raw.type : null,
|
|
184
|
+
source,
|
|
185
|
+
});
|
|
186
|
+
} catch (err) {
|
|
187
|
+
log.warn({ err: err instanceof Error ? err : new Error(String(err)), file, dir, source }, "Failed to parse entity YAML file");
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Find a specific entity YAML file by table name. Searches all entity
|
|
194
|
+
* directories. Caller must validate `name` with isValidEntityName() first.
|
|
195
|
+
*/
|
|
196
|
+
function findEntityFile(root: string, name: string): string | null {
|
|
197
|
+
const defaultDir = path.join(root, "entities");
|
|
198
|
+
const defaultFile = path.join(defaultDir, `${name}.yml`);
|
|
199
|
+
if (fs.existsSync(defaultFile)) return defaultFile;
|
|
200
|
+
|
|
201
|
+
// Search per-source subdirectories
|
|
202
|
+
const RESERVED_DIRS = new Set(["entities", "metrics"]);
|
|
203
|
+
if (fs.existsSync(root)) {
|
|
204
|
+
try {
|
|
205
|
+
const entries = fs.readdirSync(root, { withFileTypes: true });
|
|
206
|
+
for (const entry of entries) {
|
|
207
|
+
if (!entry.isDirectory() || RESERVED_DIRS.has(entry.name)) continue;
|
|
208
|
+
const subFile = path.join(root, entry.name, "entities", `${name}.yml`);
|
|
209
|
+
if (fs.existsSync(subFile)) return subFile;
|
|
210
|
+
}
|
|
211
|
+
} catch (err) {
|
|
212
|
+
log.warn({ err: err instanceof Error ? err : new Error(String(err)), root, name }, "Failed to scan subdirectories for entity file");
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function discoverMetrics(root: string): Array<{ source: string; data: unknown }> {
|
|
220
|
+
const metrics: Array<{ source: string; data: unknown }> = [];
|
|
221
|
+
|
|
222
|
+
const defaultDir = path.join(root, "metrics");
|
|
223
|
+
if (fs.existsSync(defaultDir)) {
|
|
224
|
+
loadMetricsFromDir(defaultDir, "default", metrics);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const RESERVED_DIRS = new Set(["entities", "metrics"]);
|
|
228
|
+
if (fs.existsSync(root)) {
|
|
229
|
+
try {
|
|
230
|
+
const entries = fs.readdirSync(root, { withFileTypes: true });
|
|
231
|
+
for (const entry of entries) {
|
|
232
|
+
if (!entry.isDirectory() || RESERVED_DIRS.has(entry.name)) continue;
|
|
233
|
+
const subMetrics = path.join(root, entry.name, "metrics");
|
|
234
|
+
if (fs.existsSync(subMetrics)) {
|
|
235
|
+
loadMetricsFromDir(subMetrics, entry.name, metrics);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
} catch (err) {
|
|
239
|
+
log.warn({ err: err instanceof Error ? err : new Error(String(err)), root }, "Failed to scan semantic root for per-source metrics");
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return metrics;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function loadMetricsFromDir(dir: string, source: string, out: Array<{ source: string; data: unknown }>): void {
|
|
247
|
+
let files: string[];
|
|
248
|
+
try {
|
|
249
|
+
files = fs.readdirSync(dir).filter((f) => f.endsWith(".yml"));
|
|
250
|
+
} catch (err) {
|
|
251
|
+
log.warn({ err: err instanceof Error ? err : new Error(String(err)), dir, source }, "Failed to read metrics directory");
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
for (const file of files) {
|
|
256
|
+
try {
|
|
257
|
+
const raw = readYamlFile(path.join(dir, file));
|
|
258
|
+
out.push({ source, data: raw });
|
|
259
|
+
} catch (err) {
|
|
260
|
+
log.warn({ err: err instanceof Error ? err : new Error(String(err)), file, dir, source }, "Failed to parse metric YAML file");
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Load glossary from semantic/glossary.yml and per-source glossaries.
|
|
267
|
+
*/
|
|
268
|
+
function loadGlossary(root: string): unknown[] {
|
|
269
|
+
const glossaries: unknown[] = [];
|
|
270
|
+
|
|
271
|
+
const defaultFile = path.join(root, "glossary.yml");
|
|
272
|
+
if (fs.existsSync(defaultFile)) {
|
|
273
|
+
try {
|
|
274
|
+
glossaries.push({ source: "default", data: readYamlFile(defaultFile) });
|
|
275
|
+
} catch (err) {
|
|
276
|
+
log.warn({ err: err instanceof Error ? err : new Error(String(err)), file: defaultFile }, "Failed to parse glossary YAML");
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const RESERVED_DIRS = new Set(["entities", "metrics"]);
|
|
281
|
+
if (fs.existsSync(root)) {
|
|
282
|
+
try {
|
|
283
|
+
const entries = fs.readdirSync(root, { withFileTypes: true });
|
|
284
|
+
for (const entry of entries) {
|
|
285
|
+
if (!entry.isDirectory() || RESERVED_DIRS.has(entry.name)) continue;
|
|
286
|
+
const subGlossary = path.join(root, entry.name, "glossary.yml");
|
|
287
|
+
if (fs.existsSync(subGlossary)) {
|
|
288
|
+
try {
|
|
289
|
+
glossaries.push({ source: entry.name, data: readYamlFile(subGlossary) });
|
|
290
|
+
} catch (err) {
|
|
291
|
+
log.warn({ err: err instanceof Error ? err : new Error(String(err)), file: subGlossary, source: entry.name }, "Failed to parse per-source glossary YAML");
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
} catch (err) {
|
|
296
|
+
log.warn({ err: err instanceof Error ? err : new Error(String(err)), root }, "Failed to scan semantic root for per-source glossaries");
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return glossaries;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// ---------------------------------------------------------------------------
|
|
304
|
+
// GET /overview — Dashboard data
|
|
305
|
+
// ---------------------------------------------------------------------------
|
|
306
|
+
|
|
307
|
+
admin.get("/overview", async (c) => {
|
|
308
|
+
const req = c.req.raw;
|
|
309
|
+
const requestId = crypto.randomUUID();
|
|
310
|
+
|
|
311
|
+
const preamble = await adminAuthPreamble(req, requestId);
|
|
312
|
+
if ("error" in preamble) {
|
|
313
|
+
return c.json(preamble.error, { status: preamble.status, headers: preamble.headers });
|
|
314
|
+
}
|
|
315
|
+
const { authResult } = preamble;
|
|
316
|
+
|
|
317
|
+
return withRequestContext({ requestId, user: authResult.user }, () => {
|
|
318
|
+
const root = getSemanticRoot();
|
|
319
|
+
const entities = discoverEntities(root);
|
|
320
|
+
const metrics = discoverMetrics(root);
|
|
321
|
+
const glossary = loadGlossary(root);
|
|
322
|
+
const connList = connections.describe();
|
|
323
|
+
const pluginList = plugins.describe();
|
|
324
|
+
|
|
325
|
+
// Count glossary terms
|
|
326
|
+
let glossaryTermCount = 0;
|
|
327
|
+
for (const g of glossary) {
|
|
328
|
+
const data = (g as { data: unknown }).data;
|
|
329
|
+
if (Array.isArray(data)) glossaryTermCount += data.length;
|
|
330
|
+
else if (data && typeof data === "object") {
|
|
331
|
+
const terms = (data as Record<string, unknown>).terms;
|
|
332
|
+
if (Array.isArray(terms)) glossaryTermCount += terms.length;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return c.json({
|
|
337
|
+
connections: connList.length,
|
|
338
|
+
entities: entities.length,
|
|
339
|
+
metrics: metrics.length,
|
|
340
|
+
glossaryTerms: glossaryTermCount,
|
|
341
|
+
plugins: pluginList.length,
|
|
342
|
+
pluginHealth: pluginList.map((p) => ({
|
|
343
|
+
id: p.id,
|
|
344
|
+
name: p.name,
|
|
345
|
+
type: p.type,
|
|
346
|
+
status: p.status,
|
|
347
|
+
})),
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
// ---------------------------------------------------------------------------
|
|
353
|
+
// Semantic Layer routes
|
|
354
|
+
// ---------------------------------------------------------------------------
|
|
355
|
+
|
|
356
|
+
// GET /semantic/entities — list all entities
|
|
357
|
+
admin.get("/semantic/entities", async (c) => {
|
|
358
|
+
const req = c.req.raw;
|
|
359
|
+
const requestId = crypto.randomUUID();
|
|
360
|
+
|
|
361
|
+
const preamble = await adminAuthPreamble(req, requestId);
|
|
362
|
+
if ("error" in preamble) {
|
|
363
|
+
return c.json(preamble.error, { status: preamble.status, headers: preamble.headers });
|
|
364
|
+
}
|
|
365
|
+
const { authResult } = preamble;
|
|
366
|
+
|
|
367
|
+
return withRequestContext({ requestId, user: authResult.user }, () => {
|
|
368
|
+
const root = getSemanticRoot();
|
|
369
|
+
const entities = discoverEntities(root);
|
|
370
|
+
return c.json({ entities });
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
// GET /semantic/entities/:name — full entity detail
|
|
375
|
+
admin.get("/semantic/entities/:name", async (c) => {
|
|
376
|
+
const req = c.req.raw;
|
|
377
|
+
const requestId = crypto.randomUUID();
|
|
378
|
+
|
|
379
|
+
const preamble = await adminAuthPreamble(req, requestId);
|
|
380
|
+
if ("error" in preamble) {
|
|
381
|
+
return c.json(preamble.error, { status: preamble.status, headers: preamble.headers });
|
|
382
|
+
}
|
|
383
|
+
const { authResult } = preamble;
|
|
384
|
+
|
|
385
|
+
return withRequestContext({ requestId, user: authResult.user }, () => {
|
|
386
|
+
const name = c.req.param("name");
|
|
387
|
+
|
|
388
|
+
// Path traversal protection
|
|
389
|
+
if (!isValidEntityName(name)) {
|
|
390
|
+
log.warn({ requestId, name }, "Rejected invalid entity name");
|
|
391
|
+
return c.json({ error: "invalid_request", message: "Invalid entity name." }, 400);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const root = getSemanticRoot();
|
|
395
|
+
const filePath = findEntityFile(root, name);
|
|
396
|
+
if (!filePath) {
|
|
397
|
+
return c.json({ error: "not_found", message: `Entity "${name}" not found.` }, 404);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Defense-in-depth: verify resolved path is within semantic root
|
|
401
|
+
const resolved = path.resolve(filePath);
|
|
402
|
+
if (!resolved.startsWith(path.resolve(root))) {
|
|
403
|
+
log.error({ requestId, name, resolved, root }, "Resolved entity path escaped semantic root");
|
|
404
|
+
return c.json({ error: "forbidden", message: "Access denied." }, 403);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
try {
|
|
408
|
+
const raw = readYamlFile(filePath);
|
|
409
|
+
return c.json({ entity: raw });
|
|
410
|
+
} catch (err) {
|
|
411
|
+
log.error({ err: err instanceof Error ? err : new Error(String(err)), filePath, entityName: name }, "Failed to parse entity YAML file");
|
|
412
|
+
return c.json({ error: "internal_error", message: `Failed to parse entity file for "${name}".` }, 500);
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
// GET /semantic/metrics — list all metrics
|
|
418
|
+
admin.get("/semantic/metrics", async (c) => {
|
|
419
|
+
const req = c.req.raw;
|
|
420
|
+
const requestId = crypto.randomUUID();
|
|
421
|
+
|
|
422
|
+
const preamble = await adminAuthPreamble(req, requestId);
|
|
423
|
+
if ("error" in preamble) {
|
|
424
|
+
return c.json(preamble.error, { status: preamble.status, headers: preamble.headers });
|
|
425
|
+
}
|
|
426
|
+
const { authResult } = preamble;
|
|
427
|
+
|
|
428
|
+
return withRequestContext({ requestId, user: authResult.user }, () => {
|
|
429
|
+
const root = getSemanticRoot();
|
|
430
|
+
const metrics = discoverMetrics(root);
|
|
431
|
+
return c.json({ metrics });
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
// GET /semantic/glossary
|
|
436
|
+
admin.get("/semantic/glossary", async (c) => {
|
|
437
|
+
const req = c.req.raw;
|
|
438
|
+
const requestId = crypto.randomUUID();
|
|
439
|
+
|
|
440
|
+
const preamble = await adminAuthPreamble(req, requestId);
|
|
441
|
+
if ("error" in preamble) {
|
|
442
|
+
return c.json(preamble.error, { status: preamble.status, headers: preamble.headers });
|
|
443
|
+
}
|
|
444
|
+
const { authResult } = preamble;
|
|
445
|
+
|
|
446
|
+
return withRequestContext({ requestId, user: authResult.user }, () => {
|
|
447
|
+
const root = getSemanticRoot();
|
|
448
|
+
const glossary = loadGlossary(root);
|
|
449
|
+
return c.json({ glossary });
|
|
450
|
+
});
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
// GET /semantic/catalog
|
|
454
|
+
admin.get("/semantic/catalog", async (c) => {
|
|
455
|
+
const req = c.req.raw;
|
|
456
|
+
const requestId = crypto.randomUUID();
|
|
457
|
+
|
|
458
|
+
const preamble = await adminAuthPreamble(req, requestId);
|
|
459
|
+
if ("error" in preamble) {
|
|
460
|
+
return c.json(preamble.error, { status: preamble.status, headers: preamble.headers });
|
|
461
|
+
}
|
|
462
|
+
const { authResult } = preamble;
|
|
463
|
+
|
|
464
|
+
return withRequestContext({ requestId, user: authResult.user }, () => {
|
|
465
|
+
const root = getSemanticRoot();
|
|
466
|
+
const catalogFile = path.join(root, "catalog.yml");
|
|
467
|
+
if (!fs.existsSync(catalogFile)) {
|
|
468
|
+
return c.json({ catalog: null });
|
|
469
|
+
}
|
|
470
|
+
try {
|
|
471
|
+
const raw = readYamlFile(catalogFile);
|
|
472
|
+
return c.json({ catalog: raw });
|
|
473
|
+
} catch (err) {
|
|
474
|
+
log.error({ err: err instanceof Error ? err : new Error(String(err)), file: catalogFile }, "Failed to parse catalog YAML");
|
|
475
|
+
return c.json({ error: "internal_error", message: "Failed to parse catalog file." }, 500);
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
// GET /semantic/stats — aggregate stats
|
|
481
|
+
admin.get("/semantic/stats", async (c) => {
|
|
482
|
+
const req = c.req.raw;
|
|
483
|
+
const requestId = crypto.randomUUID();
|
|
484
|
+
|
|
485
|
+
const preamble = await adminAuthPreamble(req, requestId);
|
|
486
|
+
if ("error" in preamble) {
|
|
487
|
+
return c.json(preamble.error, { status: preamble.status, headers: preamble.headers });
|
|
488
|
+
}
|
|
489
|
+
const { authResult } = preamble;
|
|
490
|
+
|
|
491
|
+
return withRequestContext({ requestId, user: authResult.user }, () => {
|
|
492
|
+
const root = getSemanticRoot();
|
|
493
|
+
const entities = discoverEntities(root);
|
|
494
|
+
|
|
495
|
+
const totalColumns = entities.reduce((sum, e) => sum + e.columnCount, 0);
|
|
496
|
+
const totalJoins = entities.reduce((sum, e) => sum + e.joinCount, 0);
|
|
497
|
+
const totalMeasures = entities.reduce((sum, e) => sum + e.measureCount, 0);
|
|
498
|
+
|
|
499
|
+
const noDescription = entities.filter((e) => !e.description.trim()).length;
|
|
500
|
+
const noColumns = entities.filter((e) => e.columnCount === 0).length;
|
|
501
|
+
const noJoins = entities.filter((e) => e.joinCount === 0).length;
|
|
502
|
+
|
|
503
|
+
return c.json({
|
|
504
|
+
totalEntities: entities.length,
|
|
505
|
+
totalColumns,
|
|
506
|
+
totalJoins,
|
|
507
|
+
totalMeasures,
|
|
508
|
+
coverageGaps: {
|
|
509
|
+
noDescription,
|
|
510
|
+
noColumns,
|
|
511
|
+
noJoins,
|
|
512
|
+
},
|
|
513
|
+
});
|
|
514
|
+
});
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
// ---------------------------------------------------------------------------
|
|
518
|
+
// Connection routes
|
|
519
|
+
// ---------------------------------------------------------------------------
|
|
520
|
+
|
|
521
|
+
// GET /connections — list connections
|
|
522
|
+
admin.get("/connections", async (c) => {
|
|
523
|
+
const req = c.req.raw;
|
|
524
|
+
const requestId = crypto.randomUUID();
|
|
525
|
+
|
|
526
|
+
const preamble = await adminAuthPreamble(req, requestId);
|
|
527
|
+
if ("error" in preamble) {
|
|
528
|
+
return c.json(preamble.error, { status: preamble.status, headers: preamble.headers });
|
|
529
|
+
}
|
|
530
|
+
const { authResult } = preamble;
|
|
531
|
+
|
|
532
|
+
return withRequestContext({ requestId, user: authResult.user }, () => {
|
|
533
|
+
const connList = connections.describe();
|
|
534
|
+
return c.json({ connections: connList });
|
|
535
|
+
});
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
// POST /connections/:id/test — health check a connection
|
|
539
|
+
admin.post("/connections/:id/test", async (c) => {
|
|
540
|
+
const req = c.req.raw;
|
|
541
|
+
const requestId = crypto.randomUUID();
|
|
542
|
+
|
|
543
|
+
const preamble = await adminAuthPreamble(req, requestId);
|
|
544
|
+
if ("error" in preamble) {
|
|
545
|
+
return c.json(preamble.error, { status: preamble.status, headers: preamble.headers });
|
|
546
|
+
}
|
|
547
|
+
const { authResult } = preamble;
|
|
548
|
+
|
|
549
|
+
return withRequestContext({ requestId, user: authResult.user }, async () => {
|
|
550
|
+
const id = c.req.param("id");
|
|
551
|
+
const registered = connections.list();
|
|
552
|
+
if (!registered.includes(id)) {
|
|
553
|
+
return c.json({ error: "not_found", message: `Connection "${id}" not found.` }, 404);
|
|
554
|
+
}
|
|
555
|
+
try {
|
|
556
|
+
const result = await connections.healthCheck(id);
|
|
557
|
+
return c.json(result);
|
|
558
|
+
} catch (err) {
|
|
559
|
+
log.error({ err: err instanceof Error ? err : new Error(String(err)), connectionId: id }, "Health check failed");
|
|
560
|
+
return c.json({ error: "internal_error", message: "Health check failed." }, 500);
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
// ---------------------------------------------------------------------------
|
|
566
|
+
// Audit routes
|
|
567
|
+
// ---------------------------------------------------------------------------
|
|
568
|
+
|
|
569
|
+
// GET /audit — query audit_log (paginated)
|
|
570
|
+
admin.get("/audit", async (c) => {
|
|
571
|
+
const req = c.req.raw;
|
|
572
|
+
const requestId = crypto.randomUUID();
|
|
573
|
+
|
|
574
|
+
// Auth before feature-availability check to avoid info disclosure
|
|
575
|
+
const preamble = await adminAuthPreamble(req, requestId);
|
|
576
|
+
if ("error" in preamble) {
|
|
577
|
+
return c.json(preamble.error, { status: preamble.status, headers: preamble.headers });
|
|
578
|
+
}
|
|
579
|
+
const { authResult } = preamble;
|
|
580
|
+
|
|
581
|
+
if (!hasInternalDB()) {
|
|
582
|
+
return c.json({ error: "not_available", message: "Audit log requires an internal database." }, 404);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
return withRequestContext({ requestId, user: authResult.user }, async () => {
|
|
586
|
+
const rawLimit = parseInt(c.req.query("limit") ?? "50", 10);
|
|
587
|
+
const rawOffset = parseInt(c.req.query("offset") ?? "0", 10);
|
|
588
|
+
const limit = Number.isFinite(rawLimit) && rawLimit > 0 ? Math.min(rawLimit, 200) : 50;
|
|
589
|
+
const offset = Number.isFinite(rawOffset) && rawOffset >= 0 ? rawOffset : 0;
|
|
590
|
+
|
|
591
|
+
// Queries the internal DB directly (not the analytics datasource),
|
|
592
|
+
// so no validateSQL pipeline needed. Parameterized queries prevent injection.
|
|
593
|
+
const conditions: string[] = [];
|
|
594
|
+
const params: unknown[] = [];
|
|
595
|
+
let paramIdx = 1;
|
|
596
|
+
|
|
597
|
+
const user = c.req.query("user");
|
|
598
|
+
if (user) {
|
|
599
|
+
conditions.push(`user_id = $${paramIdx++}`);
|
|
600
|
+
params.push(user);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const success = c.req.query("success");
|
|
604
|
+
if (success === "true" || success === "false") {
|
|
605
|
+
conditions.push(`success = $${paramIdx++}`);
|
|
606
|
+
params.push(success === "true");
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
const from = c.req.query("from");
|
|
610
|
+
if (from) {
|
|
611
|
+
if (isNaN(Date.parse(from))) {
|
|
612
|
+
return c.json({ error: "invalid_request", message: `Invalid 'from' date format: "${from}". Use ISO 8601 (e.g. 2026-01-01).` }, 400);
|
|
613
|
+
}
|
|
614
|
+
conditions.push(`timestamp >= $${paramIdx++}`);
|
|
615
|
+
params.push(from);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
const to = c.req.query("to");
|
|
619
|
+
if (to) {
|
|
620
|
+
if (isNaN(Date.parse(to))) {
|
|
621
|
+
return c.json({ error: "invalid_request", message: `Invalid 'to' date format: "${to}". Use ISO 8601 (e.g. 2026-03-03).` }, 400);
|
|
622
|
+
}
|
|
623
|
+
conditions.push(`timestamp <= $${paramIdx++}`);
|
|
624
|
+
params.push(to);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
628
|
+
|
|
629
|
+
try {
|
|
630
|
+
const countResult = await internalQuery<{ count: string }>(
|
|
631
|
+
`SELECT COUNT(*) as count FROM audit_log ${whereClause}`,
|
|
632
|
+
params,
|
|
633
|
+
);
|
|
634
|
+
const total = parseInt(String(countResult[0]?.count ?? "0"), 10);
|
|
635
|
+
|
|
636
|
+
const rows = await internalQuery(
|
|
637
|
+
`SELECT * FROM audit_log ${whereClause} ORDER BY timestamp DESC LIMIT $${paramIdx++} OFFSET $${paramIdx++}`,
|
|
638
|
+
[...params, limit, offset],
|
|
639
|
+
);
|
|
640
|
+
|
|
641
|
+
return c.json({ rows, total, limit, offset });
|
|
642
|
+
} catch (err) {
|
|
643
|
+
log.error({ err: err instanceof Error ? err : new Error(String(err)) }, "Audit query failed");
|
|
644
|
+
return c.json({ error: "internal_error", message: "Failed to query audit log." }, 500);
|
|
645
|
+
}
|
|
646
|
+
});
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
// GET /audit/stats — aggregate audit stats
|
|
650
|
+
admin.get("/audit/stats", async (c) => {
|
|
651
|
+
const req = c.req.raw;
|
|
652
|
+
const requestId = crypto.randomUUID();
|
|
653
|
+
|
|
654
|
+
// Auth before feature-availability check to avoid info disclosure
|
|
655
|
+
const preamble = await adminAuthPreamble(req, requestId);
|
|
656
|
+
if ("error" in preamble) {
|
|
657
|
+
return c.json(preamble.error, { status: preamble.status, headers: preamble.headers });
|
|
658
|
+
}
|
|
659
|
+
const { authResult } = preamble;
|
|
660
|
+
|
|
661
|
+
if (!hasInternalDB()) {
|
|
662
|
+
return c.json({ error: "not_available", message: "Audit log requires an internal database." }, 404);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
return withRequestContext({ requestId, user: authResult.user }, async () => {
|
|
666
|
+
try {
|
|
667
|
+
const totalResult = await internalQuery<{ total: string; errors: string }>(
|
|
668
|
+
`SELECT COUNT(*) as total, COUNT(*) FILTER (WHERE NOT success) as errors FROM audit_log`,
|
|
669
|
+
);
|
|
670
|
+
|
|
671
|
+
const total = parseInt(String(totalResult[0]?.total ?? "0"), 10);
|
|
672
|
+
const errors = parseInt(String(totalResult[0]?.errors ?? "0"), 10);
|
|
673
|
+
const errorRate = total > 0 ? errors / total : 0;
|
|
674
|
+
|
|
675
|
+
const dailyResult = await internalQuery<{ day: string; count: string }>(
|
|
676
|
+
`SELECT DATE(timestamp) as day, COUNT(*) as count FROM audit_log WHERE timestamp >= NOW() - INTERVAL '7 days' GROUP BY DATE(timestamp) ORDER BY day DESC`,
|
|
677
|
+
);
|
|
678
|
+
|
|
679
|
+
return c.json({
|
|
680
|
+
totalQueries: total,
|
|
681
|
+
totalErrors: errors,
|
|
682
|
+
errorRate,
|
|
683
|
+
queriesPerDay: dailyResult.map((r) => ({
|
|
684
|
+
day: r.day,
|
|
685
|
+
count: parseInt(String(r.count), 10),
|
|
686
|
+
})),
|
|
687
|
+
});
|
|
688
|
+
} catch (err) {
|
|
689
|
+
log.error({ err: err instanceof Error ? err : new Error(String(err)) }, "Audit stats query failed");
|
|
690
|
+
return c.json({ error: "internal_error", message: "Failed to query audit stats." }, 500);
|
|
691
|
+
}
|
|
692
|
+
});
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
// ---------------------------------------------------------------------------
|
|
696
|
+
// Plugin routes
|
|
697
|
+
// ---------------------------------------------------------------------------
|
|
698
|
+
|
|
699
|
+
// GET /plugins — list installed plugins
|
|
700
|
+
admin.get("/plugins", async (c) => {
|
|
701
|
+
const req = c.req.raw;
|
|
702
|
+
const requestId = crypto.randomUUID();
|
|
703
|
+
|
|
704
|
+
const preamble = await adminAuthPreamble(req, requestId);
|
|
705
|
+
if ("error" in preamble) {
|
|
706
|
+
return c.json(preamble.error, { status: preamble.status, headers: preamble.headers });
|
|
707
|
+
}
|
|
708
|
+
const { authResult } = preamble;
|
|
709
|
+
|
|
710
|
+
return withRequestContext({ requestId, user: authResult.user }, () => {
|
|
711
|
+
const pluginList = plugins.describe();
|
|
712
|
+
return c.json({ plugins: pluginList });
|
|
713
|
+
});
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
// POST /plugins/:id/health — trigger health check
|
|
717
|
+
admin.post("/plugins/:id/health", async (c) => {
|
|
718
|
+
const req = c.req.raw;
|
|
719
|
+
const requestId = crypto.randomUUID();
|
|
720
|
+
|
|
721
|
+
const preamble = await adminAuthPreamble(req, requestId);
|
|
722
|
+
if ("error" in preamble) {
|
|
723
|
+
return c.json(preamble.error, { status: preamble.status, headers: preamble.headers });
|
|
724
|
+
}
|
|
725
|
+
const { authResult } = preamble;
|
|
726
|
+
|
|
727
|
+
return withRequestContext({ requestId, user: authResult.user }, async () => {
|
|
728
|
+
const id = c.req.param("id");
|
|
729
|
+
const plugin = plugins.get(id);
|
|
730
|
+
if (!plugin) {
|
|
731
|
+
return c.json({ error: "not_found", message: `Plugin "${id}" not found.` }, 404);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
if (!plugin.healthCheck) {
|
|
735
|
+
return c.json({
|
|
736
|
+
healthy: true,
|
|
737
|
+
message: "Plugin does not implement healthCheck.",
|
|
738
|
+
status: plugins.getStatus(id),
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
try {
|
|
743
|
+
const result = await plugin.healthCheck();
|
|
744
|
+
return c.json({ ...result, status: plugins.getStatus(id) });
|
|
745
|
+
} catch (err) {
|
|
746
|
+
log.error({ err: err instanceof Error ? err : new Error(String(err)), pluginId: id }, "Plugin health check threw an exception");
|
|
747
|
+
return c.json({
|
|
748
|
+
error: "internal_error",
|
|
749
|
+
healthy: false,
|
|
750
|
+
message: "Plugin health check failed unexpectedly.",
|
|
751
|
+
status: plugins.getStatus(id),
|
|
752
|
+
}, 500);
|
|
753
|
+
}
|
|
754
|
+
});
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
export { admin };
|