@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.
Files changed (515) hide show
  1. package/README.md +231 -0
  2. package/index.ts +829 -0
  3. package/package.json +38 -0
  4. package/templates/docker/.env.example +67 -0
  5. package/templates/docker/Dockerfile +52 -0
  6. package/templates/docker/bin/__tests__/benchmark.test.ts +598 -0
  7. package/templates/docker/bin/__tests__/duckdb-ingest.test.ts +171 -0
  8. package/templates/docker/bin/__tests__/eval.test.ts +434 -0
  9. package/templates/docker/bin/__tests__/matview-partition.test.ts +615 -0
  10. package/templates/docker/bin/__tests__/multi-source.test.ts +113 -0
  11. package/templates/docker/bin/__tests__/plugin-cli.test.ts +322 -0
  12. package/templates/docker/bin/__tests__/profiler-heuristics.test.ts +608 -0
  13. package/templates/docker/bin/__tests__/query.test.ts +240 -0
  14. package/templates/docker/bin/__tests__/schema-drift.test.ts +542 -0
  15. package/templates/docker/bin/__tests__/view-yaml-generation.test.ts +146 -0
  16. package/templates/docker/bin/atlas.ts +5044 -0
  17. package/templates/docker/bin/benchmark.ts +695 -0
  18. package/templates/docker/bin/enrich.ts +559 -0
  19. package/templates/docker/bin/eval.ts +770 -0
  20. package/templates/docker/bin/smoke.ts +438 -0
  21. package/templates/docker/data/.gitkeep +0 -0
  22. package/templates/docker/data/cybersec.sql +1961 -0
  23. package/templates/docker/data/demo-semantic/catalog.yml +40 -0
  24. package/templates/docker/data/demo-semantic/entities/accounts.yml +170 -0
  25. package/templates/docker/data/demo-semantic/entities/companies.yml +207 -0
  26. package/templates/docker/data/demo-semantic/entities/people.yml +145 -0
  27. package/templates/docker/data/demo-semantic/glossary.yml +22 -0
  28. package/templates/docker/data/demo-semantic/metrics/accounts.yml +38 -0
  29. package/templates/docker/data/demo-semantic/metrics/companies.yml +89 -0
  30. package/templates/docker/data/demo.sql +373 -0
  31. package/templates/docker/data/ecommerce.sql +1690 -0
  32. package/templates/docker/data/init-demo-db.sql +8 -0
  33. package/templates/docker/docker-compose.yml +34 -0
  34. package/templates/docker/docs/deploy.md +390 -0
  35. package/templates/docker/eslint.config.mjs +18 -0
  36. package/templates/docker/gitignore +5 -0
  37. package/templates/docker/next.config.ts +9 -0
  38. package/templates/docker/package.json +59 -0
  39. package/templates/docker/postcss.config.mjs +8 -0
  40. package/templates/docker/public/.gitkeep +0 -0
  41. package/templates/docker/public/favicon.svg +4 -0
  42. package/templates/docker/railway.json +13 -0
  43. package/templates/docker/render.yaml +34 -0
  44. package/templates/docker/semantic/catalog.yml +5 -0
  45. package/templates/docker/semantic/entities/.gitkeep +0 -0
  46. package/templates/docker/semantic/glossary.yml +6 -0
  47. package/templates/docker/semantic/metrics/.gitkeep +0 -0
  48. package/templates/docker/sidecar/Dockerfile +28 -0
  49. package/templates/docker/sidecar/railway.json +14 -0
  50. package/templates/docker/sidecar/server.ts +188 -0
  51. package/templates/docker/src/api/__tests__/actions.test.ts +683 -0
  52. package/templates/docker/src/api/__tests__/admin.test.ts +820 -0
  53. package/templates/docker/src/api/__tests__/auth.test.ts +165 -0
  54. package/templates/docker/src/api/__tests__/chat.test.ts +376 -0
  55. package/templates/docker/src/api/__tests__/conversations.test.ts +555 -0
  56. package/templates/docker/src/api/__tests__/cors.test.ts +135 -0
  57. package/templates/docker/src/api/__tests__/health-plugin.test.ts +169 -0
  58. package/templates/docker/src/api/__tests__/health.test.ts +261 -0
  59. package/templates/docker/src/api/__tests__/query.test.ts +891 -0
  60. package/templates/docker/src/api/__tests__/scheduled-tasks.test.ts +601 -0
  61. package/templates/docker/src/api/__tests__/slack.test.ts +847 -0
  62. package/templates/docker/src/api/index.ts +117 -0
  63. package/templates/docker/src/api/routes/actions.ts +274 -0
  64. package/templates/docker/src/api/routes/admin.ts +757 -0
  65. package/templates/docker/src/api/routes/auth.ts +48 -0
  66. package/templates/docker/src/api/routes/chat.ts +465 -0
  67. package/templates/docker/src/api/routes/conversations.ts +266 -0
  68. package/templates/docker/src/api/routes/health.ts +287 -0
  69. package/templates/docker/src/api/routes/openapi.ts +390 -0
  70. package/templates/docker/src/api/routes/query.ts +318 -0
  71. package/templates/docker/src/api/routes/scheduled-tasks.ts +467 -0
  72. package/templates/docker/src/api/routes/slack.ts +611 -0
  73. package/templates/docker/src/api/server.ts +226 -0
  74. package/templates/docker/src/app/api/[...route]/route.ts +33 -0
  75. package/templates/docker/src/app/error.tsx +24 -0
  76. package/templates/docker/src/app/globals.css +126 -0
  77. package/templates/docker/src/app/layout.tsx +19 -0
  78. package/templates/docker/src/app/page.tsx +14 -0
  79. package/templates/docker/src/global.d.ts +1 -0
  80. package/templates/docker/src/lib/__tests__/agent-cache.test.ts +437 -0
  81. package/templates/docker/src/lib/__tests__/agent-dialect.test.ts +114 -0
  82. package/templates/docker/src/lib/__tests__/agent-health-annotations.test.ts +164 -0
  83. package/templates/docker/src/lib/__tests__/agent-integration.test.ts +514 -0
  84. package/templates/docker/src/lib/__tests__/config-actions.test.ts +166 -0
  85. package/templates/docker/src/lib/__tests__/config.test.ts +1063 -0
  86. package/templates/docker/src/lib/__tests__/conversations.test.ts +589 -0
  87. package/templates/docker/src/lib/__tests__/errors.test.ts +256 -0
  88. package/templates/docker/src/lib/__tests__/logger.test.ts +200 -0
  89. package/templates/docker/src/lib/__tests__/providers.test.ts +99 -0
  90. package/templates/docker/src/lib/__tests__/rls.test.ts +435 -0
  91. package/templates/docker/src/lib/__tests__/scheduled-task-types.test.ts +124 -0
  92. package/templates/docker/src/lib/__tests__/scheduled-tasks.test.ts +550 -0
  93. package/templates/docker/src/lib/__tests__/semantic-index.test.ts +547 -0
  94. package/templates/docker/src/lib/__tests__/semantic-multisource.test.ts +544 -0
  95. package/templates/docker/src/lib/__tests__/semantic.test.ts +363 -0
  96. package/templates/docker/src/lib/__tests__/startup-actions.test.ts +452 -0
  97. package/templates/docker/src/lib/__tests__/startup.test.ts +465 -0
  98. package/templates/docker/src/lib/__tests__/tracing.test.ts +28 -0
  99. package/templates/docker/src/lib/action-types.ts +95 -0
  100. package/templates/docker/src/lib/agent-query.ts +178 -0
  101. package/templates/docker/src/lib/agent.ts +505 -0
  102. package/templates/docker/src/lib/api-url.ts +2 -0
  103. package/templates/docker/src/lib/auth/__tests__/audit.test.ts +418 -0
  104. package/templates/docker/src/lib/auth/__tests__/byot-integration.test.ts +222 -0
  105. package/templates/docker/src/lib/auth/__tests__/byot.test.ts +366 -0
  106. package/templates/docker/src/lib/auth/__tests__/detect.test.ts +190 -0
  107. package/templates/docker/src/lib/auth/__tests__/managed.test.ts +173 -0
  108. package/templates/docker/src/lib/auth/__tests__/middleware.test.ts +456 -0
  109. package/templates/docker/src/lib/auth/__tests__/migrate.test.ts +201 -0
  110. package/templates/docker/src/lib/auth/__tests__/permissions.test.ts +225 -0
  111. package/templates/docker/src/lib/auth/__tests__/server.test.ts +34 -0
  112. package/templates/docker/src/lib/auth/__tests__/simple-key.test.ts +176 -0
  113. package/templates/docker/src/lib/auth/__tests__/types.test.ts +44 -0
  114. package/templates/docker/src/lib/auth/audit.ts +89 -0
  115. package/templates/docker/src/lib/auth/byot.ts +158 -0
  116. package/templates/docker/src/lib/auth/client.ts +35 -0
  117. package/templates/docker/src/lib/auth/detect.ts +83 -0
  118. package/templates/docker/src/lib/auth/managed.ts +73 -0
  119. package/templates/docker/src/lib/auth/middleware.ts +208 -0
  120. package/templates/docker/src/lib/auth/migrate.ts +111 -0
  121. package/templates/docker/src/lib/auth/permissions.ts +156 -0
  122. package/templates/docker/src/lib/auth/server.ts +142 -0
  123. package/templates/docker/src/lib/auth/simple-key.ts +92 -0
  124. package/templates/docker/src/lib/auth/types.ts +49 -0
  125. package/templates/docker/src/lib/config.ts +704 -0
  126. package/templates/docker/src/lib/conversation-types.ts +29 -0
  127. package/templates/docker/src/lib/conversations.ts +270 -0
  128. package/templates/docker/src/lib/db/__tests__/connection.test.ts +69 -0
  129. package/templates/docker/src/lib/db/__tests__/duckdb.test.ts +141 -0
  130. package/templates/docker/src/lib/db/__tests__/internal.test.ts +387 -0
  131. package/templates/docker/src/lib/db/__tests__/registry-health.test.ts +207 -0
  132. package/templates/docker/src/lib/db/__tests__/registry-pool-limits.test.ts +156 -0
  133. package/templates/docker/src/lib/db/__tests__/registry.test.ts +595 -0
  134. package/templates/docker/src/lib/db/__tests__/salesforce.test.ts +339 -0
  135. package/templates/docker/src/lib/db/__tests__/snowflake.test.ts +217 -0
  136. package/templates/docker/src/lib/db/__tests__/source-rate-limit.test.ts +130 -0
  137. package/templates/docker/src/lib/db/connection.ts +753 -0
  138. package/templates/docker/src/lib/db/duckdb.ts +122 -0
  139. package/templates/docker/src/lib/db/internal.ts +273 -0
  140. package/templates/docker/src/lib/db/salesforce.ts +342 -0
  141. package/templates/docker/src/lib/db/source-rate-limit.ts +191 -0
  142. package/templates/docker/src/lib/errors.ts +154 -0
  143. package/templates/docker/src/lib/logger.ts +98 -0
  144. package/templates/docker/src/lib/plugins/__tests__/hooks-integration.test.ts +202 -0
  145. package/templates/docker/src/lib/plugins/__tests__/hooks.test.ts +529 -0
  146. package/templates/docker/src/lib/plugins/__tests__/migrate.test.ts +521 -0
  147. package/templates/docker/src/lib/plugins/__tests__/registry.test.ts +346 -0
  148. package/templates/docker/src/lib/plugins/__tests__/tools.test.ts +49 -0
  149. package/templates/docker/src/lib/plugins/__tests__/wiring.test.ts +585 -0
  150. package/templates/docker/src/lib/plugins/hooks.ts +162 -0
  151. package/templates/docker/src/lib/plugins/index.ts +9 -0
  152. package/templates/docker/src/lib/plugins/migrate.ts +309 -0
  153. package/templates/docker/src/lib/plugins/registry.ts +231 -0
  154. package/templates/docker/src/lib/plugins/tools.ts +39 -0
  155. package/templates/docker/src/lib/plugins/wiring.ts +291 -0
  156. package/templates/docker/src/lib/providers.ts +102 -0
  157. package/templates/docker/src/lib/rls.ts +321 -0
  158. package/templates/docker/src/lib/scheduled-task-types.ts +132 -0
  159. package/templates/docker/src/lib/scheduled-tasks.ts +475 -0
  160. package/templates/docker/src/lib/scheduler/__tests__/delivery.test.ts +192 -0
  161. package/templates/docker/src/lib/scheduler/__tests__/engine.test.ts +248 -0
  162. package/templates/docker/src/lib/scheduler/__tests__/format-email.test.ts +96 -0
  163. package/templates/docker/src/lib/scheduler/__tests__/format-slack.test.ts +78 -0
  164. package/templates/docker/src/lib/scheduler/__tests__/format-webhook.test.ts +78 -0
  165. package/templates/docker/src/lib/scheduler/delivery.ts +248 -0
  166. package/templates/docker/src/lib/scheduler/engine.ts +317 -0
  167. package/templates/docker/src/lib/scheduler/executor.ts +73 -0
  168. package/templates/docker/src/lib/scheduler/format-email.ts +109 -0
  169. package/templates/docker/src/lib/scheduler/format-slack.ts +35 -0
  170. package/templates/docker/src/lib/scheduler/format-webhook.ts +37 -0
  171. package/templates/docker/src/lib/scheduler/index.ts +7 -0
  172. package/templates/docker/src/lib/security.ts +11 -0
  173. package/templates/docker/src/lib/semantic-index.ts +503 -0
  174. package/templates/docker/src/lib/semantic.ts +387 -0
  175. package/templates/docker/src/lib/sidecar-types.ts +16 -0
  176. package/templates/docker/src/lib/slack/__tests__/api.test.ts +160 -0
  177. package/templates/docker/src/lib/slack/__tests__/format.test.ts +237 -0
  178. package/templates/docker/src/lib/slack/__tests__/store.test.ts +188 -0
  179. package/templates/docker/src/lib/slack/__tests__/threads.test.ts +112 -0
  180. package/templates/docker/src/lib/slack/__tests__/verify.test.ts +111 -0
  181. package/templates/docker/src/lib/slack/api.ts +102 -0
  182. package/templates/docker/src/lib/slack/format.ts +209 -0
  183. package/templates/docker/src/lib/slack/store.ts +107 -0
  184. package/templates/docker/src/lib/slack/threads.ts +64 -0
  185. package/templates/docker/src/lib/slack/verify.ts +71 -0
  186. package/templates/docker/src/lib/startup.ts +730 -0
  187. package/templates/docker/src/lib/tools/__tests__/action-permissions.test.ts +594 -0
  188. package/templates/docker/src/lib/tools/__tests__/custom-validation.test.ts +238 -0
  189. package/templates/docker/src/lib/tools/__tests__/explore-backend.test.ts +267 -0
  190. package/templates/docker/src/lib/tools/__tests__/explore-nsjail.test.ts +492 -0
  191. package/templates/docker/src/lib/tools/__tests__/explore-plugin.test.ts +374 -0
  192. package/templates/docker/src/lib/tools/__tests__/explore-sdk-compat.test.ts +82 -0
  193. package/templates/docker/src/lib/tools/__tests__/explore-sidecar.test.ts +208 -0
  194. package/templates/docker/src/lib/tools/__tests__/registry-actions.test.ts +144 -0
  195. package/templates/docker/src/lib/tools/__tests__/registry.test.ts +235 -0
  196. package/templates/docker/src/lib/tools/__tests__/salesforce-tool.test.ts +154 -0
  197. package/templates/docker/src/lib/tools/__tests__/soql-validation.test.ts +303 -0
  198. package/templates/docker/src/lib/tools/__tests__/sql-audit.test.ts +225 -0
  199. package/templates/docker/src/lib/tools/__tests__/sql-connection-whitelist.test.ts +98 -0
  200. package/templates/docker/src/lib/tools/__tests__/sql-duckdb.test.ts +233 -0
  201. package/templates/docker/src/lib/tools/__tests__/sql-ratelimit.test.ts +225 -0
  202. package/templates/docker/src/lib/tools/__tests__/sql.test.ts +1012 -0
  203. package/templates/docker/src/lib/tools/actions/__tests__/audit.test.ts +211 -0
  204. package/templates/docker/src/lib/tools/actions/__tests__/email.test.ts +378 -0
  205. package/templates/docker/src/lib/tools/actions/__tests__/handler.test.ts +681 -0
  206. package/templates/docker/src/lib/tools/actions/__tests__/jira.test.ts +427 -0
  207. package/templates/docker/src/lib/tools/actions/audit.ts +47 -0
  208. package/templates/docker/src/lib/tools/actions/email.ts +191 -0
  209. package/templates/docker/src/lib/tools/actions/handler.ts +591 -0
  210. package/templates/docker/src/lib/tools/actions/index.ts +23 -0
  211. package/templates/docker/src/lib/tools/actions/jira.ts +220 -0
  212. package/templates/docker/src/lib/tools/explore-nsjail.ts +343 -0
  213. package/templates/docker/src/lib/tools/explore-sandbox.ts +264 -0
  214. package/templates/docker/src/lib/tools/explore-sidecar.ts +163 -0
  215. package/templates/docker/src/lib/tools/explore.ts +379 -0
  216. package/templates/docker/src/lib/tools/registry.ts +221 -0
  217. package/templates/docker/src/lib/tools/salesforce.ts +138 -0
  218. package/templates/docker/src/lib/tools/soql-validation.ts +172 -0
  219. package/templates/docker/src/lib/tools/sql.ts +680 -0
  220. package/templates/docker/src/lib/tracing.ts +40 -0
  221. package/templates/docker/src/lib/utils.ts +6 -0
  222. package/templates/docker/src/test-setup.ts +38 -0
  223. package/templates/docker/src/types/vercel-sandbox.d.ts +54 -0
  224. package/templates/docker/src/ui/components/actions/action-approval-card.tsx +295 -0
  225. package/templates/docker/src/ui/components/actions/action-status-badge.tsx +50 -0
  226. package/templates/docker/src/ui/components/admin/admin-layout.tsx +26 -0
  227. package/templates/docker/src/ui/components/admin/admin-sidebar.tsx +96 -0
  228. package/templates/docker/src/ui/components/admin/empty-state.tsx +24 -0
  229. package/templates/docker/src/ui/components/admin/entity-detail.tsx +233 -0
  230. package/templates/docker/src/ui/components/admin/entity-list.tsx +96 -0
  231. package/templates/docker/src/ui/components/admin/error-banner.tsx +22 -0
  232. package/templates/docker/src/ui/components/admin/feature-disabled.tsx +44 -0
  233. package/templates/docker/src/ui/components/admin/health-badge.tsx +30 -0
  234. package/templates/docker/src/ui/components/admin/loading-state.tsx +14 -0
  235. package/templates/docker/src/ui/components/admin/stat-card.tsx +32 -0
  236. package/templates/docker/src/ui/components/atlas-chat.tsx +370 -0
  237. package/templates/docker/src/ui/components/chart/chart-detection.ts +261 -0
  238. package/templates/docker/src/ui/components/chart/result-chart.tsx +375 -0
  239. package/templates/docker/src/ui/components/chat/api-key-bar.tsx +66 -0
  240. package/templates/docker/src/ui/components/chat/copy-button.tsx +25 -0
  241. package/templates/docker/src/ui/components/chat/data-table.tsx +102 -0
  242. package/templates/docker/src/ui/components/chat/error-banner.tsx +32 -0
  243. package/templates/docker/src/ui/components/chat/explore-card.tsx +41 -0
  244. package/templates/docker/src/ui/components/chat/loading-card.tsx +10 -0
  245. package/templates/docker/src/ui/components/chat/managed-auth-card.tsx +116 -0
  246. package/templates/docker/src/ui/components/chat/markdown.tsx +72 -0
  247. package/templates/docker/src/ui/components/chat/sql-block.tsx +30 -0
  248. package/templates/docker/src/ui/components/chat/sql-result-card.tsx +144 -0
  249. package/templates/docker/src/ui/components/chat/starter-prompts.ts +6 -0
  250. package/templates/docker/src/ui/components/chat/tool-part.tsx +40 -0
  251. package/templates/docker/src/ui/components/chat/typing-indicator.tsx +19 -0
  252. package/templates/docker/src/ui/components/conversations/conversation-item.tsx +120 -0
  253. package/templates/docker/src/ui/components/conversations/conversation-list.tsx +66 -0
  254. package/templates/docker/src/ui/components/conversations/conversation-sidebar.tsx +78 -0
  255. package/templates/docker/src/ui/components/conversations/delete-confirmation.tsx +27 -0
  256. package/templates/docker/src/ui/context.tsx +78 -0
  257. package/templates/docker/src/ui/hooks/use-admin-fetch.ts +104 -0
  258. package/templates/docker/src/ui/hooks/use-conversations.ts +184 -0
  259. package/templates/docker/src/ui/hooks/use-dark-mode.ts +17 -0
  260. package/templates/docker/src/ui/lib/action-types.ts +63 -0
  261. package/templates/docker/src/ui/lib/helpers.ts +104 -0
  262. package/templates/docker/src/ui/lib/types.ts +145 -0
  263. package/templates/docker/tsconfig.json +41 -0
  264. package/templates/docker/vercel.json +3 -0
  265. package/templates/nextjs-standalone/.env.example +68 -0
  266. package/templates/nextjs-standalone/bin/__tests__/benchmark.test.ts +598 -0
  267. package/templates/nextjs-standalone/bin/__tests__/duckdb-ingest.test.ts +171 -0
  268. package/templates/nextjs-standalone/bin/__tests__/eval.test.ts +434 -0
  269. package/templates/nextjs-standalone/bin/__tests__/matview-partition.test.ts +615 -0
  270. package/templates/nextjs-standalone/bin/__tests__/multi-source.test.ts +113 -0
  271. package/templates/nextjs-standalone/bin/__tests__/plugin-cli.test.ts +322 -0
  272. package/templates/nextjs-standalone/bin/__tests__/profiler-heuristics.test.ts +608 -0
  273. package/templates/nextjs-standalone/bin/__tests__/query.test.ts +240 -0
  274. package/templates/nextjs-standalone/bin/__tests__/schema-drift.test.ts +542 -0
  275. package/templates/nextjs-standalone/bin/__tests__/view-yaml-generation.test.ts +146 -0
  276. package/templates/nextjs-standalone/bin/atlas.ts +5044 -0
  277. package/templates/nextjs-standalone/bin/benchmark.ts +695 -0
  278. package/templates/nextjs-standalone/bin/enrich.ts +559 -0
  279. package/templates/nextjs-standalone/bin/eval.ts +770 -0
  280. package/templates/nextjs-standalone/bin/smoke.ts +438 -0
  281. package/templates/nextjs-standalone/data/.gitkeep +0 -0
  282. package/templates/nextjs-standalone/data/cybersec.sql +1961 -0
  283. package/templates/nextjs-standalone/data/demo-semantic/catalog.yml +40 -0
  284. package/templates/nextjs-standalone/data/demo-semantic/entities/accounts.yml +170 -0
  285. package/templates/nextjs-standalone/data/demo-semantic/entities/companies.yml +207 -0
  286. package/templates/nextjs-standalone/data/demo-semantic/entities/people.yml +145 -0
  287. package/templates/nextjs-standalone/data/demo-semantic/glossary.yml +22 -0
  288. package/templates/nextjs-standalone/data/demo-semantic/metrics/accounts.yml +38 -0
  289. package/templates/nextjs-standalone/data/demo-semantic/metrics/companies.yml +89 -0
  290. package/templates/nextjs-standalone/data/demo.sql +373 -0
  291. package/templates/nextjs-standalone/data/ecommerce.sql +1690 -0
  292. package/templates/nextjs-standalone/data/init-demo-db.sql +8 -0
  293. package/templates/nextjs-standalone/docs/deploy.md +390 -0
  294. package/templates/nextjs-standalone/eslint.config.mjs +18 -0
  295. package/templates/nextjs-standalone/gitignore +5 -0
  296. package/templates/nextjs-standalone/next.config.ts +10 -0
  297. package/templates/nextjs-standalone/package.json +63 -0
  298. package/templates/nextjs-standalone/postcss.config.mjs +8 -0
  299. package/templates/nextjs-standalone/semantic/catalog.yml +5 -0
  300. package/templates/nextjs-standalone/semantic/entities/.gitkeep +0 -0
  301. package/templates/nextjs-standalone/semantic/glossary.yml +6 -0
  302. package/templates/nextjs-standalone/semantic/metrics/.gitkeep +0 -0
  303. package/templates/nextjs-standalone/src/api/__tests__/actions.test.ts +683 -0
  304. package/templates/nextjs-standalone/src/api/__tests__/admin.test.ts +820 -0
  305. package/templates/nextjs-standalone/src/api/__tests__/auth.test.ts +165 -0
  306. package/templates/nextjs-standalone/src/api/__tests__/chat.test.ts +376 -0
  307. package/templates/nextjs-standalone/src/api/__tests__/conversations.test.ts +555 -0
  308. package/templates/nextjs-standalone/src/api/__tests__/cors.test.ts +135 -0
  309. package/templates/nextjs-standalone/src/api/__tests__/health-plugin.test.ts +169 -0
  310. package/templates/nextjs-standalone/src/api/__tests__/health.test.ts +261 -0
  311. package/templates/nextjs-standalone/src/api/__tests__/query.test.ts +891 -0
  312. package/templates/nextjs-standalone/src/api/__tests__/scheduled-tasks.test.ts +601 -0
  313. package/templates/nextjs-standalone/src/api/__tests__/slack.test.ts +847 -0
  314. package/templates/nextjs-standalone/src/api/index.ts +117 -0
  315. package/templates/nextjs-standalone/src/api/routes/actions.ts +274 -0
  316. package/templates/nextjs-standalone/src/api/routes/admin.ts +757 -0
  317. package/templates/nextjs-standalone/src/api/routes/auth.ts +48 -0
  318. package/templates/nextjs-standalone/src/api/routes/chat.ts +465 -0
  319. package/templates/nextjs-standalone/src/api/routes/conversations.ts +266 -0
  320. package/templates/nextjs-standalone/src/api/routes/health.ts +287 -0
  321. package/templates/nextjs-standalone/src/api/routes/openapi.ts +390 -0
  322. package/templates/nextjs-standalone/src/api/routes/query.ts +318 -0
  323. package/templates/nextjs-standalone/src/api/routes/scheduled-tasks.ts +467 -0
  324. package/templates/nextjs-standalone/src/api/routes/slack.ts +611 -0
  325. package/templates/nextjs-standalone/src/api/server.ts +226 -0
  326. package/templates/nextjs-standalone/src/app/api/[...route]/route.ts +33 -0
  327. package/templates/nextjs-standalone/src/app/error.tsx +24 -0
  328. package/templates/nextjs-standalone/src/app/global-error.tsx +68 -0
  329. package/templates/nextjs-standalone/src/app/globals.css +126 -0
  330. package/templates/nextjs-standalone/src/app/layout.tsx +19 -0
  331. package/templates/nextjs-standalone/src/app/page.tsx +14 -0
  332. package/templates/nextjs-standalone/src/lib/__tests__/agent-cache.test.ts +437 -0
  333. package/templates/nextjs-standalone/src/lib/__tests__/agent-dialect.test.ts +114 -0
  334. package/templates/nextjs-standalone/src/lib/__tests__/agent-health-annotations.test.ts +164 -0
  335. package/templates/nextjs-standalone/src/lib/__tests__/agent-integration.test.ts +514 -0
  336. package/templates/nextjs-standalone/src/lib/__tests__/config-actions.test.ts +166 -0
  337. package/templates/nextjs-standalone/src/lib/__tests__/config.test.ts +1063 -0
  338. package/templates/nextjs-standalone/src/lib/__tests__/conversations.test.ts +589 -0
  339. package/templates/nextjs-standalone/src/lib/__tests__/errors.test.ts +256 -0
  340. package/templates/nextjs-standalone/src/lib/__tests__/logger.test.ts +200 -0
  341. package/templates/nextjs-standalone/src/lib/__tests__/providers.test.ts +99 -0
  342. package/templates/nextjs-standalone/src/lib/__tests__/rls.test.ts +435 -0
  343. package/templates/nextjs-standalone/src/lib/__tests__/scheduled-task-types.test.ts +124 -0
  344. package/templates/nextjs-standalone/src/lib/__tests__/scheduled-tasks.test.ts +550 -0
  345. package/templates/nextjs-standalone/src/lib/__tests__/semantic-index.test.ts +547 -0
  346. package/templates/nextjs-standalone/src/lib/__tests__/semantic-multisource.test.ts +544 -0
  347. package/templates/nextjs-standalone/src/lib/__tests__/semantic.test.ts +363 -0
  348. package/templates/nextjs-standalone/src/lib/__tests__/startup-actions.test.ts +452 -0
  349. package/templates/nextjs-standalone/src/lib/__tests__/startup.test.ts +465 -0
  350. package/templates/nextjs-standalone/src/lib/__tests__/tracing.test.ts +28 -0
  351. package/templates/nextjs-standalone/src/lib/action-types.ts +95 -0
  352. package/templates/nextjs-standalone/src/lib/agent-query.ts +178 -0
  353. package/templates/nextjs-standalone/src/lib/agent.ts +505 -0
  354. package/templates/nextjs-standalone/src/lib/api-url.ts +3 -0
  355. package/templates/nextjs-standalone/src/lib/auth/__tests__/audit.test.ts +418 -0
  356. package/templates/nextjs-standalone/src/lib/auth/__tests__/byot-integration.test.ts +222 -0
  357. package/templates/nextjs-standalone/src/lib/auth/__tests__/byot.test.ts +366 -0
  358. package/templates/nextjs-standalone/src/lib/auth/__tests__/detect.test.ts +190 -0
  359. package/templates/nextjs-standalone/src/lib/auth/__tests__/managed.test.ts +173 -0
  360. package/templates/nextjs-standalone/src/lib/auth/__tests__/middleware.test.ts +456 -0
  361. package/templates/nextjs-standalone/src/lib/auth/__tests__/migrate.test.ts +201 -0
  362. package/templates/nextjs-standalone/src/lib/auth/__tests__/permissions.test.ts +225 -0
  363. package/templates/nextjs-standalone/src/lib/auth/__tests__/server.test.ts +34 -0
  364. package/templates/nextjs-standalone/src/lib/auth/__tests__/simple-key.test.ts +176 -0
  365. package/templates/nextjs-standalone/src/lib/auth/__tests__/types.test.ts +44 -0
  366. package/templates/nextjs-standalone/src/lib/auth/audit.ts +89 -0
  367. package/templates/nextjs-standalone/src/lib/auth/byot.ts +158 -0
  368. package/templates/nextjs-standalone/src/lib/auth/client.ts +23 -0
  369. package/templates/nextjs-standalone/src/lib/auth/detect.ts +83 -0
  370. package/templates/nextjs-standalone/src/lib/auth/managed.ts +73 -0
  371. package/templates/nextjs-standalone/src/lib/auth/middleware.ts +208 -0
  372. package/templates/nextjs-standalone/src/lib/auth/migrate.ts +111 -0
  373. package/templates/nextjs-standalone/src/lib/auth/permissions.ts +156 -0
  374. package/templates/nextjs-standalone/src/lib/auth/server.ts +142 -0
  375. package/templates/nextjs-standalone/src/lib/auth/simple-key.ts +92 -0
  376. package/templates/nextjs-standalone/src/lib/auth/types.ts +49 -0
  377. package/templates/nextjs-standalone/src/lib/config.ts +704 -0
  378. package/templates/nextjs-standalone/src/lib/conversation-types.ts +29 -0
  379. package/templates/nextjs-standalone/src/lib/conversations.ts +270 -0
  380. package/templates/nextjs-standalone/src/lib/db/__tests__/connection.test.ts +69 -0
  381. package/templates/nextjs-standalone/src/lib/db/__tests__/duckdb.test.ts +141 -0
  382. package/templates/nextjs-standalone/src/lib/db/__tests__/internal.test.ts +387 -0
  383. package/templates/nextjs-standalone/src/lib/db/__tests__/registry-health.test.ts +207 -0
  384. package/templates/nextjs-standalone/src/lib/db/__tests__/registry-pool-limits.test.ts +156 -0
  385. package/templates/nextjs-standalone/src/lib/db/__tests__/registry.test.ts +595 -0
  386. package/templates/nextjs-standalone/src/lib/db/__tests__/salesforce.test.ts +339 -0
  387. package/templates/nextjs-standalone/src/lib/db/__tests__/snowflake.test.ts +217 -0
  388. package/templates/nextjs-standalone/src/lib/db/__tests__/source-rate-limit.test.ts +130 -0
  389. package/templates/nextjs-standalone/src/lib/db/connection.ts +753 -0
  390. package/templates/nextjs-standalone/src/lib/db/duckdb.ts +122 -0
  391. package/templates/nextjs-standalone/src/lib/db/internal.ts +273 -0
  392. package/templates/nextjs-standalone/src/lib/db/salesforce.ts +342 -0
  393. package/templates/nextjs-standalone/src/lib/db/source-rate-limit.ts +191 -0
  394. package/templates/nextjs-standalone/src/lib/errors.ts +154 -0
  395. package/templates/nextjs-standalone/src/lib/logger.ts +98 -0
  396. package/templates/nextjs-standalone/src/lib/plugins/__tests__/hooks-integration.test.ts +202 -0
  397. package/templates/nextjs-standalone/src/lib/plugins/__tests__/hooks.test.ts +529 -0
  398. package/templates/nextjs-standalone/src/lib/plugins/__tests__/migrate.test.ts +521 -0
  399. package/templates/nextjs-standalone/src/lib/plugins/__tests__/registry.test.ts +346 -0
  400. package/templates/nextjs-standalone/src/lib/plugins/__tests__/tools.test.ts +49 -0
  401. package/templates/nextjs-standalone/src/lib/plugins/__tests__/wiring.test.ts +585 -0
  402. package/templates/nextjs-standalone/src/lib/plugins/hooks.ts +162 -0
  403. package/templates/nextjs-standalone/src/lib/plugins/index.ts +9 -0
  404. package/templates/nextjs-standalone/src/lib/plugins/migrate.ts +309 -0
  405. package/templates/nextjs-standalone/src/lib/plugins/registry.ts +231 -0
  406. package/templates/nextjs-standalone/src/lib/plugins/tools.ts +39 -0
  407. package/templates/nextjs-standalone/src/lib/plugins/wiring.ts +291 -0
  408. package/templates/nextjs-standalone/src/lib/providers.ts +102 -0
  409. package/templates/nextjs-standalone/src/lib/rls.ts +321 -0
  410. package/templates/nextjs-standalone/src/lib/scheduled-task-types.ts +132 -0
  411. package/templates/nextjs-standalone/src/lib/scheduled-tasks.ts +475 -0
  412. package/templates/nextjs-standalone/src/lib/scheduler/__tests__/delivery.test.ts +192 -0
  413. package/templates/nextjs-standalone/src/lib/scheduler/__tests__/engine.test.ts +248 -0
  414. package/templates/nextjs-standalone/src/lib/scheduler/__tests__/format-email.test.ts +96 -0
  415. package/templates/nextjs-standalone/src/lib/scheduler/__tests__/format-slack.test.ts +78 -0
  416. package/templates/nextjs-standalone/src/lib/scheduler/__tests__/format-webhook.test.ts +78 -0
  417. package/templates/nextjs-standalone/src/lib/scheduler/delivery.ts +248 -0
  418. package/templates/nextjs-standalone/src/lib/scheduler/engine.ts +317 -0
  419. package/templates/nextjs-standalone/src/lib/scheduler/executor.ts +73 -0
  420. package/templates/nextjs-standalone/src/lib/scheduler/format-email.ts +109 -0
  421. package/templates/nextjs-standalone/src/lib/scheduler/format-slack.ts +35 -0
  422. package/templates/nextjs-standalone/src/lib/scheduler/format-webhook.ts +37 -0
  423. package/templates/nextjs-standalone/src/lib/scheduler/index.ts +7 -0
  424. package/templates/nextjs-standalone/src/lib/security.ts +11 -0
  425. package/templates/nextjs-standalone/src/lib/semantic-index.ts +503 -0
  426. package/templates/nextjs-standalone/src/lib/semantic.ts +387 -0
  427. package/templates/nextjs-standalone/src/lib/sidecar-types.ts +16 -0
  428. package/templates/nextjs-standalone/src/lib/slack/__tests__/api.test.ts +160 -0
  429. package/templates/nextjs-standalone/src/lib/slack/__tests__/format.test.ts +237 -0
  430. package/templates/nextjs-standalone/src/lib/slack/__tests__/store.test.ts +188 -0
  431. package/templates/nextjs-standalone/src/lib/slack/__tests__/threads.test.ts +112 -0
  432. package/templates/nextjs-standalone/src/lib/slack/__tests__/verify.test.ts +111 -0
  433. package/templates/nextjs-standalone/src/lib/slack/api.ts +102 -0
  434. package/templates/nextjs-standalone/src/lib/slack/format.ts +209 -0
  435. package/templates/nextjs-standalone/src/lib/slack/store.ts +107 -0
  436. package/templates/nextjs-standalone/src/lib/slack/threads.ts +64 -0
  437. package/templates/nextjs-standalone/src/lib/slack/verify.ts +71 -0
  438. package/templates/nextjs-standalone/src/lib/startup.ts +730 -0
  439. package/templates/nextjs-standalone/src/lib/tools/__tests__/action-permissions.test.ts +594 -0
  440. package/templates/nextjs-standalone/src/lib/tools/__tests__/custom-validation.test.ts +238 -0
  441. package/templates/nextjs-standalone/src/lib/tools/__tests__/explore-backend.test.ts +267 -0
  442. package/templates/nextjs-standalone/src/lib/tools/__tests__/explore-nsjail.test.ts +492 -0
  443. package/templates/nextjs-standalone/src/lib/tools/__tests__/explore-plugin.test.ts +374 -0
  444. package/templates/nextjs-standalone/src/lib/tools/__tests__/explore-sdk-compat.test.ts +82 -0
  445. package/templates/nextjs-standalone/src/lib/tools/__tests__/explore-sidecar.test.ts +208 -0
  446. package/templates/nextjs-standalone/src/lib/tools/__tests__/registry-actions.test.ts +144 -0
  447. package/templates/nextjs-standalone/src/lib/tools/__tests__/registry.test.ts +235 -0
  448. package/templates/nextjs-standalone/src/lib/tools/__tests__/salesforce-tool.test.ts +154 -0
  449. package/templates/nextjs-standalone/src/lib/tools/__tests__/soql-validation.test.ts +303 -0
  450. package/templates/nextjs-standalone/src/lib/tools/__tests__/sql-audit.test.ts +225 -0
  451. package/templates/nextjs-standalone/src/lib/tools/__tests__/sql-connection-whitelist.test.ts +98 -0
  452. package/templates/nextjs-standalone/src/lib/tools/__tests__/sql-duckdb.test.ts +233 -0
  453. package/templates/nextjs-standalone/src/lib/tools/__tests__/sql-ratelimit.test.ts +225 -0
  454. package/templates/nextjs-standalone/src/lib/tools/__tests__/sql.test.ts +1012 -0
  455. package/templates/nextjs-standalone/src/lib/tools/actions/__tests__/audit.test.ts +211 -0
  456. package/templates/nextjs-standalone/src/lib/tools/actions/__tests__/email.test.ts +378 -0
  457. package/templates/nextjs-standalone/src/lib/tools/actions/__tests__/handler.test.ts +681 -0
  458. package/templates/nextjs-standalone/src/lib/tools/actions/__tests__/jira.test.ts +427 -0
  459. package/templates/nextjs-standalone/src/lib/tools/actions/audit.ts +47 -0
  460. package/templates/nextjs-standalone/src/lib/tools/actions/email.ts +191 -0
  461. package/templates/nextjs-standalone/src/lib/tools/actions/handler.ts +591 -0
  462. package/templates/nextjs-standalone/src/lib/tools/actions/index.ts +23 -0
  463. package/templates/nextjs-standalone/src/lib/tools/actions/jira.ts +220 -0
  464. package/templates/nextjs-standalone/src/lib/tools/explore-nsjail.ts +343 -0
  465. package/templates/nextjs-standalone/src/lib/tools/explore-sandbox.ts +264 -0
  466. package/templates/nextjs-standalone/src/lib/tools/explore-sidecar.ts +163 -0
  467. package/templates/nextjs-standalone/src/lib/tools/explore.ts +379 -0
  468. package/templates/nextjs-standalone/src/lib/tools/registry.ts +221 -0
  469. package/templates/nextjs-standalone/src/lib/tools/salesforce.ts +138 -0
  470. package/templates/nextjs-standalone/src/lib/tools/soql-validation.ts +172 -0
  471. package/templates/nextjs-standalone/src/lib/tools/sql.ts +680 -0
  472. package/templates/nextjs-standalone/src/lib/tracing.ts +40 -0
  473. package/templates/nextjs-standalone/src/lib/utils.ts +6 -0
  474. package/templates/nextjs-standalone/src/test-setup.ts +38 -0
  475. package/templates/nextjs-standalone/src/ui/components/actions/action-approval-card.tsx +295 -0
  476. package/templates/nextjs-standalone/src/ui/components/actions/action-status-badge.tsx +50 -0
  477. package/templates/nextjs-standalone/src/ui/components/admin/admin-layout.tsx +26 -0
  478. package/templates/nextjs-standalone/src/ui/components/admin/admin-sidebar.tsx +96 -0
  479. package/templates/nextjs-standalone/src/ui/components/admin/empty-state.tsx +24 -0
  480. package/templates/nextjs-standalone/src/ui/components/admin/entity-detail.tsx +233 -0
  481. package/templates/nextjs-standalone/src/ui/components/admin/entity-list.tsx +96 -0
  482. package/templates/nextjs-standalone/src/ui/components/admin/error-banner.tsx +22 -0
  483. package/templates/nextjs-standalone/src/ui/components/admin/feature-disabled.tsx +44 -0
  484. package/templates/nextjs-standalone/src/ui/components/admin/health-badge.tsx +30 -0
  485. package/templates/nextjs-standalone/src/ui/components/admin/loading-state.tsx +14 -0
  486. package/templates/nextjs-standalone/src/ui/components/admin/stat-card.tsx +32 -0
  487. package/templates/nextjs-standalone/src/ui/components/atlas-chat.tsx +370 -0
  488. package/templates/nextjs-standalone/src/ui/components/chart/chart-detection.ts +261 -0
  489. package/templates/nextjs-standalone/src/ui/components/chart/result-chart.tsx +375 -0
  490. package/templates/nextjs-standalone/src/ui/components/chat/api-key-bar.tsx +66 -0
  491. package/templates/nextjs-standalone/src/ui/components/chat/copy-button.tsx +25 -0
  492. package/templates/nextjs-standalone/src/ui/components/chat/data-table.tsx +102 -0
  493. package/templates/nextjs-standalone/src/ui/components/chat/error-banner.tsx +32 -0
  494. package/templates/nextjs-standalone/src/ui/components/chat/explore-card.tsx +41 -0
  495. package/templates/nextjs-standalone/src/ui/components/chat/loading-card.tsx +10 -0
  496. package/templates/nextjs-standalone/src/ui/components/chat/managed-auth-card.tsx +116 -0
  497. package/templates/nextjs-standalone/src/ui/components/chat/markdown.tsx +72 -0
  498. package/templates/nextjs-standalone/src/ui/components/chat/sql-block.tsx +30 -0
  499. package/templates/nextjs-standalone/src/ui/components/chat/sql-result-card.tsx +144 -0
  500. package/templates/nextjs-standalone/src/ui/components/chat/starter-prompts.ts +6 -0
  501. package/templates/nextjs-standalone/src/ui/components/chat/tool-part.tsx +40 -0
  502. package/templates/nextjs-standalone/src/ui/components/chat/typing-indicator.tsx +19 -0
  503. package/templates/nextjs-standalone/src/ui/components/conversations/conversation-item.tsx +120 -0
  504. package/templates/nextjs-standalone/src/ui/components/conversations/conversation-list.tsx +66 -0
  505. package/templates/nextjs-standalone/src/ui/components/conversations/conversation-sidebar.tsx +78 -0
  506. package/templates/nextjs-standalone/src/ui/components/conversations/delete-confirmation.tsx +27 -0
  507. package/templates/nextjs-standalone/src/ui/context.tsx +78 -0
  508. package/templates/nextjs-standalone/src/ui/hooks/use-admin-fetch.ts +104 -0
  509. package/templates/nextjs-standalone/src/ui/hooks/use-conversations.ts +184 -0
  510. package/templates/nextjs-standalone/src/ui/hooks/use-dark-mode.ts +17 -0
  511. package/templates/nextjs-standalone/src/ui/lib/action-types.ts +63 -0
  512. package/templates/nextjs-standalone/src/ui/lib/helpers.ts +104 -0
  513. package/templates/nextjs-standalone/src/ui/lib/types.ts +145 -0
  514. package/templates/nextjs-standalone/tsconfig.json +32 -0
  515. 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 };