@useatlas/create 0.0.2 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (296) hide show
  1. package/README.md +4 -18
  2. package/index.ts +191 -31
  3. package/package.json +1 -1
  4. package/templates/docker/.env.example +3 -3
  5. package/templates/docker/Dockerfile.sidecar +28 -0
  6. package/templates/docker/bin/__tests__/plugin-cli.test.ts +9 -9
  7. package/templates/docker/bin/atlas.ts +108 -44
  8. package/templates/docker/data/demo-semantic/catalog.yml +51 -27
  9. package/templates/docker/data/demo-semantic/entities/accounts.yml +95 -103
  10. package/templates/docker/data/demo-semantic/entities/companies.yml +88 -152
  11. package/templates/docker/data/demo-semantic/entities/people.yml +82 -95
  12. package/templates/docker/data/demo-semantic/glossary.yml +104 -8
  13. package/templates/docker/data/demo-semantic/metrics/accounts.yml +62 -23
  14. package/templates/docker/data/demo-semantic/metrics/companies.yml +52 -78
  15. package/templates/docker/docker-compose.yml +1 -1
  16. package/templates/docker/docs/deploy.md +2 -39
  17. package/templates/docker/package.json +17 -1
  18. package/templates/docker/semantic/catalog.yml +62 -3
  19. package/templates/docker/semantic/entities/accounts.yml +162 -0
  20. package/templates/docker/semantic/entities/companies.yml +143 -0
  21. package/templates/docker/semantic/entities/people.yml +132 -0
  22. package/templates/docker/semantic/glossary.yml +116 -4
  23. package/templates/docker/semantic/metrics/accounts.yml +77 -0
  24. package/templates/docker/semantic/metrics/companies.yml +63 -0
  25. package/templates/docker/sidecar/Dockerfile +5 -6
  26. package/templates/docker/sidecar/railway.json +1 -2
  27. package/templates/docker/src/api/__tests__/admin.test.ts +7 -7
  28. package/templates/docker/src/api/__tests__/health-plugin.test.ts +7 -0
  29. package/templates/docker/src/api/__tests__/health.test.ts +30 -8
  30. package/templates/docker/src/api/routes/admin.ts +549 -8
  31. package/templates/docker/src/api/routes/chat.ts +5 -20
  32. package/templates/docker/src/api/routes/health.ts +39 -27
  33. package/templates/docker/src/api/routes/openapi.ts +1329 -74
  34. package/templates/docker/src/api/routes/query.ts +2 -1
  35. package/templates/docker/src/api/server.ts +27 -0
  36. package/templates/docker/src/app/api/[...route]/route.ts +2 -2
  37. package/templates/docker/src/app/globals.css +13 -12
  38. package/templates/docker/src/app/layout.tsx +9 -2
  39. package/templates/docker/src/components/ui/alert-dialog.tsx +196 -0
  40. package/templates/docker/src/components/ui/badge.tsx +48 -0
  41. package/templates/docker/src/components/ui/button.tsx +64 -0
  42. package/templates/docker/src/components/ui/card.tsx +92 -0
  43. package/templates/docker/src/components/ui/collapsible.tsx +33 -0
  44. package/templates/docker/src/components/ui/command.tsx +184 -0
  45. package/templates/docker/src/components/ui/dialog.tsx +158 -0
  46. package/templates/docker/src/components/ui/dropdown-menu.tsx +257 -0
  47. package/templates/docker/src/components/ui/input.tsx +21 -0
  48. package/templates/docker/src/components/ui/scroll-area.tsx +58 -0
  49. package/templates/docker/src/components/ui/select.tsx +190 -0
  50. package/templates/docker/src/components/ui/separator.tsx +28 -0
  51. package/templates/docker/src/components/ui/sheet.tsx +143 -0
  52. package/templates/docker/src/components/ui/sidebar.tsx +726 -0
  53. package/templates/docker/src/components/ui/skeleton.tsx +13 -0
  54. package/templates/docker/src/components/ui/table.tsx +116 -0
  55. package/templates/docker/src/components/ui/tabs.tsx +91 -0
  56. package/templates/docker/src/components/ui/toggle-group.tsx +83 -0
  57. package/templates/docker/src/components/ui/toggle.tsx +47 -0
  58. package/templates/docker/src/components/ui/tooltip.tsx +57 -0
  59. package/templates/docker/src/hooks/use-mobile.ts +19 -0
  60. package/templates/docker/src/lib/__tests__/agent-cache.test.ts +2 -0
  61. package/templates/docker/src/lib/__tests__/agent-dialect.test.ts +17 -0
  62. package/templates/docker/src/lib/__tests__/agent-health-annotations.test.ts +2 -0
  63. package/templates/docker/src/lib/__tests__/agent-integration.test.ts +2 -0
  64. package/templates/docker/src/lib/__tests__/config.test.ts +69 -19
  65. package/templates/docker/src/lib/__tests__/plugin-aware-validation.test.ts +321 -0
  66. package/templates/docker/src/lib/__tests__/providers.test.ts +32 -1
  67. package/templates/docker/src/lib/__tests__/startup-actions.test.ts +9 -0
  68. package/templates/docker/src/lib/__tests__/startup-first-run.test.ts +429 -0
  69. package/templates/docker/src/lib/__tests__/startup.test.ts +5 -0
  70. package/templates/docker/src/lib/agent-query.ts +5 -23
  71. package/templates/docker/src/lib/agent.ts +32 -112
  72. package/templates/docker/src/lib/auth/__tests__/migrate.test.ts +5 -3
  73. package/templates/docker/src/lib/auth/middleware.ts +30 -4
  74. package/templates/docker/src/lib/auth/migrate.ts +97 -0
  75. package/templates/docker/src/lib/auth/server.ts +12 -1
  76. package/templates/docker/src/lib/config.ts +37 -39
  77. package/templates/docker/src/lib/db/__tests__/connection.test.ts +89 -14
  78. package/templates/docker/src/lib/db/__tests__/registry-health.test.ts +1 -18
  79. package/templates/docker/src/lib/db/__tests__/registry-pool-limits.test.ts +0 -19
  80. package/templates/docker/src/lib/db/__tests__/registry.test.ts +11 -208
  81. package/templates/docker/src/lib/db/connection.ts +87 -265
  82. package/templates/docker/src/lib/db/internal.ts +6 -1
  83. package/templates/docker/src/lib/plugins/__tests__/hooks-integration.test.ts +3 -1
  84. package/templates/docker/src/lib/plugins/__tests__/hooks.test.ts +2 -2
  85. package/templates/docker/src/lib/plugins/__tests__/migrate.test.ts +355 -1
  86. package/templates/docker/src/lib/plugins/__tests__/registry.test.ts +32 -5
  87. package/templates/docker/src/lib/plugins/__tests__/wiring.test.ts +228 -14
  88. package/templates/docker/src/lib/plugins/index.ts +4 -1
  89. package/templates/docker/src/lib/plugins/migrate.ts +103 -0
  90. package/templates/docker/src/lib/plugins/registry.ts +12 -6
  91. package/templates/docker/src/lib/plugins/wiring.ts +113 -4
  92. package/templates/docker/src/lib/providers.ts +6 -1
  93. package/templates/docker/src/lib/security.ts +24 -0
  94. package/templates/docker/src/lib/semantic.ts +2 -0
  95. package/templates/docker/src/lib/sidecar-types.ts +12 -1
  96. package/templates/docker/src/lib/startup.ts +71 -101
  97. package/templates/docker/src/lib/tools/__tests__/custom-validation.test.ts +2 -0
  98. package/templates/docker/src/lib/tools/__tests__/explore-nsjail.test.ts +32 -18
  99. package/templates/docker/src/lib/tools/__tests__/explore-plugin.test.ts +14 -14
  100. package/templates/docker/src/lib/tools/__tests__/explore-sidecar.test.ts +5 -3
  101. package/templates/docker/src/lib/tools/__tests__/python-nsjail.test.ts +515 -0
  102. package/templates/docker/src/lib/tools/__tests__/python-sandbox.test.ts +397 -0
  103. package/templates/docker/src/lib/tools/__tests__/python-sidecar.test.ts +365 -0
  104. package/templates/docker/src/lib/tools/__tests__/python.test.ts +331 -0
  105. package/templates/docker/src/lib/tools/__tests__/registry-actions.test.ts +1 -13
  106. package/templates/docker/src/lib/tools/__tests__/registry.test.ts +38 -31
  107. package/templates/docker/src/lib/tools/__tests__/sql-audit.test.ts +2 -0
  108. package/templates/docker/src/lib/tools/__tests__/sql-connection-whitelist.test.ts +2 -0
  109. package/templates/docker/src/lib/tools/__tests__/sql-ratelimit.test.ts +2 -0
  110. package/templates/docker/src/lib/tools/__tests__/sql.test.ts +5 -308
  111. package/templates/docker/src/lib/tools/explore-nsjail.ts +17 -12
  112. package/templates/docker/src/lib/tools/explore-sidecar.ts +25 -0
  113. package/templates/docker/src/lib/tools/explore.ts +28 -32
  114. package/templates/docker/src/lib/tools/python-nsjail.ts +396 -0
  115. package/templates/docker/src/lib/tools/python-sandbox.ts +476 -0
  116. package/templates/docker/src/lib/tools/python-sidecar.ts +150 -0
  117. package/templates/docker/src/lib/tools/python.ts +367 -0
  118. package/templates/docker/src/lib/tools/registry.ts +49 -22
  119. package/templates/docker/src/lib/tools/sql.ts +88 -88
  120. package/templates/docker/src/types/vercel-sandbox.d.ts +7 -0
  121. package/templates/docker/src/ui/components/admin/admin-layout.tsx +77 -8
  122. package/templates/docker/src/ui/components/admin/admin-sidebar.tsx +25 -17
  123. package/templates/docker/src/ui/components/admin/change-password-dialog.tsx +128 -0
  124. package/templates/docker/src/ui/components/admin/entity-detail.tsx +3 -3
  125. package/templates/docker/src/ui/components/admin/semantic-file-tree.tsx +159 -0
  126. package/templates/docker/src/ui/components/atlas-chat.tsx +64 -12
  127. package/templates/docker/src/ui/components/chart/result-chart.tsx +25 -15
  128. package/templates/docker/src/ui/components/chat/markdown.tsx +88 -42
  129. package/templates/docker/src/ui/components/chat/python-result-card.tsx +244 -0
  130. package/templates/docker/src/ui/components/chat/sql-block.tsx +39 -15
  131. package/templates/docker/src/ui/components/chat/sql-result-card.tsx +6 -1
  132. package/templates/docker/src/ui/components/chat/tool-part.tsx +12 -3
  133. package/templates/docker/src/ui/components/chat/typing-indicator.tsx +5 -2
  134. package/templates/docker/src/ui/components/conversations/conversation-item.tsx +25 -20
  135. package/templates/docker/src/ui/context.tsx +1 -1
  136. package/templates/docker/src/ui/hooks/use-conversations.ts +3 -3
  137. package/templates/docker/src/ui/hooks/use-dark-mode.ts +17 -10
  138. package/templates/docker/tsconfig.json +2 -2
  139. package/templates/nextjs-standalone/.env.example +1 -1
  140. package/templates/nextjs-standalone/bin/__tests__/plugin-cli.test.ts +9 -9
  141. package/templates/nextjs-standalone/bin/atlas.ts +108 -44
  142. package/templates/nextjs-standalone/data/demo-semantic/catalog.yml +51 -27
  143. package/templates/nextjs-standalone/data/demo-semantic/entities/accounts.yml +95 -103
  144. package/templates/nextjs-standalone/data/demo-semantic/entities/companies.yml +88 -152
  145. package/templates/nextjs-standalone/data/demo-semantic/entities/people.yml +82 -95
  146. package/templates/nextjs-standalone/data/demo-semantic/glossary.yml +104 -8
  147. package/templates/nextjs-standalone/data/demo-semantic/metrics/accounts.yml +62 -23
  148. package/templates/nextjs-standalone/data/demo-semantic/metrics/companies.yml +52 -78
  149. package/templates/nextjs-standalone/docs/deploy.md +2 -39
  150. package/templates/nextjs-standalone/package.json +11 -2
  151. package/templates/nextjs-standalone/scripts/migrate-auth.ts +25 -0
  152. package/templates/nextjs-standalone/scripts/seed-demo.ts +94 -0
  153. package/templates/nextjs-standalone/semantic/catalog.yml +62 -3
  154. package/templates/nextjs-standalone/semantic/entities/accounts.yml +162 -0
  155. package/templates/nextjs-standalone/semantic/entities/companies.yml +143 -0
  156. package/templates/nextjs-standalone/semantic/entities/people.yml +132 -0
  157. package/templates/nextjs-standalone/semantic/glossary.yml +116 -4
  158. package/templates/nextjs-standalone/semantic/metrics/accounts.yml +77 -0
  159. package/templates/nextjs-standalone/semantic/metrics/companies.yml +63 -0
  160. package/templates/nextjs-standalone/src/api/__tests__/admin.test.ts +7 -7
  161. package/templates/nextjs-standalone/src/api/__tests__/health-plugin.test.ts +7 -0
  162. package/templates/nextjs-standalone/src/api/__tests__/health.test.ts +30 -8
  163. package/templates/nextjs-standalone/src/api/routes/admin.ts +549 -8
  164. package/templates/nextjs-standalone/src/api/routes/chat.ts +5 -20
  165. package/templates/nextjs-standalone/src/api/routes/health.ts +39 -27
  166. package/templates/nextjs-standalone/src/api/routes/openapi.ts +1329 -74
  167. package/templates/nextjs-standalone/src/api/routes/query.ts +2 -1
  168. package/templates/nextjs-standalone/src/api/server.ts +27 -0
  169. package/templates/nextjs-standalone/src/app/api/[...route]/route.ts +2 -2
  170. package/templates/nextjs-standalone/src/app/globals.css +13 -12
  171. package/templates/nextjs-standalone/src/app/layout.tsx +9 -2
  172. package/templates/nextjs-standalone/src/components/ui/alert-dialog.tsx +196 -0
  173. package/templates/nextjs-standalone/src/components/ui/badge.tsx +48 -0
  174. package/templates/nextjs-standalone/src/components/ui/button.tsx +64 -0
  175. package/templates/nextjs-standalone/src/components/ui/card.tsx +92 -0
  176. package/templates/nextjs-standalone/src/components/ui/collapsible.tsx +33 -0
  177. package/templates/nextjs-standalone/src/components/ui/command.tsx +184 -0
  178. package/templates/nextjs-standalone/src/components/ui/dialog.tsx +158 -0
  179. package/templates/nextjs-standalone/src/components/ui/dropdown-menu.tsx +257 -0
  180. package/templates/nextjs-standalone/src/components/ui/input.tsx +21 -0
  181. package/templates/nextjs-standalone/src/components/ui/scroll-area.tsx +58 -0
  182. package/templates/nextjs-standalone/src/components/ui/select.tsx +190 -0
  183. package/templates/nextjs-standalone/src/components/ui/separator.tsx +28 -0
  184. package/templates/nextjs-standalone/src/components/ui/sheet.tsx +143 -0
  185. package/templates/nextjs-standalone/src/components/ui/sidebar.tsx +726 -0
  186. package/templates/nextjs-standalone/src/components/ui/skeleton.tsx +13 -0
  187. package/templates/nextjs-standalone/src/components/ui/table.tsx +116 -0
  188. package/templates/nextjs-standalone/src/components/ui/tabs.tsx +91 -0
  189. package/templates/nextjs-standalone/src/components/ui/toggle-group.tsx +83 -0
  190. package/templates/nextjs-standalone/src/components/ui/toggle.tsx +47 -0
  191. package/templates/nextjs-standalone/src/components/ui/tooltip.tsx +57 -0
  192. package/templates/nextjs-standalone/src/hooks/use-mobile.ts +19 -0
  193. package/templates/nextjs-standalone/src/lib/__tests__/agent-cache.test.ts +2 -0
  194. package/templates/nextjs-standalone/src/lib/__tests__/agent-dialect.test.ts +17 -0
  195. package/templates/nextjs-standalone/src/lib/__tests__/agent-health-annotations.test.ts +2 -0
  196. package/templates/nextjs-standalone/src/lib/__tests__/agent-integration.test.ts +2 -0
  197. package/templates/nextjs-standalone/src/lib/__tests__/config.test.ts +69 -19
  198. package/templates/nextjs-standalone/src/lib/__tests__/plugin-aware-validation.test.ts +321 -0
  199. package/templates/nextjs-standalone/src/lib/__tests__/providers.test.ts +32 -1
  200. package/templates/nextjs-standalone/src/lib/__tests__/startup-actions.test.ts +9 -0
  201. package/templates/nextjs-standalone/src/lib/__tests__/startup-first-run.test.ts +429 -0
  202. package/templates/nextjs-standalone/src/lib/__tests__/startup.test.ts +5 -0
  203. package/templates/nextjs-standalone/src/lib/agent-query.ts +5 -23
  204. package/templates/nextjs-standalone/src/lib/agent.ts +32 -112
  205. package/templates/nextjs-standalone/src/lib/auth/__tests__/migrate.test.ts +5 -3
  206. package/templates/nextjs-standalone/src/lib/auth/middleware.ts +30 -4
  207. package/templates/nextjs-standalone/src/lib/auth/migrate.ts +97 -0
  208. package/templates/nextjs-standalone/src/lib/auth/server.ts +12 -1
  209. package/templates/nextjs-standalone/src/lib/config.ts +37 -39
  210. package/templates/nextjs-standalone/src/lib/db/__tests__/connection.test.ts +89 -14
  211. package/templates/nextjs-standalone/src/lib/db/__tests__/registry-health.test.ts +1 -18
  212. package/templates/nextjs-standalone/src/lib/db/__tests__/registry-pool-limits.test.ts +0 -19
  213. package/templates/nextjs-standalone/src/lib/db/__tests__/registry.test.ts +11 -208
  214. package/templates/nextjs-standalone/src/lib/db/connection.ts +87 -265
  215. package/templates/nextjs-standalone/src/lib/db/internal.ts +6 -1
  216. package/templates/nextjs-standalone/src/lib/plugins/__tests__/hooks-integration.test.ts +3 -1
  217. package/templates/nextjs-standalone/src/lib/plugins/__tests__/hooks.test.ts +2 -2
  218. package/templates/nextjs-standalone/src/lib/plugins/__tests__/migrate.test.ts +355 -1
  219. package/templates/nextjs-standalone/src/lib/plugins/__tests__/registry.test.ts +32 -5
  220. package/templates/nextjs-standalone/src/lib/plugins/__tests__/wiring.test.ts +228 -14
  221. package/templates/nextjs-standalone/src/lib/plugins/index.ts +4 -1
  222. package/templates/nextjs-standalone/src/lib/plugins/migrate.ts +103 -0
  223. package/templates/nextjs-standalone/src/lib/plugins/registry.ts +12 -6
  224. package/templates/nextjs-standalone/src/lib/plugins/wiring.ts +113 -4
  225. package/templates/nextjs-standalone/src/lib/providers.ts +6 -1
  226. package/templates/nextjs-standalone/src/lib/security.ts +24 -0
  227. package/templates/nextjs-standalone/src/lib/semantic.ts +2 -0
  228. package/templates/nextjs-standalone/src/lib/sidecar-types.ts +12 -1
  229. package/templates/nextjs-standalone/src/lib/startup.ts +71 -101
  230. package/templates/nextjs-standalone/src/lib/tools/__tests__/custom-validation.test.ts +2 -0
  231. package/templates/nextjs-standalone/src/lib/tools/__tests__/explore-nsjail.test.ts +32 -18
  232. package/templates/nextjs-standalone/src/lib/tools/__tests__/explore-plugin.test.ts +14 -14
  233. package/templates/nextjs-standalone/src/lib/tools/__tests__/explore-sidecar.test.ts +5 -3
  234. package/templates/nextjs-standalone/src/lib/tools/__tests__/python-nsjail.test.ts +515 -0
  235. package/templates/nextjs-standalone/src/lib/tools/__tests__/python-sandbox.test.ts +397 -0
  236. package/templates/nextjs-standalone/src/lib/tools/__tests__/python-sidecar.test.ts +365 -0
  237. package/templates/nextjs-standalone/src/lib/tools/__tests__/python.test.ts +331 -0
  238. package/templates/nextjs-standalone/src/lib/tools/__tests__/registry-actions.test.ts +1 -13
  239. package/templates/nextjs-standalone/src/lib/tools/__tests__/registry.test.ts +38 -31
  240. package/templates/nextjs-standalone/src/lib/tools/__tests__/sql-audit.test.ts +2 -0
  241. package/templates/nextjs-standalone/src/lib/tools/__tests__/sql-connection-whitelist.test.ts +2 -0
  242. package/templates/nextjs-standalone/src/lib/tools/__tests__/sql-ratelimit.test.ts +2 -0
  243. package/templates/nextjs-standalone/src/lib/tools/__tests__/sql.test.ts +5 -308
  244. package/templates/nextjs-standalone/src/lib/tools/explore-nsjail.ts +17 -12
  245. package/templates/nextjs-standalone/src/lib/tools/explore-sidecar.ts +25 -0
  246. package/templates/nextjs-standalone/src/lib/tools/explore.ts +28 -32
  247. package/templates/nextjs-standalone/src/lib/tools/python-nsjail.ts +396 -0
  248. package/templates/nextjs-standalone/src/lib/tools/python-sandbox.ts +476 -0
  249. package/templates/nextjs-standalone/src/lib/tools/python-sidecar.ts +150 -0
  250. package/templates/nextjs-standalone/src/lib/tools/python.ts +367 -0
  251. package/templates/nextjs-standalone/src/lib/tools/registry.ts +49 -22
  252. package/templates/nextjs-standalone/src/lib/tools/sql.ts +88 -88
  253. package/templates/nextjs-standalone/src/ui/components/admin/admin-layout.tsx +77 -8
  254. package/templates/nextjs-standalone/src/ui/components/admin/admin-sidebar.tsx +25 -17
  255. package/templates/nextjs-standalone/src/ui/components/admin/change-password-dialog.tsx +128 -0
  256. package/templates/nextjs-standalone/src/ui/components/admin/entity-detail.tsx +3 -3
  257. package/templates/nextjs-standalone/src/ui/components/admin/semantic-file-tree.tsx +159 -0
  258. package/templates/nextjs-standalone/src/ui/components/atlas-chat.tsx +64 -12
  259. package/templates/nextjs-standalone/src/ui/components/chart/result-chart.tsx +25 -15
  260. package/templates/nextjs-standalone/src/ui/components/chat/markdown.tsx +88 -42
  261. package/templates/nextjs-standalone/src/ui/components/chat/python-result-card.tsx +244 -0
  262. package/templates/nextjs-standalone/src/ui/components/chat/sql-block.tsx +39 -15
  263. package/templates/nextjs-standalone/src/ui/components/chat/sql-result-card.tsx +6 -1
  264. package/templates/nextjs-standalone/src/ui/components/chat/tool-part.tsx +12 -3
  265. package/templates/nextjs-standalone/src/ui/components/chat/typing-indicator.tsx +5 -2
  266. package/templates/nextjs-standalone/src/ui/components/conversations/conversation-item.tsx +25 -20
  267. package/templates/nextjs-standalone/src/ui/context.tsx +1 -1
  268. package/templates/nextjs-standalone/src/ui/hooks/use-conversations.ts +3 -3
  269. package/templates/nextjs-standalone/src/ui/hooks/use-dark-mode.ts +17 -10
  270. package/templates/nextjs-standalone/tsconfig.json +0 -1
  271. package/templates/nextjs-standalone/vercel.json +4 -1
  272. package/templates/docker/render.yaml +0 -34
  273. package/templates/docker/semantic/entities/.gitkeep +0 -0
  274. package/templates/docker/semantic/metrics/.gitkeep +0 -0
  275. package/templates/docker/src/lib/db/__tests__/duckdb.test.ts +0 -141
  276. package/templates/docker/src/lib/db/__tests__/salesforce.test.ts +0 -339
  277. package/templates/docker/src/lib/db/__tests__/snowflake.test.ts +0 -217
  278. package/templates/docker/src/lib/db/duckdb.ts +0 -122
  279. package/templates/docker/src/lib/db/salesforce.ts +0 -342
  280. package/templates/docker/src/lib/tools/__tests__/salesforce-tool.test.ts +0 -154
  281. package/templates/docker/src/lib/tools/__tests__/soql-validation.test.ts +0 -303
  282. package/templates/docker/src/lib/tools/__tests__/sql-duckdb.test.ts +0 -233
  283. package/templates/docker/src/lib/tools/salesforce.ts +0 -138
  284. package/templates/docker/src/lib/tools/soql-validation.ts +0 -172
  285. package/templates/nextjs-standalone/semantic/entities/.gitkeep +0 -0
  286. package/templates/nextjs-standalone/semantic/metrics/.gitkeep +0 -0
  287. package/templates/nextjs-standalone/src/lib/db/__tests__/duckdb.test.ts +0 -141
  288. package/templates/nextjs-standalone/src/lib/db/__tests__/salesforce.test.ts +0 -339
  289. package/templates/nextjs-standalone/src/lib/db/__tests__/snowflake.test.ts +0 -217
  290. package/templates/nextjs-standalone/src/lib/db/duckdb.ts +0 -122
  291. package/templates/nextjs-standalone/src/lib/db/salesforce.ts +0 -342
  292. package/templates/nextjs-standalone/src/lib/tools/__tests__/salesforce-tool.test.ts +0 -154
  293. package/templates/nextjs-standalone/src/lib/tools/__tests__/soql-validation.test.ts +0 -303
  294. package/templates/nextjs-standalone/src/lib/tools/__tests__/sql-duckdb.test.ts +0 -233
  295. package/templates/nextjs-standalone/src/lib/tools/salesforce.ts +0 -138
  296. package/templates/nextjs-standalone/src/lib/tools/soql-validation.ts +0 -172
@@ -20,6 +20,9 @@ import {
20
20
  import { connections } from "@atlas/api/lib/db/connection";
21
21
  import { hasInternalDB, internalQuery } from "@atlas/api/lib/db/internal";
22
22
  import { plugins } from "@atlas/api/lib/plugins/registry";
23
+ import { detectAuthMode } from "@atlas/api/lib/auth/detect";
24
+ import type { AtlasRole } from "@atlas/api/lib/auth/types";
25
+ import { ATLAS_ROLES } from "@atlas/api/lib/auth/types";
23
26
 
24
27
  const log = createLogger("admin-routes");
25
28
 
@@ -64,13 +67,15 @@ async function adminAuthPreamble(req: Request, requestId: string) {
64
67
  return { error: { error: "auth_error", message: authResult.error }, status: authResult.status as 401 | 403 | 500 };
65
68
  }
66
69
 
67
- // Enforce admin role
68
- if (!authResult.user || authResult.user.role !== "admin") {
70
+ // Enforce admin role — when auth mode is "none" (no auth configured, e.g.
71
+ // local dev), treat the request as an implicit admin since there is no
72
+ // identity boundary to enforce.
73
+ if (authResult.mode !== "none" && (!authResult.user || authResult.user.role !== "admin")) {
69
74
  return { error: { error: "forbidden", message: "Admin role required." }, status: 403 as const };
70
75
  }
71
76
 
72
77
  const ip = getClientIP(req);
73
- const rateLimitKey = authResult.user.id ?? (ip ? `ip:${ip}` : "anon");
78
+ const rateLimitKey = authResult.user?.id ?? (ip ? `ip:${ip}` : "anon");
74
79
  const rateCheck = checkRateLimit(rateLimitKey);
75
80
  if (!rateCheck.allowed) {
76
81
  const retryAfterSeconds = Math.ceil((rateCheck.retryAfterMs ?? 60000) / 1000);
@@ -216,8 +221,8 @@ function findEntityFile(root: string, name: string): string | null {
216
221
  return null;
217
222
  }
218
223
 
219
- function discoverMetrics(root: string): Array<{ source: string; data: unknown }> {
220
- const metrics: Array<{ source: string; data: unknown }> = [];
224
+ function discoverMetrics(root: string): Array<{ source: string; file: string; data: unknown }> {
225
+ const metrics: Array<{ source: string; file: string; data: unknown }> = [];
221
226
 
222
227
  const defaultDir = path.join(root, "metrics");
223
228
  if (fs.existsSync(defaultDir)) {
@@ -243,7 +248,7 @@ function discoverMetrics(root: string): Array<{ source: string; data: unknown }>
243
248
  return metrics;
244
249
  }
245
250
 
246
- function loadMetricsFromDir(dir: string, source: string, out: Array<{ source: string; data: unknown }>): void {
251
+ function loadMetricsFromDir(dir: string, source: string, out: Array<{ source: string; file: string; data: unknown }>): void {
247
252
  let files: string[];
248
253
  try {
249
254
  files = fs.readdirSync(dir).filter((f) => f.endsWith(".yml"));
@@ -255,7 +260,7 @@ function loadMetricsFromDir(dir: string, source: string, out: Array<{ source: st
255
260
  for (const file of files) {
256
261
  try {
257
262
  const raw = readYamlFile(path.join(dir, file));
258
- out.push({ source, data: raw });
263
+ out.push({ source, file: file.replace(/\.yml$/, ""), data: raw });
259
264
  } catch (err) {
260
265
  log.warn({ err: err instanceof Error ? err : new Error(String(err)), file, dir, source }, "Failed to parse metric YAML file");
261
266
  }
@@ -342,7 +347,7 @@ admin.get("/overview", async (c) => {
342
347
  pluginHealth: pluginList.map((p) => ({
343
348
  id: p.id,
344
349
  name: p.name,
345
- type: p.type,
350
+ types: p.types,
346
351
  status: p.status,
347
352
  })),
348
353
  });
@@ -477,6 +482,63 @@ admin.get("/semantic/catalog", async (c) => {
477
482
  });
478
483
  });
479
484
 
485
+ // GET /semantic/raw/:file — serve raw YAML for top-level files (catalog.yml, glossary.yml)
486
+ // GET /semantic/raw/:dir/:file — serve raw YAML for subdirectory files (entities/x.yml, metrics/x.yml)
487
+
488
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
489
+ function serveRawYaml(c: any, requestId: string, filePath: string) {
490
+ // Validate: no traversal, must be .yml
491
+ if (filePath.includes("..") || filePath.includes("\0") || filePath.includes("\\") || !filePath.endsWith(".yml")) {
492
+ return c.json({ error: "invalid_request", message: "Invalid file path." }, 400);
493
+ }
494
+
495
+ const allowedPattern = /^(catalog|glossary)\.yml$|^(entities|metrics)\/[a-zA-Z0-9_-]+\.yml$/;
496
+ if (!allowedPattern.test(filePath)) {
497
+ return c.json({ error: "invalid_request", message: "File path not allowed." }, 400);
498
+ }
499
+
500
+ const root = getSemanticRoot();
501
+ const resolved = path.resolve(root, filePath);
502
+ if (!resolved.startsWith(path.resolve(root))) {
503
+ log.error({ requestId, filePath, resolved, root }, "Raw YAML path escaped semantic root");
504
+ return c.json({ error: "forbidden", message: "Access denied." }, 403);
505
+ }
506
+
507
+ if (!fs.existsSync(resolved)) {
508
+ return c.json({ error: "not_found", message: `File "${filePath}" not found.` }, 404);
509
+ }
510
+
511
+ try {
512
+ const content = fs.readFileSync(resolved, "utf-8");
513
+ return c.text(content);
514
+ } catch (err) {
515
+ log.error({ err: err instanceof Error ? err : new Error(String(err)), filePath }, "Failed to read raw YAML file");
516
+ return c.json({ error: "internal_error", message: "Failed to read file." }, 500);
517
+ }
518
+ }
519
+
520
+ admin.get("/semantic/raw/:dir/:file", async (c) => {
521
+ const req = c.req.raw;
522
+ const requestId = crypto.randomUUID();
523
+ const preamble = await adminAuthPreamble(req, requestId);
524
+ if ("error" in preamble) return c.json(preamble.error, { status: preamble.status, headers: preamble.headers });
525
+ const { authResult } = preamble;
526
+ return withRequestContext({ requestId, user: authResult.user }, () => {
527
+ return serveRawYaml(c, requestId, `${c.req.param("dir")}/${c.req.param("file")}`);
528
+ });
529
+ });
530
+
531
+ admin.get("/semantic/raw/:file", async (c) => {
532
+ const req = c.req.raw;
533
+ const requestId = crypto.randomUUID();
534
+ const preamble = await adminAuthPreamble(req, requestId);
535
+ if ("error" in preamble) return c.json(preamble.error, { status: preamble.status, headers: preamble.headers });
536
+ const { authResult } = preamble;
537
+ return withRequestContext({ requestId, user: authResult.user }, () => {
538
+ return serveRawYaml(c, requestId, c.req.param("file"));
539
+ });
540
+ });
541
+
480
542
  // GET /semantic/stats — aggregate stats
481
543
  admin.get("/semantic/stats", async (c) => {
482
544
  const req = c.req.raw;
@@ -754,4 +816,483 @@ admin.post("/plugins/:id/health", async (c) => {
754
816
  });
755
817
  });
756
818
 
819
+ // ---------------------------------------------------------------------------
820
+ // Password change required — any authenticated managed-auth user (not admin-only)
821
+ // ---------------------------------------------------------------------------
822
+
823
+ // GET /me/password-status — check if current user must change password
824
+ admin.get("/me/password-status", async (c) => {
825
+ const req = c.req.raw;
826
+ const requestId = crypto.randomUUID();
827
+
828
+ // Light auth: authenticate but don't require admin role
829
+ let authResult: AuthResult;
830
+ try {
831
+ authResult = await authenticateRequest(req);
832
+ } catch {
833
+ return c.json({ error: "auth_error", message: "Authentication system error" }, 500);
834
+ }
835
+ if (!authResult.authenticated) {
836
+ return c.json({ error: "auth_error", message: authResult.error }, authResult.status);
837
+ }
838
+ if (authResult.mode !== "managed" || !authResult.user) {
839
+ return c.json({ passwordChangeRequired: false });
840
+ }
841
+
842
+ if (!hasInternalDB()) return c.json({ passwordChangeRequired: false });
843
+
844
+ return withRequestContext({ requestId, user: authResult.user }, async () => {
845
+ try {
846
+ const rows = await internalQuery<{ password_change_required: boolean }>(
847
+ `SELECT password_change_required FROM "user" WHERE id = $1`,
848
+ [authResult.user!.id],
849
+ );
850
+ return c.json({ passwordChangeRequired: rows[0]?.password_change_required === true });
851
+ } catch {
852
+ return c.json({ passwordChangeRequired: false });
853
+ }
854
+ });
855
+ });
856
+
857
+ // POST /me/password — change password and clear the flag
858
+ admin.post("/me/password", async (c) => {
859
+ const req = c.req.raw;
860
+ const requestId = crypto.randomUUID();
861
+
862
+ let authResult: AuthResult;
863
+ try {
864
+ authResult = await authenticateRequest(req);
865
+ } catch {
866
+ return c.json({ error: "auth_error", message: "Authentication system error" }, 500);
867
+ }
868
+ if (!authResult.authenticated) {
869
+ return c.json({ error: "auth_error", message: authResult.error }, authResult.status);
870
+ }
871
+ if (authResult.mode !== "managed" || !authResult.user) {
872
+ return c.json({ error: "not_available", message: "Password change requires managed auth mode." }, 404);
873
+ }
874
+
875
+ return withRequestContext({ requestId, user: authResult.user }, async () => {
876
+ const body = await c.req.json().catch((err) => {
877
+ log.warn({ err: err instanceof Error ? err.message : String(err), requestId }, "Failed to parse JSON body in password change request");
878
+ return null;
879
+ });
880
+ const currentPassword = body?.currentPassword;
881
+ const newPassword = body?.newPassword;
882
+
883
+ if (typeof currentPassword !== "string" || typeof newPassword !== "string") {
884
+ return c.json({ error: "invalid_request", message: "currentPassword and newPassword are required." }, 400);
885
+ }
886
+ if (newPassword.length < 8) {
887
+ return c.json({ error: "invalid_request", message: "New password must be at least 8 characters." }, 400);
888
+ }
889
+
890
+ try {
891
+ const { getAuthInstance } = await import("@atlas/api/lib/auth/server");
892
+ const auth = getAuthInstance();
893
+ await (auth.api as unknown as {
894
+ changePassword(opts: { body: { currentPassword: string; newPassword: string }; headers: Headers }): Promise<unknown>;
895
+ }).changePassword({
896
+ body: { currentPassword, newPassword },
897
+ headers: req.headers,
898
+ });
899
+
900
+ // Clear the flag
901
+ if (hasInternalDB()) {
902
+ await internalQuery(
903
+ `UPDATE "user" SET password_change_required = false WHERE id = $1`,
904
+ [authResult.user!.id],
905
+ );
906
+ }
907
+
908
+ log.info({ requestId, userId: authResult.user!.id }, "Password changed and flag cleared");
909
+ return c.json({ success: true });
910
+ } catch (err) {
911
+ const message = err instanceof Error ? err.message : "Password change failed";
912
+ log.error({ err: err instanceof Error ? err : new Error(String(err)) }, "Password change failed");
913
+ // Better Auth throws if current password is wrong
914
+ if (message.includes("password") || message.includes("incorrect") || message.includes("invalid")) {
915
+ return c.json({ error: "invalid_request", message: "Current password is incorrect." }, 400);
916
+ }
917
+ return c.json({ error: "internal_error", message: "Failed to change password." }, 500);
918
+ }
919
+ });
920
+ });
921
+
922
+ // ---------------------------------------------------------------------------
923
+ // User management routes (requires managed auth mode + Better Auth admin plugin)
924
+ // ---------------------------------------------------------------------------
925
+
926
+ /**
927
+ * Server-side admin API methods from Better Auth's admin plugin.
928
+ * The base Auth type doesn't expose plugin-specific methods (see server.ts
929
+ * for why), but they exist at runtime. This interface types the subset we use.
930
+ */
931
+ interface AdminApi {
932
+ listUsers(opts: { query: Record<string, unknown>; headers: Headers }): Promise<{
933
+ users: Array<Record<string, unknown>>;
934
+ total: number;
935
+ }>;
936
+ setRole(opts: { body: { userId: string; role: string }; headers: Headers }): Promise<unknown>;
937
+ banUser(opts: { body: Record<string, unknown>; headers: Headers }): Promise<unknown>;
938
+ unbanUser(opts: { body: { userId: string }; headers: Headers }): Promise<unknown>;
939
+ removeUser(opts: { body: { userId: string }; headers: Headers }): Promise<unknown>;
940
+ revokeSessions(opts: { body: { userId: string }; headers: Headers }): Promise<unknown>;
941
+ }
942
+
943
+ /**
944
+ * Get the Better Auth instance's admin API, or null if managed auth is not active.
945
+ * Lazy-imports to avoid pulling in Better Auth when not needed.
946
+ */
947
+ async function getAdminApi(): Promise<AdminApi | null> {
948
+ if (detectAuthMode() !== "managed") return null;
949
+ const { getAuthInstance } = await import("@atlas/api/lib/auth/server");
950
+ // Cast: admin plugin methods exist at runtime but aren't in the base Auth type
951
+ return getAuthInstance().api as unknown as AdminApi;
952
+ }
953
+
954
+ /** Validate that a role string is a valid Atlas role. */
955
+ function isValidRole(role: unknown): role is AtlasRole {
956
+ return typeof role === "string" && (ATLAS_ROLES as readonly string[]).includes(role);
957
+ }
958
+
959
+ // GET /users — list users (paginated, filterable)
960
+ admin.get("/users", async (c) => {
961
+ const req = c.req.raw;
962
+ const requestId = crypto.randomUUID();
963
+
964
+ const preamble = await adminAuthPreamble(req, requestId);
965
+ if ("error" in preamble) {
966
+ return c.json(preamble.error, { status: preamble.status, headers: preamble.headers });
967
+ }
968
+ const { authResult } = preamble;
969
+
970
+ const adminApi = await getAdminApi();
971
+ if (!adminApi) {
972
+ return c.json({ error: "not_available", message: "User management requires managed auth mode." }, 404);
973
+ }
974
+
975
+ return withRequestContext({ requestId, user: authResult.user }, async () => {
976
+ const rawLimit = parseInt(c.req.query("limit") ?? "50", 10);
977
+ const rawOffset = parseInt(c.req.query("offset") ?? "0", 10);
978
+ const limit = Number.isFinite(rawLimit) && rawLimit > 0 ? Math.min(rawLimit, 200) : 50;
979
+ const offset = Number.isFinite(rawOffset) && rawOffset >= 0 ? rawOffset : 0;
980
+ const search = c.req.query("search");
981
+ const role = c.req.query("role");
982
+
983
+ try {
984
+ const result = await adminApi.listUsers({
985
+ query: {
986
+ limit,
987
+ offset,
988
+ ...(search ? { searchField: "email", searchValue: search, searchOperator: "contains" } : {}),
989
+ ...(role && isValidRole(role) ? { filterField: "role", filterValue: role, filterOperator: "eq" } : {}),
990
+ sortBy: "createdAt",
991
+ sortDirection: "desc",
992
+ },
993
+ headers: req.headers,
994
+ });
995
+
996
+ return c.json({
997
+ users: result.users.map((u: Record<string, unknown>) => ({
998
+ id: u.id,
999
+ email: u.email,
1000
+ name: u.name,
1001
+ role: u.role ?? "viewer",
1002
+ banned: u.banned ?? false,
1003
+ banReason: u.banReason ?? null,
1004
+ banExpires: u.banExpires ?? null,
1005
+ createdAt: u.createdAt,
1006
+ })),
1007
+ total: result.total,
1008
+ limit,
1009
+ offset,
1010
+ });
1011
+ } catch (err) {
1012
+ log.error({ err: err instanceof Error ? err : new Error(String(err)) }, "Failed to list users");
1013
+ return c.json({ error: "internal_error", message: "Failed to list users." }, 500);
1014
+ }
1015
+ });
1016
+ });
1017
+
1018
+ // GET /users/stats — aggregate user stats
1019
+ admin.get("/users/stats", async (c) => {
1020
+ const req = c.req.raw;
1021
+ const requestId = crypto.randomUUID();
1022
+
1023
+ const preamble = await adminAuthPreamble(req, requestId);
1024
+ if ("error" in preamble) {
1025
+ return c.json(preamble.error, { status: preamble.status, headers: preamble.headers });
1026
+ }
1027
+ const { authResult } = preamble;
1028
+
1029
+ if (!hasInternalDB() || detectAuthMode() !== "managed") {
1030
+ return c.json({ error: "not_available", message: "User management requires managed auth mode." }, 404);
1031
+ }
1032
+
1033
+ return withRequestContext({ requestId, user: authResult.user }, async () => {
1034
+ try {
1035
+ const totalResult = await internalQuery<{ count: string }>(
1036
+ `SELECT COUNT(*) as count FROM "user"`,
1037
+ );
1038
+ const roleResult = await internalQuery<{ role: string; count: string }>(
1039
+ `SELECT COALESCE(role, 'viewer') as role, COUNT(*) as count FROM "user" GROUP BY COALESCE(role, 'viewer')`,
1040
+ );
1041
+ const bannedResult = await internalQuery<{ count: string }>(
1042
+ `SELECT COUNT(*) as count FROM "user" WHERE banned = true`,
1043
+ );
1044
+
1045
+ const total = parseInt(String(totalResult[0]?.count ?? "0"), 10);
1046
+ const banned = parseInt(String(bannedResult[0]?.count ?? "0"), 10);
1047
+ const byRole: Record<string, number> = {};
1048
+ for (const r of roleResult) {
1049
+ byRole[r.role] = parseInt(String(r.count), 10);
1050
+ }
1051
+
1052
+ return c.json({ total, banned, byRole });
1053
+ } catch (err) {
1054
+ log.error({ err: err instanceof Error ? err : new Error(String(err)) }, "User stats query failed");
1055
+ return c.json({ error: "internal_error", message: "Failed to query user stats." }, 500);
1056
+ }
1057
+ });
1058
+ });
1059
+
1060
+ // PATCH /users/:id/role — change user role
1061
+ admin.patch("/users/:id/role", async (c) => {
1062
+ const req = c.req.raw;
1063
+ const requestId = crypto.randomUUID();
1064
+
1065
+ const preamble = await adminAuthPreamble(req, requestId);
1066
+ if ("error" in preamble) {
1067
+ return c.json(preamble.error, { status: preamble.status, headers: preamble.headers });
1068
+ }
1069
+ const { authResult } = preamble;
1070
+
1071
+ const adminApi = await getAdminApi();
1072
+ if (!adminApi) {
1073
+ return c.json({ error: "not_available", message: "User management requires managed auth mode." }, 404);
1074
+ }
1075
+
1076
+ return withRequestContext({ requestId, user: authResult.user }, async () => {
1077
+ const userId = c.req.param("id");
1078
+ const body = await c.req.json().catch((err) => {
1079
+ log.warn({ err: err instanceof Error ? err.message : String(err), requestId }, "Failed to parse JSON body in role change request");
1080
+ return null;
1081
+ });
1082
+ const newRole = body?.role;
1083
+
1084
+ if (!isValidRole(newRole)) {
1085
+ return c.json({ error: "invalid_request", message: `Invalid role. Must be one of: ${ATLAS_ROLES.join(", ")}` }, 400);
1086
+ }
1087
+
1088
+ // Self-protection: cannot change own role
1089
+ if (authResult.user?.id === userId) {
1090
+ return c.json({ error: "forbidden", message: "Cannot change your own role." }, 403);
1091
+ }
1092
+
1093
+ // Last admin guard: if demoting an admin, ensure at least one admin remains
1094
+ if (newRole !== "admin" && hasInternalDB()) {
1095
+ try {
1096
+ const currentUser = await internalQuery<{ role: string }>(
1097
+ `SELECT role FROM "user" WHERE id = $1`,
1098
+ [userId],
1099
+ );
1100
+ if (currentUser[0]?.role === "admin") {
1101
+ const adminCount = await internalQuery<{ count: string }>(
1102
+ `SELECT COUNT(*) as count FROM "user" WHERE role = 'admin'`,
1103
+ );
1104
+ if (parseInt(String(adminCount[0]?.count ?? "0"), 10) <= 1) {
1105
+ return c.json({ error: "forbidden", message: "Cannot demote the last admin." }, 403);
1106
+ }
1107
+ }
1108
+ } catch (err) {
1109
+ log.error({ err: err instanceof Error ? err : new Error(String(err)) }, "Last admin guard check failed");
1110
+ return c.json({ error: "internal_error", message: "Failed to verify admin count." }, 500);
1111
+ }
1112
+ }
1113
+
1114
+ try {
1115
+ await adminApi.setRole({
1116
+ body: { userId, role: newRole },
1117
+ headers: req.headers,
1118
+ });
1119
+ log.info({ requestId, targetUserId: userId, newRole, actorId: authResult.user?.id }, "User role changed");
1120
+ return c.json({ success: true });
1121
+ } catch (err) {
1122
+ log.error({ err: err instanceof Error ? err : new Error(String(err)), userId }, "Failed to set user role");
1123
+ return c.json({ error: "internal_error", message: "Failed to update user role." }, 500);
1124
+ }
1125
+ });
1126
+ });
1127
+
1128
+ // POST /users/:id/ban — ban a user
1129
+ admin.post("/users/:id/ban", async (c) => {
1130
+ const req = c.req.raw;
1131
+ const requestId = crypto.randomUUID();
1132
+
1133
+ const preamble = await adminAuthPreamble(req, requestId);
1134
+ if ("error" in preamble) {
1135
+ return c.json(preamble.error, { status: preamble.status, headers: preamble.headers });
1136
+ }
1137
+ const { authResult } = preamble;
1138
+
1139
+ const adminApi = await getAdminApi();
1140
+ if (!adminApi) {
1141
+ return c.json({ error: "not_available", message: "User management requires managed auth mode." }, 404);
1142
+ }
1143
+
1144
+ return withRequestContext({ requestId, user: authResult.user }, async () => {
1145
+ const userId = c.req.param("id");
1146
+
1147
+ if (authResult.user?.id === userId) {
1148
+ return c.json({ error: "forbidden", message: "Cannot ban yourself." }, 403);
1149
+ }
1150
+
1151
+ const body = await c.req.json().catch((err) => {
1152
+ log.warn({ err: err instanceof Error ? err.message : String(err), requestId }, "Failed to parse JSON body in ban user request");
1153
+ return {};
1154
+ });
1155
+
1156
+ try {
1157
+ await adminApi.banUser({
1158
+ body: {
1159
+ userId,
1160
+ ...(body.reason ? { banReason: body.reason } : {}),
1161
+ ...(body.expiresIn ? { banExpiresIn: body.expiresIn } : {}),
1162
+ },
1163
+ headers: req.headers,
1164
+ });
1165
+ log.info({ requestId, targetUserId: userId, reason: body.reason, actorId: authResult.user?.id }, "User banned");
1166
+ return c.json({ success: true });
1167
+ } catch (err) {
1168
+ log.error({ err: err instanceof Error ? err : new Error(String(err)), userId }, "Failed to ban user");
1169
+ return c.json({ error: "internal_error", message: "Failed to ban user." }, 500);
1170
+ }
1171
+ });
1172
+ });
1173
+
1174
+ // POST /users/:id/unban — unban a user
1175
+ admin.post("/users/:id/unban", async (c) => {
1176
+ const req = c.req.raw;
1177
+ const requestId = crypto.randomUUID();
1178
+
1179
+ const preamble = await adminAuthPreamble(req, requestId);
1180
+ if ("error" in preamble) {
1181
+ return c.json(preamble.error, { status: preamble.status, headers: preamble.headers });
1182
+ }
1183
+ const { authResult } = preamble;
1184
+
1185
+ const adminApi = await getAdminApi();
1186
+ if (!adminApi) {
1187
+ return c.json({ error: "not_available", message: "User management requires managed auth mode." }, 404);
1188
+ }
1189
+
1190
+ return withRequestContext({ requestId, user: authResult.user }, async () => {
1191
+ const userId = c.req.param("id");
1192
+
1193
+ try {
1194
+ await adminApi.unbanUser({
1195
+ body: { userId },
1196
+ headers: req.headers,
1197
+ });
1198
+ log.info({ requestId, targetUserId: userId, actorId: authResult.user?.id }, "User unbanned");
1199
+ return c.json({ success: true });
1200
+ } catch (err) {
1201
+ log.error({ err: err instanceof Error ? err : new Error(String(err)), userId }, "Failed to unban user");
1202
+ return c.json({ error: "internal_error", message: "Failed to unban user." }, 500);
1203
+ }
1204
+ });
1205
+ });
1206
+
1207
+ // DELETE /users/:id — delete a user
1208
+ admin.delete("/users/:id", async (c) => {
1209
+ const req = c.req.raw;
1210
+ const requestId = crypto.randomUUID();
1211
+
1212
+ const preamble = await adminAuthPreamble(req, requestId);
1213
+ if ("error" in preamble) {
1214
+ return c.json(preamble.error, { status: preamble.status, headers: preamble.headers });
1215
+ }
1216
+ const { authResult } = preamble;
1217
+
1218
+ const adminApi = await getAdminApi();
1219
+ if (!adminApi) {
1220
+ return c.json({ error: "not_available", message: "User management requires managed auth mode." }, 404);
1221
+ }
1222
+
1223
+ return withRequestContext({ requestId, user: authResult.user }, async () => {
1224
+ const userId = c.req.param("id");
1225
+
1226
+ if (authResult.user?.id === userId) {
1227
+ return c.json({ error: "forbidden", message: "Cannot delete yourself." }, 403);
1228
+ }
1229
+
1230
+ // Last admin guard
1231
+ if (hasInternalDB()) {
1232
+ try {
1233
+ const currentUser = await internalQuery<{ role: string }>(
1234
+ `SELECT role FROM "user" WHERE id = $1`,
1235
+ [userId],
1236
+ );
1237
+ if (currentUser[0]?.role === "admin") {
1238
+ const adminCount = await internalQuery<{ count: string }>(
1239
+ `SELECT COUNT(*) as count FROM "user" WHERE role = 'admin'`,
1240
+ );
1241
+ if (parseInt(String(adminCount[0]?.count ?? "0"), 10) <= 1) {
1242
+ return c.json({ error: "forbidden", message: "Cannot delete the last admin." }, 403);
1243
+ }
1244
+ }
1245
+ } catch (err) {
1246
+ log.error({ err: err instanceof Error ? err : new Error(String(err)) }, "Last admin guard check failed");
1247
+ return c.json({ error: "internal_error", message: "Failed to verify admin count." }, 500);
1248
+ }
1249
+ }
1250
+
1251
+ try {
1252
+ await adminApi.removeUser({
1253
+ body: { userId },
1254
+ headers: req.headers,
1255
+ });
1256
+ log.info({ requestId, targetUserId: userId, actorId: authResult.user?.id }, "User deleted");
1257
+ return c.json({ success: true });
1258
+ } catch (err) {
1259
+ log.error({ err: err instanceof Error ? err : new Error(String(err)), userId }, "Failed to delete user");
1260
+ return c.json({ error: "internal_error", message: "Failed to delete user." }, 500);
1261
+ }
1262
+ });
1263
+ });
1264
+
1265
+ // POST /users/:id/revoke — revoke all sessions (force logout)
1266
+ admin.post("/users/:id/revoke", async (c) => {
1267
+ const req = c.req.raw;
1268
+ const requestId = crypto.randomUUID();
1269
+
1270
+ const preamble = await adminAuthPreamble(req, requestId);
1271
+ if ("error" in preamble) {
1272
+ return c.json(preamble.error, { status: preamble.status, headers: preamble.headers });
1273
+ }
1274
+ const { authResult } = preamble;
1275
+
1276
+ const adminApi = await getAdminApi();
1277
+ if (!adminApi) {
1278
+ return c.json({ error: "not_available", message: "User management requires managed auth mode." }, 404);
1279
+ }
1280
+
1281
+ return withRequestContext({ requestId, user: authResult.user }, async () => {
1282
+ const userId = c.req.param("id");
1283
+
1284
+ try {
1285
+ await adminApi.revokeSessions({
1286
+ body: { userId },
1287
+ headers: req.headers,
1288
+ });
1289
+ log.info({ requestId, targetUserId: userId, actorId: authResult.user?.id }, "User sessions revoked");
1290
+ return c.json({ success: true });
1291
+ } catch (err) {
1292
+ log.error({ err: err instanceof Error ? err : new Error(String(err)), userId }, "Failed to revoke sessions");
1293
+ return c.json({ error: "internal_error", message: "Failed to revoke sessions." }, 500);
1294
+ }
1295
+ });
1296
+ });
1297
+
757
1298
  export { admin };
@@ -118,7 +118,8 @@ chat.post("/", async (c) => {
118
118
  }
119
119
 
120
120
  // Datasource guard — diagnostics pass (it's a warning) but chat requires a datasource
121
- if (!process.env.ATLAS_DATASOURCE_URL) {
121
+ const { resolveDatasourceUrl } = await import("@atlas/api/lib/db/connection");
122
+ if (!resolveDatasourceUrl()) {
122
123
  return c.json(
123
124
  {
124
125
  error: "no_datasource",
@@ -204,29 +205,13 @@ chat.post("/", async (c) => {
204
205
  }
205
206
 
206
207
  try {
207
- // Build a dynamic registry when Salesforce sources or actions are present
208
+ // Build a dynamic registry when actions are enabled
208
209
  let toolRegistry;
209
210
  const includeActions = process.env.ATLAS_ACTIONS_ENABLED === "true";
210
- let includeSalesforce = false;
211
- try {
212
- const { listSalesforceSources } = await import("@atlas/api/lib/db/salesforce");
213
- includeSalesforce = listSalesforceSources().length > 0;
214
- } catch (err) {
215
- const isModuleNotFound =
216
- err instanceof Error &&
217
- (err.message.includes("Cannot find module") ||
218
- err.message.includes("MODULE_NOT_FOUND"));
219
- if (!isModuleNotFound) {
220
- log.error(
221
- { err: err instanceof Error ? err : new Error(String(err)) },
222
- "Failed to initialize Salesforce tool registry — falling back to default tools",
223
- );
224
- }
225
- }
226
- if (includeSalesforce || includeActions) {
211
+ if (includeActions) {
227
212
  try {
228
213
  const { buildRegistry } = await import("@atlas/api/lib/tools/registry");
229
- toolRegistry = await buildRegistry({ includeSalesforce, includeActions });
214
+ toolRegistry = await buildRegistry({ includeActions });
230
215
  } catch (err) {
231
216
  log.error(
232
217
  { err: err instanceof Error ? err : new Error(String(err)) },