@useatlas/create 0.0.6 → 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.
- package/LICENSE +21 -0
- package/README.md +1 -1
- package/index.ts +253 -36
- package/package.json +4 -4
- package/templates/docker/Dockerfile +1 -1
- package/templates/docker/Dockerfile.sidecar +1 -1
- package/templates/docker/bin/__tests__/duckdb-ingest.test.ts +17 -14
- package/templates/docker/bin/__tests__/failure-threshold.test.ts +148 -0
- package/templates/docker/bin/__tests__/fatal-error-propagation.test.ts +267 -0
- package/templates/docker/bin/__tests__/profiler-heuristics.test.ts +5 -5
- package/templates/docker/bin/__tests__/schema-drift.test.ts +39 -0
- package/templates/docker/bin/atlas.ts +981 -1819
- package/templates/docker/bin/benchmark.ts +14 -16
- package/templates/docker/bin/enrich.ts +7 -2
- package/templates/docker/brand.css +13 -0
- package/templates/docker/data/cybersec-semantic/catalog.yml +222 -0
- package/templates/docker/data/cybersec-semantic/entities/alerts.yml +195 -0
- package/templates/docker/data/cybersec-semantic/entities/assets.yml +191 -0
- package/templates/docker/data/cybersec-semantic/entities/compliance_assessments.yml +170 -0
- package/templates/docker/data/cybersec-semantic/entities/incidents.yml +219 -0
- package/templates/docker/data/cybersec-semantic/entities/organizations.yml +136 -0
- package/templates/docker/data/cybersec-semantic/entities/plans.yml +114 -0
- package/templates/docker/data/cybersec-semantic/entities/remediation_actions.yml +212 -0
- package/templates/docker/data/cybersec-semantic/entities/scan_results.yml +215 -0
- package/templates/docker/data/cybersec-semantic/entities/scans.yml +180 -0
- package/templates/docker/data/cybersec-semantic/entities/subscriptions.yml +184 -0
- package/templates/docker/data/cybersec-semantic/entities/users.yml +140 -0
- package/templates/docker/data/cybersec-semantic/entities/vulnerabilities.yml +154 -0
- package/templates/docker/data/cybersec-semantic/glossary.yml +207 -0
- package/templates/docker/data/cybersec-semantic/metrics/business.yml +148 -0
- package/templates/docker/data/cybersec-semantic/metrics/compliance.yml +138 -0
- package/templates/docker/data/cybersec-semantic/metrics/security.yml +181 -0
- package/templates/docker/data/cybersec.sql +8 -8
- package/templates/docker/data/demo.sql +3 -0
- package/templates/docker/data/ecommerce-semantic/catalog.yml +221 -0
- package/templates/docker/data/ecommerce-semantic/entities/categories.yml +91 -0
- package/templates/docker/data/ecommerce-semantic/entities/customers.yml +133 -0
- package/templates/docker/data/ecommerce-semantic/entities/email_campaigns.yml +119 -0
- package/templates/docker/data/ecommerce-semantic/entities/inventory_levels.yml +153 -0
- package/templates/docker/data/ecommerce-semantic/entities/order_items.yml +159 -0
- package/templates/docker/data/ecommerce-semantic/entities/orders.yml +199 -0
- package/templates/docker/data/ecommerce-semantic/entities/payments.yml +140 -0
- package/templates/docker/data/ecommerce-semantic/entities/product_reviews.yml +155 -0
- package/templates/docker/data/ecommerce-semantic/entities/products.yml +178 -0
- package/templates/docker/data/ecommerce-semantic/entities/promotions.yml +171 -0
- package/templates/docker/data/ecommerce-semantic/entities/returns.yml +144 -0
- package/templates/docker/data/ecommerce-semantic/entities/sellers.yml +124 -0
- package/templates/docker/data/ecommerce-semantic/entities/shipments.yml +159 -0
- package/templates/docker/data/ecommerce-semantic/glossary.yml +193 -0
- package/templates/docker/data/ecommerce-semantic/metrics/customers.yml +116 -0
- package/templates/docker/data/ecommerce-semantic/metrics/operations.yml +131 -0
- package/templates/docker/data/ecommerce-semantic/metrics/revenue.yml +120 -0
- package/templates/docker/docs/deploy.md +2 -1
- package/templates/docker/ee/src/__mocks__/internal.ts +170 -0
- package/templates/docker/ee/src/audit/purge-scheduler.ts +113 -0
- package/templates/docker/ee/src/audit/retention.ts +467 -0
- package/templates/docker/ee/src/auth/ip-allowlist.ts +367 -0
- package/templates/docker/ee/src/auth/roles.ts +562 -0
- package/templates/docker/ee/src/auth/scim.ts +343 -0
- package/templates/docker/ee/src/auth/sso.ts +538 -0
- package/templates/docker/ee/src/backups/engine.ts +355 -0
- package/templates/docker/ee/src/backups/index.ts +26 -0
- package/templates/docker/ee/src/backups/restore.ts +169 -0
- package/templates/docker/ee/src/backups/scheduler.ts +153 -0
- package/templates/docker/ee/src/backups/verify.ts +124 -0
- package/templates/docker/ee/src/branding/white-label.ts +228 -0
- package/templates/docker/ee/src/compliance/masking.ts +477 -0
- package/templates/docker/ee/src/compliance/patterns.ts +16 -0
- package/templates/docker/ee/src/compliance/pii-detection.ts +217 -0
- package/templates/docker/ee/src/compliance/reports.ts +402 -0
- package/templates/docker/ee/src/deploy-mode.ts +37 -0
- package/templates/docker/ee/src/governance/approval.ts +699 -0
- package/templates/docker/ee/src/index.ts +74 -0
- package/templates/docker/ee/src/platform/domains.ts +562 -0
- package/templates/docker/ee/src/platform/model-routing.ts +382 -0
- package/templates/docker/ee/src/platform/residency.ts +265 -0
- package/templates/docker/ee/src/sla/alerting.ts +382 -0
- package/templates/docker/ee/src/sla/index.ts +12 -0
- package/templates/docker/ee/src/sla/metrics.ts +275 -0
- package/templates/docker/ee/src/test-setup.ts +1 -0
- package/templates/docker/next.config.ts +4 -1
- package/templates/docker/package.json +49 -29
- package/templates/docker/sidecar/Dockerfile +1 -1
- package/templates/docker/src/api/index.ts +336 -24
- package/templates/docker/src/api/routes/actions.ts +443 -176
- package/templates/docker/src/api/routes/admin-abuse.ts +219 -0
- package/templates/docker/src/api/routes/admin-approval.ts +418 -0
- package/templates/docker/src/api/routes/admin-audit-retention.ts +405 -0
- package/templates/docker/src/api/routes/admin-auth.ts +122 -0
- package/templates/docker/src/api/routes/admin-branding.ts +252 -0
- package/templates/docker/src/api/routes/admin-compliance.ts +352 -0
- package/templates/docker/src/api/routes/admin-domains.ts +334 -0
- package/templates/docker/src/api/routes/admin-integrations.ts +2667 -0
- package/templates/docker/src/api/routes/admin-ip-allowlist.ts +261 -0
- package/templates/docker/src/api/routes/admin-learned-patterns.ts +525 -0
- package/templates/docker/src/api/routes/admin-model-config.ts +252 -0
- package/templates/docker/src/api/routes/admin-onboarding-emails.ts +145 -0
- package/templates/docker/src/api/routes/admin-orgs.ts +710 -0
- package/templates/docker/src/api/routes/admin-prompts.ts +694 -0
- package/templates/docker/src/api/routes/admin-residency.ts +570 -0
- package/templates/docker/src/api/routes/admin-roles.ts +296 -0
- package/templates/docker/src/api/routes/admin-router.ts +120 -0
- package/templates/docker/src/api/routes/admin-sandbox.ts +417 -0
- package/templates/docker/src/api/routes/admin-scim.ts +262 -0
- package/templates/docker/src/api/routes/admin-sso.ts +545 -0
- package/templates/docker/src/api/routes/admin-suggestions.ts +176 -0
- package/templates/docker/src/api/routes/admin-usage.ts +310 -0
- package/templates/docker/src/api/routes/admin.ts +4156 -898
- package/templates/docker/src/api/routes/auth-preamble.ts +105 -0
- package/templates/docker/src/api/routes/billing.ts +397 -0
- package/templates/docker/src/api/routes/chat.ts +597 -334
- package/templates/docker/src/api/routes/conversations.ts +987 -132
- package/templates/docker/src/api/routes/demo.ts +673 -0
- package/templates/docker/src/api/routes/discord.ts +274 -0
- package/templates/docker/src/api/routes/ee-error-handler.ts +32 -0
- package/templates/docker/src/api/routes/health.ts +129 -14
- package/templates/docker/src/api/routes/middleware.ts +244 -0
- package/templates/docker/src/api/routes/onboarding-emails.ts +134 -0
- package/templates/docker/src/api/routes/onboarding.ts +1109 -0
- package/templates/docker/src/api/routes/openapi.ts +184 -1597
- package/templates/docker/src/api/routes/platform-admin.ts +760 -0
- package/templates/docker/src/api/routes/platform-backups.ts +436 -0
- package/templates/docker/src/api/routes/platform-domains.ts +235 -0
- package/templates/docker/src/api/routes/platform-residency.ts +257 -0
- package/templates/docker/src/api/routes/platform-sla.ts +379 -0
- package/templates/docker/src/api/routes/prompts.ts +221 -0
- package/templates/docker/src/api/routes/public-branding.ts +106 -0
- package/templates/docker/src/api/routes/query.ts +330 -219
- package/templates/docker/src/api/routes/scheduled-tasks.ts +393 -297
- package/templates/docker/src/api/routes/semantic.ts +179 -0
- package/templates/docker/src/api/routes/sessions.ts +210 -0
- package/templates/docker/src/api/routes/shared-domains.ts +98 -0
- package/templates/docker/src/api/routes/shared-schemas.ts +139 -0
- package/templates/docker/src/api/routes/slack.ts +209 -52
- package/templates/docker/src/api/routes/suggestions.ts +233 -0
- package/templates/docker/src/api/routes/tables.ts +67 -0
- package/templates/docker/src/api/routes/teams.ts +222 -0
- package/templates/docker/src/api/routes/validate-sql.ts +188 -0
- package/templates/docker/src/api/routes/validation-hook.ts +62 -0
- package/templates/docker/src/api/routes/widget-loader.ts +356 -0
- package/templates/docker/src/api/routes/widget.ts +428 -0
- package/templates/docker/src/api/routes/wizard.ts +852 -0
- package/templates/docker/src/api/server.ts +187 -69
- package/templates/docker/src/app/error.tsx +5 -2
- package/templates/docker/src/app/globals.css +1 -1
- package/templates/docker/src/app/layout.tsx +7 -2
- package/templates/docker/src/app/page.tsx +39 -5
- package/templates/docker/src/components/data-table/data-table-column-header.tsx +99 -0
- package/templates/docker/src/components/data-table/data-table-date-filter.tsx +225 -0
- package/templates/docker/src/components/data-table/data-table-expandable.tsx +125 -0
- package/templates/docker/src/components/data-table/data-table-faceted-filter.tsx +189 -0
- package/templates/docker/src/components/data-table/data-table-pagination.tsx +112 -0
- package/templates/docker/src/components/data-table/data-table-range-filter.tsx +122 -0
- package/templates/docker/src/components/data-table/data-table-slider-filter.tsx +256 -0
- package/templates/docker/src/components/data-table/data-table-sort-list.tsx +407 -0
- package/templates/docker/src/components/data-table/data-table-toolbar.tsx +149 -0
- package/templates/docker/src/components/data-table/data-table-view-options.tsx +89 -0
- package/templates/docker/src/components/data-table/data-table.tsx +105 -0
- package/templates/docker/src/components/form-dialog.tsx +135 -0
- package/templates/docker/src/components/ui/accordion.tsx +66 -0
- package/templates/docker/src/components/ui/calendar.tsx +220 -0
- package/templates/docker/src/components/ui/checkbox.tsx +32 -0
- package/templates/docker/src/components/ui/faceted.tsx +283 -0
- package/templates/docker/src/components/ui/form.tsx +167 -0
- package/templates/docker/src/components/ui/label.tsx +24 -0
- package/templates/docker/src/components/ui/popover.tsx +89 -0
- package/templates/docker/src/components/ui/progress.tsx +31 -0
- package/templates/docker/src/components/ui/scroll-area.tsx +6 -2
- package/templates/docker/src/components/ui/slider.tsx +63 -0
- package/templates/docker/src/components/ui/sortable.tsx +581 -0
- package/templates/docker/src/components/ui/switch.tsx +35 -0
- package/templates/docker/src/components/ui/textarea.tsx +18 -0
- package/templates/docker/src/config/data-table.ts +82 -0
- package/templates/docker/src/env-check.ts +74 -0
- package/templates/docker/src/hooks/use-callback-ref.ts +27 -0
- package/templates/docker/src/hooks/use-data-table.ts +316 -0
- package/templates/docker/src/hooks/use-debounced-callback.ts +28 -0
- package/templates/docker/src/lib/action-types.ts +7 -41
- package/templates/docker/src/lib/agent-query.ts +4 -2
- package/templates/docker/src/lib/agent.ts +363 -31
- package/templates/docker/src/lib/auth/admin-permissions.ts +38 -0
- package/templates/docker/src/lib/auth/audit.ts +19 -4
- package/templates/docker/src/lib/auth/byot.ts +3 -3
- package/templates/docker/src/lib/auth/client.ts +33 -3
- package/templates/docker/src/lib/auth/detect.ts +29 -8
- package/templates/docker/src/lib/auth/managed.ts +104 -14
- package/templates/docker/src/lib/auth/middleware.ts +53 -6
- package/templates/docker/src/lib/auth/migrate.ts +140 -15
- package/templates/docker/src/lib/auth/oauth-state.ts +123 -0
- package/templates/docker/src/lib/auth/org-permissions.ts +55 -0
- package/templates/docker/src/lib/auth/permissions.ts +26 -19
- package/templates/docker/src/lib/auth/server.ts +355 -9
- package/templates/docker/src/lib/auth/simple-key.ts +3 -3
- package/templates/docker/src/lib/auth/types.ts +15 -21
- package/templates/docker/src/lib/billing/enforcement.ts +368 -0
- package/templates/docker/src/lib/billing/plans.ts +155 -0
- package/templates/docker/src/lib/cache/index.ts +92 -0
- package/templates/docker/src/lib/cache/keys.ts +30 -0
- package/templates/docker/src/lib/cache/lru.ts +79 -0
- package/templates/docker/src/lib/cache/types.ts +31 -0
- package/templates/docker/src/lib/compose-refs.ts +62 -0
- package/templates/docker/src/lib/config.ts +563 -11
- package/templates/docker/src/lib/connection-types.ts +9 -0
- package/templates/docker/src/lib/conversation-types.ts +1 -25
- package/templates/docker/src/lib/conversations.ts +345 -14
- package/templates/docker/src/lib/data-table.ts +61 -0
- package/templates/docker/src/lib/db/connection.ts +793 -39
- package/templates/docker/src/lib/db/internal.ts +985 -139
- package/templates/docker/src/lib/db/migrate.ts +295 -0
- package/templates/docker/src/lib/db/migrations/0000_baseline.sql +703 -0
- package/templates/docker/src/lib/db/migrations/0001_teams_installations.sql +14 -0
- package/templates/docker/src/lib/db/migrations/0002_discord_installations.sql +14 -0
- package/templates/docker/src/lib/db/migrations/0003_telegram_installations.sql +15 -0
- package/templates/docker/src/lib/db/migrations/0004_sandbox_credentials.sql +18 -0
- package/templates/docker/src/lib/db/migrations/0005_oauth_state.sql +16 -0
- package/templates/docker/src/lib/db/migrations/0006_byot_credentials.sql +14 -0
- package/templates/docker/src/lib/db/migrations/0007_gchat_installations.sql +15 -0
- package/templates/docker/src/lib/db/migrations/0008_github_installations.sql +14 -0
- package/templates/docker/src/lib/db/migrations/0009_linear_installations.sql +15 -0
- package/templates/docker/src/lib/db/migrations/0010_whatsapp_installations.sql +14 -0
- package/templates/docker/src/lib/db/migrations/0011_email_installations.sql +16 -0
- package/templates/docker/src/lib/db/migrations/0012_region_migrations.sql +25 -0
- package/templates/docker/src/lib/db/schema.ts +1120 -0
- package/templates/docker/src/lib/db/source-rate-limit.ts +89 -139
- package/templates/docker/src/lib/demo.ts +308 -0
- package/templates/docker/src/lib/discord/store.ts +225 -0
- package/templates/docker/src/lib/effect/ai.ts +243 -0
- package/templates/docker/src/lib/effect/errors.ts +234 -0
- package/templates/docker/src/lib/effect/hono.ts +454 -0
- package/templates/docker/src/lib/effect/index.ts +137 -0
- package/templates/docker/src/lib/effect/layers.ts +496 -0
- package/templates/docker/src/lib/effect/services.ts +776 -0
- package/templates/docker/src/lib/effect/sql.ts +178 -0
- package/templates/docker/src/lib/effect/toolkit.ts +123 -0
- package/templates/docker/src/lib/email/delivery.ts +232 -0
- package/templates/docker/src/lib/email/engine.ts +349 -0
- package/templates/docker/src/lib/email/hooks.ts +107 -0
- package/templates/docker/src/lib/email/index.ts +16 -0
- package/templates/docker/src/lib/email/scheduler.ts +72 -0
- package/templates/docker/src/lib/email/sequence.ts +73 -0
- package/templates/docker/src/lib/email/store.ts +163 -0
- package/templates/docker/src/lib/email/templates.ts +215 -0
- package/templates/docker/src/lib/format.ts +67 -0
- package/templates/docker/src/lib/gchat/store.ts +202 -0
- package/templates/docker/src/lib/github/store.ts +197 -0
- package/templates/docker/src/lib/id.ts +29 -0
- package/templates/docker/src/lib/integrations/types.ts +166 -0
- package/templates/docker/src/lib/learn/pattern-analyzer.ts +224 -0
- package/templates/docker/src/lib/learn/pattern-cache.ts +229 -0
- package/templates/docker/src/lib/learn/pattern-proposer.ts +87 -0
- package/templates/docker/src/lib/learn/suggestion-helpers.ts +34 -0
- package/templates/docker/src/lib/learn/suggestions.ts +139 -0
- package/templates/docker/src/lib/linear/store.ts +200 -0
- package/templates/docker/src/lib/logger.ts +35 -3
- package/templates/docker/src/lib/metering.ts +272 -0
- package/templates/docker/src/lib/parsers.ts +99 -0
- package/templates/docker/src/lib/plugins/hooks.ts +13 -11
- package/templates/docker/src/lib/plugins/index.ts +3 -1
- package/templates/docker/src/lib/plugins/registry.ts +58 -6
- package/templates/docker/src/lib/plugins/settings.ts +147 -0
- package/templates/docker/src/lib/plugins/wiring.ts +6 -9
- package/templates/docker/src/lib/profiler.ts +1665 -0
- package/templates/docker/src/lib/providers.ts +188 -13
- package/templates/docker/src/lib/rls.ts +172 -60
- package/templates/docker/src/lib/sandbox/credentials.ts +206 -0
- package/templates/docker/src/lib/sandbox/validate.ts +179 -0
- package/templates/docker/src/lib/scheduled-task-types.ts +26 -94
- package/templates/docker/src/lib/scheduled-tasks.ts +174 -34
- package/templates/docker/src/lib/scheduler/delivery.ts +248 -150
- package/templates/docker/src/lib/scheduler/engine.ts +190 -154
- package/templates/docker/src/lib/scheduler/executor.ts +74 -23
- package/templates/docker/src/lib/scheduler/preview.ts +72 -0
- package/templates/docker/src/lib/security/abuse.ts +463 -0
- package/templates/docker/src/lib/semantic/diff.ts +267 -0
- package/templates/docker/src/lib/semantic/entities.ts +167 -0
- package/templates/docker/src/lib/semantic/files.ts +283 -0
- package/templates/docker/src/lib/semantic/index.ts +27 -0
- package/templates/docker/src/lib/{semantic-index.ts → semantic/search.ts} +80 -9
- package/templates/docker/src/lib/semantic/sync.ts +581 -0
- package/templates/docker/src/lib/{semantic.ts → semantic/whitelist.ts} +189 -3
- package/templates/docker/src/lib/settings.ts +817 -0
- package/templates/docker/src/lib/sidecar-types.ts +13 -0
- package/templates/docker/src/lib/slack/store.ts +134 -25
- package/templates/docker/src/lib/startup.ts +528 -362
- package/templates/docker/src/lib/teams/store.ts +216 -0
- package/templates/docker/src/lib/telegram/store.ts +202 -0
- package/templates/docker/src/lib/telemetry.ts +40 -0
- package/templates/docker/src/lib/tools/actions/audit.ts +8 -5
- package/templates/docker/src/lib/tools/actions/email.ts +3 -1
- package/templates/docker/src/lib/tools/actions/handler.ts +276 -93
- package/templates/docker/src/lib/tools/actions/jira.ts +2 -2
- package/templates/docker/src/lib/tools/backends/detect.ts +16 -0
- package/templates/docker/src/lib/tools/backends/index.ts +11 -0
- package/templates/docker/src/lib/tools/backends/nsjail.ts +213 -0
- package/templates/docker/src/lib/tools/backends/shared.ts +103 -0
- package/templates/docker/src/lib/tools/backends/types.ts +26 -0
- package/templates/docker/src/lib/tools/explore-nsjail.ts +7 -228
- package/templates/docker/src/lib/tools/explore-sandbox.ts +4 -29
- package/templates/docker/src/lib/tools/explore-sidecar.ts +18 -2
- package/templates/docker/src/lib/tools/explore.ts +246 -54
- package/templates/docker/src/lib/tools/index.ts +17 -0
- package/templates/docker/src/lib/tools/python-nsjail.ts +11 -139
- package/templates/docker/src/lib/tools/python-sandbox.ts +9 -132
- package/templates/docker/src/lib/tools/python-sidecar.ts +184 -3
- package/templates/docker/src/lib/tools/python-stream.ts +33 -0
- package/templates/docker/src/lib/tools/python-wrapper.ts +129 -0
- package/templates/docker/src/lib/tools/python.ts +115 -15
- package/templates/docker/src/lib/tools/registry.ts +14 -2
- package/templates/docker/src/lib/tools/sql.ts +778 -362
- package/templates/docker/src/lib/tracing.ts +16 -0
- package/templates/docker/src/lib/whatsapp/store.ts +198 -0
- package/templates/docker/src/lib/workspace.ts +89 -0
- package/templates/docker/src/progress.ts +121 -0
- package/templates/docker/src/types/data-table.ts +48 -0
- package/templates/docker/src/ui/atlas-chat-reexport.ts +3 -0
- package/templates/docker/src/ui/components/actions/action-approval-card.tsx +26 -19
- package/templates/docker/src/ui/components/actions/action-status-badge.tsx +3 -3
- package/templates/docker/src/ui/components/admin/admin-layout.tsx +57 -39
- package/templates/docker/src/ui/components/admin/admin-sidebar.tsx +213 -35
- package/templates/docker/src/ui/components/admin/delivery-status-badge.tsx +53 -0
- package/templates/docker/src/ui/components/admin/empty-state.tsx +27 -6
- package/templates/docker/src/ui/components/admin/entity-detail.tsx +3 -52
- package/templates/docker/src/ui/components/admin/error-banner.tsx +2 -2
- package/templates/docker/src/ui/components/admin/feature-disabled.tsx +28 -5
- package/templates/docker/src/ui/components/admin-content-wrapper.tsx +87 -0
- package/templates/docker/src/ui/components/atlas-chat.tsx +449 -166
- package/templates/docker/src/ui/components/branding-head.tsx +41 -0
- package/templates/docker/src/ui/components/chart/chart-detection.ts +62 -5
- package/templates/docker/src/ui/components/chart/result-chart.tsx +316 -125
- package/templates/docker/src/ui/components/chat/api-key-bar.tsx +4 -4
- package/templates/docker/src/ui/components/chat/data-table.tsx +45 -4
- package/templates/docker/src/ui/components/chat/error-banner.tsx +86 -5
- package/templates/docker/src/ui/components/chat/follow-up-chips.tsx +29 -0
- package/templates/docker/src/ui/components/chat/markdown.tsx +24 -0
- package/templates/docker/src/ui/components/chat/prompt-library.tsx +206 -0
- package/templates/docker/src/ui/components/chat/python-result-card.tsx +106 -78
- package/templates/docker/src/ui/components/chat/result-card-base.tsx +101 -0
- package/templates/docker/src/ui/components/chat/share-dialog.tsx +377 -0
- package/templates/docker/src/ui/components/chat/sql-result-card.tsx +94 -73
- package/templates/docker/src/ui/components/chat/suggestion-chips.tsx +46 -0
- package/templates/docker/src/ui/components/chat/tool-part.tsx +16 -4
- package/templates/docker/src/ui/components/conversations/conversation-item.tsx +48 -17
- package/templates/docker/src/ui/components/conversations/conversation-list.tsx +38 -24
- package/templates/docker/src/ui/components/conversations/conversation-sidebar.tsx +66 -7
- package/templates/docker/src/ui/components/conversations/delete-confirmation.tsx +9 -2
- package/templates/docker/src/ui/components/error-boundary.tsx +66 -0
- package/templates/docker/src/ui/components/notebook/delete-cell-dialog.tsx +48 -0
- package/templates/docker/src/ui/components/notebook/fork-branch-selector.tsx +68 -0
- package/templates/docker/src/ui/components/notebook/notebook-cell-input.tsx +76 -0
- package/templates/docker/src/ui/components/notebook/notebook-cell-output.tsx +58 -0
- package/templates/docker/src/ui/components/notebook/notebook-cell-toolbar.tsx +91 -0
- package/templates/docker/src/ui/components/notebook/notebook-cell.tsx +119 -0
- package/templates/docker/src/ui/components/notebook/notebook-empty-state.tsx +19 -0
- package/templates/docker/src/ui/components/notebook/notebook-export.ts +287 -0
- package/templates/docker/src/ui/components/notebook/notebook-input-bar.tsx +49 -0
- package/templates/docker/src/ui/components/notebook/notebook-shell.tsx +266 -0
- package/templates/docker/src/ui/components/notebook/notebook-text-cell.tsx +152 -0
- package/templates/docker/src/ui/components/notebook/types.ts +39 -0
- package/templates/docker/src/ui/components/notebook/use-keyboard-nav.ts +109 -0
- package/templates/docker/src/ui/components/notebook/use-notebook.ts +684 -0
- package/templates/docker/src/ui/components/org-switcher.tsx +111 -0
- package/templates/docker/src/ui/components/region-picker.tsx +103 -0
- package/templates/docker/src/ui/components/schema-explorer/schema-explorer.tsx +522 -0
- package/templates/docker/src/ui/components/social-icons.tsx +26 -0
- package/templates/docker/src/ui/components/tour/guided-tour.tsx +81 -0
- package/templates/docker/src/ui/components/tour/index.ts +5 -0
- package/templates/docker/src/ui/components/tour/nav-bar.tsx +100 -0
- package/templates/docker/src/ui/components/tour/tour-overlay.tsx +298 -0
- package/templates/docker/src/ui/components/tour/tour-steps.ts +43 -0
- package/templates/docker/src/ui/components/tour/types.ts +21 -0
- package/templates/docker/src/ui/components/tour/use-tour.ts +193 -0
- package/templates/docker/src/ui/context-reexport.ts +3 -0
- package/templates/docker/src/ui/hooks/theme-init-script.ts +17 -0
- package/templates/docker/src/ui/hooks/use-admin-fetch.ts +38 -30
- package/templates/docker/src/ui/hooks/use-admin-mutation.ts +188 -0
- package/templates/docker/src/ui/hooks/use-atlas-transport.ts +225 -0
- package/templates/docker/src/ui/hooks/use-branding.ts +68 -0
- package/templates/docker/src/ui/hooks/use-conversations.ts +106 -83
- package/templates/docker/src/ui/hooks/use-dark-mode.ts +134 -10
- package/templates/docker/src/ui/hooks/use-deploy-mode.ts +36 -0
- package/templates/docker/src/ui/hooks/use-platform-admin-guard.ts +49 -0
- package/templates/docker/src/ui/lib/action-types.ts +11 -63
- package/templates/docker/src/ui/lib/admin-schemas.ts +744 -0
- package/templates/docker/src/ui/lib/fetch-client.ts +84 -0
- package/templates/docker/src/ui/lib/fetch-error.ts +54 -0
- package/templates/docker/src/ui/lib/helpers.ts +94 -1
- package/templates/docker/src/ui/lib/types.ts +149 -140
- package/templates/docker/tsconfig.json +4 -2
- package/templates/nextjs-standalone/bin/__tests__/duckdb-ingest.test.ts +17 -14
- package/templates/nextjs-standalone/bin/__tests__/failure-threshold.test.ts +148 -0
- package/templates/nextjs-standalone/bin/__tests__/fatal-error-propagation.test.ts +267 -0
- package/templates/nextjs-standalone/bin/__tests__/profiler-heuristics.test.ts +5 -5
- package/templates/nextjs-standalone/bin/__tests__/schema-drift.test.ts +39 -0
- package/templates/nextjs-standalone/bin/atlas.ts +981 -1819
- package/templates/nextjs-standalone/bin/benchmark.ts +14 -16
- package/templates/nextjs-standalone/bin/enrich.ts +7 -2
- package/templates/nextjs-standalone/brand.css +13 -0
- package/templates/nextjs-standalone/data/cybersec-semantic/catalog.yml +222 -0
- package/templates/nextjs-standalone/data/cybersec-semantic/entities/alerts.yml +195 -0
- package/templates/nextjs-standalone/data/cybersec-semantic/entities/assets.yml +191 -0
- package/templates/nextjs-standalone/data/cybersec-semantic/entities/compliance_assessments.yml +170 -0
- package/templates/nextjs-standalone/data/cybersec-semantic/entities/incidents.yml +219 -0
- package/templates/nextjs-standalone/data/cybersec-semantic/entities/organizations.yml +136 -0
- package/templates/nextjs-standalone/data/cybersec-semantic/entities/plans.yml +114 -0
- package/templates/nextjs-standalone/data/cybersec-semantic/entities/remediation_actions.yml +212 -0
- package/templates/nextjs-standalone/data/cybersec-semantic/entities/scan_results.yml +215 -0
- package/templates/nextjs-standalone/data/cybersec-semantic/entities/scans.yml +180 -0
- package/templates/nextjs-standalone/data/cybersec-semantic/entities/subscriptions.yml +184 -0
- package/templates/nextjs-standalone/data/cybersec-semantic/entities/users.yml +140 -0
- package/templates/nextjs-standalone/data/cybersec-semantic/entities/vulnerabilities.yml +154 -0
- package/templates/nextjs-standalone/data/cybersec-semantic/glossary.yml +207 -0
- package/templates/nextjs-standalone/data/cybersec-semantic/metrics/business.yml +148 -0
- package/templates/nextjs-standalone/data/cybersec-semantic/metrics/compliance.yml +138 -0
- package/templates/nextjs-standalone/data/cybersec-semantic/metrics/security.yml +181 -0
- package/templates/nextjs-standalone/data/cybersec.sql +8 -8
- package/templates/nextjs-standalone/data/demo.sql +3 -0
- package/templates/nextjs-standalone/data/ecommerce-semantic/catalog.yml +221 -0
- package/templates/nextjs-standalone/data/ecommerce-semantic/entities/categories.yml +91 -0
- package/templates/nextjs-standalone/data/ecommerce-semantic/entities/customers.yml +133 -0
- package/templates/nextjs-standalone/data/ecommerce-semantic/entities/email_campaigns.yml +119 -0
- package/templates/nextjs-standalone/data/ecommerce-semantic/entities/inventory_levels.yml +153 -0
- package/templates/nextjs-standalone/data/ecommerce-semantic/entities/order_items.yml +159 -0
- package/templates/nextjs-standalone/data/ecommerce-semantic/entities/orders.yml +199 -0
- package/templates/nextjs-standalone/data/ecommerce-semantic/entities/payments.yml +140 -0
- package/templates/nextjs-standalone/data/ecommerce-semantic/entities/product_reviews.yml +155 -0
- package/templates/nextjs-standalone/data/ecommerce-semantic/entities/products.yml +178 -0
- package/templates/nextjs-standalone/data/ecommerce-semantic/entities/promotions.yml +171 -0
- package/templates/nextjs-standalone/data/ecommerce-semantic/entities/returns.yml +144 -0
- package/templates/nextjs-standalone/data/ecommerce-semantic/entities/sellers.yml +124 -0
- package/templates/nextjs-standalone/data/ecommerce-semantic/entities/shipments.yml +159 -0
- package/templates/nextjs-standalone/data/ecommerce-semantic/glossary.yml +193 -0
- package/templates/nextjs-standalone/data/ecommerce-semantic/metrics/customers.yml +116 -0
- package/templates/nextjs-standalone/data/ecommerce-semantic/metrics/operations.yml +131 -0
- package/templates/nextjs-standalone/data/ecommerce-semantic/metrics/revenue.yml +120 -0
- package/templates/nextjs-standalone/docs/deploy.md +2 -1
- package/templates/nextjs-standalone/ee/src/__mocks__/internal.ts +170 -0
- package/templates/nextjs-standalone/ee/src/audit/purge-scheduler.ts +113 -0
- package/templates/nextjs-standalone/ee/src/audit/retention.ts +467 -0
- package/templates/nextjs-standalone/ee/src/auth/ip-allowlist.ts +367 -0
- package/templates/nextjs-standalone/ee/src/auth/roles.ts +562 -0
- package/templates/nextjs-standalone/ee/src/auth/scim.ts +343 -0
- package/templates/nextjs-standalone/ee/src/auth/sso.ts +538 -0
- package/templates/nextjs-standalone/ee/src/backups/engine.ts +355 -0
- package/templates/nextjs-standalone/ee/src/backups/index.ts +26 -0
- package/templates/nextjs-standalone/ee/src/backups/restore.ts +169 -0
- package/templates/nextjs-standalone/ee/src/backups/scheduler.ts +153 -0
- package/templates/nextjs-standalone/ee/src/backups/verify.ts +124 -0
- package/templates/nextjs-standalone/ee/src/branding/white-label.ts +228 -0
- package/templates/nextjs-standalone/ee/src/compliance/masking.ts +477 -0
- package/templates/nextjs-standalone/ee/src/compliance/patterns.ts +16 -0
- package/templates/nextjs-standalone/ee/src/compliance/pii-detection.ts +217 -0
- package/templates/nextjs-standalone/ee/src/compliance/reports.ts +402 -0
- package/templates/nextjs-standalone/ee/src/deploy-mode.ts +37 -0
- package/templates/nextjs-standalone/ee/src/governance/approval.ts +699 -0
- package/templates/nextjs-standalone/ee/src/index.ts +74 -0
- package/templates/nextjs-standalone/ee/src/platform/domains.ts +562 -0
- package/templates/nextjs-standalone/ee/src/platform/model-routing.ts +382 -0
- package/templates/nextjs-standalone/ee/src/platform/residency.ts +265 -0
- package/templates/nextjs-standalone/ee/src/sla/alerting.ts +382 -0
- package/templates/nextjs-standalone/ee/src/sla/index.ts +12 -0
- package/templates/nextjs-standalone/ee/src/sla/metrics.ts +275 -0
- package/templates/nextjs-standalone/ee/src/test-setup.ts +1 -0
- package/templates/nextjs-standalone/next.config.ts +1 -1
- package/templates/nextjs-standalone/package.json +50 -30
- package/templates/nextjs-standalone/src/api/index.ts +336 -24
- package/templates/nextjs-standalone/src/api/routes/actions.ts +443 -176
- package/templates/nextjs-standalone/src/api/routes/admin-abuse.ts +219 -0
- package/templates/nextjs-standalone/src/api/routes/admin-approval.ts +418 -0
- package/templates/nextjs-standalone/src/api/routes/admin-audit-retention.ts +405 -0
- package/templates/nextjs-standalone/src/api/routes/admin-auth.ts +122 -0
- package/templates/nextjs-standalone/src/api/routes/admin-branding.ts +252 -0
- package/templates/nextjs-standalone/src/api/routes/admin-compliance.ts +352 -0
- package/templates/nextjs-standalone/src/api/routes/admin-domains.ts +334 -0
- package/templates/nextjs-standalone/src/api/routes/admin-integrations.ts +2667 -0
- package/templates/nextjs-standalone/src/api/routes/admin-ip-allowlist.ts +261 -0
- package/templates/nextjs-standalone/src/api/routes/admin-learned-patterns.ts +525 -0
- package/templates/nextjs-standalone/src/api/routes/admin-model-config.ts +252 -0
- package/templates/nextjs-standalone/src/api/routes/admin-onboarding-emails.ts +145 -0
- package/templates/nextjs-standalone/src/api/routes/admin-orgs.ts +710 -0
- package/templates/nextjs-standalone/src/api/routes/admin-prompts.ts +694 -0
- package/templates/nextjs-standalone/src/api/routes/admin-residency.ts +570 -0
- package/templates/nextjs-standalone/src/api/routes/admin-roles.ts +296 -0
- package/templates/nextjs-standalone/src/api/routes/admin-router.ts +120 -0
- package/templates/nextjs-standalone/src/api/routes/admin-sandbox.ts +417 -0
- package/templates/nextjs-standalone/src/api/routes/admin-scim.ts +262 -0
- package/templates/nextjs-standalone/src/api/routes/admin-sso.ts +545 -0
- package/templates/nextjs-standalone/src/api/routes/admin-suggestions.ts +176 -0
- package/templates/nextjs-standalone/src/api/routes/admin-usage.ts +310 -0
- package/templates/nextjs-standalone/src/api/routes/admin.ts +4156 -898
- package/templates/nextjs-standalone/src/api/routes/auth-preamble.ts +105 -0
- package/templates/nextjs-standalone/src/api/routes/billing.ts +397 -0
- package/templates/nextjs-standalone/src/api/routes/chat.ts +597 -334
- package/templates/nextjs-standalone/src/api/routes/conversations.ts +987 -132
- package/templates/nextjs-standalone/src/api/routes/demo.ts +673 -0
- package/templates/nextjs-standalone/src/api/routes/discord.ts +274 -0
- package/templates/nextjs-standalone/src/api/routes/ee-error-handler.ts +32 -0
- package/templates/nextjs-standalone/src/api/routes/health.ts +129 -14
- package/templates/nextjs-standalone/src/api/routes/middleware.ts +244 -0
- package/templates/nextjs-standalone/src/api/routes/onboarding-emails.ts +134 -0
- package/templates/nextjs-standalone/src/api/routes/onboarding.ts +1109 -0
- package/templates/nextjs-standalone/src/api/routes/openapi.ts +184 -1597
- package/templates/nextjs-standalone/src/api/routes/platform-admin.ts +760 -0
- package/templates/nextjs-standalone/src/api/routes/platform-backups.ts +436 -0
- package/templates/nextjs-standalone/src/api/routes/platform-domains.ts +235 -0
- package/templates/nextjs-standalone/src/api/routes/platform-residency.ts +257 -0
- package/templates/nextjs-standalone/src/api/routes/platform-sla.ts +379 -0
- package/templates/nextjs-standalone/src/api/routes/prompts.ts +221 -0
- package/templates/nextjs-standalone/src/api/routes/public-branding.ts +106 -0
- package/templates/nextjs-standalone/src/api/routes/query.ts +330 -219
- package/templates/nextjs-standalone/src/api/routes/scheduled-tasks.ts +393 -297
- package/templates/nextjs-standalone/src/api/routes/semantic.ts +179 -0
- package/templates/nextjs-standalone/src/api/routes/sessions.ts +210 -0
- package/templates/nextjs-standalone/src/api/routes/shared-domains.ts +98 -0
- package/templates/nextjs-standalone/src/api/routes/shared-schemas.ts +139 -0
- package/templates/nextjs-standalone/src/api/routes/slack.ts +209 -52
- package/templates/nextjs-standalone/src/api/routes/suggestions.ts +233 -0
- package/templates/nextjs-standalone/src/api/routes/tables.ts +67 -0
- package/templates/nextjs-standalone/src/api/routes/teams.ts +222 -0
- package/templates/nextjs-standalone/src/api/routes/validate-sql.ts +188 -0
- package/templates/nextjs-standalone/src/api/routes/validation-hook.ts +62 -0
- package/templates/nextjs-standalone/src/api/routes/widget-loader.ts +356 -0
- package/templates/nextjs-standalone/src/api/routes/widget.ts +428 -0
- package/templates/nextjs-standalone/src/api/routes/wizard.ts +852 -0
- package/templates/nextjs-standalone/src/api/server.ts +187 -69
- package/templates/nextjs-standalone/src/app/error.tsx +5 -2
- package/templates/nextjs-standalone/src/app/globals.css +1 -1
- package/templates/nextjs-standalone/src/app/layout.tsx +7 -2
- package/templates/nextjs-standalone/src/app/page.tsx +39 -5
- package/templates/nextjs-standalone/src/components/data-table/data-table-column-header.tsx +99 -0
- package/templates/nextjs-standalone/src/components/data-table/data-table-date-filter.tsx +225 -0
- package/templates/nextjs-standalone/src/components/data-table/data-table-expandable.tsx +125 -0
- package/templates/nextjs-standalone/src/components/data-table/data-table-faceted-filter.tsx +189 -0
- package/templates/nextjs-standalone/src/components/data-table/data-table-pagination.tsx +112 -0
- package/templates/nextjs-standalone/src/components/data-table/data-table-range-filter.tsx +122 -0
- package/templates/nextjs-standalone/src/components/data-table/data-table-slider-filter.tsx +256 -0
- package/templates/nextjs-standalone/src/components/data-table/data-table-sort-list.tsx +407 -0
- package/templates/nextjs-standalone/src/components/data-table/data-table-toolbar.tsx +149 -0
- package/templates/nextjs-standalone/src/components/data-table/data-table-view-options.tsx +89 -0
- package/templates/nextjs-standalone/src/components/data-table/data-table.tsx +105 -0
- package/templates/nextjs-standalone/src/components/form-dialog.tsx +135 -0
- package/templates/nextjs-standalone/src/components/ui/accordion.tsx +66 -0
- package/templates/nextjs-standalone/src/components/ui/calendar.tsx +220 -0
- package/templates/nextjs-standalone/src/components/ui/checkbox.tsx +32 -0
- package/templates/nextjs-standalone/src/components/ui/faceted.tsx +283 -0
- package/templates/nextjs-standalone/src/components/ui/form.tsx +167 -0
- package/templates/nextjs-standalone/src/components/ui/label.tsx +24 -0
- package/templates/nextjs-standalone/src/components/ui/popover.tsx +89 -0
- package/templates/nextjs-standalone/src/components/ui/progress.tsx +31 -0
- package/templates/nextjs-standalone/src/components/ui/scroll-area.tsx +6 -2
- package/templates/nextjs-standalone/src/components/ui/slider.tsx +63 -0
- package/templates/nextjs-standalone/src/components/ui/sortable.tsx +581 -0
- package/templates/nextjs-standalone/src/components/ui/switch.tsx +35 -0
- package/templates/nextjs-standalone/src/components/ui/textarea.tsx +18 -0
- package/templates/nextjs-standalone/src/config/data-table.ts +82 -0
- package/templates/nextjs-standalone/src/env-check.ts +74 -0
- package/templates/nextjs-standalone/src/hooks/use-callback-ref.ts +27 -0
- package/templates/nextjs-standalone/src/hooks/use-data-table.ts +316 -0
- package/templates/nextjs-standalone/src/hooks/use-debounced-callback.ts +28 -0
- package/templates/nextjs-standalone/src/lib/action-types.ts +7 -41
- package/templates/nextjs-standalone/src/lib/agent-query.ts +4 -2
- package/templates/nextjs-standalone/src/lib/agent.ts +363 -31
- package/templates/nextjs-standalone/src/lib/api-url.ts +2 -3
- package/templates/nextjs-standalone/src/lib/auth/admin-permissions.ts +38 -0
- package/templates/nextjs-standalone/src/lib/auth/audit.ts +19 -4
- package/templates/nextjs-standalone/src/lib/auth/byot.ts +3 -3
- package/templates/nextjs-standalone/src/lib/auth/detect.ts +29 -8
- package/templates/nextjs-standalone/src/lib/auth/managed.ts +104 -14
- package/templates/nextjs-standalone/src/lib/auth/middleware.ts +53 -6
- package/templates/nextjs-standalone/src/lib/auth/migrate.ts +140 -15
- package/templates/nextjs-standalone/src/lib/auth/oauth-state.ts +123 -0
- package/templates/nextjs-standalone/src/lib/auth/org-permissions.ts +55 -0
- package/templates/nextjs-standalone/src/lib/auth/permissions.ts +26 -19
- package/templates/nextjs-standalone/src/lib/auth/server.ts +355 -9
- package/templates/nextjs-standalone/src/lib/auth/simple-key.ts +3 -3
- package/templates/nextjs-standalone/src/lib/auth/types.ts +15 -21
- package/templates/nextjs-standalone/src/lib/billing/enforcement.ts +368 -0
- package/templates/nextjs-standalone/src/lib/billing/plans.ts +155 -0
- package/templates/nextjs-standalone/src/lib/cache/index.ts +92 -0
- package/templates/nextjs-standalone/src/lib/cache/keys.ts +30 -0
- package/templates/nextjs-standalone/src/lib/cache/lru.ts +79 -0
- package/templates/nextjs-standalone/src/lib/cache/types.ts +31 -0
- package/templates/nextjs-standalone/src/lib/compose-refs.ts +62 -0
- package/templates/nextjs-standalone/src/lib/config.ts +563 -11
- package/templates/nextjs-standalone/src/lib/connection-types.ts +9 -0
- package/templates/nextjs-standalone/src/lib/conversation-types.ts +1 -25
- package/templates/nextjs-standalone/src/lib/conversations.ts +345 -14
- package/templates/nextjs-standalone/src/lib/data-table.ts +61 -0
- package/templates/nextjs-standalone/src/lib/db/connection.ts +793 -39
- package/templates/nextjs-standalone/src/lib/db/internal.ts +985 -139
- package/templates/nextjs-standalone/src/lib/db/migrate.ts +295 -0
- package/templates/nextjs-standalone/src/lib/db/migrations/0000_baseline.sql +703 -0
- package/templates/nextjs-standalone/src/lib/db/migrations/0001_teams_installations.sql +14 -0
- package/templates/nextjs-standalone/src/lib/db/migrations/0002_discord_installations.sql +14 -0
- package/templates/nextjs-standalone/src/lib/db/migrations/0003_telegram_installations.sql +15 -0
- package/templates/nextjs-standalone/src/lib/db/migrations/0004_sandbox_credentials.sql +18 -0
- package/templates/nextjs-standalone/src/lib/db/migrations/0005_oauth_state.sql +16 -0
- package/templates/nextjs-standalone/src/lib/db/migrations/0006_byot_credentials.sql +14 -0
- package/templates/nextjs-standalone/src/lib/db/migrations/0007_gchat_installations.sql +15 -0
- package/templates/nextjs-standalone/src/lib/db/migrations/0008_github_installations.sql +14 -0
- package/templates/nextjs-standalone/src/lib/db/migrations/0009_linear_installations.sql +15 -0
- package/templates/nextjs-standalone/src/lib/db/migrations/0010_whatsapp_installations.sql +14 -0
- package/templates/nextjs-standalone/src/lib/db/migrations/0011_email_installations.sql +16 -0
- package/templates/nextjs-standalone/src/lib/db/migrations/0012_region_migrations.sql +25 -0
- package/templates/nextjs-standalone/src/lib/db/schema.ts +1120 -0
- package/templates/nextjs-standalone/src/lib/db/source-rate-limit.ts +89 -139
- package/templates/nextjs-standalone/src/lib/demo.ts +308 -0
- package/templates/nextjs-standalone/src/lib/discord/store.ts +225 -0
- package/templates/nextjs-standalone/src/lib/effect/ai.ts +243 -0
- package/templates/nextjs-standalone/src/lib/effect/errors.ts +234 -0
- package/templates/nextjs-standalone/src/lib/effect/hono.ts +454 -0
- package/templates/nextjs-standalone/src/lib/effect/index.ts +137 -0
- package/templates/nextjs-standalone/src/lib/effect/layers.ts +496 -0
- package/templates/nextjs-standalone/src/lib/effect/services.ts +776 -0
- package/templates/nextjs-standalone/src/lib/effect/sql.ts +178 -0
- package/templates/nextjs-standalone/src/lib/effect/toolkit.ts +123 -0
- package/templates/nextjs-standalone/src/lib/email/delivery.ts +232 -0
- package/templates/nextjs-standalone/src/lib/email/engine.ts +349 -0
- package/templates/nextjs-standalone/src/lib/email/hooks.ts +107 -0
- package/templates/nextjs-standalone/src/lib/email/index.ts +16 -0
- package/templates/nextjs-standalone/src/lib/email/scheduler.ts +72 -0
- package/templates/nextjs-standalone/src/lib/email/sequence.ts +73 -0
- package/templates/nextjs-standalone/src/lib/email/store.ts +163 -0
- package/templates/nextjs-standalone/src/lib/email/templates.ts +215 -0
- package/templates/nextjs-standalone/src/lib/format.test.ts +117 -0
- package/templates/nextjs-standalone/src/lib/format.ts +67 -0
- package/templates/nextjs-standalone/src/lib/gchat/store.ts +202 -0
- package/templates/nextjs-standalone/src/lib/github/store.ts +197 -0
- package/templates/nextjs-standalone/src/lib/id.ts +29 -0
- package/templates/nextjs-standalone/src/lib/integrations/types.ts +166 -0
- package/templates/nextjs-standalone/src/lib/learn/pattern-analyzer.ts +224 -0
- package/templates/nextjs-standalone/src/lib/learn/pattern-cache.ts +229 -0
- package/templates/nextjs-standalone/src/lib/learn/pattern-proposer.ts +87 -0
- package/templates/nextjs-standalone/src/lib/learn/suggestion-helpers.ts +34 -0
- package/templates/nextjs-standalone/src/lib/learn/suggestions.ts +139 -0
- package/templates/nextjs-standalone/src/lib/linear/store.ts +200 -0
- package/templates/nextjs-standalone/src/lib/logger.ts +35 -3
- package/templates/nextjs-standalone/src/lib/metering.ts +272 -0
- package/templates/nextjs-standalone/src/lib/parsers.ts +99 -0
- package/templates/nextjs-standalone/src/lib/plugins/hooks.ts +13 -11
- package/templates/nextjs-standalone/src/lib/plugins/index.ts +3 -1
- package/templates/nextjs-standalone/src/lib/plugins/registry.ts +58 -6
- package/templates/nextjs-standalone/src/lib/plugins/settings.ts +147 -0
- package/templates/nextjs-standalone/src/lib/plugins/wiring.ts +6 -9
- package/templates/nextjs-standalone/src/lib/profiler.ts +1665 -0
- package/templates/nextjs-standalone/src/lib/providers.ts +188 -13
- package/templates/nextjs-standalone/src/lib/rls.ts +172 -60
- package/templates/nextjs-standalone/src/lib/sandbox/credentials.ts +206 -0
- package/templates/nextjs-standalone/src/lib/sandbox/validate.ts +179 -0
- package/templates/nextjs-standalone/src/lib/scheduled-task-types.ts +26 -94
- package/templates/nextjs-standalone/src/lib/scheduled-tasks.ts +174 -34
- package/templates/nextjs-standalone/src/lib/scheduler/delivery.ts +248 -150
- package/templates/nextjs-standalone/src/lib/scheduler/engine.ts +190 -154
- package/templates/nextjs-standalone/src/lib/scheduler/executor.ts +74 -23
- package/templates/nextjs-standalone/src/lib/scheduler/preview.ts +72 -0
- package/templates/nextjs-standalone/src/lib/security/abuse.ts +463 -0
- package/templates/nextjs-standalone/src/lib/semantic/diff.ts +267 -0
- package/templates/nextjs-standalone/src/lib/semantic/entities.ts +167 -0
- package/templates/nextjs-standalone/src/lib/semantic/files.ts +283 -0
- package/templates/nextjs-standalone/src/lib/semantic/index.ts +27 -0
- package/templates/nextjs-standalone/src/lib/{semantic-index.ts → semantic/search.ts} +80 -9
- package/templates/nextjs-standalone/src/lib/semantic/sync.ts +581 -0
- package/templates/nextjs-standalone/src/lib/{semantic.ts → semantic/whitelist.ts} +189 -3
- package/templates/nextjs-standalone/src/lib/settings.ts +817 -0
- package/templates/nextjs-standalone/src/lib/sidecar-types.ts +13 -0
- package/templates/nextjs-standalone/src/lib/slack/store.ts +134 -25
- package/templates/nextjs-standalone/src/lib/startup.ts +528 -362
- package/templates/nextjs-standalone/src/lib/teams/store.ts +216 -0
- package/templates/nextjs-standalone/src/lib/telegram/store.ts +202 -0
- package/templates/nextjs-standalone/src/lib/telemetry.ts +40 -0
- package/templates/nextjs-standalone/src/lib/tools/actions/audit.ts +8 -5
- package/templates/nextjs-standalone/src/lib/tools/actions/email.ts +3 -1
- package/templates/nextjs-standalone/src/lib/tools/actions/handler.ts +276 -93
- package/templates/nextjs-standalone/src/lib/tools/actions/jira.ts +2 -2
- package/templates/nextjs-standalone/src/lib/tools/backends/detect.ts +16 -0
- package/templates/nextjs-standalone/src/lib/tools/backends/index.ts +11 -0
- package/templates/nextjs-standalone/src/lib/tools/backends/nsjail.ts +213 -0
- package/templates/nextjs-standalone/src/lib/tools/backends/shared.ts +103 -0
- package/templates/nextjs-standalone/src/lib/tools/backends/types.ts +26 -0
- package/templates/nextjs-standalone/src/lib/tools/explore-nsjail.ts +7 -228
- package/templates/nextjs-standalone/src/lib/tools/explore-sandbox.ts +4 -29
- package/templates/nextjs-standalone/src/lib/tools/explore-sidecar.ts +18 -2
- package/templates/nextjs-standalone/src/lib/tools/explore.ts +246 -54
- package/templates/nextjs-standalone/src/lib/tools/index.ts +17 -0
- package/templates/nextjs-standalone/src/lib/tools/python-nsjail.ts +11 -139
- package/templates/nextjs-standalone/src/lib/tools/python-sandbox.ts +9 -132
- package/templates/nextjs-standalone/src/lib/tools/python-sidecar.ts +184 -3
- package/templates/nextjs-standalone/src/lib/tools/python-stream.ts +33 -0
- package/templates/nextjs-standalone/src/lib/tools/python-wrapper.ts +129 -0
- package/templates/nextjs-standalone/src/lib/tools/python.ts +115 -15
- package/templates/nextjs-standalone/src/lib/tools/registry.ts +14 -2
- package/templates/nextjs-standalone/src/lib/tools/sql.ts +778 -362
- package/templates/nextjs-standalone/src/lib/tracing.ts +16 -0
- package/templates/nextjs-standalone/src/lib/whatsapp/store.ts +198 -0
- package/templates/nextjs-standalone/src/lib/workspace.ts +89 -0
- package/templates/nextjs-standalone/src/progress.ts +121 -0
- package/templates/nextjs-standalone/src/types/data-table.ts +48 -0
- package/templates/nextjs-standalone/src/ui/atlas-chat-reexport.ts +3 -0
- package/templates/nextjs-standalone/src/ui/components/actions/action-approval-card.tsx +26 -19
- package/templates/nextjs-standalone/src/ui/components/actions/action-status-badge.tsx +3 -3
- package/templates/nextjs-standalone/src/ui/components/admin/admin-layout.tsx +57 -39
- package/templates/nextjs-standalone/src/ui/components/admin/admin-sidebar.tsx +213 -35
- package/templates/nextjs-standalone/src/ui/components/admin/delivery-status-badge.tsx +53 -0
- package/templates/nextjs-standalone/src/ui/components/admin/empty-state.tsx +27 -6
- package/templates/nextjs-standalone/src/ui/components/admin/entity-detail.tsx +3 -52
- package/templates/nextjs-standalone/src/ui/components/admin/error-banner.tsx +2 -2
- package/templates/nextjs-standalone/src/ui/components/admin/feature-disabled.tsx +28 -5
- package/templates/nextjs-standalone/src/ui/components/admin-content-wrapper.tsx +87 -0
- package/templates/nextjs-standalone/src/ui/components/atlas-chat.tsx +449 -166
- package/templates/nextjs-standalone/src/ui/components/branding-head.tsx +41 -0
- package/templates/nextjs-standalone/src/ui/components/chart/chart-detection.ts +62 -5
- package/templates/nextjs-standalone/src/ui/components/chart/result-chart.tsx +316 -125
- package/templates/nextjs-standalone/src/ui/components/chat/api-key-bar.tsx +4 -4
- package/templates/nextjs-standalone/src/ui/components/chat/data-table.tsx +45 -4
- package/templates/nextjs-standalone/src/ui/components/chat/error-banner.tsx +86 -5
- package/templates/nextjs-standalone/src/ui/components/chat/follow-up-chips.tsx +29 -0
- package/templates/nextjs-standalone/src/ui/components/chat/markdown.tsx +24 -0
- package/templates/nextjs-standalone/src/ui/components/chat/prompt-library.tsx +206 -0
- package/templates/nextjs-standalone/src/ui/components/chat/python-result-card.tsx +106 -78
- package/templates/nextjs-standalone/src/ui/components/chat/result-card-base.tsx +101 -0
- package/templates/nextjs-standalone/src/ui/components/chat/share-dialog.tsx +377 -0
- package/templates/nextjs-standalone/src/ui/components/chat/sql-result-card.tsx +94 -73
- package/templates/nextjs-standalone/src/ui/components/chat/suggestion-chips.tsx +46 -0
- package/templates/nextjs-standalone/src/ui/components/chat/tool-part.tsx +16 -4
- package/templates/nextjs-standalone/src/ui/components/conversations/conversation-item.tsx +48 -17
- package/templates/nextjs-standalone/src/ui/components/conversations/conversation-list.tsx +38 -24
- package/templates/nextjs-standalone/src/ui/components/conversations/conversation-sidebar.tsx +66 -7
- package/templates/nextjs-standalone/src/ui/components/conversations/delete-confirmation.tsx +9 -2
- package/templates/nextjs-standalone/src/ui/components/error-boundary.tsx +66 -0
- package/templates/nextjs-standalone/src/ui/components/notebook/delete-cell-dialog.tsx +48 -0
- package/templates/nextjs-standalone/src/ui/components/notebook/fork-branch-selector.tsx +68 -0
- package/templates/nextjs-standalone/src/ui/components/notebook/notebook-cell-input.tsx +76 -0
- package/templates/nextjs-standalone/src/ui/components/notebook/notebook-cell-output.tsx +58 -0
- package/templates/nextjs-standalone/src/ui/components/notebook/notebook-cell-toolbar.tsx +91 -0
- package/templates/nextjs-standalone/src/ui/components/notebook/notebook-cell.tsx +119 -0
- package/templates/nextjs-standalone/src/ui/components/notebook/notebook-empty-state.tsx +19 -0
- package/templates/nextjs-standalone/src/ui/components/notebook/notebook-export.ts +287 -0
- package/templates/nextjs-standalone/src/ui/components/notebook/notebook-input-bar.tsx +49 -0
- package/templates/nextjs-standalone/src/ui/components/notebook/notebook-shell.tsx +266 -0
- package/templates/nextjs-standalone/src/ui/components/notebook/notebook-text-cell.tsx +152 -0
- package/templates/nextjs-standalone/src/ui/components/notebook/types.ts +39 -0
- package/templates/nextjs-standalone/src/ui/components/notebook/use-keyboard-nav.ts +109 -0
- package/templates/nextjs-standalone/src/ui/components/notebook/use-notebook.ts +684 -0
- package/templates/nextjs-standalone/src/ui/components/org-switcher.tsx +111 -0
- package/templates/nextjs-standalone/src/ui/components/region-picker.tsx +103 -0
- package/templates/nextjs-standalone/src/ui/components/schema-explorer/schema-explorer.tsx +522 -0
- package/templates/nextjs-standalone/src/ui/components/social-icons.tsx +26 -0
- package/templates/nextjs-standalone/src/ui/components/tour/guided-tour.tsx +81 -0
- package/templates/nextjs-standalone/src/ui/components/tour/index.ts +5 -0
- package/templates/nextjs-standalone/src/ui/components/tour/nav-bar.tsx +100 -0
- package/templates/nextjs-standalone/src/ui/components/tour/tour-overlay.tsx +298 -0
- package/templates/nextjs-standalone/src/ui/components/tour/tour-steps.ts +43 -0
- package/templates/nextjs-standalone/src/ui/components/tour/types.ts +21 -0
- package/templates/nextjs-standalone/src/ui/components/tour/use-tour.ts +193 -0
- package/templates/nextjs-standalone/src/ui/context-reexport.ts +3 -0
- package/templates/nextjs-standalone/src/ui/hooks/theme-init-script.ts +17 -0
- package/templates/nextjs-standalone/src/ui/hooks/use-admin-fetch.ts +38 -30
- package/templates/nextjs-standalone/src/ui/hooks/use-admin-mutation.ts +188 -0
- package/templates/nextjs-standalone/src/ui/hooks/use-atlas-transport.ts +225 -0
- package/templates/nextjs-standalone/src/ui/hooks/use-branding.ts +68 -0
- package/templates/nextjs-standalone/src/ui/hooks/use-conversations.ts +106 -83
- package/templates/nextjs-standalone/src/ui/hooks/use-dark-mode.ts +134 -10
- package/templates/nextjs-standalone/src/ui/hooks/use-deploy-mode.ts +36 -0
- package/templates/nextjs-standalone/src/ui/hooks/use-platform-admin-guard.ts +49 -0
- package/templates/nextjs-standalone/src/ui/lib/action-types.ts +11 -63
- package/templates/nextjs-standalone/src/ui/lib/admin-schemas.ts +744 -0
- package/templates/nextjs-standalone/src/ui/lib/fetch-client.ts +84 -0
- package/templates/nextjs-standalone/src/ui/lib/fetch-error.ts +54 -0
- package/templates/nextjs-standalone/src/ui/lib/helpers.ts +94 -1
- package/templates/nextjs-standalone/src/ui/lib/types.ts +149 -140
- package/templates/nextjs-standalone/tsconfig.json +3 -2
- package/templates/docker/src/api/__tests__/actions.test.ts +0 -683
- package/templates/docker/src/api/__tests__/admin.test.ts +0 -820
- package/templates/docker/src/api/__tests__/auth.test.ts +0 -165
- package/templates/docker/src/api/__tests__/chat.test.ts +0 -376
- package/templates/docker/src/api/__tests__/conversations.test.ts +0 -555
- package/templates/docker/src/api/__tests__/cors.test.ts +0 -135
- package/templates/docker/src/api/__tests__/health-plugin.test.ts +0 -176
- package/templates/docker/src/api/__tests__/health.test.ts +0 -283
- package/templates/docker/src/api/__tests__/query.test.ts +0 -891
- package/templates/docker/src/api/__tests__/scheduled-tasks.test.ts +0 -601
- package/templates/docker/src/api/__tests__/slack.test.ts +0 -847
- package/templates/docker/src/lib/__tests__/agent-cache.test.ts +0 -439
- package/templates/docker/src/lib/__tests__/agent-dialect.test.ts +0 -131
- package/templates/docker/src/lib/__tests__/agent-health-annotations.test.ts +0 -166
- package/templates/docker/src/lib/__tests__/agent-integration.test.ts +0 -516
- package/templates/docker/src/lib/__tests__/config-actions.test.ts +0 -166
- package/templates/docker/src/lib/__tests__/config.test.ts +0 -1113
- package/templates/docker/src/lib/__tests__/conversations.test.ts +0 -589
- package/templates/docker/src/lib/__tests__/errors.test.ts +0 -256
- package/templates/docker/src/lib/__tests__/logger.test.ts +0 -200
- package/templates/docker/src/lib/__tests__/plugin-aware-validation.test.ts +0 -321
- package/templates/docker/src/lib/__tests__/providers.test.ts +0 -130
- package/templates/docker/src/lib/__tests__/rls.test.ts +0 -435
- package/templates/docker/src/lib/__tests__/scheduled-task-types.test.ts +0 -124
- package/templates/docker/src/lib/__tests__/scheduled-tasks.test.ts +0 -550
- package/templates/docker/src/lib/__tests__/semantic-index.test.ts +0 -547
- package/templates/docker/src/lib/__tests__/semantic-multisource.test.ts +0 -544
- package/templates/docker/src/lib/__tests__/semantic.test.ts +0 -363
- package/templates/docker/src/lib/__tests__/startup-actions.test.ts +0 -461
- package/templates/docker/src/lib/__tests__/startup-first-run.test.ts +0 -429
- package/templates/docker/src/lib/__tests__/startup.test.ts +0 -470
- package/templates/docker/src/lib/__tests__/tracing.test.ts +0 -28
- package/templates/docker/src/lib/auth/__tests__/audit.test.ts +0 -418
- package/templates/docker/src/lib/auth/__tests__/byot-integration.test.ts +0 -222
- package/templates/docker/src/lib/auth/__tests__/byot.test.ts +0 -366
- package/templates/docker/src/lib/auth/__tests__/detect.test.ts +0 -190
- package/templates/docker/src/lib/auth/__tests__/managed.test.ts +0 -173
- package/templates/docker/src/lib/auth/__tests__/middleware.test.ts +0 -456
- package/templates/docker/src/lib/auth/__tests__/migrate.test.ts +0 -203
- package/templates/docker/src/lib/auth/__tests__/permissions.test.ts +0 -225
- package/templates/docker/src/lib/auth/__tests__/server.test.ts +0 -34
- package/templates/docker/src/lib/auth/__tests__/simple-key.test.ts +0 -176
- package/templates/docker/src/lib/auth/__tests__/types.test.ts +0 -44
- package/templates/docker/src/lib/db/__tests__/connection.test.ts +0 -144
- package/templates/docker/src/lib/db/__tests__/internal.test.ts +0 -387
- package/templates/docker/src/lib/db/__tests__/registry-health.test.ts +0 -190
- package/templates/docker/src/lib/db/__tests__/registry-pool-limits.test.ts +0 -137
- package/templates/docker/src/lib/db/__tests__/registry.test.ts +0 -398
- package/templates/docker/src/lib/db/__tests__/source-rate-limit.test.ts +0 -130
- package/templates/docker/src/lib/errors.ts +0 -154
- package/templates/docker/src/lib/plugins/__tests__/hooks-integration.test.ts +0 -204
- package/templates/docker/src/lib/plugins/__tests__/hooks.test.ts +0 -529
- package/templates/docker/src/lib/plugins/__tests__/migrate.test.ts +0 -875
- package/templates/docker/src/lib/plugins/__tests__/registry.test.ts +0 -373
- package/templates/docker/src/lib/plugins/__tests__/tools.test.ts +0 -49
- package/templates/docker/src/lib/plugins/__tests__/wiring.test.ts +0 -799
- package/templates/docker/src/lib/scheduler/__tests__/delivery.test.ts +0 -192
- package/templates/docker/src/lib/scheduler/__tests__/engine.test.ts +0 -248
- package/templates/docker/src/lib/scheduler/__tests__/format-email.test.ts +0 -96
- package/templates/docker/src/lib/scheduler/__tests__/format-slack.test.ts +0 -78
- package/templates/docker/src/lib/scheduler/__tests__/format-webhook.test.ts +0 -78
- package/templates/docker/src/lib/scheduler/index.ts +0 -7
- package/templates/docker/src/lib/slack/__tests__/api.test.ts +0 -160
- package/templates/docker/src/lib/slack/__tests__/format.test.ts +0 -237
- package/templates/docker/src/lib/slack/__tests__/store.test.ts +0 -188
- package/templates/docker/src/lib/slack/__tests__/threads.test.ts +0 -112
- package/templates/docker/src/lib/slack/__tests__/verify.test.ts +0 -111
- package/templates/docker/src/lib/tools/__tests__/action-permissions.test.ts +0 -594
- package/templates/docker/src/lib/tools/__tests__/custom-validation.test.ts +0 -240
- package/templates/docker/src/lib/tools/__tests__/explore-backend.test.ts +0 -267
- package/templates/docker/src/lib/tools/__tests__/explore-nsjail.test.ts +0 -506
- package/templates/docker/src/lib/tools/__tests__/explore-plugin.test.ts +0 -374
- package/templates/docker/src/lib/tools/__tests__/explore-sdk-compat.test.ts +0 -82
- package/templates/docker/src/lib/tools/__tests__/explore-sidecar.test.ts +0 -210
- package/templates/docker/src/lib/tools/__tests__/python-nsjail.test.ts +0 -515
- package/templates/docker/src/lib/tools/__tests__/python-sandbox.test.ts +0 -397
- package/templates/docker/src/lib/tools/__tests__/python-sidecar.test.ts +0 -365
- package/templates/docker/src/lib/tools/__tests__/python.test.ts +0 -331
- package/templates/docker/src/lib/tools/__tests__/registry-actions.test.ts +0 -132
- package/templates/docker/src/lib/tools/__tests__/registry.test.ts +0 -242
- package/templates/docker/src/lib/tools/__tests__/sql-audit.test.ts +0 -227
- package/templates/docker/src/lib/tools/__tests__/sql-connection-whitelist.test.ts +0 -100
- package/templates/docker/src/lib/tools/__tests__/sql-ratelimit.test.ts +0 -227
- package/templates/docker/src/lib/tools/__tests__/sql.test.ts +0 -709
- package/templates/docker/src/lib/tools/actions/__tests__/audit.test.ts +0 -211
- package/templates/docker/src/lib/tools/actions/__tests__/email.test.ts +0 -378
- package/templates/docker/src/lib/tools/actions/__tests__/handler.test.ts +0 -681
- package/templates/docker/src/lib/tools/actions/__tests__/jira.test.ts +0 -427
- package/templates/docker/src/test-setup.ts +0 -38
- package/templates/docker/src/types/vercel-sandbox.d.ts +0 -61
- package/templates/docker/src/ui/components/chat/managed-auth-card.tsx +0 -116
- package/templates/nextjs-standalone/src/api/__tests__/actions.test.ts +0 -683
- package/templates/nextjs-standalone/src/api/__tests__/admin.test.ts +0 -820
- package/templates/nextjs-standalone/src/api/__tests__/auth.test.ts +0 -165
- package/templates/nextjs-standalone/src/api/__tests__/chat.test.ts +0 -376
- package/templates/nextjs-standalone/src/api/__tests__/conversations.test.ts +0 -555
- package/templates/nextjs-standalone/src/api/__tests__/cors.test.ts +0 -135
- package/templates/nextjs-standalone/src/api/__tests__/health-plugin.test.ts +0 -176
- package/templates/nextjs-standalone/src/api/__tests__/health.test.ts +0 -283
- package/templates/nextjs-standalone/src/api/__tests__/query.test.ts +0 -891
- package/templates/nextjs-standalone/src/api/__tests__/scheduled-tasks.test.ts +0 -601
- package/templates/nextjs-standalone/src/api/__tests__/slack.test.ts +0 -847
- package/templates/nextjs-standalone/src/app/global-error.tsx +0 -68
- package/templates/nextjs-standalone/src/lib/__tests__/agent-cache.test.ts +0 -439
- package/templates/nextjs-standalone/src/lib/__tests__/agent-dialect.test.ts +0 -131
- package/templates/nextjs-standalone/src/lib/__tests__/agent-health-annotations.test.ts +0 -166
- package/templates/nextjs-standalone/src/lib/__tests__/agent-integration.test.ts +0 -516
- package/templates/nextjs-standalone/src/lib/__tests__/config-actions.test.ts +0 -166
- package/templates/nextjs-standalone/src/lib/__tests__/config.test.ts +0 -1113
- package/templates/nextjs-standalone/src/lib/__tests__/conversations.test.ts +0 -589
- package/templates/nextjs-standalone/src/lib/__tests__/errors.test.ts +0 -256
- package/templates/nextjs-standalone/src/lib/__tests__/logger.test.ts +0 -200
- package/templates/nextjs-standalone/src/lib/__tests__/plugin-aware-validation.test.ts +0 -321
- package/templates/nextjs-standalone/src/lib/__tests__/providers.test.ts +0 -130
- package/templates/nextjs-standalone/src/lib/__tests__/rls.test.ts +0 -435
- package/templates/nextjs-standalone/src/lib/__tests__/scheduled-task-types.test.ts +0 -124
- package/templates/nextjs-standalone/src/lib/__tests__/scheduled-tasks.test.ts +0 -550
- package/templates/nextjs-standalone/src/lib/__tests__/semantic-index.test.ts +0 -547
- package/templates/nextjs-standalone/src/lib/__tests__/semantic-multisource.test.ts +0 -544
- package/templates/nextjs-standalone/src/lib/__tests__/semantic.test.ts +0 -363
- package/templates/nextjs-standalone/src/lib/__tests__/startup-actions.test.ts +0 -461
- package/templates/nextjs-standalone/src/lib/__tests__/startup-first-run.test.ts +0 -429
- package/templates/nextjs-standalone/src/lib/__tests__/startup.test.ts +0 -470
- package/templates/nextjs-standalone/src/lib/__tests__/tracing.test.ts +0 -28
- package/templates/nextjs-standalone/src/lib/auth/__tests__/audit.test.ts +0 -418
- package/templates/nextjs-standalone/src/lib/auth/__tests__/byot-integration.test.ts +0 -222
- package/templates/nextjs-standalone/src/lib/auth/__tests__/byot.test.ts +0 -366
- package/templates/nextjs-standalone/src/lib/auth/__tests__/detect.test.ts +0 -190
- package/templates/nextjs-standalone/src/lib/auth/__tests__/managed.test.ts +0 -173
- package/templates/nextjs-standalone/src/lib/auth/__tests__/middleware.test.ts +0 -456
- package/templates/nextjs-standalone/src/lib/auth/__tests__/migrate.test.ts +0 -203
- package/templates/nextjs-standalone/src/lib/auth/__tests__/permissions.test.ts +0 -225
- package/templates/nextjs-standalone/src/lib/auth/__tests__/server.test.ts +0 -34
- package/templates/nextjs-standalone/src/lib/auth/__tests__/simple-key.test.ts +0 -176
- package/templates/nextjs-standalone/src/lib/auth/__tests__/types.test.ts +0 -44
- package/templates/nextjs-standalone/src/lib/db/__tests__/connection.test.ts +0 -144
- package/templates/nextjs-standalone/src/lib/db/__tests__/internal.test.ts +0 -387
- package/templates/nextjs-standalone/src/lib/db/__tests__/registry-health.test.ts +0 -190
- package/templates/nextjs-standalone/src/lib/db/__tests__/registry-pool-limits.test.ts +0 -137
- package/templates/nextjs-standalone/src/lib/db/__tests__/registry.test.ts +0 -398
- package/templates/nextjs-standalone/src/lib/db/__tests__/source-rate-limit.test.ts +0 -130
- package/templates/nextjs-standalone/src/lib/errors.ts +0 -154
- package/templates/nextjs-standalone/src/lib/plugins/__tests__/hooks-integration.test.ts +0 -204
- package/templates/nextjs-standalone/src/lib/plugins/__tests__/hooks.test.ts +0 -529
- package/templates/nextjs-standalone/src/lib/plugins/__tests__/migrate.test.ts +0 -875
- package/templates/nextjs-standalone/src/lib/plugins/__tests__/registry.test.ts +0 -373
- package/templates/nextjs-standalone/src/lib/plugins/__tests__/tools.test.ts +0 -49
- package/templates/nextjs-standalone/src/lib/plugins/__tests__/wiring.test.ts +0 -799
- package/templates/nextjs-standalone/src/lib/scheduler/__tests__/delivery.test.ts +0 -192
- package/templates/nextjs-standalone/src/lib/scheduler/__tests__/engine.test.ts +0 -248
- package/templates/nextjs-standalone/src/lib/scheduler/__tests__/format-email.test.ts +0 -96
- package/templates/nextjs-standalone/src/lib/scheduler/__tests__/format-slack.test.ts +0 -78
- package/templates/nextjs-standalone/src/lib/scheduler/__tests__/format-webhook.test.ts +0 -78
- package/templates/nextjs-standalone/src/lib/scheduler/index.ts +0 -7
- package/templates/nextjs-standalone/src/lib/slack/__tests__/api.test.ts +0 -160
- package/templates/nextjs-standalone/src/lib/slack/__tests__/format.test.ts +0 -237
- package/templates/nextjs-standalone/src/lib/slack/__tests__/store.test.ts +0 -188
- package/templates/nextjs-standalone/src/lib/slack/__tests__/threads.test.ts +0 -112
- package/templates/nextjs-standalone/src/lib/slack/__tests__/verify.test.ts +0 -111
- package/templates/nextjs-standalone/src/lib/tools/__tests__/action-permissions.test.ts +0 -594
- package/templates/nextjs-standalone/src/lib/tools/__tests__/custom-validation.test.ts +0 -240
- package/templates/nextjs-standalone/src/lib/tools/__tests__/explore-backend.test.ts +0 -267
- package/templates/nextjs-standalone/src/lib/tools/__tests__/explore-nsjail.test.ts +0 -506
- package/templates/nextjs-standalone/src/lib/tools/__tests__/explore-plugin.test.ts +0 -374
- package/templates/nextjs-standalone/src/lib/tools/__tests__/explore-sdk-compat.test.ts +0 -82
- package/templates/nextjs-standalone/src/lib/tools/__tests__/explore-sidecar.test.ts +0 -210
- package/templates/nextjs-standalone/src/lib/tools/__tests__/python-nsjail.test.ts +0 -515
- package/templates/nextjs-standalone/src/lib/tools/__tests__/python-sandbox.test.ts +0 -397
- package/templates/nextjs-standalone/src/lib/tools/__tests__/python-sidecar.test.ts +0 -365
- package/templates/nextjs-standalone/src/lib/tools/__tests__/python.test.ts +0 -331
- package/templates/nextjs-standalone/src/lib/tools/__tests__/registry-actions.test.ts +0 -132
- package/templates/nextjs-standalone/src/lib/tools/__tests__/registry.test.ts +0 -242
- package/templates/nextjs-standalone/src/lib/tools/__tests__/sql-audit.test.ts +0 -227
- package/templates/nextjs-standalone/src/lib/tools/__tests__/sql-connection-whitelist.test.ts +0 -100
- package/templates/nextjs-standalone/src/lib/tools/__tests__/sql-ratelimit.test.ts +0 -227
- package/templates/nextjs-standalone/src/lib/tools/__tests__/sql.test.ts +0 -709
- package/templates/nextjs-standalone/src/lib/tools/actions/__tests__/audit.test.ts +0 -211
- package/templates/nextjs-standalone/src/lib/tools/actions/__tests__/email.test.ts +0 -378
- package/templates/nextjs-standalone/src/lib/tools/actions/__tests__/handler.test.ts +0 -681
- package/templates/nextjs-standalone/src/lib/tools/actions/__tests__/jira.test.ts +0 -427
- package/templates/nextjs-standalone/src/test-setup.ts +0 -38
- 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 };
|