@useatlas/create 0.0.5 → 0.0.7

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 (952) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1 -1
  3. package/index.ts +253 -36
  4. package/package.json +4 -4
  5. package/templates/docker/Dockerfile +1 -1
  6. package/templates/docker/Dockerfile.sidecar +1 -1
  7. package/templates/docker/bin/__tests__/duckdb-ingest.test.ts +17 -14
  8. package/templates/docker/bin/__tests__/failure-threshold.test.ts +148 -0
  9. package/templates/docker/bin/__tests__/fatal-error-propagation.test.ts +267 -0
  10. package/templates/docker/bin/__tests__/profiler-heuristics.test.ts +5 -5
  11. package/templates/docker/bin/__tests__/schema-drift.test.ts +39 -0
  12. package/templates/docker/bin/atlas.ts +981 -1819
  13. package/templates/docker/bin/benchmark.ts +14 -16
  14. package/templates/docker/bin/enrich.ts +7 -2
  15. package/templates/docker/brand.css +13 -0
  16. package/templates/docker/data/cybersec-semantic/catalog.yml +222 -0
  17. package/templates/docker/data/cybersec-semantic/entities/alerts.yml +195 -0
  18. package/templates/docker/data/cybersec-semantic/entities/assets.yml +191 -0
  19. package/templates/docker/data/cybersec-semantic/entities/compliance_assessments.yml +170 -0
  20. package/templates/docker/data/cybersec-semantic/entities/incidents.yml +219 -0
  21. package/templates/docker/data/cybersec-semantic/entities/organizations.yml +136 -0
  22. package/templates/docker/data/cybersec-semantic/entities/plans.yml +114 -0
  23. package/templates/docker/data/cybersec-semantic/entities/remediation_actions.yml +212 -0
  24. package/templates/docker/data/cybersec-semantic/entities/scan_results.yml +215 -0
  25. package/templates/docker/data/cybersec-semantic/entities/scans.yml +180 -0
  26. package/templates/docker/data/cybersec-semantic/entities/subscriptions.yml +184 -0
  27. package/templates/docker/data/cybersec-semantic/entities/users.yml +140 -0
  28. package/templates/docker/data/cybersec-semantic/entities/vulnerabilities.yml +154 -0
  29. package/templates/docker/data/cybersec-semantic/glossary.yml +207 -0
  30. package/templates/docker/data/cybersec-semantic/metrics/business.yml +148 -0
  31. package/templates/docker/data/cybersec-semantic/metrics/compliance.yml +138 -0
  32. package/templates/docker/data/cybersec-semantic/metrics/security.yml +181 -0
  33. package/templates/docker/data/cybersec.sql +8 -8
  34. package/templates/docker/data/demo.sql +3 -0
  35. package/templates/docker/data/ecommerce-semantic/catalog.yml +221 -0
  36. package/templates/docker/data/ecommerce-semantic/entities/categories.yml +91 -0
  37. package/templates/docker/data/ecommerce-semantic/entities/customers.yml +133 -0
  38. package/templates/docker/data/ecommerce-semantic/entities/email_campaigns.yml +119 -0
  39. package/templates/docker/data/ecommerce-semantic/entities/inventory_levels.yml +153 -0
  40. package/templates/docker/data/ecommerce-semantic/entities/order_items.yml +159 -0
  41. package/templates/docker/data/ecommerce-semantic/entities/orders.yml +199 -0
  42. package/templates/docker/data/ecommerce-semantic/entities/payments.yml +140 -0
  43. package/templates/docker/data/ecommerce-semantic/entities/product_reviews.yml +155 -0
  44. package/templates/docker/data/ecommerce-semantic/entities/products.yml +178 -0
  45. package/templates/docker/data/ecommerce-semantic/entities/promotions.yml +171 -0
  46. package/templates/docker/data/ecommerce-semantic/entities/returns.yml +144 -0
  47. package/templates/docker/data/ecommerce-semantic/entities/sellers.yml +124 -0
  48. package/templates/docker/data/ecommerce-semantic/entities/shipments.yml +159 -0
  49. package/templates/docker/data/ecommerce-semantic/glossary.yml +193 -0
  50. package/templates/docker/data/ecommerce-semantic/metrics/customers.yml +116 -0
  51. package/templates/docker/data/ecommerce-semantic/metrics/operations.yml +131 -0
  52. package/templates/docker/data/ecommerce-semantic/metrics/revenue.yml +120 -0
  53. package/templates/docker/docs/deploy.md +2 -1
  54. package/templates/docker/ee/src/__mocks__/internal.ts +170 -0
  55. package/templates/docker/ee/src/audit/purge-scheduler.ts +113 -0
  56. package/templates/docker/ee/src/audit/retention.ts +467 -0
  57. package/templates/docker/ee/src/auth/ip-allowlist.ts +367 -0
  58. package/templates/docker/ee/src/auth/roles.ts +562 -0
  59. package/templates/docker/ee/src/auth/scim.ts +343 -0
  60. package/templates/docker/ee/src/auth/sso.ts +538 -0
  61. package/templates/docker/ee/src/backups/engine.ts +355 -0
  62. package/templates/docker/ee/src/backups/index.ts +26 -0
  63. package/templates/docker/ee/src/backups/restore.ts +169 -0
  64. package/templates/docker/ee/src/backups/scheduler.ts +153 -0
  65. package/templates/docker/ee/src/backups/verify.ts +124 -0
  66. package/templates/docker/ee/src/branding/white-label.ts +228 -0
  67. package/templates/docker/ee/src/compliance/masking.ts +477 -0
  68. package/templates/docker/ee/src/compliance/patterns.ts +16 -0
  69. package/templates/docker/ee/src/compliance/pii-detection.ts +217 -0
  70. package/templates/docker/ee/src/compliance/reports.ts +402 -0
  71. package/templates/docker/ee/src/deploy-mode.ts +37 -0
  72. package/templates/docker/ee/src/governance/approval.ts +699 -0
  73. package/templates/docker/ee/src/index.ts +74 -0
  74. package/templates/docker/ee/src/platform/domains.ts +562 -0
  75. package/templates/docker/ee/src/platform/model-routing.ts +382 -0
  76. package/templates/docker/ee/src/platform/residency.ts +265 -0
  77. package/templates/docker/ee/src/sla/alerting.ts +382 -0
  78. package/templates/docker/ee/src/sla/index.ts +12 -0
  79. package/templates/docker/ee/src/sla/metrics.ts +275 -0
  80. package/templates/docker/ee/src/test-setup.ts +1 -0
  81. package/templates/docker/next.config.ts +4 -1
  82. package/templates/docker/package.json +49 -29
  83. package/templates/docker/sidecar/Dockerfile +1 -1
  84. package/templates/docker/src/api/index.ts +336 -24
  85. package/templates/docker/src/api/routes/actions.ts +443 -176
  86. package/templates/docker/src/api/routes/admin-abuse.ts +219 -0
  87. package/templates/docker/src/api/routes/admin-approval.ts +418 -0
  88. package/templates/docker/src/api/routes/admin-audit-retention.ts +405 -0
  89. package/templates/docker/src/api/routes/admin-auth.ts +122 -0
  90. package/templates/docker/src/api/routes/admin-branding.ts +252 -0
  91. package/templates/docker/src/api/routes/admin-compliance.ts +352 -0
  92. package/templates/docker/src/api/routes/admin-domains.ts +334 -0
  93. package/templates/docker/src/api/routes/admin-integrations.ts +2667 -0
  94. package/templates/docker/src/api/routes/admin-ip-allowlist.ts +261 -0
  95. package/templates/docker/src/api/routes/admin-learned-patterns.ts +525 -0
  96. package/templates/docker/src/api/routes/admin-model-config.ts +252 -0
  97. package/templates/docker/src/api/routes/admin-onboarding-emails.ts +145 -0
  98. package/templates/docker/src/api/routes/admin-orgs.ts +710 -0
  99. package/templates/docker/src/api/routes/admin-prompts.ts +694 -0
  100. package/templates/docker/src/api/routes/admin-residency.ts +570 -0
  101. package/templates/docker/src/api/routes/admin-roles.ts +296 -0
  102. package/templates/docker/src/api/routes/admin-router.ts +120 -0
  103. package/templates/docker/src/api/routes/admin-sandbox.ts +417 -0
  104. package/templates/docker/src/api/routes/admin-scim.ts +262 -0
  105. package/templates/docker/src/api/routes/admin-sso.ts +545 -0
  106. package/templates/docker/src/api/routes/admin-suggestions.ts +176 -0
  107. package/templates/docker/src/api/routes/admin-usage.ts +310 -0
  108. package/templates/docker/src/api/routes/admin.ts +4156 -898
  109. package/templates/docker/src/api/routes/auth-preamble.ts +105 -0
  110. package/templates/docker/src/api/routes/billing.ts +397 -0
  111. package/templates/docker/src/api/routes/chat.ts +597 -334
  112. package/templates/docker/src/api/routes/conversations.ts +987 -132
  113. package/templates/docker/src/api/routes/demo.ts +673 -0
  114. package/templates/docker/src/api/routes/discord.ts +274 -0
  115. package/templates/docker/src/api/routes/ee-error-handler.ts +32 -0
  116. package/templates/docker/src/api/routes/health.ts +129 -14
  117. package/templates/docker/src/api/routes/middleware.ts +244 -0
  118. package/templates/docker/src/api/routes/onboarding-emails.ts +134 -0
  119. package/templates/docker/src/api/routes/onboarding.ts +1109 -0
  120. package/templates/docker/src/api/routes/openapi.ts +184 -1597
  121. package/templates/docker/src/api/routes/platform-admin.ts +760 -0
  122. package/templates/docker/src/api/routes/platform-backups.ts +436 -0
  123. package/templates/docker/src/api/routes/platform-domains.ts +235 -0
  124. package/templates/docker/src/api/routes/platform-residency.ts +257 -0
  125. package/templates/docker/src/api/routes/platform-sla.ts +379 -0
  126. package/templates/docker/src/api/routes/prompts.ts +221 -0
  127. package/templates/docker/src/api/routes/public-branding.ts +106 -0
  128. package/templates/docker/src/api/routes/query.ts +330 -219
  129. package/templates/docker/src/api/routes/scheduled-tasks.ts +393 -297
  130. package/templates/docker/src/api/routes/semantic.ts +179 -0
  131. package/templates/docker/src/api/routes/sessions.ts +210 -0
  132. package/templates/docker/src/api/routes/shared-domains.ts +98 -0
  133. package/templates/docker/src/api/routes/shared-schemas.ts +139 -0
  134. package/templates/docker/src/api/routes/slack.ts +209 -52
  135. package/templates/docker/src/api/routes/suggestions.ts +233 -0
  136. package/templates/docker/src/api/routes/tables.ts +67 -0
  137. package/templates/docker/src/api/routes/teams.ts +222 -0
  138. package/templates/docker/src/api/routes/validate-sql.ts +188 -0
  139. package/templates/docker/src/api/routes/validation-hook.ts +62 -0
  140. package/templates/docker/src/api/routes/widget-loader.ts +356 -0
  141. package/templates/docker/src/api/routes/widget.ts +428 -0
  142. package/templates/docker/src/api/routes/wizard.ts +852 -0
  143. package/templates/docker/src/api/server.ts +187 -69
  144. package/templates/docker/src/app/error.tsx +5 -2
  145. package/templates/docker/src/app/globals.css +1 -1
  146. package/templates/docker/src/app/layout.tsx +7 -2
  147. package/templates/docker/src/app/page.tsx +39 -5
  148. package/templates/docker/src/components/data-table/data-table-column-header.tsx +99 -0
  149. package/templates/docker/src/components/data-table/data-table-date-filter.tsx +225 -0
  150. package/templates/docker/src/components/data-table/data-table-expandable.tsx +125 -0
  151. package/templates/docker/src/components/data-table/data-table-faceted-filter.tsx +189 -0
  152. package/templates/docker/src/components/data-table/data-table-pagination.tsx +112 -0
  153. package/templates/docker/src/components/data-table/data-table-range-filter.tsx +122 -0
  154. package/templates/docker/src/components/data-table/data-table-slider-filter.tsx +256 -0
  155. package/templates/docker/src/components/data-table/data-table-sort-list.tsx +407 -0
  156. package/templates/docker/src/components/data-table/data-table-toolbar.tsx +149 -0
  157. package/templates/docker/src/components/data-table/data-table-view-options.tsx +89 -0
  158. package/templates/docker/src/components/data-table/data-table.tsx +105 -0
  159. package/templates/docker/src/components/form-dialog.tsx +135 -0
  160. package/templates/docker/src/components/ui/accordion.tsx +66 -0
  161. package/templates/docker/src/components/ui/calendar.tsx +220 -0
  162. package/templates/docker/src/components/ui/checkbox.tsx +32 -0
  163. package/templates/docker/src/components/ui/faceted.tsx +283 -0
  164. package/templates/docker/src/components/ui/form.tsx +167 -0
  165. package/templates/docker/src/components/ui/label.tsx +24 -0
  166. package/templates/docker/src/components/ui/popover.tsx +89 -0
  167. package/templates/docker/src/components/ui/progress.tsx +31 -0
  168. package/templates/docker/src/components/ui/scroll-area.tsx +6 -2
  169. package/templates/docker/src/components/ui/slider.tsx +63 -0
  170. package/templates/docker/src/components/ui/sortable.tsx +581 -0
  171. package/templates/docker/src/components/ui/switch.tsx +35 -0
  172. package/templates/docker/src/components/ui/textarea.tsx +18 -0
  173. package/templates/docker/src/config/data-table.ts +82 -0
  174. package/templates/docker/src/env-check.ts +74 -0
  175. package/templates/docker/src/hooks/use-callback-ref.ts +27 -0
  176. package/templates/docker/src/hooks/use-data-table.ts +316 -0
  177. package/templates/docker/src/hooks/use-debounced-callback.ts +28 -0
  178. package/templates/docker/src/lib/action-types.ts +7 -41
  179. package/templates/docker/src/lib/agent-query.ts +4 -2
  180. package/templates/docker/src/lib/agent.ts +363 -31
  181. package/templates/docker/src/lib/auth/admin-permissions.ts +38 -0
  182. package/templates/docker/src/lib/auth/audit.ts +19 -4
  183. package/templates/docker/src/lib/auth/byot.ts +3 -3
  184. package/templates/docker/src/lib/auth/client.ts +33 -3
  185. package/templates/docker/src/lib/auth/detect.ts +29 -8
  186. package/templates/docker/src/lib/auth/managed.ts +104 -14
  187. package/templates/docker/src/lib/auth/middleware.ts +53 -6
  188. package/templates/docker/src/lib/auth/migrate.ts +140 -15
  189. package/templates/docker/src/lib/auth/oauth-state.ts +123 -0
  190. package/templates/docker/src/lib/auth/org-permissions.ts +55 -0
  191. package/templates/docker/src/lib/auth/permissions.ts +26 -19
  192. package/templates/docker/src/lib/auth/server.ts +355 -9
  193. package/templates/docker/src/lib/auth/simple-key.ts +3 -3
  194. package/templates/docker/src/lib/auth/types.ts +15 -21
  195. package/templates/docker/src/lib/billing/enforcement.ts +368 -0
  196. package/templates/docker/src/lib/billing/plans.ts +155 -0
  197. package/templates/docker/src/lib/cache/index.ts +92 -0
  198. package/templates/docker/src/lib/cache/keys.ts +30 -0
  199. package/templates/docker/src/lib/cache/lru.ts +79 -0
  200. package/templates/docker/src/lib/cache/types.ts +31 -0
  201. package/templates/docker/src/lib/compose-refs.ts +62 -0
  202. package/templates/docker/src/lib/config.ts +563 -11
  203. package/templates/docker/src/lib/connection-types.ts +9 -0
  204. package/templates/docker/src/lib/conversation-types.ts +1 -25
  205. package/templates/docker/src/lib/conversations.ts +345 -14
  206. package/templates/docker/src/lib/data-table.ts +61 -0
  207. package/templates/docker/src/lib/db/connection.ts +793 -39
  208. package/templates/docker/src/lib/db/internal.ts +985 -139
  209. package/templates/docker/src/lib/db/migrate.ts +295 -0
  210. package/templates/docker/src/lib/db/migrations/0000_baseline.sql +703 -0
  211. package/templates/docker/src/lib/db/migrations/0001_teams_installations.sql +14 -0
  212. package/templates/docker/src/lib/db/migrations/0002_discord_installations.sql +14 -0
  213. package/templates/docker/src/lib/db/migrations/0003_telegram_installations.sql +15 -0
  214. package/templates/docker/src/lib/db/migrations/0004_sandbox_credentials.sql +18 -0
  215. package/templates/docker/src/lib/db/migrations/0005_oauth_state.sql +16 -0
  216. package/templates/docker/src/lib/db/migrations/0006_byot_credentials.sql +14 -0
  217. package/templates/docker/src/lib/db/migrations/0007_gchat_installations.sql +15 -0
  218. package/templates/docker/src/lib/db/migrations/0008_github_installations.sql +14 -0
  219. package/templates/docker/src/lib/db/migrations/0009_linear_installations.sql +15 -0
  220. package/templates/docker/src/lib/db/migrations/0010_whatsapp_installations.sql +14 -0
  221. package/templates/docker/src/lib/db/migrations/0011_email_installations.sql +16 -0
  222. package/templates/docker/src/lib/db/migrations/0012_region_migrations.sql +25 -0
  223. package/templates/docker/src/lib/db/schema.ts +1120 -0
  224. package/templates/docker/src/lib/db/source-rate-limit.ts +89 -139
  225. package/templates/docker/src/lib/demo.ts +308 -0
  226. package/templates/docker/src/lib/discord/store.ts +225 -0
  227. package/templates/docker/src/lib/effect/ai.ts +243 -0
  228. package/templates/docker/src/lib/effect/errors.ts +234 -0
  229. package/templates/docker/src/lib/effect/hono.ts +454 -0
  230. package/templates/docker/src/lib/effect/index.ts +137 -0
  231. package/templates/docker/src/lib/effect/layers.ts +496 -0
  232. package/templates/docker/src/lib/effect/services.ts +776 -0
  233. package/templates/docker/src/lib/effect/sql.ts +178 -0
  234. package/templates/docker/src/lib/effect/toolkit.ts +123 -0
  235. package/templates/docker/src/lib/email/delivery.ts +232 -0
  236. package/templates/docker/src/lib/email/engine.ts +349 -0
  237. package/templates/docker/src/lib/email/hooks.ts +107 -0
  238. package/templates/docker/src/lib/email/index.ts +16 -0
  239. package/templates/docker/src/lib/email/scheduler.ts +72 -0
  240. package/templates/docker/src/lib/email/sequence.ts +73 -0
  241. package/templates/docker/src/lib/email/store.ts +163 -0
  242. package/templates/docker/src/lib/email/templates.ts +215 -0
  243. package/templates/docker/src/lib/format.ts +67 -0
  244. package/templates/docker/src/lib/gchat/store.ts +202 -0
  245. package/templates/docker/src/lib/github/store.ts +197 -0
  246. package/templates/docker/src/lib/id.ts +29 -0
  247. package/templates/docker/src/lib/integrations/types.ts +166 -0
  248. package/templates/docker/src/lib/learn/pattern-analyzer.ts +224 -0
  249. package/templates/docker/src/lib/learn/pattern-cache.ts +229 -0
  250. package/templates/docker/src/lib/learn/pattern-proposer.ts +87 -0
  251. package/templates/docker/src/lib/learn/suggestion-helpers.ts +34 -0
  252. package/templates/docker/src/lib/learn/suggestions.ts +139 -0
  253. package/templates/docker/src/lib/linear/store.ts +200 -0
  254. package/templates/docker/src/lib/logger.ts +35 -3
  255. package/templates/docker/src/lib/metering.ts +272 -0
  256. package/templates/docker/src/lib/parsers.ts +99 -0
  257. package/templates/docker/src/lib/plugins/hooks.ts +13 -11
  258. package/templates/docker/src/lib/plugins/index.ts +3 -1
  259. package/templates/docker/src/lib/plugins/registry.ts +58 -6
  260. package/templates/docker/src/lib/plugins/settings.ts +147 -0
  261. package/templates/docker/src/lib/plugins/wiring.ts +6 -9
  262. package/templates/docker/src/lib/profiler.ts +1665 -0
  263. package/templates/docker/src/lib/providers.ts +188 -13
  264. package/templates/docker/src/lib/rls.ts +172 -60
  265. package/templates/docker/src/lib/sandbox/credentials.ts +206 -0
  266. package/templates/docker/src/lib/sandbox/validate.ts +179 -0
  267. package/templates/docker/src/lib/scheduled-task-types.ts +26 -94
  268. package/templates/docker/src/lib/scheduled-tasks.ts +174 -34
  269. package/templates/docker/src/lib/scheduler/delivery.ts +248 -150
  270. package/templates/docker/src/lib/scheduler/engine.ts +190 -154
  271. package/templates/docker/src/lib/scheduler/executor.ts +74 -23
  272. package/templates/docker/src/lib/scheduler/preview.ts +72 -0
  273. package/templates/docker/src/lib/security/abuse.ts +463 -0
  274. package/templates/docker/src/lib/semantic/diff.ts +267 -0
  275. package/templates/docker/src/lib/semantic/entities.ts +167 -0
  276. package/templates/docker/src/lib/semantic/files.ts +283 -0
  277. package/templates/docker/src/lib/semantic/index.ts +27 -0
  278. package/templates/docker/src/lib/{semantic-index.ts → semantic/search.ts} +80 -9
  279. package/templates/docker/src/lib/semantic/sync.ts +581 -0
  280. package/templates/docker/src/lib/{semantic.ts → semantic/whitelist.ts} +189 -3
  281. package/templates/docker/src/lib/settings.ts +817 -0
  282. package/templates/docker/src/lib/sidecar-types.ts +13 -0
  283. package/templates/docker/src/lib/slack/store.ts +134 -25
  284. package/templates/docker/src/lib/startup.ts +528 -362
  285. package/templates/docker/src/lib/teams/store.ts +216 -0
  286. package/templates/docker/src/lib/telegram/store.ts +202 -0
  287. package/templates/docker/src/lib/telemetry.ts +40 -0
  288. package/templates/docker/src/lib/tools/actions/audit.ts +8 -5
  289. package/templates/docker/src/lib/tools/actions/email.ts +3 -1
  290. package/templates/docker/src/lib/tools/actions/handler.ts +276 -93
  291. package/templates/docker/src/lib/tools/actions/jira.ts +2 -2
  292. package/templates/docker/src/lib/tools/backends/detect.ts +16 -0
  293. package/templates/docker/src/lib/tools/backends/index.ts +11 -0
  294. package/templates/docker/src/lib/tools/backends/nsjail.ts +213 -0
  295. package/templates/docker/src/lib/tools/backends/shared.ts +103 -0
  296. package/templates/docker/src/lib/tools/backends/types.ts +26 -0
  297. package/templates/docker/src/lib/tools/explore-nsjail.ts +7 -228
  298. package/templates/docker/src/lib/tools/explore-sandbox.ts +4 -29
  299. package/templates/docker/src/lib/tools/explore-sidecar.ts +18 -2
  300. package/templates/docker/src/lib/tools/explore.ts +246 -54
  301. package/templates/docker/src/lib/tools/index.ts +17 -0
  302. package/templates/docker/src/lib/tools/python-nsjail.ts +11 -139
  303. package/templates/docker/src/lib/tools/python-sandbox.ts +9 -132
  304. package/templates/docker/src/lib/tools/python-sidecar.ts +184 -3
  305. package/templates/docker/src/lib/tools/python-stream.ts +33 -0
  306. package/templates/docker/src/lib/tools/python-wrapper.ts +129 -0
  307. package/templates/docker/src/lib/tools/python.ts +115 -15
  308. package/templates/docker/src/lib/tools/registry.ts +14 -2
  309. package/templates/docker/src/lib/tools/sql.ts +778 -362
  310. package/templates/docker/src/lib/tracing.ts +16 -0
  311. package/templates/docker/src/lib/whatsapp/store.ts +198 -0
  312. package/templates/docker/src/lib/workspace.ts +89 -0
  313. package/templates/docker/src/progress.ts +121 -0
  314. package/templates/docker/src/types/data-table.ts +48 -0
  315. package/templates/docker/src/ui/atlas-chat-reexport.ts +3 -0
  316. package/templates/docker/src/ui/components/actions/action-approval-card.tsx +26 -19
  317. package/templates/docker/src/ui/components/actions/action-status-badge.tsx +3 -3
  318. package/templates/docker/src/ui/components/admin/admin-layout.tsx +57 -39
  319. package/templates/docker/src/ui/components/admin/admin-sidebar.tsx +213 -35
  320. package/templates/docker/src/ui/components/admin/delivery-status-badge.tsx +53 -0
  321. package/templates/docker/src/ui/components/admin/empty-state.tsx +27 -6
  322. package/templates/docker/src/ui/components/admin/entity-detail.tsx +3 -52
  323. package/templates/docker/src/ui/components/admin/error-banner.tsx +2 -2
  324. package/templates/docker/src/ui/components/admin/feature-disabled.tsx +28 -5
  325. package/templates/docker/src/ui/components/admin-content-wrapper.tsx +87 -0
  326. package/templates/docker/src/ui/components/atlas-chat.tsx +449 -166
  327. package/templates/docker/src/ui/components/branding-head.tsx +41 -0
  328. package/templates/docker/src/ui/components/chart/chart-detection.ts +62 -5
  329. package/templates/docker/src/ui/components/chart/result-chart.tsx +316 -125
  330. package/templates/docker/src/ui/components/chat/api-key-bar.tsx +4 -4
  331. package/templates/docker/src/ui/components/chat/data-table.tsx +45 -4
  332. package/templates/docker/src/ui/components/chat/error-banner.tsx +86 -5
  333. package/templates/docker/src/ui/components/chat/follow-up-chips.tsx +29 -0
  334. package/templates/docker/src/ui/components/chat/markdown.tsx +24 -0
  335. package/templates/docker/src/ui/components/chat/prompt-library.tsx +206 -0
  336. package/templates/docker/src/ui/components/chat/python-result-card.tsx +106 -78
  337. package/templates/docker/src/ui/components/chat/result-card-base.tsx +101 -0
  338. package/templates/docker/src/ui/components/chat/share-dialog.tsx +377 -0
  339. package/templates/docker/src/ui/components/chat/sql-result-card.tsx +94 -73
  340. package/templates/docker/src/ui/components/chat/suggestion-chips.tsx +46 -0
  341. package/templates/docker/src/ui/components/chat/tool-part.tsx +16 -4
  342. package/templates/docker/src/ui/components/conversations/conversation-item.tsx +48 -17
  343. package/templates/docker/src/ui/components/conversations/conversation-list.tsx +38 -24
  344. package/templates/docker/src/ui/components/conversations/conversation-sidebar.tsx +66 -7
  345. package/templates/docker/src/ui/components/conversations/delete-confirmation.tsx +9 -2
  346. package/templates/docker/src/ui/components/error-boundary.tsx +66 -0
  347. package/templates/docker/src/ui/components/notebook/delete-cell-dialog.tsx +48 -0
  348. package/templates/docker/src/ui/components/notebook/fork-branch-selector.tsx +68 -0
  349. package/templates/docker/src/ui/components/notebook/notebook-cell-input.tsx +76 -0
  350. package/templates/docker/src/ui/components/notebook/notebook-cell-output.tsx +58 -0
  351. package/templates/docker/src/ui/components/notebook/notebook-cell-toolbar.tsx +91 -0
  352. package/templates/docker/src/ui/components/notebook/notebook-cell.tsx +119 -0
  353. package/templates/docker/src/ui/components/notebook/notebook-empty-state.tsx +19 -0
  354. package/templates/docker/src/ui/components/notebook/notebook-export.ts +287 -0
  355. package/templates/docker/src/ui/components/notebook/notebook-input-bar.tsx +49 -0
  356. package/templates/docker/src/ui/components/notebook/notebook-shell.tsx +266 -0
  357. package/templates/docker/src/ui/components/notebook/notebook-text-cell.tsx +152 -0
  358. package/templates/docker/src/ui/components/notebook/types.ts +39 -0
  359. package/templates/docker/src/ui/components/notebook/use-keyboard-nav.ts +109 -0
  360. package/templates/docker/src/ui/components/notebook/use-notebook.ts +684 -0
  361. package/templates/docker/src/ui/components/org-switcher.tsx +111 -0
  362. package/templates/docker/src/ui/components/region-picker.tsx +103 -0
  363. package/templates/docker/src/ui/components/schema-explorer/schema-explorer.tsx +522 -0
  364. package/templates/docker/src/ui/components/social-icons.tsx +26 -0
  365. package/templates/docker/src/ui/components/tour/guided-tour.tsx +81 -0
  366. package/templates/docker/src/ui/components/tour/index.ts +5 -0
  367. package/templates/docker/src/ui/components/tour/nav-bar.tsx +100 -0
  368. package/templates/docker/src/ui/components/tour/tour-overlay.tsx +298 -0
  369. package/templates/docker/src/ui/components/tour/tour-steps.ts +43 -0
  370. package/templates/docker/src/ui/components/tour/types.ts +21 -0
  371. package/templates/docker/src/ui/components/tour/use-tour.ts +193 -0
  372. package/templates/docker/src/ui/context-reexport.ts +3 -0
  373. package/templates/docker/src/ui/hooks/theme-init-script.ts +17 -0
  374. package/templates/docker/src/ui/hooks/use-admin-fetch.ts +38 -30
  375. package/templates/docker/src/ui/hooks/use-admin-mutation.ts +188 -0
  376. package/templates/docker/src/ui/hooks/use-atlas-transport.ts +225 -0
  377. package/templates/docker/src/ui/hooks/use-branding.ts +68 -0
  378. package/templates/docker/src/ui/hooks/use-conversations.ts +106 -83
  379. package/templates/docker/src/ui/hooks/use-dark-mode.ts +134 -10
  380. package/templates/docker/src/ui/hooks/use-deploy-mode.ts +36 -0
  381. package/templates/docker/src/ui/hooks/use-platform-admin-guard.ts +49 -0
  382. package/templates/docker/src/ui/lib/action-types.ts +11 -63
  383. package/templates/docker/src/ui/lib/admin-schemas.ts +744 -0
  384. package/templates/docker/src/ui/lib/fetch-client.ts +84 -0
  385. package/templates/docker/src/ui/lib/fetch-error.ts +54 -0
  386. package/templates/docker/src/ui/lib/helpers.ts +94 -1
  387. package/templates/docker/src/ui/lib/types.ts +149 -140
  388. package/templates/docker/tsconfig.json +4 -2
  389. package/templates/nextjs-standalone/bin/__tests__/duckdb-ingest.test.ts +17 -14
  390. package/templates/nextjs-standalone/bin/__tests__/failure-threshold.test.ts +148 -0
  391. package/templates/nextjs-standalone/bin/__tests__/fatal-error-propagation.test.ts +267 -0
  392. package/templates/nextjs-standalone/bin/__tests__/profiler-heuristics.test.ts +5 -5
  393. package/templates/nextjs-standalone/bin/__tests__/schema-drift.test.ts +39 -0
  394. package/templates/nextjs-standalone/bin/atlas.ts +981 -1819
  395. package/templates/nextjs-standalone/bin/benchmark.ts +14 -16
  396. package/templates/nextjs-standalone/bin/enrich.ts +7 -2
  397. package/templates/nextjs-standalone/brand.css +13 -0
  398. package/templates/nextjs-standalone/data/cybersec-semantic/catalog.yml +222 -0
  399. package/templates/nextjs-standalone/data/cybersec-semantic/entities/alerts.yml +195 -0
  400. package/templates/nextjs-standalone/data/cybersec-semantic/entities/assets.yml +191 -0
  401. package/templates/nextjs-standalone/data/cybersec-semantic/entities/compliance_assessments.yml +170 -0
  402. package/templates/nextjs-standalone/data/cybersec-semantic/entities/incidents.yml +219 -0
  403. package/templates/nextjs-standalone/data/cybersec-semantic/entities/organizations.yml +136 -0
  404. package/templates/nextjs-standalone/data/cybersec-semantic/entities/plans.yml +114 -0
  405. package/templates/nextjs-standalone/data/cybersec-semantic/entities/remediation_actions.yml +212 -0
  406. package/templates/nextjs-standalone/data/cybersec-semantic/entities/scan_results.yml +215 -0
  407. package/templates/nextjs-standalone/data/cybersec-semantic/entities/scans.yml +180 -0
  408. package/templates/nextjs-standalone/data/cybersec-semantic/entities/subscriptions.yml +184 -0
  409. package/templates/nextjs-standalone/data/cybersec-semantic/entities/users.yml +140 -0
  410. package/templates/nextjs-standalone/data/cybersec-semantic/entities/vulnerabilities.yml +154 -0
  411. package/templates/nextjs-standalone/data/cybersec-semantic/glossary.yml +207 -0
  412. package/templates/nextjs-standalone/data/cybersec-semantic/metrics/business.yml +148 -0
  413. package/templates/nextjs-standalone/data/cybersec-semantic/metrics/compliance.yml +138 -0
  414. package/templates/nextjs-standalone/data/cybersec-semantic/metrics/security.yml +181 -0
  415. package/templates/nextjs-standalone/data/cybersec.sql +8 -8
  416. package/templates/nextjs-standalone/data/demo.sql +3 -0
  417. package/templates/nextjs-standalone/data/ecommerce-semantic/catalog.yml +221 -0
  418. package/templates/nextjs-standalone/data/ecommerce-semantic/entities/categories.yml +91 -0
  419. package/templates/nextjs-standalone/data/ecommerce-semantic/entities/customers.yml +133 -0
  420. package/templates/nextjs-standalone/data/ecommerce-semantic/entities/email_campaigns.yml +119 -0
  421. package/templates/nextjs-standalone/data/ecommerce-semantic/entities/inventory_levels.yml +153 -0
  422. package/templates/nextjs-standalone/data/ecommerce-semantic/entities/order_items.yml +159 -0
  423. package/templates/nextjs-standalone/data/ecommerce-semantic/entities/orders.yml +199 -0
  424. package/templates/nextjs-standalone/data/ecommerce-semantic/entities/payments.yml +140 -0
  425. package/templates/nextjs-standalone/data/ecommerce-semantic/entities/product_reviews.yml +155 -0
  426. package/templates/nextjs-standalone/data/ecommerce-semantic/entities/products.yml +178 -0
  427. package/templates/nextjs-standalone/data/ecommerce-semantic/entities/promotions.yml +171 -0
  428. package/templates/nextjs-standalone/data/ecommerce-semantic/entities/returns.yml +144 -0
  429. package/templates/nextjs-standalone/data/ecommerce-semantic/entities/sellers.yml +124 -0
  430. package/templates/nextjs-standalone/data/ecommerce-semantic/entities/shipments.yml +159 -0
  431. package/templates/nextjs-standalone/data/ecommerce-semantic/glossary.yml +193 -0
  432. package/templates/nextjs-standalone/data/ecommerce-semantic/metrics/customers.yml +116 -0
  433. package/templates/nextjs-standalone/data/ecommerce-semantic/metrics/operations.yml +131 -0
  434. package/templates/nextjs-standalone/data/ecommerce-semantic/metrics/revenue.yml +120 -0
  435. package/templates/nextjs-standalone/docs/deploy.md +2 -1
  436. package/templates/nextjs-standalone/ee/src/__mocks__/internal.ts +170 -0
  437. package/templates/nextjs-standalone/ee/src/audit/purge-scheduler.ts +113 -0
  438. package/templates/nextjs-standalone/ee/src/audit/retention.ts +467 -0
  439. package/templates/nextjs-standalone/ee/src/auth/ip-allowlist.ts +367 -0
  440. package/templates/nextjs-standalone/ee/src/auth/roles.ts +562 -0
  441. package/templates/nextjs-standalone/ee/src/auth/scim.ts +343 -0
  442. package/templates/nextjs-standalone/ee/src/auth/sso.ts +538 -0
  443. package/templates/nextjs-standalone/ee/src/backups/engine.ts +355 -0
  444. package/templates/nextjs-standalone/ee/src/backups/index.ts +26 -0
  445. package/templates/nextjs-standalone/ee/src/backups/restore.ts +169 -0
  446. package/templates/nextjs-standalone/ee/src/backups/scheduler.ts +153 -0
  447. package/templates/nextjs-standalone/ee/src/backups/verify.ts +124 -0
  448. package/templates/nextjs-standalone/ee/src/branding/white-label.ts +228 -0
  449. package/templates/nextjs-standalone/ee/src/compliance/masking.ts +477 -0
  450. package/templates/nextjs-standalone/ee/src/compliance/patterns.ts +16 -0
  451. package/templates/nextjs-standalone/ee/src/compliance/pii-detection.ts +217 -0
  452. package/templates/nextjs-standalone/ee/src/compliance/reports.ts +402 -0
  453. package/templates/nextjs-standalone/ee/src/deploy-mode.ts +37 -0
  454. package/templates/nextjs-standalone/ee/src/governance/approval.ts +699 -0
  455. package/templates/nextjs-standalone/ee/src/index.ts +74 -0
  456. package/templates/nextjs-standalone/ee/src/platform/domains.ts +562 -0
  457. package/templates/nextjs-standalone/ee/src/platform/model-routing.ts +382 -0
  458. package/templates/nextjs-standalone/ee/src/platform/residency.ts +265 -0
  459. package/templates/nextjs-standalone/ee/src/sla/alerting.ts +382 -0
  460. package/templates/nextjs-standalone/ee/src/sla/index.ts +12 -0
  461. package/templates/nextjs-standalone/ee/src/sla/metrics.ts +275 -0
  462. package/templates/nextjs-standalone/ee/src/test-setup.ts +1 -0
  463. package/templates/nextjs-standalone/next.config.ts +1 -1
  464. package/templates/nextjs-standalone/package.json +50 -30
  465. package/templates/nextjs-standalone/src/api/index.ts +336 -24
  466. package/templates/nextjs-standalone/src/api/routes/actions.ts +443 -176
  467. package/templates/nextjs-standalone/src/api/routes/admin-abuse.ts +219 -0
  468. package/templates/nextjs-standalone/src/api/routes/admin-approval.ts +418 -0
  469. package/templates/nextjs-standalone/src/api/routes/admin-audit-retention.ts +405 -0
  470. package/templates/nextjs-standalone/src/api/routes/admin-auth.ts +122 -0
  471. package/templates/nextjs-standalone/src/api/routes/admin-branding.ts +252 -0
  472. package/templates/nextjs-standalone/src/api/routes/admin-compliance.ts +352 -0
  473. package/templates/nextjs-standalone/src/api/routes/admin-domains.ts +334 -0
  474. package/templates/nextjs-standalone/src/api/routes/admin-integrations.ts +2667 -0
  475. package/templates/nextjs-standalone/src/api/routes/admin-ip-allowlist.ts +261 -0
  476. package/templates/nextjs-standalone/src/api/routes/admin-learned-patterns.ts +525 -0
  477. package/templates/nextjs-standalone/src/api/routes/admin-model-config.ts +252 -0
  478. package/templates/nextjs-standalone/src/api/routes/admin-onboarding-emails.ts +145 -0
  479. package/templates/nextjs-standalone/src/api/routes/admin-orgs.ts +710 -0
  480. package/templates/nextjs-standalone/src/api/routes/admin-prompts.ts +694 -0
  481. package/templates/nextjs-standalone/src/api/routes/admin-residency.ts +570 -0
  482. package/templates/nextjs-standalone/src/api/routes/admin-roles.ts +296 -0
  483. package/templates/nextjs-standalone/src/api/routes/admin-router.ts +120 -0
  484. package/templates/nextjs-standalone/src/api/routes/admin-sandbox.ts +417 -0
  485. package/templates/nextjs-standalone/src/api/routes/admin-scim.ts +262 -0
  486. package/templates/nextjs-standalone/src/api/routes/admin-sso.ts +545 -0
  487. package/templates/nextjs-standalone/src/api/routes/admin-suggestions.ts +176 -0
  488. package/templates/nextjs-standalone/src/api/routes/admin-usage.ts +310 -0
  489. package/templates/nextjs-standalone/src/api/routes/admin.ts +4156 -898
  490. package/templates/nextjs-standalone/src/api/routes/auth-preamble.ts +105 -0
  491. package/templates/nextjs-standalone/src/api/routes/billing.ts +397 -0
  492. package/templates/nextjs-standalone/src/api/routes/chat.ts +597 -334
  493. package/templates/nextjs-standalone/src/api/routes/conversations.ts +987 -132
  494. package/templates/nextjs-standalone/src/api/routes/demo.ts +673 -0
  495. package/templates/nextjs-standalone/src/api/routes/discord.ts +274 -0
  496. package/templates/nextjs-standalone/src/api/routes/ee-error-handler.ts +32 -0
  497. package/templates/nextjs-standalone/src/api/routes/health.ts +129 -14
  498. package/templates/nextjs-standalone/src/api/routes/middleware.ts +244 -0
  499. package/templates/nextjs-standalone/src/api/routes/onboarding-emails.ts +134 -0
  500. package/templates/nextjs-standalone/src/api/routes/onboarding.ts +1109 -0
  501. package/templates/nextjs-standalone/src/api/routes/openapi.ts +184 -1597
  502. package/templates/nextjs-standalone/src/api/routes/platform-admin.ts +760 -0
  503. package/templates/nextjs-standalone/src/api/routes/platform-backups.ts +436 -0
  504. package/templates/nextjs-standalone/src/api/routes/platform-domains.ts +235 -0
  505. package/templates/nextjs-standalone/src/api/routes/platform-residency.ts +257 -0
  506. package/templates/nextjs-standalone/src/api/routes/platform-sla.ts +379 -0
  507. package/templates/nextjs-standalone/src/api/routes/prompts.ts +221 -0
  508. package/templates/nextjs-standalone/src/api/routes/public-branding.ts +106 -0
  509. package/templates/nextjs-standalone/src/api/routes/query.ts +330 -219
  510. package/templates/nextjs-standalone/src/api/routes/scheduled-tasks.ts +393 -297
  511. package/templates/nextjs-standalone/src/api/routes/semantic.ts +179 -0
  512. package/templates/nextjs-standalone/src/api/routes/sessions.ts +210 -0
  513. package/templates/nextjs-standalone/src/api/routes/shared-domains.ts +98 -0
  514. package/templates/nextjs-standalone/src/api/routes/shared-schemas.ts +139 -0
  515. package/templates/nextjs-standalone/src/api/routes/slack.ts +209 -52
  516. package/templates/nextjs-standalone/src/api/routes/suggestions.ts +233 -0
  517. package/templates/nextjs-standalone/src/api/routes/tables.ts +67 -0
  518. package/templates/nextjs-standalone/src/api/routes/teams.ts +222 -0
  519. package/templates/nextjs-standalone/src/api/routes/validate-sql.ts +188 -0
  520. package/templates/nextjs-standalone/src/api/routes/validation-hook.ts +62 -0
  521. package/templates/nextjs-standalone/src/api/routes/widget-loader.ts +356 -0
  522. package/templates/nextjs-standalone/src/api/routes/widget.ts +428 -0
  523. package/templates/nextjs-standalone/src/api/routes/wizard.ts +852 -0
  524. package/templates/nextjs-standalone/src/api/server.ts +187 -69
  525. package/templates/nextjs-standalone/src/app/error.tsx +5 -2
  526. package/templates/nextjs-standalone/src/app/globals.css +1 -1
  527. package/templates/nextjs-standalone/src/app/layout.tsx +7 -2
  528. package/templates/nextjs-standalone/src/app/page.tsx +39 -5
  529. package/templates/nextjs-standalone/src/components/data-table/data-table-column-header.tsx +99 -0
  530. package/templates/nextjs-standalone/src/components/data-table/data-table-date-filter.tsx +225 -0
  531. package/templates/nextjs-standalone/src/components/data-table/data-table-expandable.tsx +125 -0
  532. package/templates/nextjs-standalone/src/components/data-table/data-table-faceted-filter.tsx +189 -0
  533. package/templates/nextjs-standalone/src/components/data-table/data-table-pagination.tsx +112 -0
  534. package/templates/nextjs-standalone/src/components/data-table/data-table-range-filter.tsx +122 -0
  535. package/templates/nextjs-standalone/src/components/data-table/data-table-slider-filter.tsx +256 -0
  536. package/templates/nextjs-standalone/src/components/data-table/data-table-sort-list.tsx +407 -0
  537. package/templates/nextjs-standalone/src/components/data-table/data-table-toolbar.tsx +149 -0
  538. package/templates/nextjs-standalone/src/components/data-table/data-table-view-options.tsx +89 -0
  539. package/templates/nextjs-standalone/src/components/data-table/data-table.tsx +105 -0
  540. package/templates/nextjs-standalone/src/components/form-dialog.tsx +135 -0
  541. package/templates/nextjs-standalone/src/components/ui/accordion.tsx +66 -0
  542. package/templates/nextjs-standalone/src/components/ui/calendar.tsx +220 -0
  543. package/templates/nextjs-standalone/src/components/ui/checkbox.tsx +32 -0
  544. package/templates/nextjs-standalone/src/components/ui/faceted.tsx +283 -0
  545. package/templates/nextjs-standalone/src/components/ui/form.tsx +167 -0
  546. package/templates/nextjs-standalone/src/components/ui/label.tsx +24 -0
  547. package/templates/nextjs-standalone/src/components/ui/popover.tsx +89 -0
  548. package/templates/nextjs-standalone/src/components/ui/progress.tsx +31 -0
  549. package/templates/nextjs-standalone/src/components/ui/scroll-area.tsx +6 -2
  550. package/templates/nextjs-standalone/src/components/ui/slider.tsx +63 -0
  551. package/templates/nextjs-standalone/src/components/ui/sortable.tsx +581 -0
  552. package/templates/nextjs-standalone/src/components/ui/switch.tsx +35 -0
  553. package/templates/nextjs-standalone/src/components/ui/textarea.tsx +18 -0
  554. package/templates/nextjs-standalone/src/config/data-table.ts +82 -0
  555. package/templates/nextjs-standalone/src/env-check.ts +74 -0
  556. package/templates/nextjs-standalone/src/hooks/use-callback-ref.ts +27 -0
  557. package/templates/nextjs-standalone/src/hooks/use-data-table.ts +316 -0
  558. package/templates/nextjs-standalone/src/hooks/use-debounced-callback.ts +28 -0
  559. package/templates/nextjs-standalone/src/lib/action-types.ts +7 -41
  560. package/templates/nextjs-standalone/src/lib/agent-query.ts +4 -2
  561. package/templates/nextjs-standalone/src/lib/agent.ts +363 -31
  562. package/templates/nextjs-standalone/src/lib/api-url.ts +2 -3
  563. package/templates/nextjs-standalone/src/lib/auth/admin-permissions.ts +38 -0
  564. package/templates/nextjs-standalone/src/lib/auth/audit.ts +19 -4
  565. package/templates/nextjs-standalone/src/lib/auth/byot.ts +3 -3
  566. package/templates/nextjs-standalone/src/lib/auth/detect.ts +29 -8
  567. package/templates/nextjs-standalone/src/lib/auth/managed.ts +104 -14
  568. package/templates/nextjs-standalone/src/lib/auth/middleware.ts +53 -6
  569. package/templates/nextjs-standalone/src/lib/auth/migrate.ts +140 -15
  570. package/templates/nextjs-standalone/src/lib/auth/oauth-state.ts +123 -0
  571. package/templates/nextjs-standalone/src/lib/auth/org-permissions.ts +55 -0
  572. package/templates/nextjs-standalone/src/lib/auth/permissions.ts +26 -19
  573. package/templates/nextjs-standalone/src/lib/auth/server.ts +355 -9
  574. package/templates/nextjs-standalone/src/lib/auth/simple-key.ts +3 -3
  575. package/templates/nextjs-standalone/src/lib/auth/types.ts +15 -21
  576. package/templates/nextjs-standalone/src/lib/billing/enforcement.ts +368 -0
  577. package/templates/nextjs-standalone/src/lib/billing/plans.ts +155 -0
  578. package/templates/nextjs-standalone/src/lib/cache/index.ts +92 -0
  579. package/templates/nextjs-standalone/src/lib/cache/keys.ts +30 -0
  580. package/templates/nextjs-standalone/src/lib/cache/lru.ts +79 -0
  581. package/templates/nextjs-standalone/src/lib/cache/types.ts +31 -0
  582. package/templates/nextjs-standalone/src/lib/compose-refs.ts +62 -0
  583. package/templates/nextjs-standalone/src/lib/config.ts +563 -11
  584. package/templates/nextjs-standalone/src/lib/connection-types.ts +9 -0
  585. package/templates/nextjs-standalone/src/lib/conversation-types.ts +1 -25
  586. package/templates/nextjs-standalone/src/lib/conversations.ts +345 -14
  587. package/templates/nextjs-standalone/src/lib/data-table.ts +61 -0
  588. package/templates/nextjs-standalone/src/lib/db/connection.ts +793 -39
  589. package/templates/nextjs-standalone/src/lib/db/internal.ts +985 -139
  590. package/templates/nextjs-standalone/src/lib/db/migrate.ts +295 -0
  591. package/templates/nextjs-standalone/src/lib/db/migrations/0000_baseline.sql +703 -0
  592. package/templates/nextjs-standalone/src/lib/db/migrations/0001_teams_installations.sql +14 -0
  593. package/templates/nextjs-standalone/src/lib/db/migrations/0002_discord_installations.sql +14 -0
  594. package/templates/nextjs-standalone/src/lib/db/migrations/0003_telegram_installations.sql +15 -0
  595. package/templates/nextjs-standalone/src/lib/db/migrations/0004_sandbox_credentials.sql +18 -0
  596. package/templates/nextjs-standalone/src/lib/db/migrations/0005_oauth_state.sql +16 -0
  597. package/templates/nextjs-standalone/src/lib/db/migrations/0006_byot_credentials.sql +14 -0
  598. package/templates/nextjs-standalone/src/lib/db/migrations/0007_gchat_installations.sql +15 -0
  599. package/templates/nextjs-standalone/src/lib/db/migrations/0008_github_installations.sql +14 -0
  600. package/templates/nextjs-standalone/src/lib/db/migrations/0009_linear_installations.sql +15 -0
  601. package/templates/nextjs-standalone/src/lib/db/migrations/0010_whatsapp_installations.sql +14 -0
  602. package/templates/nextjs-standalone/src/lib/db/migrations/0011_email_installations.sql +16 -0
  603. package/templates/nextjs-standalone/src/lib/db/migrations/0012_region_migrations.sql +25 -0
  604. package/templates/nextjs-standalone/src/lib/db/schema.ts +1120 -0
  605. package/templates/nextjs-standalone/src/lib/db/source-rate-limit.ts +89 -139
  606. package/templates/nextjs-standalone/src/lib/demo.ts +308 -0
  607. package/templates/nextjs-standalone/src/lib/discord/store.ts +225 -0
  608. package/templates/nextjs-standalone/src/lib/effect/ai.ts +243 -0
  609. package/templates/nextjs-standalone/src/lib/effect/errors.ts +234 -0
  610. package/templates/nextjs-standalone/src/lib/effect/hono.ts +454 -0
  611. package/templates/nextjs-standalone/src/lib/effect/index.ts +137 -0
  612. package/templates/nextjs-standalone/src/lib/effect/layers.ts +496 -0
  613. package/templates/nextjs-standalone/src/lib/effect/services.ts +776 -0
  614. package/templates/nextjs-standalone/src/lib/effect/sql.ts +178 -0
  615. package/templates/nextjs-standalone/src/lib/effect/toolkit.ts +123 -0
  616. package/templates/nextjs-standalone/src/lib/email/delivery.ts +232 -0
  617. package/templates/nextjs-standalone/src/lib/email/engine.ts +349 -0
  618. package/templates/nextjs-standalone/src/lib/email/hooks.ts +107 -0
  619. package/templates/nextjs-standalone/src/lib/email/index.ts +16 -0
  620. package/templates/nextjs-standalone/src/lib/email/scheduler.ts +72 -0
  621. package/templates/nextjs-standalone/src/lib/email/sequence.ts +73 -0
  622. package/templates/nextjs-standalone/src/lib/email/store.ts +163 -0
  623. package/templates/nextjs-standalone/src/lib/email/templates.ts +215 -0
  624. package/templates/nextjs-standalone/src/lib/format.test.ts +117 -0
  625. package/templates/nextjs-standalone/src/lib/format.ts +67 -0
  626. package/templates/nextjs-standalone/src/lib/gchat/store.ts +202 -0
  627. package/templates/nextjs-standalone/src/lib/github/store.ts +197 -0
  628. package/templates/nextjs-standalone/src/lib/id.ts +29 -0
  629. package/templates/nextjs-standalone/src/lib/integrations/types.ts +166 -0
  630. package/templates/nextjs-standalone/src/lib/learn/pattern-analyzer.ts +224 -0
  631. package/templates/nextjs-standalone/src/lib/learn/pattern-cache.ts +229 -0
  632. package/templates/nextjs-standalone/src/lib/learn/pattern-proposer.ts +87 -0
  633. package/templates/nextjs-standalone/src/lib/learn/suggestion-helpers.ts +34 -0
  634. package/templates/nextjs-standalone/src/lib/learn/suggestions.ts +139 -0
  635. package/templates/nextjs-standalone/src/lib/linear/store.ts +200 -0
  636. package/templates/nextjs-standalone/src/lib/logger.ts +35 -3
  637. package/templates/nextjs-standalone/src/lib/metering.ts +272 -0
  638. package/templates/nextjs-standalone/src/lib/parsers.ts +99 -0
  639. package/templates/nextjs-standalone/src/lib/plugins/hooks.ts +13 -11
  640. package/templates/nextjs-standalone/src/lib/plugins/index.ts +3 -1
  641. package/templates/nextjs-standalone/src/lib/plugins/registry.ts +58 -6
  642. package/templates/nextjs-standalone/src/lib/plugins/settings.ts +147 -0
  643. package/templates/nextjs-standalone/src/lib/plugins/wiring.ts +6 -9
  644. package/templates/nextjs-standalone/src/lib/profiler.ts +1665 -0
  645. package/templates/nextjs-standalone/src/lib/providers.ts +188 -13
  646. package/templates/nextjs-standalone/src/lib/rls.ts +172 -60
  647. package/templates/nextjs-standalone/src/lib/sandbox/credentials.ts +206 -0
  648. package/templates/nextjs-standalone/src/lib/sandbox/validate.ts +179 -0
  649. package/templates/nextjs-standalone/src/lib/scheduled-task-types.ts +26 -94
  650. package/templates/nextjs-standalone/src/lib/scheduled-tasks.ts +174 -34
  651. package/templates/nextjs-standalone/src/lib/scheduler/delivery.ts +248 -150
  652. package/templates/nextjs-standalone/src/lib/scheduler/engine.ts +190 -154
  653. package/templates/nextjs-standalone/src/lib/scheduler/executor.ts +74 -23
  654. package/templates/nextjs-standalone/src/lib/scheduler/preview.ts +72 -0
  655. package/templates/nextjs-standalone/src/lib/security/abuse.ts +463 -0
  656. package/templates/nextjs-standalone/src/lib/semantic/diff.ts +267 -0
  657. package/templates/nextjs-standalone/src/lib/semantic/entities.ts +167 -0
  658. package/templates/nextjs-standalone/src/lib/semantic/files.ts +283 -0
  659. package/templates/nextjs-standalone/src/lib/semantic/index.ts +27 -0
  660. package/templates/nextjs-standalone/src/lib/{semantic-index.ts → semantic/search.ts} +80 -9
  661. package/templates/nextjs-standalone/src/lib/semantic/sync.ts +581 -0
  662. package/templates/nextjs-standalone/src/lib/{semantic.ts → semantic/whitelist.ts} +189 -3
  663. package/templates/nextjs-standalone/src/lib/settings.ts +817 -0
  664. package/templates/nextjs-standalone/src/lib/sidecar-types.ts +13 -0
  665. package/templates/nextjs-standalone/src/lib/slack/store.ts +134 -25
  666. package/templates/nextjs-standalone/src/lib/startup.ts +528 -362
  667. package/templates/nextjs-standalone/src/lib/teams/store.ts +216 -0
  668. package/templates/nextjs-standalone/src/lib/telegram/store.ts +202 -0
  669. package/templates/nextjs-standalone/src/lib/telemetry.ts +40 -0
  670. package/templates/nextjs-standalone/src/lib/tools/actions/audit.ts +8 -5
  671. package/templates/nextjs-standalone/src/lib/tools/actions/email.ts +3 -1
  672. package/templates/nextjs-standalone/src/lib/tools/actions/handler.ts +276 -93
  673. package/templates/nextjs-standalone/src/lib/tools/actions/jira.ts +2 -2
  674. package/templates/nextjs-standalone/src/lib/tools/backends/detect.ts +16 -0
  675. package/templates/nextjs-standalone/src/lib/tools/backends/index.ts +11 -0
  676. package/templates/nextjs-standalone/src/lib/tools/backends/nsjail.ts +213 -0
  677. package/templates/nextjs-standalone/src/lib/tools/backends/shared.ts +103 -0
  678. package/templates/nextjs-standalone/src/lib/tools/backends/types.ts +26 -0
  679. package/templates/nextjs-standalone/src/lib/tools/explore-nsjail.ts +7 -228
  680. package/templates/nextjs-standalone/src/lib/tools/explore-sandbox.ts +4 -29
  681. package/templates/nextjs-standalone/src/lib/tools/explore-sidecar.ts +18 -2
  682. package/templates/nextjs-standalone/src/lib/tools/explore.ts +246 -54
  683. package/templates/nextjs-standalone/src/lib/tools/index.ts +17 -0
  684. package/templates/nextjs-standalone/src/lib/tools/python-nsjail.ts +11 -139
  685. package/templates/nextjs-standalone/src/lib/tools/python-sandbox.ts +9 -132
  686. package/templates/nextjs-standalone/src/lib/tools/python-sidecar.ts +184 -3
  687. package/templates/nextjs-standalone/src/lib/tools/python-stream.ts +33 -0
  688. package/templates/nextjs-standalone/src/lib/tools/python-wrapper.ts +129 -0
  689. package/templates/nextjs-standalone/src/lib/tools/python.ts +115 -15
  690. package/templates/nextjs-standalone/src/lib/tools/registry.ts +14 -2
  691. package/templates/nextjs-standalone/src/lib/tools/sql.ts +778 -362
  692. package/templates/nextjs-standalone/src/lib/tracing.ts +16 -0
  693. package/templates/nextjs-standalone/src/lib/whatsapp/store.ts +198 -0
  694. package/templates/nextjs-standalone/src/lib/workspace.ts +89 -0
  695. package/templates/nextjs-standalone/src/progress.ts +121 -0
  696. package/templates/nextjs-standalone/src/types/data-table.ts +48 -0
  697. package/templates/nextjs-standalone/src/ui/atlas-chat-reexport.ts +3 -0
  698. package/templates/nextjs-standalone/src/ui/components/actions/action-approval-card.tsx +26 -19
  699. package/templates/nextjs-standalone/src/ui/components/actions/action-status-badge.tsx +3 -3
  700. package/templates/nextjs-standalone/src/ui/components/admin/admin-layout.tsx +57 -39
  701. package/templates/nextjs-standalone/src/ui/components/admin/admin-sidebar.tsx +213 -35
  702. package/templates/nextjs-standalone/src/ui/components/admin/delivery-status-badge.tsx +53 -0
  703. package/templates/nextjs-standalone/src/ui/components/admin/empty-state.tsx +27 -6
  704. package/templates/nextjs-standalone/src/ui/components/admin/entity-detail.tsx +3 -52
  705. package/templates/nextjs-standalone/src/ui/components/admin/error-banner.tsx +2 -2
  706. package/templates/nextjs-standalone/src/ui/components/admin/feature-disabled.tsx +28 -5
  707. package/templates/nextjs-standalone/src/ui/components/admin-content-wrapper.tsx +87 -0
  708. package/templates/nextjs-standalone/src/ui/components/atlas-chat.tsx +449 -166
  709. package/templates/nextjs-standalone/src/ui/components/branding-head.tsx +41 -0
  710. package/templates/nextjs-standalone/src/ui/components/chart/chart-detection.ts +62 -5
  711. package/templates/nextjs-standalone/src/ui/components/chart/result-chart.tsx +316 -125
  712. package/templates/nextjs-standalone/src/ui/components/chat/api-key-bar.tsx +4 -4
  713. package/templates/nextjs-standalone/src/ui/components/chat/data-table.tsx +45 -4
  714. package/templates/nextjs-standalone/src/ui/components/chat/error-banner.tsx +86 -5
  715. package/templates/nextjs-standalone/src/ui/components/chat/follow-up-chips.tsx +29 -0
  716. package/templates/nextjs-standalone/src/ui/components/chat/markdown.tsx +24 -0
  717. package/templates/nextjs-standalone/src/ui/components/chat/prompt-library.tsx +206 -0
  718. package/templates/nextjs-standalone/src/ui/components/chat/python-result-card.tsx +106 -78
  719. package/templates/nextjs-standalone/src/ui/components/chat/result-card-base.tsx +101 -0
  720. package/templates/nextjs-standalone/src/ui/components/chat/share-dialog.tsx +377 -0
  721. package/templates/nextjs-standalone/src/ui/components/chat/sql-result-card.tsx +94 -73
  722. package/templates/nextjs-standalone/src/ui/components/chat/suggestion-chips.tsx +46 -0
  723. package/templates/nextjs-standalone/src/ui/components/chat/tool-part.tsx +16 -4
  724. package/templates/nextjs-standalone/src/ui/components/conversations/conversation-item.tsx +48 -17
  725. package/templates/nextjs-standalone/src/ui/components/conversations/conversation-list.tsx +38 -24
  726. package/templates/nextjs-standalone/src/ui/components/conversations/conversation-sidebar.tsx +66 -7
  727. package/templates/nextjs-standalone/src/ui/components/conversations/delete-confirmation.tsx +9 -2
  728. package/templates/nextjs-standalone/src/ui/components/error-boundary.tsx +66 -0
  729. package/templates/nextjs-standalone/src/ui/components/notebook/delete-cell-dialog.tsx +48 -0
  730. package/templates/nextjs-standalone/src/ui/components/notebook/fork-branch-selector.tsx +68 -0
  731. package/templates/nextjs-standalone/src/ui/components/notebook/notebook-cell-input.tsx +76 -0
  732. package/templates/nextjs-standalone/src/ui/components/notebook/notebook-cell-output.tsx +58 -0
  733. package/templates/nextjs-standalone/src/ui/components/notebook/notebook-cell-toolbar.tsx +91 -0
  734. package/templates/nextjs-standalone/src/ui/components/notebook/notebook-cell.tsx +119 -0
  735. package/templates/nextjs-standalone/src/ui/components/notebook/notebook-empty-state.tsx +19 -0
  736. package/templates/nextjs-standalone/src/ui/components/notebook/notebook-export.ts +287 -0
  737. package/templates/nextjs-standalone/src/ui/components/notebook/notebook-input-bar.tsx +49 -0
  738. package/templates/nextjs-standalone/src/ui/components/notebook/notebook-shell.tsx +266 -0
  739. package/templates/nextjs-standalone/src/ui/components/notebook/notebook-text-cell.tsx +152 -0
  740. package/templates/nextjs-standalone/src/ui/components/notebook/types.ts +39 -0
  741. package/templates/nextjs-standalone/src/ui/components/notebook/use-keyboard-nav.ts +109 -0
  742. package/templates/nextjs-standalone/src/ui/components/notebook/use-notebook.ts +684 -0
  743. package/templates/nextjs-standalone/src/ui/components/org-switcher.tsx +111 -0
  744. package/templates/nextjs-standalone/src/ui/components/region-picker.tsx +103 -0
  745. package/templates/nextjs-standalone/src/ui/components/schema-explorer/schema-explorer.tsx +522 -0
  746. package/templates/nextjs-standalone/src/ui/components/social-icons.tsx +26 -0
  747. package/templates/nextjs-standalone/src/ui/components/tour/guided-tour.tsx +81 -0
  748. package/templates/nextjs-standalone/src/ui/components/tour/index.ts +5 -0
  749. package/templates/nextjs-standalone/src/ui/components/tour/nav-bar.tsx +100 -0
  750. package/templates/nextjs-standalone/src/ui/components/tour/tour-overlay.tsx +298 -0
  751. package/templates/nextjs-standalone/src/ui/components/tour/tour-steps.ts +43 -0
  752. package/templates/nextjs-standalone/src/ui/components/tour/types.ts +21 -0
  753. package/templates/nextjs-standalone/src/ui/components/tour/use-tour.ts +193 -0
  754. package/templates/nextjs-standalone/src/ui/context-reexport.ts +3 -0
  755. package/templates/nextjs-standalone/src/ui/hooks/theme-init-script.ts +17 -0
  756. package/templates/nextjs-standalone/src/ui/hooks/use-admin-fetch.ts +38 -30
  757. package/templates/nextjs-standalone/src/ui/hooks/use-admin-mutation.ts +188 -0
  758. package/templates/nextjs-standalone/src/ui/hooks/use-atlas-transport.ts +225 -0
  759. package/templates/nextjs-standalone/src/ui/hooks/use-branding.ts +68 -0
  760. package/templates/nextjs-standalone/src/ui/hooks/use-conversations.ts +106 -83
  761. package/templates/nextjs-standalone/src/ui/hooks/use-dark-mode.ts +134 -10
  762. package/templates/nextjs-standalone/src/ui/hooks/use-deploy-mode.ts +36 -0
  763. package/templates/nextjs-standalone/src/ui/hooks/use-platform-admin-guard.ts +49 -0
  764. package/templates/nextjs-standalone/src/ui/lib/action-types.ts +11 -63
  765. package/templates/nextjs-standalone/src/ui/lib/admin-schemas.ts +744 -0
  766. package/templates/nextjs-standalone/src/ui/lib/fetch-client.ts +84 -0
  767. package/templates/nextjs-standalone/src/ui/lib/fetch-error.ts +54 -0
  768. package/templates/nextjs-standalone/src/ui/lib/helpers.ts +94 -1
  769. package/templates/nextjs-standalone/src/ui/lib/types.ts +149 -140
  770. package/templates/nextjs-standalone/tsconfig.json +3 -2
  771. package/templates/docker/src/api/__tests__/actions.test.ts +0 -683
  772. package/templates/docker/src/api/__tests__/admin.test.ts +0 -820
  773. package/templates/docker/src/api/__tests__/auth.test.ts +0 -165
  774. package/templates/docker/src/api/__tests__/chat.test.ts +0 -376
  775. package/templates/docker/src/api/__tests__/conversations.test.ts +0 -555
  776. package/templates/docker/src/api/__tests__/cors.test.ts +0 -135
  777. package/templates/docker/src/api/__tests__/health-plugin.test.ts +0 -176
  778. package/templates/docker/src/api/__tests__/health.test.ts +0 -283
  779. package/templates/docker/src/api/__tests__/query.test.ts +0 -891
  780. package/templates/docker/src/api/__tests__/scheduled-tasks.test.ts +0 -601
  781. package/templates/docker/src/api/__tests__/slack.test.ts +0 -847
  782. package/templates/docker/src/lib/__tests__/agent-cache.test.ts +0 -439
  783. package/templates/docker/src/lib/__tests__/agent-dialect.test.ts +0 -131
  784. package/templates/docker/src/lib/__tests__/agent-health-annotations.test.ts +0 -166
  785. package/templates/docker/src/lib/__tests__/agent-integration.test.ts +0 -516
  786. package/templates/docker/src/lib/__tests__/config-actions.test.ts +0 -166
  787. package/templates/docker/src/lib/__tests__/config.test.ts +0 -1113
  788. package/templates/docker/src/lib/__tests__/conversations.test.ts +0 -589
  789. package/templates/docker/src/lib/__tests__/errors.test.ts +0 -256
  790. package/templates/docker/src/lib/__tests__/logger.test.ts +0 -200
  791. package/templates/docker/src/lib/__tests__/plugin-aware-validation.test.ts +0 -321
  792. package/templates/docker/src/lib/__tests__/providers.test.ts +0 -130
  793. package/templates/docker/src/lib/__tests__/rls.test.ts +0 -435
  794. package/templates/docker/src/lib/__tests__/scheduled-task-types.test.ts +0 -124
  795. package/templates/docker/src/lib/__tests__/scheduled-tasks.test.ts +0 -550
  796. package/templates/docker/src/lib/__tests__/semantic-index.test.ts +0 -547
  797. package/templates/docker/src/lib/__tests__/semantic-multisource.test.ts +0 -544
  798. package/templates/docker/src/lib/__tests__/semantic.test.ts +0 -363
  799. package/templates/docker/src/lib/__tests__/startup-actions.test.ts +0 -461
  800. package/templates/docker/src/lib/__tests__/startup-first-run.test.ts +0 -429
  801. package/templates/docker/src/lib/__tests__/startup.test.ts +0 -470
  802. package/templates/docker/src/lib/__tests__/tracing.test.ts +0 -28
  803. package/templates/docker/src/lib/auth/__tests__/audit.test.ts +0 -418
  804. package/templates/docker/src/lib/auth/__tests__/byot-integration.test.ts +0 -222
  805. package/templates/docker/src/lib/auth/__tests__/byot.test.ts +0 -366
  806. package/templates/docker/src/lib/auth/__tests__/detect.test.ts +0 -190
  807. package/templates/docker/src/lib/auth/__tests__/managed.test.ts +0 -173
  808. package/templates/docker/src/lib/auth/__tests__/middleware.test.ts +0 -456
  809. package/templates/docker/src/lib/auth/__tests__/migrate.test.ts +0 -203
  810. package/templates/docker/src/lib/auth/__tests__/permissions.test.ts +0 -225
  811. package/templates/docker/src/lib/auth/__tests__/server.test.ts +0 -34
  812. package/templates/docker/src/lib/auth/__tests__/simple-key.test.ts +0 -176
  813. package/templates/docker/src/lib/auth/__tests__/types.test.ts +0 -44
  814. package/templates/docker/src/lib/db/__tests__/connection.test.ts +0 -144
  815. package/templates/docker/src/lib/db/__tests__/internal.test.ts +0 -387
  816. package/templates/docker/src/lib/db/__tests__/registry-health.test.ts +0 -190
  817. package/templates/docker/src/lib/db/__tests__/registry-pool-limits.test.ts +0 -137
  818. package/templates/docker/src/lib/db/__tests__/registry.test.ts +0 -398
  819. package/templates/docker/src/lib/db/__tests__/source-rate-limit.test.ts +0 -130
  820. package/templates/docker/src/lib/errors.ts +0 -154
  821. package/templates/docker/src/lib/plugins/__tests__/hooks-integration.test.ts +0 -204
  822. package/templates/docker/src/lib/plugins/__tests__/hooks.test.ts +0 -529
  823. package/templates/docker/src/lib/plugins/__tests__/migrate.test.ts +0 -875
  824. package/templates/docker/src/lib/plugins/__tests__/registry.test.ts +0 -373
  825. package/templates/docker/src/lib/plugins/__tests__/tools.test.ts +0 -49
  826. package/templates/docker/src/lib/plugins/__tests__/wiring.test.ts +0 -799
  827. package/templates/docker/src/lib/scheduler/__tests__/delivery.test.ts +0 -192
  828. package/templates/docker/src/lib/scheduler/__tests__/engine.test.ts +0 -248
  829. package/templates/docker/src/lib/scheduler/__tests__/format-email.test.ts +0 -96
  830. package/templates/docker/src/lib/scheduler/__tests__/format-slack.test.ts +0 -78
  831. package/templates/docker/src/lib/scheduler/__tests__/format-webhook.test.ts +0 -78
  832. package/templates/docker/src/lib/scheduler/index.ts +0 -7
  833. package/templates/docker/src/lib/slack/__tests__/api.test.ts +0 -160
  834. package/templates/docker/src/lib/slack/__tests__/format.test.ts +0 -237
  835. package/templates/docker/src/lib/slack/__tests__/store.test.ts +0 -188
  836. package/templates/docker/src/lib/slack/__tests__/threads.test.ts +0 -112
  837. package/templates/docker/src/lib/slack/__tests__/verify.test.ts +0 -111
  838. package/templates/docker/src/lib/tools/__tests__/action-permissions.test.ts +0 -594
  839. package/templates/docker/src/lib/tools/__tests__/custom-validation.test.ts +0 -240
  840. package/templates/docker/src/lib/tools/__tests__/explore-backend.test.ts +0 -267
  841. package/templates/docker/src/lib/tools/__tests__/explore-nsjail.test.ts +0 -506
  842. package/templates/docker/src/lib/tools/__tests__/explore-plugin.test.ts +0 -374
  843. package/templates/docker/src/lib/tools/__tests__/explore-sdk-compat.test.ts +0 -82
  844. package/templates/docker/src/lib/tools/__tests__/explore-sidecar.test.ts +0 -210
  845. package/templates/docker/src/lib/tools/__tests__/python-nsjail.test.ts +0 -515
  846. package/templates/docker/src/lib/tools/__tests__/python-sandbox.test.ts +0 -397
  847. package/templates/docker/src/lib/tools/__tests__/python-sidecar.test.ts +0 -365
  848. package/templates/docker/src/lib/tools/__tests__/python.test.ts +0 -331
  849. package/templates/docker/src/lib/tools/__tests__/registry-actions.test.ts +0 -132
  850. package/templates/docker/src/lib/tools/__tests__/registry.test.ts +0 -242
  851. package/templates/docker/src/lib/tools/__tests__/sql-audit.test.ts +0 -227
  852. package/templates/docker/src/lib/tools/__tests__/sql-connection-whitelist.test.ts +0 -100
  853. package/templates/docker/src/lib/tools/__tests__/sql-ratelimit.test.ts +0 -227
  854. package/templates/docker/src/lib/tools/__tests__/sql.test.ts +0 -709
  855. package/templates/docker/src/lib/tools/actions/__tests__/audit.test.ts +0 -211
  856. package/templates/docker/src/lib/tools/actions/__tests__/email.test.ts +0 -378
  857. package/templates/docker/src/lib/tools/actions/__tests__/handler.test.ts +0 -681
  858. package/templates/docker/src/lib/tools/actions/__tests__/jira.test.ts +0 -427
  859. package/templates/docker/src/test-setup.ts +0 -38
  860. package/templates/docker/src/types/vercel-sandbox.d.ts +0 -61
  861. package/templates/docker/src/ui/components/chat/managed-auth-card.tsx +0 -116
  862. package/templates/nextjs-standalone/src/api/__tests__/actions.test.ts +0 -683
  863. package/templates/nextjs-standalone/src/api/__tests__/admin.test.ts +0 -820
  864. package/templates/nextjs-standalone/src/api/__tests__/auth.test.ts +0 -165
  865. package/templates/nextjs-standalone/src/api/__tests__/chat.test.ts +0 -376
  866. package/templates/nextjs-standalone/src/api/__tests__/conversations.test.ts +0 -555
  867. package/templates/nextjs-standalone/src/api/__tests__/cors.test.ts +0 -135
  868. package/templates/nextjs-standalone/src/api/__tests__/health-plugin.test.ts +0 -176
  869. package/templates/nextjs-standalone/src/api/__tests__/health.test.ts +0 -283
  870. package/templates/nextjs-standalone/src/api/__tests__/query.test.ts +0 -891
  871. package/templates/nextjs-standalone/src/api/__tests__/scheduled-tasks.test.ts +0 -601
  872. package/templates/nextjs-standalone/src/api/__tests__/slack.test.ts +0 -847
  873. package/templates/nextjs-standalone/src/app/global-error.tsx +0 -68
  874. package/templates/nextjs-standalone/src/lib/__tests__/agent-cache.test.ts +0 -439
  875. package/templates/nextjs-standalone/src/lib/__tests__/agent-dialect.test.ts +0 -131
  876. package/templates/nextjs-standalone/src/lib/__tests__/agent-health-annotations.test.ts +0 -166
  877. package/templates/nextjs-standalone/src/lib/__tests__/agent-integration.test.ts +0 -516
  878. package/templates/nextjs-standalone/src/lib/__tests__/config-actions.test.ts +0 -166
  879. package/templates/nextjs-standalone/src/lib/__tests__/config.test.ts +0 -1113
  880. package/templates/nextjs-standalone/src/lib/__tests__/conversations.test.ts +0 -589
  881. package/templates/nextjs-standalone/src/lib/__tests__/errors.test.ts +0 -256
  882. package/templates/nextjs-standalone/src/lib/__tests__/logger.test.ts +0 -200
  883. package/templates/nextjs-standalone/src/lib/__tests__/plugin-aware-validation.test.ts +0 -321
  884. package/templates/nextjs-standalone/src/lib/__tests__/providers.test.ts +0 -130
  885. package/templates/nextjs-standalone/src/lib/__tests__/rls.test.ts +0 -435
  886. package/templates/nextjs-standalone/src/lib/__tests__/scheduled-task-types.test.ts +0 -124
  887. package/templates/nextjs-standalone/src/lib/__tests__/scheduled-tasks.test.ts +0 -550
  888. package/templates/nextjs-standalone/src/lib/__tests__/semantic-index.test.ts +0 -547
  889. package/templates/nextjs-standalone/src/lib/__tests__/semantic-multisource.test.ts +0 -544
  890. package/templates/nextjs-standalone/src/lib/__tests__/semantic.test.ts +0 -363
  891. package/templates/nextjs-standalone/src/lib/__tests__/startup-actions.test.ts +0 -461
  892. package/templates/nextjs-standalone/src/lib/__tests__/startup-first-run.test.ts +0 -429
  893. package/templates/nextjs-standalone/src/lib/__tests__/startup.test.ts +0 -470
  894. package/templates/nextjs-standalone/src/lib/__tests__/tracing.test.ts +0 -28
  895. package/templates/nextjs-standalone/src/lib/auth/__tests__/audit.test.ts +0 -418
  896. package/templates/nextjs-standalone/src/lib/auth/__tests__/byot-integration.test.ts +0 -222
  897. package/templates/nextjs-standalone/src/lib/auth/__tests__/byot.test.ts +0 -366
  898. package/templates/nextjs-standalone/src/lib/auth/__tests__/detect.test.ts +0 -190
  899. package/templates/nextjs-standalone/src/lib/auth/__tests__/managed.test.ts +0 -173
  900. package/templates/nextjs-standalone/src/lib/auth/__tests__/middleware.test.ts +0 -456
  901. package/templates/nextjs-standalone/src/lib/auth/__tests__/migrate.test.ts +0 -203
  902. package/templates/nextjs-standalone/src/lib/auth/__tests__/permissions.test.ts +0 -225
  903. package/templates/nextjs-standalone/src/lib/auth/__tests__/server.test.ts +0 -34
  904. package/templates/nextjs-standalone/src/lib/auth/__tests__/simple-key.test.ts +0 -176
  905. package/templates/nextjs-standalone/src/lib/auth/__tests__/types.test.ts +0 -44
  906. package/templates/nextjs-standalone/src/lib/db/__tests__/connection.test.ts +0 -144
  907. package/templates/nextjs-standalone/src/lib/db/__tests__/internal.test.ts +0 -387
  908. package/templates/nextjs-standalone/src/lib/db/__tests__/registry-health.test.ts +0 -190
  909. package/templates/nextjs-standalone/src/lib/db/__tests__/registry-pool-limits.test.ts +0 -137
  910. package/templates/nextjs-standalone/src/lib/db/__tests__/registry.test.ts +0 -398
  911. package/templates/nextjs-standalone/src/lib/db/__tests__/source-rate-limit.test.ts +0 -130
  912. package/templates/nextjs-standalone/src/lib/errors.ts +0 -154
  913. package/templates/nextjs-standalone/src/lib/plugins/__tests__/hooks-integration.test.ts +0 -204
  914. package/templates/nextjs-standalone/src/lib/plugins/__tests__/hooks.test.ts +0 -529
  915. package/templates/nextjs-standalone/src/lib/plugins/__tests__/migrate.test.ts +0 -875
  916. package/templates/nextjs-standalone/src/lib/plugins/__tests__/registry.test.ts +0 -373
  917. package/templates/nextjs-standalone/src/lib/plugins/__tests__/tools.test.ts +0 -49
  918. package/templates/nextjs-standalone/src/lib/plugins/__tests__/wiring.test.ts +0 -799
  919. package/templates/nextjs-standalone/src/lib/scheduler/__tests__/delivery.test.ts +0 -192
  920. package/templates/nextjs-standalone/src/lib/scheduler/__tests__/engine.test.ts +0 -248
  921. package/templates/nextjs-standalone/src/lib/scheduler/__tests__/format-email.test.ts +0 -96
  922. package/templates/nextjs-standalone/src/lib/scheduler/__tests__/format-slack.test.ts +0 -78
  923. package/templates/nextjs-standalone/src/lib/scheduler/__tests__/format-webhook.test.ts +0 -78
  924. package/templates/nextjs-standalone/src/lib/scheduler/index.ts +0 -7
  925. package/templates/nextjs-standalone/src/lib/slack/__tests__/api.test.ts +0 -160
  926. package/templates/nextjs-standalone/src/lib/slack/__tests__/format.test.ts +0 -237
  927. package/templates/nextjs-standalone/src/lib/slack/__tests__/store.test.ts +0 -188
  928. package/templates/nextjs-standalone/src/lib/slack/__tests__/threads.test.ts +0 -112
  929. package/templates/nextjs-standalone/src/lib/slack/__tests__/verify.test.ts +0 -111
  930. package/templates/nextjs-standalone/src/lib/tools/__tests__/action-permissions.test.ts +0 -594
  931. package/templates/nextjs-standalone/src/lib/tools/__tests__/custom-validation.test.ts +0 -240
  932. package/templates/nextjs-standalone/src/lib/tools/__tests__/explore-backend.test.ts +0 -267
  933. package/templates/nextjs-standalone/src/lib/tools/__tests__/explore-nsjail.test.ts +0 -506
  934. package/templates/nextjs-standalone/src/lib/tools/__tests__/explore-plugin.test.ts +0 -374
  935. package/templates/nextjs-standalone/src/lib/tools/__tests__/explore-sdk-compat.test.ts +0 -82
  936. package/templates/nextjs-standalone/src/lib/tools/__tests__/explore-sidecar.test.ts +0 -210
  937. package/templates/nextjs-standalone/src/lib/tools/__tests__/python-nsjail.test.ts +0 -515
  938. package/templates/nextjs-standalone/src/lib/tools/__tests__/python-sandbox.test.ts +0 -397
  939. package/templates/nextjs-standalone/src/lib/tools/__tests__/python-sidecar.test.ts +0 -365
  940. package/templates/nextjs-standalone/src/lib/tools/__tests__/python.test.ts +0 -331
  941. package/templates/nextjs-standalone/src/lib/tools/__tests__/registry-actions.test.ts +0 -132
  942. package/templates/nextjs-standalone/src/lib/tools/__tests__/registry.test.ts +0 -242
  943. package/templates/nextjs-standalone/src/lib/tools/__tests__/sql-audit.test.ts +0 -227
  944. package/templates/nextjs-standalone/src/lib/tools/__tests__/sql-connection-whitelist.test.ts +0 -100
  945. package/templates/nextjs-standalone/src/lib/tools/__tests__/sql-ratelimit.test.ts +0 -227
  946. package/templates/nextjs-standalone/src/lib/tools/__tests__/sql.test.ts +0 -709
  947. package/templates/nextjs-standalone/src/lib/tools/actions/__tests__/audit.test.ts +0 -211
  948. package/templates/nextjs-standalone/src/lib/tools/actions/__tests__/email.test.ts +0 -378
  949. package/templates/nextjs-standalone/src/lib/tools/actions/__tests__/handler.test.ts +0 -681
  950. package/templates/nextjs-standalone/src/lib/tools/actions/__tests__/jira.test.ts +0 -427
  951. package/templates/nextjs-standalone/src/test-setup.ts +0 -38
  952. package/templates/nextjs-standalone/src/ui/components/chat/managed-auth-card.tsx +0 -116
@@ -0,0 +1,2667 @@
1
+ /**
2
+ * Admin integrations routes.
3
+ *
4
+ * Mounted under /api/v1/admin/integrations. All routes require admin role
5
+ * and org context. Provides aggregated integration status, connect,
6
+ * and disconnect operations for Slack, Teams, Discord, Telegram, Google Chat, GitHub, Linear, WhatsApp, and Email.
7
+ */
8
+
9
+ import { Effect } from "effect";
10
+ import { createRoute, z } from "@hono/zod-openapi";
11
+ import { runEffect } from "@atlas/api/lib/effect/hono";
12
+ import { AuthContext } from "@atlas/api/lib/effect/services";
13
+ import { internalQuery, hasInternalDB } from "@atlas/api/lib/db/internal";
14
+ import { getInstallationByOrg, saveInstallation, deleteInstallationByOrg } from "@atlas/api/lib/slack/store";
15
+ import {
16
+ getTeamsInstallationByOrg,
17
+ saveTeamsInstallation,
18
+ deleteTeamsInstallationByOrg,
19
+ } from "@atlas/api/lib/teams/store";
20
+ import {
21
+ getDiscordInstallationByOrg,
22
+ saveDiscordInstallation,
23
+ deleteDiscordInstallationByOrg,
24
+ } from "@atlas/api/lib/discord/store";
25
+ import {
26
+ getTelegramInstallationByOrg,
27
+ saveTelegramInstallation,
28
+ deleteTelegramInstallationByOrg,
29
+ } from "@atlas/api/lib/telegram/store";
30
+ import {
31
+ getGChatInstallationByOrg,
32
+ saveGChatInstallation,
33
+ deleteGChatInstallationByOrg,
34
+ } from "@atlas/api/lib/gchat/store";
35
+ import {
36
+ getGitHubInstallationByOrg,
37
+ saveGitHubInstallation,
38
+ deleteGitHubInstallationByOrg,
39
+ } from "@atlas/api/lib/github/store";
40
+ import {
41
+ getLinearInstallationByOrg,
42
+ saveLinearInstallation,
43
+ deleteLinearInstallationByOrg,
44
+ } from "@atlas/api/lib/linear/store";
45
+ import {
46
+ getWhatsAppInstallationByOrg,
47
+ saveWhatsAppInstallation,
48
+ deleteWhatsAppInstallationByOrg,
49
+ } from "@atlas/api/lib/whatsapp/store";
50
+ import {
51
+ getEmailInstallationByOrg,
52
+ saveEmailInstallation,
53
+ deleteEmailInstallationByOrg,
54
+ } from "@atlas/api/lib/email/store";
55
+ import type { EmailProvider, ProviderConfig } from "@atlas/api/lib/email/store";
56
+ import { getConfig } from "@atlas/api/lib/config";
57
+ import { createLogger } from "@atlas/api/lib/logger";
58
+ import { ErrorSchema, AuthErrorSchema } from "./shared-schemas";
59
+ import { createAdminRouter, requireOrgContext } from "./admin-router";
60
+
61
+ const log = createLogger("admin-integrations");
62
+
63
+ // ---------------------------------------------------------------------------
64
+ // Schemas
65
+ // ---------------------------------------------------------------------------
66
+
67
+ const DeliveryChannelEnum = z.enum(["email", "slack", "webhook"]);
68
+
69
+ const SlackStatusSchema = z.object({
70
+ connected: z.boolean(),
71
+ teamId: z.string().nullable(),
72
+ workspaceName: z.string().nullable(),
73
+ installedAt: z.string().datetime().nullable(),
74
+ /** Whether Slack OAuth env vars are configured (SLACK_CLIENT_ID etc.) */
75
+ oauthConfigured: z.boolean(),
76
+ /** Whether env-based token is set (single-workspace mode) */
77
+ envConfigured: z.boolean(),
78
+ /** Whether the workspace admin can connect/disconnect (true) or it's platform-level only (false) */
79
+ configurable: z.boolean(),
80
+ });
81
+
82
+ const TeamsStatusSchema = z.object({
83
+ connected: z.boolean(),
84
+ tenantId: z.string().nullable(),
85
+ tenantName: z.string().nullable(),
86
+ installedAt: z.string().datetime().nullable(),
87
+ /** Whether the workspace admin can connect/disconnect (true when TEAMS_APP_ID is set) */
88
+ configurable: z.boolean(),
89
+ });
90
+
91
+ const DiscordStatusSchema = z.object({
92
+ connected: z.boolean(),
93
+ guildId: z.string().nullable(),
94
+ guildName: z.string().nullable(),
95
+ installedAt: z.string().datetime().nullable(),
96
+ /** Whether the workspace admin can connect/disconnect (true when DISCORD_CLIENT_ID is set) */
97
+ configurable: z.boolean(),
98
+ });
99
+
100
+ const TelegramStatusSchema = z.object({
101
+ connected: z.boolean(),
102
+ botId: z.string().nullable(),
103
+ botUsername: z.string().nullable(),
104
+ installedAt: z.string().datetime().nullable(),
105
+ /** Configurable when internal DB is available (SaaS or self-hosted with DATABASE_URL). BYOT — bring your own token */
106
+ configurable: z.boolean(),
107
+ });
108
+
109
+ const GChatStatusSchema = z.object({
110
+ connected: z.boolean(),
111
+ projectId: z.string().nullable(),
112
+ serviceAccountEmail: z.string().nullable(),
113
+ installedAt: z.string().datetime().nullable(),
114
+ /** Configurable when internal DB is available. BYOT — bring your own service account */
115
+ configurable: z.boolean(),
116
+ });
117
+
118
+ const GitHubStatusSchema = z.object({
119
+ connected: z.boolean(),
120
+ username: z.string().nullable(),
121
+ installedAt: z.string().datetime().nullable(),
122
+ /** Configurable when internal DB is available. BYOT — bring your own PAT */
123
+ configurable: z.boolean(),
124
+ });
125
+
126
+ const LinearStatusSchema = z.object({
127
+ connected: z.boolean(),
128
+ userName: z.string().nullable(),
129
+ userEmail: z.string().nullable(),
130
+ installedAt: z.string().datetime().nullable(),
131
+ /** Configurable when internal DB is available. BYOT — bring your own API key */
132
+ configurable: z.boolean(),
133
+ });
134
+
135
+ const WhatsAppStatusSchema = z.object({
136
+ connected: z.boolean(),
137
+ phoneNumberId: z.string().nullable(),
138
+ displayPhone: z.string().nullable(),
139
+ installedAt: z.string().datetime().nullable(),
140
+ /** Configurable when internal DB is available. BYOT — bring your own Cloud API credentials */
141
+ configurable: z.boolean(),
142
+ });
143
+
144
+ const EmailStatusSchema = z.object({
145
+ connected: z.boolean(),
146
+ provider: z.string().nullable(),
147
+ senderAddress: z.string().nullable(),
148
+ installedAt: z.string().datetime().nullable(),
149
+ /** Configurable when internal DB is available. BYOT — bring your own email provider credentials */
150
+ configurable: z.boolean(),
151
+ });
152
+
153
+ const WebhookStatusSchema = z.object({
154
+ activeCount: z.number().int().nonnegative(),
155
+ /** Whether the workspace admin can create/manage webhooks */
156
+ configurable: z.boolean(),
157
+ });
158
+
159
+ const IntegrationStatusSchema = z.object({
160
+ slack: SlackStatusSchema,
161
+ teams: TeamsStatusSchema,
162
+ discord: DiscordStatusSchema,
163
+ telegram: TelegramStatusSchema,
164
+ gchat: GChatStatusSchema,
165
+ github: GitHubStatusSchema,
166
+ linear: LinearStatusSchema,
167
+ whatsapp: WhatsAppStatusSchema,
168
+ email: EmailStatusSchema,
169
+ webhooks: WebhookStatusSchema,
170
+ /** Delivery channels available for scheduled tasks */
171
+ deliveryChannels: z.array(DeliveryChannelEnum),
172
+ /** Resolved deploy mode — lets the frontend branch UI for SaaS vs self-hosted */
173
+ deployMode: z.enum(["saas", "self-hosted"]),
174
+ /** Whether the internal database is available (enables BYOT credential storage) */
175
+ hasInternalDB: z.boolean(),
176
+ });
177
+
178
+ // ---------------------------------------------------------------------------
179
+ // Route definitions
180
+ // ---------------------------------------------------------------------------
181
+
182
+ const getStatusRoute = createRoute({
183
+ method: "get",
184
+ path: "/status",
185
+ tags: ["Admin — Integrations"],
186
+ summary: "Get integration status",
187
+ description:
188
+ "Returns the status of all configured integrations for the current workspace: " +
189
+ "Slack, Teams, Discord, Telegram, Google Chat, GitHub, Linear, WhatsApp, Email, webhooks, available delivery channels, deploy mode, and internal database availability.",
190
+ responses: {
191
+ 200: {
192
+ description: "Integration status",
193
+ content: {
194
+ "application/json": { schema: IntegrationStatusSchema },
195
+ },
196
+ },
197
+ 400: {
198
+ description: "No active organization",
199
+ content: { "application/json": { schema: ErrorSchema } },
200
+ },
201
+ 401: {
202
+ description: "Authentication required",
203
+ content: { "application/json": { schema: AuthErrorSchema } },
204
+ },
205
+ 404: {
206
+ description: "Internal database not configured",
207
+ content: { "application/json": { schema: ErrorSchema } },
208
+ },
209
+ 500: {
210
+ description: "Internal server error",
211
+ content: { "application/json": { schema: ErrorSchema } },
212
+ },
213
+ },
214
+ });
215
+
216
+ const disconnectSlackRoute = createRoute({
217
+ method: "delete",
218
+ path: "/slack",
219
+ tags: ["Admin — Integrations"],
220
+ summary: "Disconnect Slack",
221
+ description:
222
+ "Removes the Slack installation for the current workspace. " +
223
+ "Any Slack bot functionality will stop working until reconnected.",
224
+ responses: {
225
+ 200: {
226
+ description: "Slack disconnected",
227
+ content: {
228
+ "application/json": {
229
+ schema: z.object({ message: z.string() }),
230
+ },
231
+ },
232
+ },
233
+ 400: {
234
+ description: "No active organization",
235
+ content: { "application/json": { schema: ErrorSchema } },
236
+ },
237
+ 401: {
238
+ description: "Authentication required",
239
+ content: { "application/json": { schema: AuthErrorSchema } },
240
+ },
241
+ 404: {
242
+ description: "No Slack installation found or internal database not configured",
243
+ content: { "application/json": { schema: ErrorSchema } },
244
+ },
245
+ 500: {
246
+ description: "Internal server error",
247
+ content: { "application/json": { schema: ErrorSchema } },
248
+ },
249
+ },
250
+ });
251
+
252
+ const disconnectTeamsRoute = createRoute({
253
+ method: "delete",
254
+ path: "/teams",
255
+ tags: ["Admin — Integrations"],
256
+ summary: "Disconnect Teams",
257
+ description:
258
+ "Removes the Teams installation for the current workspace. " +
259
+ "Any Teams bot functionality will stop working until reconnected.",
260
+ responses: {
261
+ 200: {
262
+ description: "Teams disconnected",
263
+ content: {
264
+ "application/json": {
265
+ schema: z.object({ message: z.string() }),
266
+ },
267
+ },
268
+ },
269
+ 400: {
270
+ description: "No active organization",
271
+ content: { "application/json": { schema: ErrorSchema } },
272
+ },
273
+ 401: {
274
+ description: "Authentication required",
275
+ content: { "application/json": { schema: AuthErrorSchema } },
276
+ },
277
+ 404: {
278
+ description: "No Teams installation found or internal database not configured",
279
+ content: { "application/json": { schema: ErrorSchema } },
280
+ },
281
+ 500: {
282
+ description: "Internal server error",
283
+ content: { "application/json": { schema: ErrorSchema } },
284
+ },
285
+ },
286
+ });
287
+
288
+ // ---------------------------------------------------------------------------
289
+ // Router
290
+ // ---------------------------------------------------------------------------
291
+
292
+ const adminIntegrations = createAdminRouter();
293
+
294
+ adminIntegrations.use(requireOrgContext());
295
+
296
+ // GET /status — aggregated integration status
297
+ adminIntegrations.openapi(getStatusRoute, async (c) => {
298
+ return runEffect(
299
+ c,
300
+ Effect.gen(function* () {
301
+ const { orgId } = yield* AuthContext;
302
+
303
+ // requireOrgContext() middleware guarantees orgId is set, but verify
304
+ // at the Effect boundary to avoid non-null assertions
305
+ if (!orgId) {
306
+ return c.json(
307
+ { error: "bad_request", message: "No active organization." },
308
+ 400,
309
+ );
310
+ }
311
+
312
+ const deployMode = getConfig()?.deployMode ?? "self-hosted";
313
+
314
+ // Run all integration lookups in parallel — they are independent
315
+ const [slackInstall, teamsInstall, discordInstall, telegramInstall, gchatInstall, githubInstall, linearInstall, whatsappInstall, emailInstall, webhookActiveCount] =
316
+ yield* Effect.all(
317
+ [
318
+ Effect.tryPromise({
319
+ try: () => getInstallationByOrg(orgId),
320
+ catch: (err) => err instanceof Error ? err : new Error(String(err)),
321
+ }),
322
+ Effect.tryPromise({
323
+ try: () => getTeamsInstallationByOrg(orgId),
324
+ catch: (err) => err instanceof Error ? err : new Error(String(err)),
325
+ }),
326
+ Effect.tryPromise({
327
+ try: () => getDiscordInstallationByOrg(orgId),
328
+ catch: (err) => err instanceof Error ? err : new Error(String(err)),
329
+ }),
330
+ Effect.tryPromise({
331
+ try: () => getTelegramInstallationByOrg(orgId),
332
+ catch: (err) => err instanceof Error ? err : new Error(String(err)),
333
+ }),
334
+ Effect.tryPromise({
335
+ try: () => getGChatInstallationByOrg(orgId),
336
+ catch: (err) => err instanceof Error ? err : new Error(String(err)),
337
+ }),
338
+ Effect.tryPromise({
339
+ try: () => getGitHubInstallationByOrg(orgId),
340
+ catch: (err) => err instanceof Error ? err : new Error(String(err)),
341
+ }),
342
+ Effect.tryPromise({
343
+ try: () => getLinearInstallationByOrg(orgId),
344
+ catch: (err) => err instanceof Error ? err : new Error(String(err)),
345
+ }),
346
+ Effect.tryPromise({
347
+ try: () => getWhatsAppInstallationByOrg(orgId),
348
+ catch: (err) => err instanceof Error ? err : new Error(String(err)),
349
+ }),
350
+ Effect.tryPromise({
351
+ try: () => getEmailInstallationByOrg(orgId),
352
+ catch: (err) => err instanceof Error ? err : new Error(String(err)),
353
+ }),
354
+ Effect.tryPromise({
355
+ try: async () => {
356
+ if (!hasInternalDB()) return 0;
357
+ const rows = await internalQuery<{ count: number }>(
358
+ `SELECT COUNT(*)::int AS count FROM scheduled_tasks
359
+ WHERE org_id = $1 AND enabled = true
360
+ AND recipients @> $2::jsonb`,
361
+ [orgId, JSON.stringify([{ type: "webhook" }])],
362
+ );
363
+ return rows[0]?.count ?? 0;
364
+ },
365
+ catch: (err) => err instanceof Error ? err : new Error(String(err)),
366
+ }),
367
+ ],
368
+ { concurrency: "unbounded" },
369
+ );
370
+
371
+ // Slack status
372
+ const oauthConfigured = !!(
373
+ process.env.SLACK_CLIENT_ID && process.env.SLACK_CLIENT_SECRET
374
+ );
375
+ const envConfigured = !!process.env.SLACK_BOT_TOKEN;
376
+ const slackConfigurable = oauthConfigured;
377
+
378
+ const slack = {
379
+ connected: slackInstall !== null,
380
+ teamId: slackInstall?.team_id ?? null,
381
+ workspaceName: slackInstall?.workspace_name ?? null,
382
+ installedAt: slackInstall?.installed_at ?? null,
383
+ oauthConfigured,
384
+ envConfigured,
385
+ configurable: slackConfigurable,
386
+ };
387
+
388
+ // Teams status
389
+ const teamsConfigurable = !!process.env.TEAMS_APP_ID;
390
+ const teams = {
391
+ connected: teamsInstall !== null,
392
+ tenantId: teamsInstall?.tenant_id ?? null,
393
+ tenantName: teamsInstall?.tenant_name ?? null,
394
+ installedAt: teamsInstall?.installed_at ?? null,
395
+ configurable: teamsConfigurable,
396
+ };
397
+
398
+ // Discord status
399
+ const discordConfigurable = !!process.env.DISCORD_CLIENT_ID;
400
+ const discord = {
401
+ connected: discordInstall !== null,
402
+ guildId: discordInstall?.guild_id ?? null,
403
+ guildName: discordInstall?.guild_name ?? null,
404
+ installedAt: discordInstall?.installed_at ?? null,
405
+ configurable: discordConfigurable,
406
+ };
407
+
408
+ // Telegram status — configurable in SaaS mode or when internal DB is available (BYOT)
409
+ const telegramConfigurable = deployMode === "saas" || hasInternalDB();
410
+ const telegram = {
411
+ connected: telegramInstall !== null,
412
+ botId: telegramInstall?.bot_id ?? null,
413
+ botUsername: telegramInstall?.bot_username ?? null,
414
+ installedAt: telegramInstall?.installed_at ?? null,
415
+ configurable: telegramConfigurable,
416
+ };
417
+
418
+ // Google Chat status — BYOT-only, configurable when internal DB is available.
419
+ // SaaS always has internal DB, so hasInternalDB() alone suffices (no deployMode check needed).
420
+ const gchatConfigurable = hasInternalDB();
421
+ const gchat = {
422
+ connected: gchatInstall !== null,
423
+ projectId: gchatInstall?.project_id ?? null,
424
+ serviceAccountEmail: gchatInstall?.service_account_email ?? null,
425
+ installedAt: gchatInstall?.installed_at ?? null,
426
+ configurable: gchatConfigurable,
427
+ };
428
+
429
+ // GitHub status — BYOT-only, configurable when internal DB is available.
430
+ const githubConfigurable = hasInternalDB();
431
+ const github = {
432
+ connected: githubInstall !== null,
433
+ username: githubInstall?.username ?? null,
434
+ installedAt: githubInstall?.installed_at ?? null,
435
+ configurable: githubConfigurable,
436
+ };
437
+
438
+ // Linear status — BYOT-only, configurable when internal DB is available.
439
+ const linearConfigurable = hasInternalDB();
440
+ const linear = {
441
+ connected: linearInstall !== null,
442
+ userName: linearInstall?.user_name ?? null,
443
+ userEmail: linearInstall?.user_email ?? null,
444
+ installedAt: linearInstall?.installed_at ?? null,
445
+ configurable: linearConfigurable,
446
+ };
447
+
448
+ // WhatsApp status — BYOT-only, configurable when internal DB is available.
449
+ const whatsappConfigurable = hasInternalDB();
450
+ const whatsapp = {
451
+ connected: whatsappInstall !== null,
452
+ phoneNumberId: whatsappInstall?.phone_number_id ?? null,
453
+ displayPhone: whatsappInstall?.display_phone ?? null,
454
+ installedAt: whatsappInstall?.installed_at ?? null,
455
+ configurable: whatsappConfigurable,
456
+ };
457
+
458
+ // Email status — BYOT-only, configurable when internal DB is available.
459
+ const emailConfigurable = hasInternalDB();
460
+ const email = {
461
+ connected: emailInstall !== null,
462
+ provider: emailInstall?.provider ?? null,
463
+ senderAddress: emailInstall?.sender_address ?? null,
464
+ installedAt: emailInstall?.installed_at ?? null,
465
+ configurable: emailConfigurable,
466
+ };
467
+
468
+ // Available delivery channels
469
+ const deliveryChannels: Array<"email" | "slack" | "webhook"> = ["email"];
470
+ if (slack.connected || slack.envConfigured) {
471
+ deliveryChannels.push("slack");
472
+ }
473
+ deliveryChannels.push("webhook");
474
+
475
+ // Webhooks are always configurable by workspace admins (they create scheduled tasks)
476
+ const webhooksConfigurable = hasInternalDB();
477
+
478
+ return c.json(
479
+ {
480
+ slack,
481
+ teams,
482
+ discord,
483
+ telegram,
484
+ gchat,
485
+ github,
486
+ linear,
487
+ whatsapp,
488
+ email,
489
+ webhooks: { activeCount: webhookActiveCount, configurable: webhooksConfigurable },
490
+ deliveryChannels,
491
+ deployMode,
492
+ hasInternalDB: hasInternalDB(),
493
+ },
494
+ 200,
495
+ );
496
+ }),
497
+ { label: "get integration status" },
498
+ );
499
+ });
500
+
501
+ // DELETE /slack — disconnect Slack for current org
502
+ adminIntegrations.openapi(disconnectSlackRoute, async (c) => {
503
+ return runEffect(
504
+ c,
505
+ Effect.gen(function* () {
506
+ const { orgId } = yield* AuthContext;
507
+
508
+ if (!orgId) {
509
+ return c.json(
510
+ { error: "bad_request", message: "No active organization." },
511
+ 400,
512
+ );
513
+ }
514
+
515
+ const deleted = yield* Effect.tryPromise({
516
+ try: () => deleteInstallationByOrg(orgId),
517
+ catch: (err) => err instanceof Error ? err : new Error(String(err)),
518
+ });
519
+
520
+ if (!deleted) {
521
+ return c.json(
522
+ { error: "not_found", message: "No Slack installation found for this workspace." },
523
+ 404,
524
+ );
525
+ }
526
+
527
+ log.info({ orgId }, "Slack installation disconnected by admin");
528
+ return c.json({ message: "Slack disconnected successfully." }, 200);
529
+ }),
530
+ { label: "disconnect slack" },
531
+ );
532
+ });
533
+
534
+ // DELETE /teams — disconnect Teams for current org
535
+ adminIntegrations.openapi(disconnectTeamsRoute, async (c) => {
536
+ return runEffect(
537
+ c,
538
+ Effect.gen(function* () {
539
+ const { orgId } = yield* AuthContext;
540
+
541
+ if (!orgId) {
542
+ return c.json(
543
+ { error: "bad_request", message: "No active organization." },
544
+ 400,
545
+ );
546
+ }
547
+
548
+ const deleted = yield* Effect.tryPromise({
549
+ try: () => deleteTeamsInstallationByOrg(orgId),
550
+ catch: (err) => err instanceof Error ? err : new Error(String(err)),
551
+ });
552
+
553
+ if (!deleted) {
554
+ return c.json(
555
+ { error: "not_found", message: "No Teams installation found for this workspace." },
556
+ 404,
557
+ );
558
+ }
559
+
560
+ log.info({ orgId }, "Teams installation disconnected by admin");
561
+ return c.json({ message: "Teams disconnected successfully." }, 200);
562
+ }),
563
+ { label: "disconnect teams" },
564
+ );
565
+ });
566
+
567
+ // DELETE /discord — disconnect Discord for current org
568
+ const disconnectDiscordRoute = createRoute({
569
+ method: "delete",
570
+ path: "/discord",
571
+ tags: ["Admin — Integrations"],
572
+ summary: "Disconnect Discord",
573
+ description:
574
+ "Removes the Discord installation for the current workspace. " +
575
+ "Any Discord bot functionality will stop working until reconnected.",
576
+ responses: {
577
+ 200: {
578
+ description: "Discord disconnected",
579
+ content: {
580
+ "application/json": {
581
+ schema: z.object({ message: z.string() }),
582
+ },
583
+ },
584
+ },
585
+ 400: {
586
+ description: "No active organization",
587
+ content: { "application/json": { schema: ErrorSchema } },
588
+ },
589
+ 401: {
590
+ description: "Authentication required",
591
+ content: { "application/json": { schema: AuthErrorSchema } },
592
+ },
593
+ 404: {
594
+ description: "No Discord installation found or internal database not configured",
595
+ content: { "application/json": { schema: ErrorSchema } },
596
+ },
597
+ 500: {
598
+ description: "Internal server error",
599
+ content: { "application/json": { schema: ErrorSchema } },
600
+ },
601
+ },
602
+ });
603
+
604
+ adminIntegrations.openapi(disconnectDiscordRoute, async (c) => {
605
+ return runEffect(
606
+ c,
607
+ Effect.gen(function* () {
608
+ const { orgId } = yield* AuthContext;
609
+
610
+ if (!orgId) {
611
+ return c.json(
612
+ { error: "bad_request", message: "No active organization." },
613
+ 400,
614
+ );
615
+ }
616
+
617
+ const deleted = yield* Effect.tryPromise({
618
+ try: () => deleteDiscordInstallationByOrg(orgId),
619
+ catch: (err) => err instanceof Error ? err : new Error(String(err)),
620
+ });
621
+
622
+ if (!deleted) {
623
+ return c.json(
624
+ { error: "not_found", message: "No Discord installation found for this workspace." },
625
+ 404,
626
+ );
627
+ }
628
+
629
+ log.info({ orgId }, "Discord installation disconnected by admin");
630
+ return c.json({ message: "Discord disconnected successfully." }, 200);
631
+ }),
632
+ { label: "disconnect discord" },
633
+ );
634
+ });
635
+
636
+ // ---------------------------------------------------------------------------
637
+ // BYOT (Bring Your Own Token) routes
638
+ // ---------------------------------------------------------------------------
639
+
640
+ // POST /slack/byot — connect Slack via bot token (no platform OAuth needed)
641
+ const connectSlackByotRoute = createRoute({
642
+ method: "post",
643
+ path: "/slack/byot",
644
+ tags: ["Admin — Integrations"],
645
+ summary: "Connect Slack via bot token (BYOT)",
646
+ description:
647
+ "Validates a Slack bot token via auth.test and saves the installation " +
648
+ "for the current workspace. Use when platform OAuth is not configured.",
649
+ request: {
650
+ body: {
651
+ content: {
652
+ "application/json": {
653
+ schema: z.object({
654
+ botToken: z
655
+ .string()
656
+ .min(1)
657
+ .refine((t) => t.startsWith("xoxb-"), { message: "Bot token must start with xoxb-" })
658
+ .openapi({ description: "Slack bot token (xoxb-...)" }),
659
+ }),
660
+ },
661
+ },
662
+ },
663
+ },
664
+ responses: {
665
+ 200: {
666
+ description: "Slack connected via BYOT",
667
+ content: {
668
+ "application/json": {
669
+ schema: z.object({
670
+ message: z.string(),
671
+ workspaceName: z.string().nullable(),
672
+ teamId: z.string().nullable(),
673
+ }),
674
+ },
675
+ },
676
+ },
677
+ 400: {
678
+ description: "Invalid bot token, no active organization, or internal database not configured",
679
+ content: { "application/json": { schema: ErrorSchema } },
680
+ },
681
+ 401: {
682
+ description: "Authentication required",
683
+ content: { "application/json": { schema: AuthErrorSchema } },
684
+ },
685
+ 500: {
686
+ description: "Internal server error",
687
+ content: { "application/json": { schema: ErrorSchema } },
688
+ },
689
+ },
690
+ });
691
+
692
+ adminIntegrations.openapi(connectSlackByotRoute, async (c) => {
693
+ return runEffect(
694
+ c,
695
+ Effect.gen(function* () {
696
+ const { orgId } = yield* AuthContext;
697
+
698
+ if (!orgId) {
699
+ return c.json(
700
+ { error: "bad_request", message: "No active organization." },
701
+ 400,
702
+ );
703
+ }
704
+
705
+ if (!hasInternalDB()) {
706
+ return c.json(
707
+ { error: "not_configured", message: "Slack BYOT requires an internal database. Configure DATABASE_URL." },
708
+ 400,
709
+ );
710
+ }
711
+
712
+ const { botToken } = c.req.valid("json");
713
+
714
+ // Validate token by calling Slack's auth.test API.
715
+ // Inner catches log the original error for debugging but return sanitized user-facing messages.
716
+ const authResult = yield* Effect.tryPromise({
717
+ try: async () => {
718
+ let res: Response;
719
+ try {
720
+ res = await fetch("https://slack.com/api/auth.test", {
721
+ method: "POST",
722
+ headers: { Authorization: `Bearer ${botToken}`, "Content-Type": "application/x-www-form-urlencoded" },
723
+ });
724
+ } catch (err) {
725
+ log.warn({ err: err instanceof Error ? err.message : String(err) }, "Slack auth.test fetch failed");
726
+ return { ok: false as const, error: "Could not reach Slack API. Please try again." };
727
+ }
728
+ let data: { ok: boolean; team_id?: string; team?: string; error?: string };
729
+ try {
730
+ data = (await res.json()) as typeof data;
731
+ } catch (err) {
732
+ log.warn({ err: err instanceof Error ? err.message : String(err) }, "Slack auth.test response parse failed");
733
+ return { ok: false as const, error: "Slack API returned an invalid response" };
734
+ }
735
+ if (!data.ok) {
736
+ return { ok: false as const, error: data.error ?? "Invalid bot token" };
737
+ }
738
+ return { ok: true as const, teamId: data.team_id ?? null, workspaceName: data.team ?? null };
739
+ },
740
+ catch: (err) => err instanceof Error ? err : new Error(String(err)),
741
+ });
742
+
743
+ if (!authResult.ok) {
744
+ return c.json(
745
+ { error: "invalid_token", message: `Invalid Slack bot token: ${authResult.error}` },
746
+ 400,
747
+ );
748
+ }
749
+
750
+ yield* Effect.tryPromise({
751
+ try: () =>
752
+ saveInstallation(authResult.teamId ?? `byot-${orgId}`, botToken, {
753
+ orgId,
754
+ workspaceName: authResult.workspaceName ?? undefined,
755
+ }),
756
+ catch: (err) => err instanceof Error ? err : new Error(String(err)),
757
+ });
758
+
759
+ log.info({ orgId, teamId: authResult.teamId, workspaceName: authResult.workspaceName }, "Slack BYOT installation saved by admin");
760
+ return c.json(
761
+ { message: "Slack connected successfully.", workspaceName: authResult.workspaceName, teamId: authResult.teamId },
762
+ 200,
763
+ );
764
+ }),
765
+ { label: "connect slack byot" },
766
+ );
767
+ });
768
+
769
+ // POST /teams/byot — connect Teams via app credentials (no platform OAuth needed)
770
+ const connectTeamsByotRoute = createRoute({
771
+ method: "post",
772
+ path: "/teams/byot",
773
+ tags: ["Admin — Integrations"],
774
+ summary: "Connect Teams via app credentials (BYOT)",
775
+ description:
776
+ "Validates Azure Bot app credentials via client credentials token acquisition " +
777
+ "and saves the installation for the current workspace.",
778
+ request: {
779
+ body: {
780
+ content: {
781
+ "application/json": {
782
+ schema: z.object({
783
+ appId: z.string().min(1).openapi({ description: "Azure Bot App ID (client_id)" }),
784
+ appPassword: z.string().min(1).openapi({ description: "Azure Bot App Password (client_secret)" }),
785
+ }),
786
+ },
787
+ },
788
+ },
789
+ },
790
+ responses: {
791
+ 200: {
792
+ description: "Teams connected via BYOT",
793
+ content: {
794
+ "application/json": {
795
+ schema: z.object({
796
+ message: z.string(),
797
+ appId: z.string(),
798
+ }),
799
+ },
800
+ },
801
+ },
802
+ 400: {
803
+ description: "Invalid credentials, no active organization, or internal database not configured",
804
+ content: { "application/json": { schema: ErrorSchema } },
805
+ },
806
+ 401: {
807
+ description: "Authentication required",
808
+ content: { "application/json": { schema: AuthErrorSchema } },
809
+ },
810
+ 500: {
811
+ description: "Internal server error",
812
+ content: { "application/json": { schema: ErrorSchema } },
813
+ },
814
+ },
815
+ });
816
+
817
+ adminIntegrations.openapi(connectTeamsByotRoute, async (c) => {
818
+ return runEffect(
819
+ c,
820
+ Effect.gen(function* () {
821
+ const { orgId } = yield* AuthContext;
822
+
823
+ if (!orgId) {
824
+ return c.json(
825
+ { error: "bad_request", message: "No active organization." },
826
+ 400,
827
+ );
828
+ }
829
+
830
+ if (!hasInternalDB()) {
831
+ return c.json(
832
+ { error: "not_configured", message: "Teams BYOT requires an internal database. Configure DATABASE_URL." },
833
+ 400,
834
+ );
835
+ }
836
+
837
+ const { appId, appPassword } = c.req.valid("json");
838
+
839
+ // Validate credentials by requesting a client credentials token from Azure AD.
840
+ // Inner catches log the original error for debugging but return sanitized user-facing messages.
841
+ const tokenResult = yield* Effect.tryPromise({
842
+ try: async () => {
843
+ let res: Response;
844
+ try {
845
+ res = await fetch(
846
+ "https://login.microsoftonline.com/botframework.com/oauth2/v2.0/token",
847
+ {
848
+ method: "POST",
849
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
850
+ body: new URLSearchParams({
851
+ grant_type: "client_credentials",
852
+ client_id: appId,
853
+ client_secret: appPassword,
854
+ scope: "https://api.botframework.com/.default",
855
+ }),
856
+ },
857
+ );
858
+ } catch (err) {
859
+ log.warn({ err: err instanceof Error ? err.message : String(err) }, "Azure AD token fetch failed");
860
+ return { ok: false as const, error: "Could not reach Azure AD. Please try again." };
861
+ }
862
+ let data: { access_token?: string; error?: string; error_description?: string };
863
+ try {
864
+ data = (await res.json()) as typeof data;
865
+ } catch (err) {
866
+ log.warn({ err: err instanceof Error ? err.message : String(err) }, "Azure AD token response parse failed");
867
+ return { ok: false as const, error: "Azure AD returned an invalid response" };
868
+ }
869
+ if (!data.access_token) {
870
+ return { ok: false as const, error: data.error_description ?? data.error ?? "Invalid credentials" };
871
+ }
872
+ return { ok: true as const };
873
+ },
874
+ catch: (err) => err instanceof Error ? err : new Error(String(err)),
875
+ });
876
+
877
+ if (!tokenResult.ok) {
878
+ return c.json(
879
+ { error: "invalid_credentials", message: `Invalid Teams credentials: ${tokenResult.error}` },
880
+ 400,
881
+ );
882
+ }
883
+
884
+ // BYOT has no tenant context — use appId as the primary key (tenant_id column)
885
+ yield* Effect.tryPromise({
886
+ try: () =>
887
+ saveTeamsInstallation(appId, {
888
+ orgId,
889
+ appPassword,
890
+ }),
891
+ catch: (err) => err instanceof Error ? err : new Error(String(err)),
892
+ });
893
+
894
+ log.info({ orgId, appId }, "Teams BYOT installation saved by admin");
895
+ return c.json(
896
+ { message: "Teams connected successfully.", appId },
897
+ 200,
898
+ );
899
+ }),
900
+ { label: "connect teams byot" },
901
+ );
902
+ });
903
+
904
+ // POST /discord/byot — connect Discord via bot credentials (no platform OAuth needed)
905
+ const connectDiscordByotRoute = createRoute({
906
+ method: "post",
907
+ path: "/discord/byot",
908
+ tags: ["Admin — Integrations"],
909
+ summary: "Connect Discord via bot credentials (BYOT)",
910
+ description:
911
+ "Validates a Discord bot token via the Discord API and saves the installation " +
912
+ "for the current workspace.",
913
+ request: {
914
+ body: {
915
+ content: {
916
+ "application/json": {
917
+ schema: z.object({
918
+ botToken: z.string().min(1).openapi({ description: "Discord bot token" }),
919
+ applicationId: z.string().min(1).openapi({ description: "Discord application ID" }),
920
+ publicKey: z.string().min(1).openapi({ description: "Discord application public key (for interaction verification)" }),
921
+ }),
922
+ },
923
+ },
924
+ },
925
+ },
926
+ responses: {
927
+ 200: {
928
+ description: "Discord connected via BYOT",
929
+ content: {
930
+ "application/json": {
931
+ schema: z.object({
932
+ message: z.string(),
933
+ botUsername: z.string().nullable(),
934
+ }),
935
+ },
936
+ },
937
+ },
938
+ 400: {
939
+ description: "Invalid bot token, no active organization, or internal database not configured",
940
+ content: { "application/json": { schema: ErrorSchema } },
941
+ },
942
+ 401: {
943
+ description: "Authentication required",
944
+ content: { "application/json": { schema: AuthErrorSchema } },
945
+ },
946
+ 500: {
947
+ description: "Internal server error",
948
+ content: { "application/json": { schema: ErrorSchema } },
949
+ },
950
+ },
951
+ });
952
+
953
+ adminIntegrations.openapi(connectDiscordByotRoute, async (c) => {
954
+ return runEffect(
955
+ c,
956
+ Effect.gen(function* () {
957
+ const { orgId } = yield* AuthContext;
958
+
959
+ if (!orgId) {
960
+ return c.json(
961
+ { error: "bad_request", message: "No active organization." },
962
+ 400,
963
+ );
964
+ }
965
+
966
+ if (!hasInternalDB()) {
967
+ return c.json(
968
+ { error: "not_configured", message: "Discord BYOT requires an internal database. Configure DATABASE_URL." },
969
+ 400,
970
+ );
971
+ }
972
+
973
+ const { botToken, applicationId, publicKey } = c.req.valid("json");
974
+
975
+ // Validate token by calling Discord's /users/@me API.
976
+ // Inner catches log the original error for debugging but return sanitized user-facing messages.
977
+ const meResult = yield* Effect.tryPromise({
978
+ try: async () => {
979
+ let res: Response;
980
+ try {
981
+ res = await fetch("https://discord.com/api/v10/users/@me", {
982
+ headers: { Authorization: `Bot ${botToken}` },
983
+ });
984
+ } catch (err) {
985
+ log.warn({ err: err instanceof Error ? err.message : String(err) }, "Discord /users/@me fetch failed");
986
+ return { ok: false as const, error: "Could not reach Discord API. Please try again." };
987
+ }
988
+ if (!res.ok) {
989
+ let detail = `status ${res.status}`;
990
+ try {
991
+ const errBody = (await res.json()) as { message?: string };
992
+ if (errBody.message) detail = errBody.message;
993
+ } catch {
994
+ // intentionally ignored: response body may not be JSON
995
+ }
996
+ return { ok: false as const, error: `Discord API error: ${detail}` };
997
+ }
998
+ let data: { id?: string; username?: string };
999
+ try {
1000
+ data = (await res.json()) as typeof data;
1001
+ } catch (err) {
1002
+ log.warn({ err: err instanceof Error ? err.message : String(err) }, "Discord /users/@me response parse failed");
1003
+ return { ok: false as const, error: "Discord API returned an invalid response" };
1004
+ }
1005
+ if (!data.id) {
1006
+ return { ok: false as const, error: "Invalid bot token" };
1007
+ }
1008
+ return { ok: true as const, botId: data.id, botUsername: data.username ?? null };
1009
+ },
1010
+ catch: (err) => err instanceof Error ? err : new Error(String(err)),
1011
+ });
1012
+
1013
+ if (!meResult.ok) {
1014
+ return c.json(
1015
+ { error: "invalid_token", message: `Discord validation failed: ${meResult.error}` },
1016
+ 400,
1017
+ );
1018
+ }
1019
+
1020
+ // Use applicationId as guild_id primary key for BYOT — no real guild context from
1021
+ // token validation, so each BYOT installation maps 1:1 to a Discord application
1022
+ yield* Effect.tryPromise({
1023
+ try: () =>
1024
+ saveDiscordInstallation(applicationId, {
1025
+ orgId,
1026
+ guildName: meResult.botUsername ? `@${meResult.botUsername}` : undefined,
1027
+ botToken,
1028
+ applicationId,
1029
+ publicKey,
1030
+ }),
1031
+ catch: (err) => err instanceof Error ? err : new Error(String(err)),
1032
+ });
1033
+
1034
+ log.info({ orgId, applicationId, botUsername: meResult.botUsername }, "Discord BYOT installation saved by admin");
1035
+ return c.json(
1036
+ { message: "Discord connected successfully.", botUsername: meResult.botUsername },
1037
+ 200,
1038
+ );
1039
+ }),
1040
+ { label: "connect discord byot" },
1041
+ );
1042
+ });
1043
+
1044
+ // POST /telegram — connect Telegram for current org (bot token submission)
1045
+ const connectTelegramRoute = createRoute({
1046
+ method: "post",
1047
+ path: "/telegram",
1048
+ tags: ["Admin — Integrations"],
1049
+ summary: "Connect Telegram",
1050
+ description:
1051
+ "Validates a Telegram bot token via the Telegram Bot API and saves the installation " +
1052
+ "for the current workspace.",
1053
+ request: {
1054
+ body: {
1055
+ content: {
1056
+ "application/json": {
1057
+ schema: z.object({
1058
+ botToken: z.string().min(1).openapi({ description: "Telegram bot token from @BotFather" }),
1059
+ }),
1060
+ },
1061
+ },
1062
+ },
1063
+ },
1064
+ responses: {
1065
+ 200: {
1066
+ description: "Telegram connected",
1067
+ content: {
1068
+ "application/json": {
1069
+ schema: z.object({
1070
+ message: z.string(),
1071
+ botUsername: z.string().nullable(),
1072
+ }),
1073
+ },
1074
+ },
1075
+ },
1076
+ 400: {
1077
+ description: "Invalid bot token or no active organization",
1078
+ content: { "application/json": { schema: ErrorSchema } },
1079
+ },
1080
+ 401: {
1081
+ description: "Authentication required",
1082
+ content: { "application/json": { schema: AuthErrorSchema } },
1083
+ },
1084
+ 500: {
1085
+ description: "Internal server error",
1086
+ content: { "application/json": { schema: ErrorSchema } },
1087
+ },
1088
+ },
1089
+ });
1090
+
1091
+ adminIntegrations.openapi(connectTelegramRoute, async (c) => {
1092
+ return runEffect(
1093
+ c,
1094
+ Effect.gen(function* () {
1095
+ const { orgId } = yield* AuthContext;
1096
+
1097
+ if (!orgId) {
1098
+ return c.json(
1099
+ { error: "bad_request", message: "No active organization." },
1100
+ 400,
1101
+ );
1102
+ }
1103
+
1104
+ // Check internal DB availability before making the external API call
1105
+ if (!hasInternalDB()) {
1106
+ return c.json(
1107
+ { error: "not_configured", message: "Telegram integration requires an internal database. Contact your platform administrator." },
1108
+ 400,
1109
+ );
1110
+ }
1111
+
1112
+ const { botToken } = c.req.valid("json");
1113
+
1114
+ // Validate token by calling Telegram's getMe API.
1115
+ // Wrap in a sanitized try/catch to prevent the bot token from leaking
1116
+ // into error messages (the token is embedded in the URL path).
1117
+ const getMeResult = yield* Effect.tryPromise({
1118
+ try: async () => {
1119
+ let res: Response;
1120
+ try {
1121
+ res = await fetch(`https://api.telegram.org/bot${botToken}/getMe`);
1122
+ } catch {
1123
+ return { ok: false as const, error: "Could not reach Telegram API. Please try again." };
1124
+ }
1125
+ if (!res.ok) {
1126
+ return { ok: false as const, error: `Telegram API returned ${res.status}` };
1127
+ }
1128
+ let data: { ok: boolean; result?: { id: number; username?: string } };
1129
+ try {
1130
+ data = (await res.json()) as typeof data;
1131
+ } catch {
1132
+ return { ok: false as const, error: "Telegram API returned an invalid response" };
1133
+ }
1134
+ if (!data.ok || !data.result) {
1135
+ return { ok: false as const, error: "Invalid bot token" };
1136
+ }
1137
+ return { ok: true as const, botId: String(data.result.id), botUsername: data.result.username ?? null };
1138
+ },
1139
+ catch: () => new Error("Telegram token validation failed"),
1140
+ });
1141
+
1142
+ if (!getMeResult.ok) {
1143
+ return c.json(
1144
+ { error: "invalid_token", message: `Invalid Telegram bot token: ${getMeResult.error}` },
1145
+ 400,
1146
+ );
1147
+ }
1148
+
1149
+ yield* Effect.tryPromise({
1150
+ try: () =>
1151
+ saveTelegramInstallation(getMeResult.botId, {
1152
+ orgId,
1153
+ botUsername: getMeResult.botUsername ?? undefined,
1154
+ botToken,
1155
+ }),
1156
+ catch: (err) => err instanceof Error ? err : new Error(String(err)),
1157
+ });
1158
+
1159
+ log.info({ orgId, botId: getMeResult.botId, botUsername: getMeResult.botUsername }, "Telegram installation saved by admin");
1160
+ return c.json(
1161
+ { message: "Telegram connected successfully.", botUsername: getMeResult.botUsername },
1162
+ 200,
1163
+ );
1164
+ }),
1165
+ { label: "connect telegram" },
1166
+ );
1167
+ });
1168
+
1169
+ // DELETE /telegram — disconnect Telegram for current org
1170
+ const disconnectTelegramRoute = createRoute({
1171
+ method: "delete",
1172
+ path: "/telegram",
1173
+ tags: ["Admin — Integrations"],
1174
+ summary: "Disconnect Telegram",
1175
+ description:
1176
+ "Removes the Telegram installation for the current workspace. " +
1177
+ "Any Telegram bot functionality will stop working until reconnected.",
1178
+ responses: {
1179
+ 200: {
1180
+ description: "Telegram disconnected",
1181
+ content: {
1182
+ "application/json": {
1183
+ schema: z.object({ message: z.string() }),
1184
+ },
1185
+ },
1186
+ },
1187
+ 400: {
1188
+ description: "No active organization",
1189
+ content: { "application/json": { schema: ErrorSchema } },
1190
+ },
1191
+ 401: {
1192
+ description: "Authentication required",
1193
+ content: { "application/json": { schema: AuthErrorSchema } },
1194
+ },
1195
+ 404: {
1196
+ description: "No Telegram installation found or internal database not configured",
1197
+ content: { "application/json": { schema: ErrorSchema } },
1198
+ },
1199
+ 500: {
1200
+ description: "Internal server error",
1201
+ content: { "application/json": { schema: ErrorSchema } },
1202
+ },
1203
+ },
1204
+ });
1205
+
1206
+ adminIntegrations.openapi(disconnectTelegramRoute, async (c) => {
1207
+ return runEffect(
1208
+ c,
1209
+ Effect.gen(function* () {
1210
+ const { orgId } = yield* AuthContext;
1211
+
1212
+ if (!orgId) {
1213
+ return c.json(
1214
+ { error: "bad_request", message: "No active organization." },
1215
+ 400,
1216
+ );
1217
+ }
1218
+
1219
+ const deleted = yield* Effect.tryPromise({
1220
+ try: () => deleteTelegramInstallationByOrg(orgId),
1221
+ catch: (err) => err instanceof Error ? err : new Error(String(err)),
1222
+ });
1223
+
1224
+ if (!deleted) {
1225
+ return c.json(
1226
+ { error: "not_found", message: "No Telegram installation found for this workspace." },
1227
+ 404,
1228
+ );
1229
+ }
1230
+
1231
+ log.info({ orgId }, "Telegram installation disconnected by admin");
1232
+ return c.json({ message: "Telegram disconnected successfully." }, 200);
1233
+ }),
1234
+ { label: "disconnect telegram" },
1235
+ );
1236
+ });
1237
+
1238
+ // ---------------------------------------------------------------------------
1239
+ // Google Chat routes (BYOT-only — no platform OAuth variant)
1240
+ // ---------------------------------------------------------------------------
1241
+
1242
+ const connectGChatRoute = createRoute({
1243
+ method: "post",
1244
+ path: "/gchat",
1245
+ tags: ["Admin — Integrations"],
1246
+ summary: "Connect Google Chat via service account",
1247
+ description:
1248
+ "Parses a Google Chat service account JSON key, validates required fields " +
1249
+ "(client_email, private_key), and saves the installation for the current workspace. " +
1250
+ "Structural validation only — does not call the Google API.",
1251
+ request: {
1252
+ body: {
1253
+ content: {
1254
+ "application/json": {
1255
+ schema: z.object({
1256
+ credentialsJson: z
1257
+ .string()
1258
+ .min(1)
1259
+ .openapi({ description: "Google Cloud service account JSON key" }),
1260
+ }),
1261
+ },
1262
+ },
1263
+ },
1264
+ },
1265
+ responses: {
1266
+ 200: {
1267
+ description: "Google Chat connected",
1268
+ content: {
1269
+ "application/json": {
1270
+ schema: z.object({
1271
+ message: z.string(),
1272
+ projectId: z.string().nullable(),
1273
+ serviceAccountEmail: z.string().nullable(),
1274
+ }),
1275
+ },
1276
+ },
1277
+ },
1278
+ 400: {
1279
+ description: "Invalid credentials, no active organization, or internal database not configured",
1280
+ content: { "application/json": { schema: ErrorSchema } },
1281
+ },
1282
+ 401: {
1283
+ description: "Authentication required",
1284
+ content: { "application/json": { schema: AuthErrorSchema } },
1285
+ },
1286
+ 409: {
1287
+ description: "Service account already bound to a different organization",
1288
+ content: { "application/json": { schema: ErrorSchema } },
1289
+ },
1290
+ 500: {
1291
+ description: "Internal server error",
1292
+ content: { "application/json": { schema: ErrorSchema } },
1293
+ },
1294
+ },
1295
+ });
1296
+
1297
+ adminIntegrations.openapi(connectGChatRoute, async (c) => {
1298
+ return runEffect(
1299
+ c,
1300
+ Effect.gen(function* () {
1301
+ const { orgId } = yield* AuthContext;
1302
+
1303
+ if (!orgId) {
1304
+ return c.json(
1305
+ { error: "bad_request", message: "No active organization." },
1306
+ 400,
1307
+ );
1308
+ }
1309
+
1310
+ if (!hasInternalDB()) {
1311
+ return c.json(
1312
+ { error: "not_configured", message: "Google Chat integration requires an internal database. Configure DATABASE_URL." },
1313
+ 400,
1314
+ );
1315
+ }
1316
+
1317
+ const { credentialsJson } = c.req.valid("json");
1318
+
1319
+ // Parse and validate the service account JSON
1320
+ let parsed: { client_email?: string; private_key?: string; project_id?: string };
1321
+ try {
1322
+ parsed = JSON.parse(credentialsJson) as typeof parsed;
1323
+ } catch (err) {
1324
+ log.warn({ err: err instanceof Error ? err.message : String(err) }, "Google Chat credentials JSON parse failed");
1325
+ return c.json(
1326
+ { error: "invalid_credentials", message: "Invalid JSON. Paste the full service account key file contents." },
1327
+ 400,
1328
+ );
1329
+ }
1330
+
1331
+ if (!parsed.client_email || typeof parsed.client_email !== "string") {
1332
+ return c.json(
1333
+ { error: "invalid_credentials", message: "Service account JSON is missing the 'client_email' field." },
1334
+ 400,
1335
+ );
1336
+ }
1337
+
1338
+ if (!parsed.private_key || typeof parsed.private_key !== "string") {
1339
+ return c.json(
1340
+ { error: "invalid_credentials", message: "Service account JSON is missing the 'private_key' field." },
1341
+ 400,
1342
+ );
1343
+ }
1344
+
1345
+ if (!parsed.private_key.startsWith("-----BEGIN")) {
1346
+ return c.json(
1347
+ { error: "invalid_credentials", message: "Service account JSON has an invalid 'private_key'. Ensure you pasted the full key file." },
1348
+ 400,
1349
+ );
1350
+ }
1351
+
1352
+ const clientEmail = parsed.client_email;
1353
+ const projectId = typeof parsed.project_id === "string" && parsed.project_id
1354
+ ? parsed.project_id
1355
+ : clientEmail.split("@")[1]?.replace(".iam.gserviceaccount.com", "") ?? `gchat-${orgId}`;
1356
+
1357
+ const saveResult = yield* Effect.tryPromise({
1358
+ try: () =>
1359
+ saveGChatInstallation(projectId, {
1360
+ orgId,
1361
+ serviceAccountEmail: clientEmail,
1362
+ credentialsJson,
1363
+ }),
1364
+ catch: (err) => err instanceof Error ? err : new Error(String(err)),
1365
+ }).pipe(
1366
+ Effect.map(() => ({ ok: true as const })),
1367
+ Effect.catchAll((err) => Effect.succeed({ ok: false as const, message: err.message })),
1368
+ );
1369
+
1370
+ if (!saveResult.ok) {
1371
+ return c.json(
1372
+ { error: "conflict", message: saveResult.message },
1373
+ 409,
1374
+ );
1375
+ }
1376
+
1377
+ log.info({ orgId, projectId, serviceAccountEmail: clientEmail }, "Google Chat installation saved by admin");
1378
+ return c.json(
1379
+ { message: "Google Chat connected successfully.", projectId, serviceAccountEmail: clientEmail },
1380
+ 200,
1381
+ );
1382
+ }),
1383
+ { label: "connect gchat" },
1384
+ );
1385
+ });
1386
+
1387
+ const disconnectGChatRoute = createRoute({
1388
+ method: "delete",
1389
+ path: "/gchat",
1390
+ tags: ["Admin — Integrations"],
1391
+ summary: "Disconnect Google Chat",
1392
+ description:
1393
+ "Removes the Google Chat installation for the current workspace. " +
1394
+ "Any Google Chat bot functionality will stop working until reconnected.",
1395
+ responses: {
1396
+ 200: {
1397
+ description: "Google Chat disconnected",
1398
+ content: {
1399
+ "application/json": {
1400
+ schema: z.object({ message: z.string() }),
1401
+ },
1402
+ },
1403
+ },
1404
+ 400: {
1405
+ description: "No active organization",
1406
+ content: { "application/json": { schema: ErrorSchema } },
1407
+ },
1408
+ 401: {
1409
+ description: "Authentication required",
1410
+ content: { "application/json": { schema: AuthErrorSchema } },
1411
+ },
1412
+ 404: {
1413
+ description: "No Google Chat installation found",
1414
+ content: { "application/json": { schema: ErrorSchema } },
1415
+ },
1416
+ 500: {
1417
+ description: "Internal server error",
1418
+ content: { "application/json": { schema: ErrorSchema } },
1419
+ },
1420
+ },
1421
+ });
1422
+
1423
+ adminIntegrations.openapi(disconnectGChatRoute, async (c) => {
1424
+ return runEffect(
1425
+ c,
1426
+ Effect.gen(function* () {
1427
+ const { orgId } = yield* AuthContext;
1428
+
1429
+ if (!orgId) {
1430
+ return c.json(
1431
+ { error: "bad_request", message: "No active organization." },
1432
+ 400,
1433
+ );
1434
+ }
1435
+
1436
+ const deleted = yield* Effect.tryPromise({
1437
+ try: () => deleteGChatInstallationByOrg(orgId),
1438
+ catch: (err) => err instanceof Error ? err : new Error(String(err)),
1439
+ });
1440
+
1441
+ if (!deleted) {
1442
+ return c.json(
1443
+ { error: "not_found", message: "No Google Chat installation found for this workspace." },
1444
+ 404,
1445
+ );
1446
+ }
1447
+
1448
+ log.info({ orgId }, "Google Chat installation disconnected by admin");
1449
+ return c.json({ message: "Google Chat disconnected successfully." }, 200);
1450
+ }),
1451
+ { label: "disconnect gchat" },
1452
+ );
1453
+ });
1454
+
1455
+ // ---------------------------------------------------------------------------
1456
+ // GitHub routes (BYOT-only — no platform OAuth variant)
1457
+ // ---------------------------------------------------------------------------
1458
+
1459
+ const connectGitHubRoute = createRoute({
1460
+ method: "post",
1461
+ path: "/github",
1462
+ tags: ["Admin — Integrations"],
1463
+ summary: "Connect GitHub via personal access token",
1464
+ description:
1465
+ "Validates a GitHub personal access token via the GitHub API and saves the installation " +
1466
+ "for the current workspace.",
1467
+ request: {
1468
+ body: {
1469
+ content: {
1470
+ "application/json": {
1471
+ schema: z.object({
1472
+ accessToken: z
1473
+ .string()
1474
+ .min(1)
1475
+ .openapi({ description: "GitHub personal access token (ghp_ or github_pat_ prefix)" }),
1476
+ }),
1477
+ },
1478
+ },
1479
+ },
1480
+ },
1481
+ responses: {
1482
+ 200: {
1483
+ description: "GitHub connected",
1484
+ content: {
1485
+ "application/json": {
1486
+ schema: z.object({
1487
+ message: z.string(),
1488
+ username: z.string().nullable(),
1489
+ }),
1490
+ },
1491
+ },
1492
+ },
1493
+ 400: {
1494
+ description: "Invalid token, no active organization, or internal database not configured",
1495
+ content: { "application/json": { schema: ErrorSchema } },
1496
+ },
1497
+ 401: {
1498
+ description: "Authentication required",
1499
+ content: { "application/json": { schema: AuthErrorSchema } },
1500
+ },
1501
+ 409: {
1502
+ description: "GitHub user already bound to a different organization",
1503
+ content: { "application/json": { schema: ErrorSchema } },
1504
+ },
1505
+ 500: {
1506
+ description: "Internal server error",
1507
+ content: { "application/json": { schema: ErrorSchema } },
1508
+ },
1509
+ },
1510
+ });
1511
+
1512
+ adminIntegrations.openapi(connectGitHubRoute, async (c) => {
1513
+ return runEffect(
1514
+ c,
1515
+ Effect.gen(function* () {
1516
+ const { orgId } = yield* AuthContext;
1517
+
1518
+ if (!orgId) {
1519
+ return c.json(
1520
+ { error: "bad_request", message: "No active organization." },
1521
+ 400,
1522
+ );
1523
+ }
1524
+
1525
+ if (!hasInternalDB()) {
1526
+ return c.json(
1527
+ { error: "not_configured", message: "GitHub integration requires an internal database. Configure DATABASE_URL." },
1528
+ 400,
1529
+ );
1530
+ }
1531
+
1532
+ const { accessToken } = c.req.valid("json");
1533
+
1534
+ // Validate token by calling GitHub's /user API.
1535
+ const userResult = yield* Effect.tryPromise({
1536
+ try: async () => {
1537
+ let res: Response;
1538
+ try {
1539
+ res = await fetch("https://api.github.com/user", {
1540
+ headers: {
1541
+ Authorization: `Bearer ${accessToken}`,
1542
+ Accept: "application/vnd.github+json",
1543
+ "User-Agent": "Atlas-Integration",
1544
+ },
1545
+ });
1546
+ } catch (err) {
1547
+ log.warn({ err: err instanceof Error ? err.message : String(err) }, "GitHub /user fetch failed");
1548
+ return { ok: false as const, error: "Could not reach GitHub API. Please try again." };
1549
+ }
1550
+ if (!res.ok) {
1551
+ let detail = `status ${res.status}`;
1552
+ try {
1553
+ const errBody = (await res.json()) as { message?: string };
1554
+ if (errBody.message) detail = errBody.message;
1555
+ } catch {
1556
+ // intentionally ignored: response body may not be JSON
1557
+ }
1558
+ return { ok: false as const, error: `GitHub API error: ${detail}` };
1559
+ }
1560
+ let data: { id?: number; login?: string };
1561
+ try {
1562
+ data = (await res.json()) as typeof data;
1563
+ } catch (err) {
1564
+ log.warn({ err: err instanceof Error ? err.message : String(err) }, "GitHub /user response parse failed");
1565
+ return { ok: false as const, error: "GitHub API returned an invalid response" };
1566
+ }
1567
+ if (!data.id) {
1568
+ return { ok: false as const, error: "Invalid personal access token" };
1569
+ }
1570
+ return { ok: true as const, userId: String(data.id), username: data.login ?? null };
1571
+ },
1572
+ catch: (err) => err instanceof Error ? err : new Error(String(err)),
1573
+ });
1574
+
1575
+ if (!userResult.ok) {
1576
+ return c.json(
1577
+ { error: "invalid_token", message: `Invalid GitHub token: ${userResult.error}` },
1578
+ 400,
1579
+ );
1580
+ }
1581
+
1582
+ const saveResult = yield* Effect.tryPromise({
1583
+ try: () =>
1584
+ saveGitHubInstallation(userResult.userId, {
1585
+ orgId,
1586
+ username: userResult.username ?? undefined,
1587
+ accessToken,
1588
+ }),
1589
+ catch: (err) => err instanceof Error ? err : new Error(String(err)),
1590
+ }).pipe(
1591
+ Effect.map(() => ({ ok: true as const })),
1592
+ Effect.catchAll((err) => Effect.succeed({ ok: false as const, message: err.message })),
1593
+ );
1594
+
1595
+ if (!saveResult.ok) {
1596
+ return c.json(
1597
+ { error: "conflict", message: saveResult.message },
1598
+ 409,
1599
+ );
1600
+ }
1601
+
1602
+ log.info({ orgId, userId: userResult.userId, username: userResult.username }, "GitHub installation saved by admin");
1603
+ return c.json(
1604
+ { message: "GitHub connected successfully.", username: userResult.username },
1605
+ 200,
1606
+ );
1607
+ }),
1608
+ { label: "connect github" },
1609
+ );
1610
+ });
1611
+
1612
+ const disconnectGitHubRoute = createRoute({
1613
+ method: "delete",
1614
+ path: "/github",
1615
+ tags: ["Admin — Integrations"],
1616
+ summary: "Disconnect GitHub",
1617
+ description:
1618
+ "Removes the GitHub installation for the current workspace. " +
1619
+ "Any GitHub integration functionality will stop working until reconnected.",
1620
+ responses: {
1621
+ 200: {
1622
+ description: "GitHub disconnected",
1623
+ content: {
1624
+ "application/json": {
1625
+ schema: z.object({ message: z.string() }),
1626
+ },
1627
+ },
1628
+ },
1629
+ 400: {
1630
+ description: "No active organization",
1631
+ content: { "application/json": { schema: ErrorSchema } },
1632
+ },
1633
+ 401: {
1634
+ description: "Authentication required",
1635
+ content: { "application/json": { schema: AuthErrorSchema } },
1636
+ },
1637
+ 404: {
1638
+ description: "No GitHub installation found",
1639
+ content: { "application/json": { schema: ErrorSchema } },
1640
+ },
1641
+ 500: {
1642
+ description: "Internal server error",
1643
+ content: { "application/json": { schema: ErrorSchema } },
1644
+ },
1645
+ },
1646
+ });
1647
+
1648
+ adminIntegrations.openapi(disconnectGitHubRoute, async (c) => {
1649
+ return runEffect(
1650
+ c,
1651
+ Effect.gen(function* () {
1652
+ const { orgId } = yield* AuthContext;
1653
+
1654
+ if (!orgId) {
1655
+ return c.json(
1656
+ { error: "bad_request", message: "No active organization." },
1657
+ 400,
1658
+ );
1659
+ }
1660
+
1661
+ const deleted = yield* Effect.tryPromise({
1662
+ try: () => deleteGitHubInstallationByOrg(orgId),
1663
+ catch: (err) => err instanceof Error ? err : new Error(String(err)),
1664
+ });
1665
+
1666
+ if (!deleted) {
1667
+ return c.json(
1668
+ { error: "not_found", message: "No GitHub installation found for this workspace." },
1669
+ 404,
1670
+ );
1671
+ }
1672
+
1673
+ log.info({ orgId }, "GitHub installation disconnected by admin");
1674
+ return c.json({ message: "GitHub disconnected successfully." }, 200);
1675
+ }),
1676
+ { label: "disconnect github" },
1677
+ );
1678
+ });
1679
+
1680
+ // ---------------------------------------------------------------------------
1681
+ // Linear routes (BYOT-only — API key)
1682
+ // ---------------------------------------------------------------------------
1683
+
1684
+ const connectLinearRoute = createRoute({
1685
+ method: "post",
1686
+ path: "/linear",
1687
+ tags: ["Admin — Integrations"],
1688
+ summary: "Connect Linear via API key",
1689
+ description:
1690
+ "Validates a Linear personal API key via the Linear GraphQL API and saves the installation " +
1691
+ "for the current workspace.",
1692
+ request: {
1693
+ body: {
1694
+ content: {
1695
+ "application/json": {
1696
+ schema: z.object({
1697
+ apiKey: z
1698
+ .string()
1699
+ .min(1)
1700
+ .openapi({ description: "Linear personal API key" }),
1701
+ }),
1702
+ },
1703
+ },
1704
+ },
1705
+ },
1706
+ responses: {
1707
+ 200: {
1708
+ description: "Linear connected",
1709
+ content: {
1710
+ "application/json": {
1711
+ schema: z.object({
1712
+ message: z.string(),
1713
+ userName: z.string().nullable(),
1714
+ userEmail: z.string().nullable(),
1715
+ }),
1716
+ },
1717
+ },
1718
+ },
1719
+ 400: {
1720
+ description: "Invalid API key, no active organization, or internal database not configured",
1721
+ content: { "application/json": { schema: ErrorSchema } },
1722
+ },
1723
+ 401: {
1724
+ description: "Authentication required",
1725
+ content: { "application/json": { schema: AuthErrorSchema } },
1726
+ },
1727
+ 409: {
1728
+ description: "Linear user already bound to a different organization",
1729
+ content: { "application/json": { schema: ErrorSchema } },
1730
+ },
1731
+ 500: {
1732
+ description: "Internal server error",
1733
+ content: { "application/json": { schema: ErrorSchema } },
1734
+ },
1735
+ },
1736
+ });
1737
+
1738
+ adminIntegrations.openapi(connectLinearRoute, async (c) => {
1739
+ return runEffect(
1740
+ c,
1741
+ Effect.gen(function* () {
1742
+ const { orgId } = yield* AuthContext;
1743
+
1744
+ if (!orgId) {
1745
+ return c.json(
1746
+ { error: "bad_request", message: "No active organization." },
1747
+ 400,
1748
+ );
1749
+ }
1750
+
1751
+ if (!hasInternalDB()) {
1752
+ return c.json(
1753
+ { error: "not_configured", message: "Linear integration requires an internal database. Configure DATABASE_URL." },
1754
+ 400,
1755
+ );
1756
+ }
1757
+
1758
+ const { apiKey } = c.req.valid("json");
1759
+
1760
+ // Validate key by calling Linear's GraphQL API with a viewer query.
1761
+ const viewerResult = yield* Effect.tryPromise({
1762
+ try: async () => {
1763
+ let res: Response;
1764
+ try {
1765
+ res = await fetch("https://api.linear.app/graphql", {
1766
+ method: "POST",
1767
+ headers: {
1768
+ Authorization: `Bearer ${apiKey}`,
1769
+ "Content-Type": "application/json",
1770
+ },
1771
+ body: JSON.stringify({ query: "{ viewer { id name email } }" }),
1772
+ signal: AbortSignal.timeout(10_000),
1773
+ });
1774
+ } catch (err) {
1775
+ log.warn({ err: err instanceof Error ? err.message : String(err) }, "Linear GraphQL fetch failed");
1776
+ return { ok: false as const, error: "Could not reach Linear API. Please try again." };
1777
+ }
1778
+ if (!res.ok) {
1779
+ let detail = `status ${res.status}`;
1780
+ try {
1781
+ const errBody = (await res.json()) as { errors?: Array<{ message?: string }> };
1782
+ if (errBody.errors?.[0]?.message) detail = errBody.errors[0].message;
1783
+ } catch {
1784
+ // intentionally ignored: response body may not be JSON
1785
+ }
1786
+ return { ok: false as const, error: `Linear API error: ${detail}` };
1787
+ }
1788
+ let data: { data?: { viewer?: { id?: string; name?: string; email?: string } }; errors?: Array<{ message?: string }> };
1789
+ try {
1790
+ data = (await res.json()) as typeof data;
1791
+ } catch (err) {
1792
+ log.warn({ err: err instanceof Error ? err.message : String(err) }, "Linear GraphQL response parse failed");
1793
+ return { ok: false as const, error: "Linear API returned an invalid response" };
1794
+ }
1795
+ if (data.errors?.length) {
1796
+ return { ok: false as const, error: data.errors[0].message ?? "GraphQL error" };
1797
+ }
1798
+ if (!data.data?.viewer?.id) {
1799
+ return { ok: false as const, error: "Invalid API key" };
1800
+ }
1801
+ return {
1802
+ ok: true as const,
1803
+ userId: data.data.viewer.id,
1804
+ userName: data.data.viewer.name ?? null,
1805
+ userEmail: data.data.viewer.email ?? null,
1806
+ };
1807
+ },
1808
+ catch: (err) => err instanceof Error ? err : new Error(String(err)),
1809
+ });
1810
+
1811
+ if (!viewerResult.ok) {
1812
+ return c.json(
1813
+ { error: "invalid_token", message: `Invalid Linear API key: ${viewerResult.error}` },
1814
+ 400,
1815
+ );
1816
+ }
1817
+
1818
+ const saveResult = yield* Effect.tryPromise({
1819
+ try: () =>
1820
+ saveLinearInstallation(viewerResult.userId, {
1821
+ orgId,
1822
+ userName: viewerResult.userName ?? undefined,
1823
+ userEmail: viewerResult.userEmail ?? undefined,
1824
+ apiKey,
1825
+ }),
1826
+ catch: (err) => err instanceof Error ? err : new Error(String(err)),
1827
+ }).pipe(
1828
+ Effect.map(() => ({ ok: true as const })),
1829
+ Effect.catchAll((err) => {
1830
+ if (err.message.includes("already bound to a different organization")) {
1831
+ return Effect.succeed({ ok: false as const, message: err.message });
1832
+ }
1833
+ return Effect.fail(err);
1834
+ }),
1835
+ );
1836
+
1837
+ if (!saveResult.ok) {
1838
+ return c.json(
1839
+ { error: "conflict", message: saveResult.message },
1840
+ 409,
1841
+ );
1842
+ }
1843
+
1844
+ log.info({ orgId, userId: viewerResult.userId, userName: viewerResult.userName }, "Linear installation saved by admin");
1845
+ return c.json(
1846
+ { message: "Linear connected successfully.", userName: viewerResult.userName, userEmail: viewerResult.userEmail },
1847
+ 200,
1848
+ );
1849
+ }),
1850
+ { label: "connect linear" },
1851
+ );
1852
+ });
1853
+
1854
+ const disconnectLinearRoute = createRoute({
1855
+ method: "delete",
1856
+ path: "/linear",
1857
+ tags: ["Admin — Integrations"],
1858
+ summary: "Disconnect Linear",
1859
+ description:
1860
+ "Removes the Linear installation for the current workspace. " +
1861
+ "Any Linear integration functionality will stop working until reconnected.",
1862
+ responses: {
1863
+ 200: {
1864
+ description: "Linear disconnected",
1865
+ content: {
1866
+ "application/json": {
1867
+ schema: z.object({ message: z.string() }),
1868
+ },
1869
+ },
1870
+ },
1871
+ 400: {
1872
+ description: "No active organization",
1873
+ content: { "application/json": { schema: ErrorSchema } },
1874
+ },
1875
+ 401: {
1876
+ description: "Authentication required",
1877
+ content: { "application/json": { schema: AuthErrorSchema } },
1878
+ },
1879
+ 404: {
1880
+ description: "No Linear installation found",
1881
+ content: { "application/json": { schema: ErrorSchema } },
1882
+ },
1883
+ 500: {
1884
+ description: "Internal server error",
1885
+ content: { "application/json": { schema: ErrorSchema } },
1886
+ },
1887
+ },
1888
+ });
1889
+
1890
+ adminIntegrations.openapi(disconnectLinearRoute, async (c) => {
1891
+ return runEffect(
1892
+ c,
1893
+ Effect.gen(function* () {
1894
+ const { orgId } = yield* AuthContext;
1895
+
1896
+ if (!orgId) {
1897
+ return c.json(
1898
+ { error: "bad_request", message: "No active organization." },
1899
+ 400,
1900
+ );
1901
+ }
1902
+
1903
+ const deleted = yield* Effect.tryPromise({
1904
+ try: () => deleteLinearInstallationByOrg(orgId),
1905
+ catch: (err) => err instanceof Error ? err : new Error(String(err)),
1906
+ });
1907
+
1908
+ if (!deleted) {
1909
+ return c.json(
1910
+ { error: "not_found", message: "No Linear installation found for this workspace." },
1911
+ 404,
1912
+ );
1913
+ }
1914
+
1915
+ log.info({ orgId }, "Linear installation disconnected by admin");
1916
+ return c.json({ message: "Linear disconnected successfully." }, 200);
1917
+ }),
1918
+ { label: "disconnect linear" },
1919
+ );
1920
+ });
1921
+
1922
+ // ---------------------------------------------------------------------------
1923
+ // WhatsApp routes (BYOT-only — Cloud API credentials)
1924
+ // ---------------------------------------------------------------------------
1925
+
1926
+ const connectWhatsAppRoute = createRoute({
1927
+ method: "post",
1928
+ path: "/whatsapp",
1929
+ tags: ["Admin — Integrations"],
1930
+ summary: "Connect WhatsApp via Cloud API credentials",
1931
+ description:
1932
+ "Validates WhatsApp Cloud API credentials via the Meta Graph API and saves the installation " +
1933
+ "for the current workspace.",
1934
+ request: {
1935
+ body: {
1936
+ content: {
1937
+ "application/json": {
1938
+ schema: z.object({
1939
+ phoneNumberId: z
1940
+ .string()
1941
+ .min(1)
1942
+ .regex(/^\d+$/, "Phone number ID must be numeric")
1943
+ .openapi({ description: "WhatsApp phone number ID from Meta Business Suite" }),
1944
+ accessToken: z
1945
+ .string()
1946
+ .min(1)
1947
+ .openapi({ description: "Permanent access token from Meta" }),
1948
+ }),
1949
+ },
1950
+ },
1951
+ },
1952
+ },
1953
+ responses: {
1954
+ 200: {
1955
+ description: "WhatsApp connected",
1956
+ content: {
1957
+ "application/json": {
1958
+ schema: z.object({
1959
+ message: z.string(),
1960
+ displayPhone: z.string().nullable(),
1961
+ }),
1962
+ },
1963
+ },
1964
+ },
1965
+ 400: {
1966
+ description: "Invalid credentials, no active organization, or internal database not configured",
1967
+ content: { "application/json": { schema: ErrorSchema } },
1968
+ },
1969
+ 401: {
1970
+ description: "Authentication required",
1971
+ content: { "application/json": { schema: AuthErrorSchema } },
1972
+ },
1973
+ 409: {
1974
+ description: "Phone number already bound to a different organization",
1975
+ content: { "application/json": { schema: ErrorSchema } },
1976
+ },
1977
+ 500: {
1978
+ description: "Internal server error",
1979
+ content: { "application/json": { schema: ErrorSchema } },
1980
+ },
1981
+ },
1982
+ });
1983
+
1984
+ adminIntegrations.openapi(connectWhatsAppRoute, async (c) => {
1985
+ return runEffect(
1986
+ c,
1987
+ Effect.gen(function* () {
1988
+ const { orgId } = yield* AuthContext;
1989
+
1990
+ if (!orgId) {
1991
+ return c.json(
1992
+ { error: "bad_request", message: "No active organization." },
1993
+ 400,
1994
+ );
1995
+ }
1996
+
1997
+ if (!hasInternalDB()) {
1998
+ return c.json(
1999
+ { error: "not_configured", message: "WhatsApp integration requires an internal database. Configure DATABASE_URL." },
2000
+ 400,
2001
+ );
2002
+ }
2003
+
2004
+ const { phoneNumberId, accessToken } = c.req.valid("json");
2005
+
2006
+ // Validate credentials by calling Meta's Graph API.
2007
+ const phoneResult = yield* Effect.tryPromise({
2008
+ try: async () => {
2009
+ let res: Response;
2010
+ try {
2011
+ res = await fetch(`https://graph.facebook.com/v18.0/${encodeURIComponent(phoneNumberId)}`, {
2012
+ headers: { Authorization: `Bearer ${accessToken}` },
2013
+ signal: AbortSignal.timeout(10_000),
2014
+ });
2015
+ } catch (err) {
2016
+ log.warn({ err: err instanceof Error ? err.message : String(err) }, "WhatsApp Graph API fetch failed");
2017
+ return { ok: false as const, error: "Could not reach Meta API. Please try again." };
2018
+ }
2019
+ if (!res.ok) {
2020
+ let detail = `status ${res.status}`;
2021
+ try {
2022
+ const errBody = (await res.json()) as { error?: { message?: string } };
2023
+ if (errBody.error?.message) detail = errBody.error.message;
2024
+ } catch {
2025
+ // intentionally ignored: response body may not be JSON
2026
+ }
2027
+ return { ok: false as const, error: `Meta API error: ${detail}` };
2028
+ }
2029
+ let data: { id?: string; display_phone_number?: string };
2030
+ try {
2031
+ data = (await res.json()) as typeof data;
2032
+ } catch (err) {
2033
+ log.warn({ err: err instanceof Error ? err.message : String(err) }, "WhatsApp Graph API response parse failed");
2034
+ return { ok: false as const, error: "Meta API returned an invalid response" };
2035
+ }
2036
+ if (!data.id) {
2037
+ return { ok: false as const, error: "Invalid phone number ID or access token" };
2038
+ }
2039
+ return { ok: true as const, displayPhone: data.display_phone_number ?? null };
2040
+ },
2041
+ catch: (err) => err instanceof Error ? err : new Error(String(err)),
2042
+ });
2043
+
2044
+ if (!phoneResult.ok) {
2045
+ return c.json(
2046
+ { error: "invalid_credentials", message: `Invalid WhatsApp credentials: ${phoneResult.error}` },
2047
+ 400,
2048
+ );
2049
+ }
2050
+
2051
+ const saveResult = yield* Effect.tryPromise({
2052
+ try: () =>
2053
+ saveWhatsAppInstallation(phoneNumberId, {
2054
+ orgId,
2055
+ displayPhone: phoneResult.displayPhone ?? undefined,
2056
+ accessToken,
2057
+ }),
2058
+ catch: (err) => err instanceof Error ? err : new Error(String(err)),
2059
+ }).pipe(
2060
+ Effect.map(() => ({ ok: true as const })),
2061
+ Effect.catchAll((err) => {
2062
+ if (err.message.includes("already bound to a different organization")) {
2063
+ return Effect.succeed({ ok: false as const, message: err.message });
2064
+ }
2065
+ return Effect.fail(err);
2066
+ }),
2067
+ );
2068
+
2069
+ if (!saveResult.ok) {
2070
+ return c.json(
2071
+ { error: "conflict", message: saveResult.message },
2072
+ 409,
2073
+ );
2074
+ }
2075
+
2076
+ log.info({ orgId, phoneNumberId, displayPhone: phoneResult.displayPhone }, "WhatsApp installation saved by admin");
2077
+ return c.json(
2078
+ { message: "WhatsApp connected successfully.", displayPhone: phoneResult.displayPhone },
2079
+ 200,
2080
+ );
2081
+ }),
2082
+ { label: "connect whatsapp" },
2083
+ );
2084
+ });
2085
+
2086
+ const disconnectWhatsAppRoute = createRoute({
2087
+ method: "delete",
2088
+ path: "/whatsapp",
2089
+ tags: ["Admin — Integrations"],
2090
+ summary: "Disconnect WhatsApp",
2091
+ description:
2092
+ "Removes the WhatsApp installation for the current workspace. " +
2093
+ "Any WhatsApp messaging functionality will stop working until reconnected.",
2094
+ responses: {
2095
+ 200: {
2096
+ description: "WhatsApp disconnected",
2097
+ content: {
2098
+ "application/json": {
2099
+ schema: z.object({ message: z.string() }),
2100
+ },
2101
+ },
2102
+ },
2103
+ 400: {
2104
+ description: "No active organization",
2105
+ content: { "application/json": { schema: ErrorSchema } },
2106
+ },
2107
+ 401: {
2108
+ description: "Authentication required",
2109
+ content: { "application/json": { schema: AuthErrorSchema } },
2110
+ },
2111
+ 404: {
2112
+ description: "No WhatsApp installation found",
2113
+ content: { "application/json": { schema: ErrorSchema } },
2114
+ },
2115
+ 500: {
2116
+ description: "Internal server error",
2117
+ content: { "application/json": { schema: ErrorSchema } },
2118
+ },
2119
+ },
2120
+ });
2121
+
2122
+ adminIntegrations.openapi(disconnectWhatsAppRoute, async (c) => {
2123
+ return runEffect(
2124
+ c,
2125
+ Effect.gen(function* () {
2126
+ const { orgId } = yield* AuthContext;
2127
+
2128
+ if (!orgId) {
2129
+ return c.json(
2130
+ { error: "bad_request", message: "No active organization." },
2131
+ 400,
2132
+ );
2133
+ }
2134
+
2135
+ const deleted = yield* Effect.tryPromise({
2136
+ try: () => deleteWhatsAppInstallationByOrg(orgId),
2137
+ catch: (err) => err instanceof Error ? err : new Error(String(err)),
2138
+ });
2139
+
2140
+ if (!deleted) {
2141
+ return c.json(
2142
+ { error: "not_found", message: "No WhatsApp installation found for this workspace." },
2143
+ 404,
2144
+ );
2145
+ }
2146
+
2147
+ log.info({ orgId }, "WhatsApp installation disconnected by admin");
2148
+ return c.json({ message: "WhatsApp disconnected successfully." }, 200);
2149
+ }),
2150
+ { label: "disconnect whatsapp" },
2151
+ );
2152
+ });
2153
+
2154
+ // ---------------------------------------------------------------------------
2155
+ // Email routes (BYOT-only — SMTP, SendGrid, Postmark, SES)
2156
+ // ---------------------------------------------------------------------------
2157
+
2158
+ const EmailProviderEnum = z.enum(["smtp", "sendgrid", "postmark", "ses"]);
2159
+
2160
+ const SmtpConfigSchema = z.object({
2161
+ host: z.string().min(1),
2162
+ port: z.number().int().min(1).max(65535),
2163
+ username: z.string().min(1),
2164
+ password: z.string().min(1),
2165
+ tls: z.boolean(),
2166
+ });
2167
+
2168
+ const SendGridConfigSchema = z.object({
2169
+ apiKey: z.string().min(1),
2170
+ });
2171
+
2172
+ const PostmarkConfigSchema = z.object({
2173
+ serverToken: z.string().min(1),
2174
+ });
2175
+
2176
+ const SesConfigSchema = z.object({
2177
+ region: z.string().min(1),
2178
+ accessKeyId: z.string().min(1),
2179
+ secretAccessKey: z.string().min(1),
2180
+ });
2181
+
2182
+ const connectEmailRoute = createRoute({
2183
+ method: "post",
2184
+ path: "/email",
2185
+ tags: ["Admin — Integrations"],
2186
+ summary: "Connect email delivery provider",
2187
+ description:
2188
+ "Saves email delivery configuration for the current workspace. " +
2189
+ "Supports SMTP, SendGrid, Postmark, and SES providers.",
2190
+ request: {
2191
+ body: {
2192
+ content: {
2193
+ "application/json": {
2194
+ schema: z.object({
2195
+ provider: EmailProviderEnum.openapi({ description: "Email provider type" }),
2196
+ senderAddress: z
2197
+ .string()
2198
+ .email()
2199
+ .openapi({ description: "Sender email address (From header)" }),
2200
+ config: z.union([SmtpConfigSchema, SendGridConfigSchema, PostmarkConfigSchema, SesConfigSchema])
2201
+ .openapi({ description: "Provider-specific configuration" }),
2202
+ }),
2203
+ },
2204
+ },
2205
+ },
2206
+ },
2207
+ responses: {
2208
+ 200: {
2209
+ description: "Email connected",
2210
+ content: {
2211
+ "application/json": {
2212
+ schema: z.object({
2213
+ message: z.string(),
2214
+ provider: z.string(),
2215
+ senderAddress: z.string(),
2216
+ }),
2217
+ },
2218
+ },
2219
+ },
2220
+ 400: {
2221
+ description: "Invalid configuration, no active organization, or internal database not configured",
2222
+ content: { "application/json": { schema: ErrorSchema } },
2223
+ },
2224
+ 401: {
2225
+ description: "Authentication required",
2226
+ content: { "application/json": { schema: AuthErrorSchema } },
2227
+ },
2228
+ 500: {
2229
+ description: "Internal server error",
2230
+ content: { "application/json": { schema: ErrorSchema } },
2231
+ },
2232
+ },
2233
+ });
2234
+
2235
+ adminIntegrations.openapi(connectEmailRoute, async (c) => {
2236
+ return runEffect(
2237
+ c,
2238
+ Effect.gen(function* () {
2239
+ const { orgId } = yield* AuthContext;
2240
+
2241
+ if (!orgId) {
2242
+ return c.json(
2243
+ { error: "bad_request", message: "No active organization." },
2244
+ 400,
2245
+ );
2246
+ }
2247
+
2248
+ if (!hasInternalDB()) {
2249
+ return c.json(
2250
+ { error: "not_configured", message: "Email integration requires an internal database. Configure DATABASE_URL." },
2251
+ 400,
2252
+ );
2253
+ }
2254
+
2255
+ const { provider, senderAddress, config } = c.req.valid("json");
2256
+
2257
+ // Validate provider-specific config shape
2258
+ const configResult = validateProviderConfig(provider, config);
2259
+ if (!configResult.ok) {
2260
+ return c.json(
2261
+ { error: "invalid_config", message: configResult.error },
2262
+ 400,
2263
+ );
2264
+ }
2265
+
2266
+ yield* Effect.tryPromise({
2267
+ try: () =>
2268
+ saveEmailInstallation(orgId, {
2269
+ provider: provider as EmailProvider,
2270
+ senderAddress,
2271
+ config: config as ProviderConfig,
2272
+ }),
2273
+ catch: (err) => err instanceof Error ? err : new Error(String(err)),
2274
+ });
2275
+
2276
+ log.info({ orgId, provider, senderAddress }, "Email installation saved by admin");
2277
+ return c.json(
2278
+ { message: "Email connected successfully.", provider, senderAddress },
2279
+ 200,
2280
+ );
2281
+ }),
2282
+ { label: "connect email" },
2283
+ );
2284
+ });
2285
+
2286
+ const testEmailRoute = createRoute({
2287
+ method: "post",
2288
+ path: "/email/test",
2289
+ tags: ["Admin — Integrations"],
2290
+ summary: "Send test email",
2291
+ description:
2292
+ "Sends a test email using the saved email configuration for the current workspace.",
2293
+ request: {
2294
+ body: {
2295
+ content: {
2296
+ "application/json": {
2297
+ schema: z.object({
2298
+ recipientEmail: z
2299
+ .string()
2300
+ .email()
2301
+ .openapi({ description: "Recipient email address for the test" }),
2302
+ }),
2303
+ },
2304
+ },
2305
+ },
2306
+ },
2307
+ responses: {
2308
+ 200: {
2309
+ description: "Test email sent",
2310
+ content: {
2311
+ "application/json": {
2312
+ schema: z.object({
2313
+ message: z.string(),
2314
+ success: z.boolean(),
2315
+ }),
2316
+ },
2317
+ },
2318
+ },
2319
+ 400: {
2320
+ description: "No active organization, internal database not configured, or no email config found",
2321
+ content: { "application/json": { schema: ErrorSchema } },
2322
+ },
2323
+ 401: {
2324
+ description: "Authentication required",
2325
+ content: { "application/json": { schema: AuthErrorSchema } },
2326
+ },
2327
+ 500: {
2328
+ description: "Internal server error",
2329
+ content: { "application/json": { schema: ErrorSchema } },
2330
+ },
2331
+ },
2332
+ });
2333
+
2334
+ adminIntegrations.openapi(testEmailRoute, async (c) => {
2335
+ return runEffect(
2336
+ c,
2337
+ Effect.gen(function* () {
2338
+ const { orgId } = yield* AuthContext;
2339
+
2340
+ if (!orgId) {
2341
+ return c.json(
2342
+ { error: "bad_request", message: "No active organization." },
2343
+ 400,
2344
+ );
2345
+ }
2346
+
2347
+ if (!hasInternalDB()) {
2348
+ return c.json(
2349
+ { error: "not_configured", message: "Email integration requires an internal database. Configure DATABASE_URL." },
2350
+ 400,
2351
+ );
2352
+ }
2353
+
2354
+ const { recipientEmail } = c.req.valid("json");
2355
+
2356
+ const install = yield* Effect.tryPromise({
2357
+ try: () => getEmailInstallationByOrg(orgId),
2358
+ catch: (err) => err instanceof Error ? err : new Error(String(err)),
2359
+ });
2360
+
2361
+ if (!install) {
2362
+ return c.json(
2363
+ { error: "not_found", message: "No email configuration found for this workspace. Connect an email provider first." },
2364
+ 400,
2365
+ );
2366
+ }
2367
+
2368
+ const result = yield* Effect.tryPromise({
2369
+ try: () => sendTestEmail(install, recipientEmail),
2370
+ catch: (err) => err instanceof Error ? err : new Error(String(err)),
2371
+ });
2372
+
2373
+ if (!result.success) {
2374
+ log.warn({ orgId, provider: install.provider, error: result.error }, "Test email failed");
2375
+ return c.json(
2376
+ { message: `Test email failed: ${result.error}`, success: false },
2377
+ 200,
2378
+ );
2379
+ }
2380
+
2381
+ log.info({ orgId, provider: install.provider, recipientEmail }, "Test email sent successfully");
2382
+ return c.json(
2383
+ { message: "Test email sent successfully.", success: true },
2384
+ 200,
2385
+ );
2386
+ }),
2387
+ { label: "test email" },
2388
+ );
2389
+ });
2390
+
2391
+ const disconnectEmailRoute = createRoute({
2392
+ method: "delete",
2393
+ path: "/email",
2394
+ tags: ["Admin — Integrations"],
2395
+ summary: "Disconnect email",
2396
+ description:
2397
+ "Removes the email configuration for the current workspace. " +
2398
+ "Email delivery will fall back to environment variables or be disabled until reconnected.",
2399
+ responses: {
2400
+ 200: {
2401
+ description: "Email disconnected",
2402
+ content: {
2403
+ "application/json": {
2404
+ schema: z.object({ message: z.string() }),
2405
+ },
2406
+ },
2407
+ },
2408
+ 400: {
2409
+ description: "No active organization",
2410
+ content: { "application/json": { schema: ErrorSchema } },
2411
+ },
2412
+ 401: {
2413
+ description: "Authentication required",
2414
+ content: { "application/json": { schema: AuthErrorSchema } },
2415
+ },
2416
+ 404: {
2417
+ description: "No email installation found",
2418
+ content: { "application/json": { schema: ErrorSchema } },
2419
+ },
2420
+ 500: {
2421
+ description: "Internal server error",
2422
+ content: { "application/json": { schema: ErrorSchema } },
2423
+ },
2424
+ },
2425
+ });
2426
+
2427
+ adminIntegrations.openapi(disconnectEmailRoute, async (c) => {
2428
+ return runEffect(
2429
+ c,
2430
+ Effect.gen(function* () {
2431
+ const { orgId } = yield* AuthContext;
2432
+
2433
+ if (!orgId) {
2434
+ return c.json(
2435
+ { error: "bad_request", message: "No active organization." },
2436
+ 400,
2437
+ );
2438
+ }
2439
+
2440
+ const deleted = yield* Effect.tryPromise({
2441
+ try: () => deleteEmailInstallationByOrg(orgId),
2442
+ catch: (err) => err instanceof Error ? err : new Error(String(err)),
2443
+ });
2444
+
2445
+ if (!deleted) {
2446
+ return c.json(
2447
+ { error: "not_found", message: "No email installation found for this workspace." },
2448
+ 404,
2449
+ );
2450
+ }
2451
+
2452
+ log.info({ orgId }, "Email installation disconnected by admin");
2453
+ return c.json({ message: "Email disconnected successfully." }, 200);
2454
+ }),
2455
+ { label: "disconnect email" },
2456
+ );
2457
+ });
2458
+
2459
+ // ---------------------------------------------------------------------------
2460
+ // Email helpers
2461
+ // ---------------------------------------------------------------------------
2462
+
2463
+ function validateProviderConfig(
2464
+ provider: string,
2465
+ config: unknown,
2466
+ ): { ok: true } | { ok: false; error: string } {
2467
+ switch (provider) {
2468
+ case "smtp": {
2469
+ const result = SmtpConfigSchema.safeParse(config);
2470
+ if (!result.success) return { ok: false, error: `Invalid SMTP config: ${result.error.issues.map(i => i.message).join(", ")}` };
2471
+ return { ok: true };
2472
+ }
2473
+ case "sendgrid": {
2474
+ const result = SendGridConfigSchema.safeParse(config);
2475
+ if (!result.success) return { ok: false, error: `Invalid SendGrid config: ${result.error.issues.map(i => i.message).join(", ")}` };
2476
+ return { ok: true };
2477
+ }
2478
+ case "postmark": {
2479
+ const result = PostmarkConfigSchema.safeParse(config);
2480
+ if (!result.success) return { ok: false, error: `Invalid Postmark config: ${result.error.issues.map(i => i.message).join(", ")}` };
2481
+ return { ok: true };
2482
+ }
2483
+ case "ses": {
2484
+ const result = SesConfigSchema.safeParse(config);
2485
+ if (!result.success) return { ok: false, error: `Invalid SES config: ${result.error.issues.map(i => i.message).join(", ")}` };
2486
+ return { ok: true };
2487
+ }
2488
+ default:
2489
+ return { ok: false, error: `Unknown provider: ${provider}` };
2490
+ }
2491
+ }
2492
+
2493
+ interface TestEmailResult {
2494
+ success: boolean;
2495
+ error?: string;
2496
+ }
2497
+
2498
+ async function sendTestEmail(
2499
+ install: { provider: string; sender_address: string; config: unknown },
2500
+ recipientEmail: string,
2501
+ ): Promise<TestEmailResult> {
2502
+ const config = install.config as Record<string, unknown>;
2503
+ const subject = "Atlas Email Test";
2504
+ const html = "<h1>Atlas Email Test</h1><p>This is a test email from Atlas to verify your email configuration is working correctly.</p>";
2505
+
2506
+ switch (install.provider) {
2507
+ case "smtp":
2508
+ return sendSmtpTestEmail(config, install.sender_address, recipientEmail, subject, html);
2509
+ case "sendgrid":
2510
+ return sendSendGridTestEmail(config, install.sender_address, recipientEmail, subject, html);
2511
+ case "postmark":
2512
+ return sendPostmarkTestEmail(config, install.sender_address, recipientEmail, subject, html);
2513
+ case "ses":
2514
+ return sendSesTestEmail(config, install.sender_address, recipientEmail, subject, html);
2515
+ default:
2516
+ return { success: false, error: `Unknown provider: ${install.provider}` };
2517
+ }
2518
+ }
2519
+
2520
+ async function sendSendGridTestEmail(
2521
+ config: Record<string, unknown>,
2522
+ from: string,
2523
+ to: string,
2524
+ subject: string,
2525
+ html: string,
2526
+ ): Promise<TestEmailResult> {
2527
+ const apiKey = config.apiKey;
2528
+ if (typeof apiKey !== "string") return { success: false, error: "Missing SendGrid API key" };
2529
+
2530
+ try {
2531
+ const res = await fetch("https://api.sendgrid.com/v3/mail/send", {
2532
+ method: "POST",
2533
+ headers: {
2534
+ "Content-Type": "application/json",
2535
+ Authorization: `Bearer ${apiKey}`,
2536
+ },
2537
+ body: JSON.stringify({
2538
+ personalizations: [{ to: [{ email: to }] }],
2539
+ from: { email: from },
2540
+ subject,
2541
+ content: [{ type: "text/html", value: html }],
2542
+ }),
2543
+ signal: AbortSignal.timeout(15_000),
2544
+ });
2545
+
2546
+ if (!res.ok) {
2547
+ const text = await res.text().catch(() => "");
2548
+ return { success: false, error: `SendGrid API error (${res.status}): ${text.slice(0, 200)}` };
2549
+ }
2550
+ return { success: true };
2551
+ } catch (err) {
2552
+ return { success: false, error: err instanceof Error ? err.message : String(err) };
2553
+ }
2554
+ }
2555
+
2556
+ async function sendPostmarkTestEmail(
2557
+ config: Record<string, unknown>,
2558
+ from: string,
2559
+ to: string,
2560
+ subject: string,
2561
+ html: string,
2562
+ ): Promise<TestEmailResult> {
2563
+ const serverToken = config.serverToken;
2564
+ if (typeof serverToken !== "string") return { success: false, error: "Missing Postmark server token" };
2565
+
2566
+ try {
2567
+ const res = await fetch("https://api.postmarkapp.com/email", {
2568
+ method: "POST",
2569
+ headers: {
2570
+ "Content-Type": "application/json",
2571
+ "X-Postmark-Server-Token": serverToken,
2572
+ },
2573
+ body: JSON.stringify({
2574
+ From: from,
2575
+ To: to,
2576
+ Subject: subject,
2577
+ HtmlBody: html,
2578
+ }),
2579
+ signal: AbortSignal.timeout(15_000),
2580
+ });
2581
+
2582
+ if (!res.ok) {
2583
+ const text = await res.text().catch(() => "");
2584
+ return { success: false, error: `Postmark API error (${res.status}): ${text.slice(0, 200)}` };
2585
+ }
2586
+ return { success: true };
2587
+ } catch (err) {
2588
+ return { success: false, error: err instanceof Error ? err.message : String(err) };
2589
+ }
2590
+ }
2591
+
2592
+ async function sendSmtpTestEmail(
2593
+ _config: Record<string, unknown>,
2594
+ from: string,
2595
+ to: string,
2596
+ subject: string,
2597
+ html: string,
2598
+ ): Promise<TestEmailResult> {
2599
+ // SMTP delivery delegates to the ATLAS_SMTP_URL webhook bridge.
2600
+ // The bridge endpoint is responsible for connecting to the SMTP server
2601
+ // using the config stored in the database — we do not send credentials
2602
+ // over the wire in this payload.
2603
+ const smtpUrl = process.env.ATLAS_SMTP_URL;
2604
+ if (!smtpUrl) {
2605
+ return {
2606
+ success: false,
2607
+ error: "SMTP test requires ATLAS_SMTP_URL to be configured as an SMTP-to-HTTP bridge endpoint. " +
2608
+ "Configuration has been saved and will be used when ATLAS_SMTP_URL is available.",
2609
+ };
2610
+ }
2611
+
2612
+ try {
2613
+ const res = await fetch(smtpUrl, {
2614
+ method: "POST",
2615
+ headers: { "Content-Type": "application/json" },
2616
+ body: JSON.stringify({ from, to, subject, html }),
2617
+ signal: AbortSignal.timeout(15_000),
2618
+ });
2619
+
2620
+ if (!res.ok) {
2621
+ const text = await res.text().catch(() => "");
2622
+ return { success: false, error: `SMTP webhook error (${res.status}): ${text.slice(0, 200)}` };
2623
+ }
2624
+ return { success: true };
2625
+ } catch (err) {
2626
+ return { success: false, error: err instanceof Error ? err.message : String(err) };
2627
+ }
2628
+ }
2629
+
2630
+ async function sendSesTestEmail(
2631
+ _config: Record<string, unknown>,
2632
+ from: string,
2633
+ to: string,
2634
+ subject: string,
2635
+ html: string,
2636
+ ): Promise<TestEmailResult> {
2637
+ // AWS Signature V4 is complex — for the test email we delegate to the
2638
+ // ATLAS_SMTP_URL webhook bridge if available. We do not send AWS credentials
2639
+ // over the wire; the bridge reads them from its own config or the database.
2640
+ const smtpUrl = process.env.ATLAS_SMTP_URL;
2641
+ if (!smtpUrl) {
2642
+ return {
2643
+ success: false,
2644
+ error: "SES test email requires ATLAS_SMTP_URL configured as an SES-compatible bridge. " +
2645
+ "Configuration has been saved.",
2646
+ };
2647
+ }
2648
+
2649
+ try {
2650
+ const res = await fetch(smtpUrl, {
2651
+ method: "POST",
2652
+ headers: { "Content-Type": "application/json" },
2653
+ body: JSON.stringify({ from, to, subject, html }),
2654
+ signal: AbortSignal.timeout(15_000),
2655
+ });
2656
+
2657
+ if (!res.ok) {
2658
+ const text = await res.text().catch(() => "");
2659
+ return { success: false, error: `SES webhook error (${res.status}): ${text.slice(0, 200)}` };
2660
+ }
2661
+ return { success: true };
2662
+ } catch (err) {
2663
+ return { success: false, error: err instanceof Error ? err.message : String(err) };
2664
+ }
2665
+ }
2666
+
2667
+ export { adminIntegrations };