@agenticmail/enterprise 0.5.327 → 0.5.328
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/dist/dashboard/app.js +1 -1
- package/logs/cloudflared-error.log +6 -0
- package/logs/enterprise-out.log +1 -0
- package/package.json +1 -1
- package/src/admin/page-registry.ts +0 -290
- package/src/admin/routes.ts +0 -2968
- package/src/agent-tools/common.ts +0 -260
- package/src/agent-tools/index.ts +0 -542
- package/src/agent-tools/merge.ts +0 -62
- package/src/agent-tools/middleware.ts +0 -436
- package/src/agent-tools/schema/typebox.ts +0 -25
- package/src/agent-tools/security.ts +0 -352
- package/src/agent-tools/tool-resolver.ts +0 -1018
- package/src/agent-tools/tools/agenticmail.ts +0 -1017
- package/src/agent-tools/tools/bash.ts +0 -179
- package/src/agent-tools/tools/browser-tool.schema.ts +0 -112
- package/src/agent-tools/tools/browser-tool.ts +0 -388
- package/src/agent-tools/tools/browser.ts +0 -764
- package/src/agent-tools/tools/edit.ts +0 -100
- package/src/agent-tools/tools/enterprise-code-sandbox.ts +0 -395
- package/src/agent-tools/tools/enterprise-database.ts +0 -377
- package/src/agent-tools/tools/enterprise-diff.ts +0 -580
- package/src/agent-tools/tools/enterprise-documents.ts +0 -896
- package/src/agent-tools/tools/enterprise-http.ts +0 -485
- package/src/agent-tools/tools/enterprise-security-scan.ts +0 -528
- package/src/agent-tools/tools/enterprise-spreadsheet.ts +0 -825
- package/src/agent-tools/tools/glob.ts +0 -129
- package/src/agent-tools/tools/google/calendar.ts +0 -230
- package/src/agent-tools/tools/google/chat.ts +0 -725
- package/src/agent-tools/tools/google/contacts.ts +0 -209
- package/src/agent-tools/tools/google/docs.ts +0 -162
- package/src/agent-tools/tools/google/drive.ts +0 -392
- package/src/agent-tools/tools/google/forms.ts +0 -367
- package/src/agent-tools/tools/google/gmail.ts +0 -897
- package/src/agent-tools/tools/google/index.ts +0 -86
- package/src/agent-tools/tools/google/maps.ts +0 -543
- package/src/agent-tools/tools/google/meeting-voice.ts +0 -885
- package/src/agent-tools/tools/google/meetings.ts +0 -1094
- package/src/agent-tools/tools/google/sheets.ts +0 -215
- package/src/agent-tools/tools/google/slides.ts +0 -559
- package/src/agent-tools/tools/google/tasks.ts +0 -200
- package/src/agent-tools/tools/grep.ts +0 -178
- package/src/agent-tools/tools/integrations/_factory.ts +0 -102
- package/src/agent-tools/tools/integrations/activecampaign.ts +0 -14
- package/src/agent-tools/tools/integrations/adobe-sign.ts +0 -14
- package/src/agent-tools/tools/integrations/adp.ts +0 -14
- package/src/agent-tools/tools/integrations/airtable.ts +0 -14
- package/src/agent-tools/tools/integrations/apollo.ts +0 -14
- package/src/agent-tools/tools/integrations/asana.ts +0 -14
- package/src/agent-tools/tools/integrations/auth0.ts +0 -14
- package/src/agent-tools/tools/integrations/aws.ts +0 -14
- package/src/agent-tools/tools/integrations/azure-devops.ts +0 -14
- package/src/agent-tools/tools/integrations/bamboohr.ts +0 -14
- package/src/agent-tools/tools/integrations/basecamp.ts +0 -14
- package/src/agent-tools/tools/integrations/bigcommerce.ts +0 -14
- package/src/agent-tools/tools/integrations/bitbucket.ts +0 -14
- package/src/agent-tools/tools/integrations/box.ts +0 -14
- package/src/agent-tools/tools/integrations/brex.ts +0 -14
- package/src/agent-tools/tools/integrations/buffer.ts +0 -14
- package/src/agent-tools/tools/integrations/calendly.ts +0 -14
- package/src/agent-tools/tools/integrations/canva.ts +0 -14
- package/src/agent-tools/tools/integrations/chargebee.ts +0 -14
- package/src/agent-tools/tools/integrations/circleci.ts +0 -14
- package/src/agent-tools/tools/integrations/clickup.ts +0 -14
- package/src/agent-tools/tools/integrations/close.ts +0 -14
- package/src/agent-tools/tools/integrations/cloudflare.ts +0 -14
- package/src/agent-tools/tools/integrations/confluence.ts +0 -14
- package/src/agent-tools/tools/integrations/contentful.ts +0 -14
- package/src/agent-tools/tools/integrations/copper.ts +0 -14
- package/src/agent-tools/tools/integrations/crisp.ts +0 -14
- package/src/agent-tools/tools/integrations/crowdstrike.ts +0 -14
- package/src/agent-tools/tools/integrations/datadog.ts +0 -14
- package/src/agent-tools/tools/integrations/digitalocean.ts +0 -14
- package/src/agent-tools/tools/integrations/discord.ts +0 -14
- package/src/agent-tools/tools/integrations/docker.ts +0 -14
- package/src/agent-tools/tools/integrations/docusign.ts +0 -14
- package/src/agent-tools/tools/integrations/drift.ts +0 -14
- package/src/agent-tools/tools/integrations/dropbox.ts +0 -14
- package/src/agent-tools/tools/integrations/figma.ts +0 -14
- package/src/agent-tools/tools/integrations/firebase.ts +0 -14
- package/src/agent-tools/tools/integrations/flyio.ts +0 -14
- package/src/agent-tools/tools/integrations/freshbooks.ts +0 -14
- package/src/agent-tools/tools/integrations/freshdesk.ts +0 -14
- package/src/agent-tools/tools/integrations/freshsales.ts +0 -14
- package/src/agent-tools/tools/integrations/freshservice.ts +0 -14
- package/src/agent-tools/tools/integrations/front.ts +0 -14
- package/src/agent-tools/tools/integrations/github-actions.ts +0 -14
- package/src/agent-tools/tools/integrations/github.ts +0 -14
- package/src/agent-tools/tools/integrations/gitlab.ts +0 -14
- package/src/agent-tools/tools/integrations/gong.ts +0 -14
- package/src/agent-tools/tools/integrations/google-ads.ts +0 -14
- package/src/agent-tools/tools/integrations/google-analytics.ts +0 -14
- package/src/agent-tools/tools/integrations/google-cloud.ts +0 -14
- package/src/agent-tools/tools/integrations/gotomeeting.ts +0 -14
- package/src/agent-tools/tools/integrations/grafana.ts +0 -14
- package/src/agent-tools/tools/integrations/greenhouse.ts +0 -14
- package/src/agent-tools/tools/integrations/gusto.ts +0 -14
- package/src/agent-tools/tools/integrations/hashicorp-vault.ts +0 -14
- package/src/agent-tools/tools/integrations/heroku.ts +0 -14
- package/src/agent-tools/tools/integrations/hibob.ts +0 -14
- package/src/agent-tools/tools/integrations/hootsuite.ts +0 -14
- package/src/agent-tools/tools/integrations/hubspot.ts +0 -14
- package/src/agent-tools/tools/integrations/huggingface.ts +0 -14
- package/src/agent-tools/tools/integrations/index.ts +0 -474
- package/src/agent-tools/tools/integrations/intercom.ts +0 -14
- package/src/agent-tools/tools/integrations/jira.ts +0 -14
- package/src/agent-tools/tools/integrations/klaviyo.ts +0 -14
- package/src/agent-tools/tools/integrations/kubernetes.ts +0 -14
- package/src/agent-tools/tools/integrations/lattice.ts +0 -14
- package/src/agent-tools/tools/integrations/launchdarkly.ts +0 -14
- package/src/agent-tools/tools/integrations/lever.ts +0 -14
- package/src/agent-tools/tools/integrations/linear.ts +0 -14
- package/src/agent-tools/tools/integrations/linkedin.ts +0 -14
- package/src/agent-tools/tools/integrations/livechat.ts +0 -14
- package/src/agent-tools/tools/integrations/loom.ts +0 -14
- package/src/agent-tools/tools/integrations/mailchimp.ts +0 -14
- package/src/agent-tools/tools/integrations/mailgun.ts +0 -14
- package/src/agent-tools/tools/integrations/miro.ts +0 -14
- package/src/agent-tools/tools/integrations/mixpanel.ts +0 -14
- package/src/agent-tools/tools/integrations/monday.ts +0 -14
- package/src/agent-tools/tools/integrations/mongodb-atlas.ts +0 -14
- package/src/agent-tools/tools/integrations/neon.ts +0 -14
- package/src/agent-tools/tools/integrations/netlify.ts +0 -14
- package/src/agent-tools/tools/integrations/netsuite.ts +0 -14
- package/src/agent-tools/tools/integrations/newrelic.ts +0 -14
- package/src/agent-tools/tools/integrations/notion.ts +0 -14
- package/src/agent-tools/tools/integrations/okta.ts +0 -14
- package/src/agent-tools/tools/integrations/openai.ts +0 -14
- package/src/agent-tools/tools/integrations/opsgenie.ts +0 -14
- package/src/agent-tools/tools/integrations/outreach.ts +0 -14
- package/src/agent-tools/tools/integrations/paddle.ts +0 -14
- package/src/agent-tools/tools/integrations/pagerduty.ts +0 -14
- package/src/agent-tools/tools/integrations/pandadoc.ts +0 -14
- package/src/agent-tools/tools/integrations/paypal.ts +0 -14
- package/src/agent-tools/tools/integrations/personio.ts +0 -14
- package/src/agent-tools/tools/integrations/pinecone.ts +0 -14
- package/src/agent-tools/tools/integrations/pipedrive.ts +0 -14
- package/src/agent-tools/tools/integrations/plaid.ts +0 -14
- package/src/agent-tools/tools/integrations/postmark.ts +0 -14
- package/src/agent-tools/tools/integrations/power-automate.ts +0 -14
- package/src/agent-tools/tools/integrations/quickbooks.ts +0 -14
- package/src/agent-tools/tools/integrations/recurly.ts +0 -14
- package/src/agent-tools/tools/integrations/reddit.ts +0 -14
- package/src/agent-tools/tools/integrations/render.ts +0 -14
- package/src/agent-tools/tools/integrations/ringcentral.ts +0 -14
- package/src/agent-tools/tools/integrations/rippling.ts +0 -14
- package/src/agent-tools/tools/integrations/salesforce.ts +0 -14
- package/src/agent-tools/tools/integrations/salesloft.ts +0 -14
- package/src/agent-tools/tools/integrations/sanity.ts +0 -14
- package/src/agent-tools/tools/integrations/sap.ts +0 -14
- package/src/agent-tools/tools/integrations/segment.ts +0 -14
- package/src/agent-tools/tools/integrations/sendgrid.ts +0 -14
- package/src/agent-tools/tools/integrations/sentry.ts +0 -14
- package/src/agent-tools/tools/integrations/servicenow.ts +0 -14
- package/src/agent-tools/tools/integrations/shopify.ts +0 -14
- package/src/agent-tools/tools/integrations/shortcut.ts +0 -14
- package/src/agent-tools/tools/integrations/slack.ts +0 -14
- package/src/agent-tools/tools/integrations/smartsheet.ts +0 -14
- package/src/agent-tools/tools/integrations/snowflake.ts +0 -14
- package/src/agent-tools/tools/integrations/snyk.ts +0 -14
- package/src/agent-tools/tools/integrations/splunk.ts +0 -14
- package/src/agent-tools/tools/integrations/square.ts +0 -14
- package/src/agent-tools/tools/integrations/statuspage.ts +0 -14
- package/src/agent-tools/tools/integrations/stripe.ts +0 -14
- package/src/agent-tools/tools/integrations/supabase.ts +0 -14
- package/src/agent-tools/tools/integrations/teamwork.ts +0 -14
- package/src/agent-tools/tools/integrations/telegram.ts +0 -14
- package/src/agent-tools/tools/integrations/terraform.ts +0 -14
- package/src/agent-tools/tools/integrations/todoist.ts +0 -14
- package/src/agent-tools/tools/integrations/trello.ts +0 -14
- package/src/agent-tools/tools/integrations/twilio.ts +0 -14
- package/src/agent-tools/tools/integrations/twitter.ts +0 -14
- package/src/agent-tools/tools/integrations/vercel.ts +0 -14
- package/src/agent-tools/tools/integrations/weaviate.ts +0 -14
- package/src/agent-tools/tools/integrations/webex.ts +0 -14
- package/src/agent-tools/tools/integrations/webflow.ts +0 -14
- package/src/agent-tools/tools/integrations/whatsapp.ts +0 -14
- package/src/agent-tools/tools/integrations/whereby.ts +0 -14
- package/src/agent-tools/tools/integrations/woocommerce.ts +0 -14
- package/src/agent-tools/tools/integrations/wordpress.ts +0 -14
- package/src/agent-tools/tools/integrations/workday.ts +0 -14
- package/src/agent-tools/tools/integrations/wrike.ts +0 -14
- package/src/agent-tools/tools/integrations/xero.ts +0 -14
- package/src/agent-tools/tools/integrations/youtube.ts +0 -14
- package/src/agent-tools/tools/integrations/zendesk.ts +0 -14
- package/src/agent-tools/tools/integrations/zoho-crm.ts +0 -14
- package/src/agent-tools/tools/integrations/zoom.ts +0 -14
- package/src/agent-tools/tools/integrations/zuora.ts +0 -14
- package/src/agent-tools/tools/knowledge-search.ts +0 -318
- package/src/agent-tools/tools/local/coding.ts +0 -626
- package/src/agent-tools/tools/local/dependency-manager.ts +0 -647
- package/src/agent-tools/tools/local/file-edit.ts +0 -31
- package/src/agent-tools/tools/local/file-list.ts +0 -39
- package/src/agent-tools/tools/local/file-ops.ts +0 -48
- package/src/agent-tools/tools/local/file-read.ts +0 -39
- package/src/agent-tools/tools/local/file-search.ts +0 -46
- package/src/agent-tools/tools/local/file-write.ts +0 -28
- package/src/agent-tools/tools/local/filesystem.ts +0 -5
- package/src/agent-tools/tools/local/index.ts +0 -55
- package/src/agent-tools/tools/local/resolve-path.ts +0 -18
- package/src/agent-tools/tools/local/shell.ts +0 -277
- package/src/agent-tools/tools/local/system-info.ts +0 -29
- package/src/agent-tools/tools/management.ts +0 -425
- package/src/agent-tools/tools/mcp-bridge.ts +0 -142
- package/src/agent-tools/tools/mcp-server-tools.ts +0 -91
- package/src/agent-tools/tools/meeting-lifecycle.ts +0 -438
- package/src/agent-tools/tools/memory.ts +0 -509
- package/src/agent-tools/tools/messaging/index.ts +0 -6
- package/src/agent-tools/tools/messaging/telegram.ts +0 -167
- package/src/agent-tools/tools/messaging/whatsapp.ts +0 -651
- package/src/agent-tools/tools/microsoft/contacts.ts +0 -176
- package/src/agent-tools/tools/microsoft/excel-vba.ts +0 -331
- package/src/agent-tools/tools/microsoft/excel.ts +0 -261
- package/src/agent-tools/tools/microsoft/graph-api.ts +0 -161
- package/src/agent-tools/tools/microsoft/index.ts +0 -95
- package/src/agent-tools/tools/microsoft/onedrive.ts +0 -429
- package/src/agent-tools/tools/microsoft/onenote.ts +0 -186
- package/src/agent-tools/tools/microsoft/outlook-calendar.ts +0 -286
- package/src/agent-tools/tools/microsoft/outlook-mail.ts +0 -723
- package/src/agent-tools/tools/microsoft/planner.ts +0 -200
- package/src/agent-tools/tools/microsoft/powerbi.ts +0 -266
- package/src/agent-tools/tools/microsoft/powerpoint.ts +0 -186
- package/src/agent-tools/tools/microsoft/sharepoint.ts +0 -328
- package/src/agent-tools/tools/microsoft/teams.ts +0 -463
- package/src/agent-tools/tools/microsoft/todo.ts +0 -181
- package/src/agent-tools/tools/oauth-token-provider.ts +0 -101
- package/src/agent-tools/tools/read.ts +0 -160
- package/src/agent-tools/tools/visual-memory/capture.ts +0 -217
- package/src/agent-tools/tools/visual-memory/diff.ts +0 -283
- package/src/agent-tools/tools/visual-memory/index.ts +0 -698
- package/src/agent-tools/tools/visual-memory/phash.ts +0 -120
- package/src/agent-tools/tools/visual-memory/similarity.ts +0 -354
- package/src/agent-tools/tools/visual-memory/storage.ts +0 -534
- package/src/agent-tools/tools/visual-memory/types.ts +0 -100
- package/src/agent-tools/tools/web-fetch-utils.ts +0 -202
- package/src/agent-tools/tools/web-fetch.ts +0 -464
- package/src/agent-tools/tools/web-search.ts +0 -480
- package/src/agent-tools/tools/web-shared.ts +0 -232
- package/src/agent-tools/tools/write.ts +0 -68
- package/src/agent-tools/types.ts +0 -214
- package/src/agenticmail/index.ts +0 -34
- package/src/agenticmail/manager.ts +0 -253
- package/src/agenticmail/providers/google.ts +0 -391
- package/src/agenticmail/providers/imap.ts +0 -454
- package/src/agenticmail/providers/index.ts +0 -28
- package/src/agenticmail/providers/microsoft.ts +0 -260
- package/src/agenticmail/types.ts +0 -173
- package/src/auth/routes.ts +0 -1589
- package/src/browser/bridge-auth-registry.ts +0 -34
- package/src/browser/bridge-server.ts +0 -93
- package/src/browser/cdp.helpers.ts +0 -180
- package/src/browser/cdp.ts +0 -466
- package/src/browser/chrome.executables.ts +0 -625
- package/src/browser/chrome.profile-decoration.ts +0 -198
- package/src/browser/chrome.ts +0 -349
- package/src/browser/client-actions-core.ts +0 -259
- package/src/browser/client-actions-observe.ts +0 -184
- package/src/browser/client-actions-state.ts +0 -284
- package/src/browser/client-actions-types.ts +0 -16
- package/src/browser/client-actions-url.ts +0 -11
- package/src/browser/client-actions.ts +0 -4
- package/src/browser/client-fetch.ts +0 -253
- package/src/browser/client.ts +0 -337
- package/src/browser/config.ts +0 -301
- package/src/browser/constants.ts +0 -8
- package/src/browser/control-auth.ts +0 -94
- package/src/browser/control-service.ts +0 -81
- package/src/browser/csrf.ts +0 -87
- package/src/browser/enterprise-compat.ts +0 -562
- package/src/browser/extension-relay.ts +0 -834
- package/src/browser/http-auth.ts +0 -63
- package/src/browser/navigation-guard.ts +0 -50
- package/src/browser/paths.ts +0 -49
- package/src/browser/playwright.d.ts +0 -12
- package/src/browser/profiles-service.ts +0 -187
- package/src/browser/profiles.ts +0 -114
- package/src/browser/proxy-files.ts +0 -41
- package/src/browser/pw-ai-module.ts +0 -52
- package/src/browser/pw-ai-state.ts +0 -9
- package/src/browser/pw-ai.ts +0 -65
- package/src/browser/pw-role-snapshot.ts +0 -434
- package/src/browser/pw-session.ts +0 -810
- package/src/browser/pw-tools-core.activity.ts +0 -68
- package/src/browser/pw-tools-core.downloads.ts +0 -281
- package/src/browser/pw-tools-core.interactions.ts +0 -646
- package/src/browser/pw-tools-core.responses.ts +0 -124
- package/src/browser/pw-tools-core.shared.ts +0 -70
- package/src/browser/pw-tools-core.snapshot.ts +0 -213
- package/src/browser/pw-tools-core.state.ts +0 -209
- package/src/browser/pw-tools-core.storage.ts +0 -128
- package/src/browser/pw-tools-core.trace.ts +0 -37
- package/src/browser/pw-tools-core.ts +0 -8
- package/src/browser/resolved-config-refresh.ts +0 -59
- package/src/browser/routes/agent.act.shared.ts +0 -52
- package/src/browser/routes/agent.act.ts +0 -575
- package/src/browser/routes/agent.debug.ts +0 -149
- package/src/browser/routes/agent.shared.ts +0 -143
- package/src/browser/routes/agent.snapshot.ts +0 -333
- package/src/browser/routes/agent.storage.ts +0 -451
- package/src/browser/routes/agent.ts +0 -13
- package/src/browser/routes/basic.ts +0 -202
- package/src/browser/routes/dispatcher.ts +0 -126
- package/src/browser/routes/index.ts +0 -11
- package/src/browser/routes/path-output.ts +0 -1
- package/src/browser/routes/tabs.ts +0 -217
- package/src/browser/routes/types.ts +0 -26
- package/src/browser/routes/utils.ts +0 -73
- package/src/browser/screenshot.ts +0 -54
- package/src/browser/server-context.ts +0 -688
- package/src/browser/server-context.types.ts +0 -65
- package/src/browser/server-lifecycle.ts +0 -48
- package/src/browser/server-middleware.ts +0 -37
- package/src/browser/server.ts +0 -110
- package/src/browser/target-id.ts +0 -30
- package/src/browser/trash.ts +0 -21
- package/src/cli-agent.ts +0 -2452
- package/src/cli-reset-password.ts +0 -138
- package/src/cli-serve.ts +0 -314
- package/src/cli.ts +0 -103
- package/src/dashboard/HELP-TOOLTIPS-GUIDE.md +0 -45
- package/src/dashboard/app.js +0 -579
- package/src/dashboard/assets/brand-logos.js +0 -350
- package/src/dashboard/assets/icons/emoji-icons.js +0 -893
- package/src/dashboard/assets/logo.png +0 -0
- package/src/dashboard/assets/provider-logos.js +0 -139
- package/src/dashboard/components/error-boundary.js +0 -21
- package/src/dashboard/components/help-button.js +0 -65
- package/src/dashboard/components/icons.js +0 -64
- package/src/dashboard/components/knowledge-link.js +0 -79
- package/src/dashboard/components/modal.js +0 -125
- package/src/dashboard/components/org-switcher.js +0 -156
- package/src/dashboard/components/persona-fields.js +0 -460
- package/src/dashboard/components/settings-help.js +0 -193
- package/src/dashboard/components/tag-input.js +0 -96
- package/src/dashboard/components/timezones.js +0 -352
- package/src/dashboard/components/transport-encryption.js +0 -288
- package/src/dashboard/components/utils.js +0 -205
- package/src/dashboard/data/countries.js +0 -255
- package/src/dashboard/docs/activity.html +0 -253
- package/src/dashboard/docs/agent-activity.html +0 -199
- package/src/dashboard/docs/agent-autonomy.html +0 -161
- package/src/dashboard/docs/agent-budget.html +0 -190
- package/src/dashboard/docs/agent-channels.html +0 -189
- package/src/dashboard/docs/agent-communication.html +0 -171
- package/src/dashboard/docs/agent-configuration.html +0 -194
- package/src/dashboard/docs/agent-deployment.html +0 -323
- package/src/dashboard/docs/agent-email.html +0 -184
- package/src/dashboard/docs/agent-guardrails.html +0 -206
- package/src/dashboard/docs/agent-manager.html +0 -226
- package/src/dashboard/docs/agent-memory.html +0 -215
- package/src/dashboard/docs/agent-overview.html +0 -226
- package/src/dashboard/docs/agent-permissions.html +0 -305
- package/src/dashboard/docs/agent-personal.html +0 -155
- package/src/dashboard/docs/agent-security.html +0 -188
- package/src/dashboard/docs/agent-skills.html +0 -224
- package/src/dashboard/docs/agent-tool-security.html +0 -205
- package/src/dashboard/docs/agent-tools.html +0 -238
- package/src/dashboard/docs/agent-whatsapp.html +0 -210
- package/src/dashboard/docs/agent-workforce.html +0 -199
- package/src/dashboard/docs/agents.html +0 -258
- package/src/dashboard/docs/approvals.html +0 -200
- package/src/dashboard/docs/audit.html +0 -206
- package/src/dashboard/docs/browser-providers.html +0 -313
- package/src/dashboard/docs/cluster.html +0 -285
- package/src/dashboard/docs/community-skills.html +0 -253
- package/src/dashboard/docs/compliance.html +0 -221
- package/src/dashboard/docs/dashboard.html +0 -84
- package/src/dashboard/docs/database-access.html +0 -322
- package/src/dashboard/docs/dlp.html +0 -268
- package/src/dashboard/docs/docs-style.css +0 -26
- package/src/dashboard/docs/domain-status.html +0 -294
- package/src/dashboard/docs/guardrails.html +0 -265
- package/src/dashboard/docs/journal.html +0 -197
- package/src/dashboard/docs/knowledge-contributions.html +0 -286
- package/src/dashboard/docs/knowledge.html +0 -268
- package/src/dashboard/docs/memory-transfer.html +0 -311
- package/src/dashboard/docs/messages.html +0 -217
- package/src/dashboard/docs/multi-tenant.html +0 -311
- package/src/dashboard/docs/org-chart.html +0 -239
- package/src/dashboard/docs/organizations.html +0 -182
- package/src/dashboard/docs/roles.html +0 -195
- package/src/dashboard/docs/settings-network.html +0 -321
- package/src/dashboard/docs/settings-security.html +0 -347
- package/src/dashboard/docs/settings-tool-security.html +0 -176
- package/src/dashboard/docs/settings.html +0 -280
- package/src/dashboard/docs/skill-connections.html +0 -270
- package/src/dashboard/docs/skills.html +0 -206
- package/src/dashboard/docs/task-pipeline.html +0 -261
- package/src/dashboard/docs/transport-encryption.html +0 -359
- package/src/dashboard/docs/users.html +0 -225
- package/src/dashboard/docs/vault.html +0 -260
- package/src/dashboard/docs/workforce.html +0 -245
- package/src/dashboard/index.html +0 -444
- package/src/dashboard/pages/activity.js +0 -379
- package/src/dashboard/pages/agent-detail/activity.js +0 -277
- package/src/dashboard/pages/agent-detail/autonomy.js +0 -244
- package/src/dashboard/pages/agent-detail/budget.js +0 -269
- package/src/dashboard/pages/agent-detail/channels.js +0 -494
- package/src/dashboard/pages/agent-detail/communication.js +0 -296
- package/src/dashboard/pages/agent-detail/configuration.js +0 -882
- package/src/dashboard/pages/agent-detail/deployment.js +0 -958
- package/src/dashboard/pages/agent-detail/email.js +0 -674
- package/src/dashboard/pages/agent-detail/guardrails.js +0 -521
- package/src/dashboard/pages/agent-detail/index.js +0 -261
- package/src/dashboard/pages/agent-detail/manager.js +0 -357
- package/src/dashboard/pages/agent-detail/meeting-browser.js +0 -933
- package/src/dashboard/pages/agent-detail/memory.js +0 -368
- package/src/dashboard/pages/agent-detail/overview.js +0 -844
- package/src/dashboard/pages/agent-detail/permissions.js +0 -1163
- package/src/dashboard/pages/agent-detail/personal-details.js +0 -404
- package/src/dashboard/pages/agent-detail/security.js +0 -409
- package/src/dashboard/pages/agent-detail/shared.js +0 -85
- package/src/dashboard/pages/agent-detail/skills-section.js +0 -183
- package/src/dashboard/pages/agent-detail/tool-security.js +0 -380
- package/src/dashboard/pages/agent-detail/tools.js +0 -322
- package/src/dashboard/pages/agent-detail/whatsapp.js +0 -824
- package/src/dashboard/pages/agent-detail/workforce.js +0 -683
- package/src/dashboard/pages/agents.js +0 -1242
- package/src/dashboard/pages/approvals.js +0 -100
- package/src/dashboard/pages/audit.js +0 -198
- package/src/dashboard/pages/cluster.js +0 -512
- package/src/dashboard/pages/community-skills.js +0 -1219
- package/src/dashboard/pages/compliance.js +0 -475
- package/src/dashboard/pages/dashboard.js +0 -180
- package/src/dashboard/pages/database-access.js +0 -812
- package/src/dashboard/pages/dlp.js +0 -293
- package/src/dashboard/pages/domain-status.js +0 -951
- package/src/dashboard/pages/guardrails.js +0 -1035
- package/src/dashboard/pages/journal.js +0 -172
- package/src/dashboard/pages/knowledge-contributions.js +0 -1682
- package/src/dashboard/pages/knowledge-import.js +0 -455
- package/src/dashboard/pages/knowledge.js +0 -582
- package/src/dashboard/pages/login.js +0 -1056
- package/src/dashboard/pages/memory-transfer.js +0 -631
- package/src/dashboard/pages/messages.js +0 -303
- package/src/dashboard/pages/org-chart.js +0 -349
- package/src/dashboard/pages/organizations.js +0 -1081
- package/src/dashboard/pages/roles.js +0 -780
- package/src/dashboard/pages/settings.js +0 -3790
- package/src/dashboard/pages/skill-connections.js +0 -982
- package/src/dashboard/pages/skills.js +0 -879
- package/src/dashboard/pages/task-pipeline.js +0 -684
- package/src/dashboard/pages/users.js +0 -867
- package/src/dashboard/pages/vault.js +0 -791
- package/src/dashboard/pages/workforce.js +0 -851
- package/src/dashboard/vendor/react-dom.development.js +0 -29924
- package/src/dashboard/vendor/react-dom.production.min.js +0 -267
- package/src/dashboard/vendor/react.development.js +0 -3343
- package/src/dashboard/vendor/react.production.min.js +0 -31
- package/src/database-access/agent-tools.ts +0 -193
- package/src/database-access/connection-manager.ts +0 -1341
- package/src/database-access/index.ts +0 -21
- package/src/database-access/query-sanitizer.ts +0 -220
- package/src/database-access/routes.ts +0 -226
- package/src/database-access/types.ts +0 -226
- package/src/db/adapter.ts +0 -510
- package/src/db/dynamodb.ts +0 -454
- package/src/db/factory.ts +0 -129
- package/src/db/mongodb.ts +0 -360
- package/src/db/mysql.ts +0 -531
- package/src/db/postgres.ts +0 -863
- package/src/db/proxy.ts +0 -39
- package/src/db/resolve-driver.ts +0 -29
- package/src/db/sql-schema.ts +0 -124
- package/src/db/sqlite.ts +0 -493
- package/src/db/turso.ts +0 -470
- package/src/deploy/fly.ts +0 -368
- package/src/deploy/managed.ts +0 -235
- package/src/domain-lock/cli-recover.ts +0 -591
- package/src/domain-lock/cli-verify.ts +0 -190
- package/src/domain-lock/index.ts +0 -220
- package/src/engine/activity-routes.ts +0 -154
- package/src/engine/activity.ts +0 -568
- package/src/engine/agent-autonomy.ts +0 -974
- package/src/engine/agent-config.ts +0 -646
- package/src/engine/agent-heartbeat.ts +0 -720
- package/src/engine/agent-hierarchy.ts +0 -1064
- package/src/engine/agent-memory.ts +0 -806
- package/src/engine/agent-notify.ts +0 -50
- package/src/engine/agent-routes.ts +0 -2583
- package/src/engine/agent-status.ts +0 -311
- package/src/engine/ambient-memory.ts +0 -401
- package/src/engine/approvals.ts +0 -615
- package/src/engine/assets/thinking-hum.mp3 +0 -0
- package/src/engine/catalog-routes.ts +0 -232
- package/src/engine/chat-poller.ts +0 -913
- package/src/engine/chat-webhook-routes.ts +0 -304
- package/src/engine/cli-build-skill.ts +0 -285
- package/src/engine/cli-submit-skill.ts +0 -200
- package/src/engine/cli-validate.ts +0 -188
- package/src/engine/cluster.ts +0 -278
- package/src/engine/communication-routes.ts +0 -139
- package/src/engine/communication.ts +0 -765
- package/src/engine/community-registry.ts +0 -1529
- package/src/engine/community-routes.ts +0 -260
- package/src/engine/compliance-routes.ts +0 -133
- package/src/engine/compliance.ts +0 -1679
- package/src/engine/config-bus.ts +0 -103
- package/src/engine/db-adapter.ts +0 -1156
- package/src/engine/db-schema.ts +0 -1945
- package/src/engine/deploy-schema-routes.ts +0 -176
- package/src/engine/deployer.ts +0 -957
- package/src/engine/dlp-routes.ts +0 -101
- package/src/engine/dlp.ts +0 -410
- package/src/engine/email-poller.ts +0 -855
- package/src/engine/emoji.ts +0 -106
- package/src/engine/guardrail-routes.ts +0 -125
- package/src/engine/guardrails.ts +0 -465
- package/src/engine/index.ts +0 -255
- package/src/engine/journal-routes.ts +0 -56
- package/src/engine/journal.ts +0 -249
- package/src/engine/knowledge-contribution-routes.ts +0 -633
- package/src/engine/knowledge-contribution.ts +0 -1386
- package/src/engine/knowledge-import/chunker.ts +0 -241
- package/src/engine/knowledge-import/import-manager.ts +0 -416
- package/src/engine/knowledge-import/index.ts +0 -27
- package/src/engine/knowledge-import/processors/clean.ts +0 -149
- package/src/engine/knowledge-import/processors/extract-gdrive.ts +0 -102
- package/src/engine/knowledge-import/processors/extract-github.ts +0 -74
- package/src/engine/knowledge-import/processors/extract-sharepoint.ts +0 -69
- package/src/engine/knowledge-import/processors/extract-web.ts +0 -275
- package/src/engine/knowledge-import/processors/index.ts +0 -18
- package/src/engine/knowledge-import/processors/pipeline.ts +0 -171
- package/src/engine/knowledge-import/processors/types.ts +0 -78
- package/src/engine/knowledge-import/processors/validate.ts +0 -150
- package/src/engine/knowledge-import/provider-file-upload.ts +0 -95
- package/src/engine/knowledge-import/provider-github.ts +0 -144
- package/src/engine/knowledge-import/provider-google-sites.ts +0 -323
- package/src/engine/knowledge-import/provider-sharepoint.ts +0 -276
- package/src/engine/knowledge-import/provider-url.ts +0 -218
- package/src/engine/knowledge-import/routes.ts +0 -94
- package/src/engine/knowledge-import/types.ts +0 -92
- package/src/engine/knowledge-routes.ts +0 -231
- package/src/engine/knowledge.ts +0 -587
- package/src/engine/lifecycle.ts +0 -1420
- package/src/engine/mcp-process-manager.ts +0 -573
- package/src/engine/meeting-monitor.ts +0 -483
- package/src/engine/meeting-voice-intelligence.ts +0 -340
- package/src/engine/memory-routes.ts +0 -142
- package/src/engine/memory-transfer-routes.ts +0 -339
- package/src/engine/messaging-history.ts +0 -177
- package/src/engine/messaging-poller.ts +0 -786
- package/src/engine/model-fallback.ts +0 -141
- package/src/engine/oauth-connect-routes.ts +0 -603
- package/src/engine/oauth-connect.ts +0 -304
- package/src/engine/onboarding-routes.ts +0 -148
- package/src/engine/onboarding.ts +0 -574
- package/src/engine/org-approval-routes.ts +0 -146
- package/src/engine/org-integration-routes.ts +0 -399
- package/src/engine/org-integrations.ts +0 -608
- package/src/engine/org-policies.ts +0 -502
- package/src/engine/policy-import-routes.ts +0 -125
- package/src/engine/policy-import.ts +0 -1186
- package/src/engine/policy-routes.ts +0 -163
- package/src/engine/routes.ts +0 -1236
- package/src/engine/screen-unlock.ts +0 -136
- package/src/engine/session-router.ts +0 -212
- package/src/engine/skill-updater-routes.ts +0 -132
- package/src/engine/skill-updater.ts +0 -480
- package/src/engine/skill-validator.ts +0 -331
- package/src/engine/skills/agent-management.ts +0 -119
- package/src/engine/skills/agent-memory.ts +0 -19
- package/src/engine/skills/agenticmail.ts +0 -116
- package/src/engine/skills/core-tools.ts +0 -25
- package/src/engine/skills/database-access.ts +0 -78
- package/src/engine/skills/enterprise-code-sandbox.ts +0 -113
- package/src/engine/skills/enterprise-database.ts +0 -123
- package/src/engine/skills/enterprise-diff.ts +0 -95
- package/src/engine/skills/enterprise-documents.ts +0 -162
- package/src/engine/skills/enterprise-http.ts +0 -99
- package/src/engine/skills/enterprise-security-scan.ts +0 -125
- package/src/engine/skills/enterprise-spreadsheet.ts +0 -171
- package/src/engine/skills/gws-admin.ts +0 -18
- package/src/engine/skills/gws-calendar.ts +0 -21
- package/src/engine/skills/gws-chat.ts +0 -29
- package/src/engine/skills/gws-contacts.ts +0 -20
- package/src/engine/skills/gws-docs.ts +0 -18
- package/src/engine/skills/gws-drive.ts +0 -23
- package/src/engine/skills/gws-forms.ts +0 -23
- package/src/engine/skills/gws-gmail.ts +0 -30
- package/src/engine/skills/gws-groups.ts +0 -17
- package/src/engine/skills/gws-keep.ts +0 -17
- package/src/engine/skills/gws-maps.ts +0 -25
- package/src/engine/skills/gws-meet.ts +0 -23
- package/src/engine/skills/gws-sheets.ts +0 -22
- package/src/engine/skills/gws-sites.ts +0 -16
- package/src/engine/skills/gws-slides.ts +0 -27
- package/src/engine/skills/gws-tasks.ts +0 -22
- package/src/engine/skills/gws-vault.ts +0 -17
- package/src/engine/skills/index.ts +0 -159
- package/src/engine/skills/knowledge-search.ts +0 -18
- package/src/engine/skills/local-system.ts +0 -61
- package/src/engine/skills/m365-admin.ts +0 -18
- package/src/engine/skills/m365-bookings.ts +0 -17
- package/src/engine/skills/m365-copilot.ts +0 -17
- package/src/engine/skills/m365-excel.ts +0 -60
- package/src/engine/skills/m365-forms.ts +0 -17
- package/src/engine/skills/m365-onedrive.ts +0 -60
- package/src/engine/skills/m365-onenote.ts +0 -17
- package/src/engine/skills/m365-outlook.ts +0 -27
- package/src/engine/skills/m365-planner.ts +0 -18
- package/src/engine/skills/m365-power-automate.ts +0 -18
- package/src/engine/skills/m365-power-bi.ts +0 -19
- package/src/engine/skills/m365-powerpoint.ts +0 -33
- package/src/engine/skills/m365-sharepoint.ts +0 -20
- package/src/engine/skills/m365-teams.ts +0 -21
- package/src/engine/skills/m365-todo.ts +0 -17
- package/src/engine/skills/m365-whiteboard.ts +0 -16
- package/src/engine/skills/m365-word.ts +0 -42
- package/src/engine/skills/mcp-bridge.ts +0 -45
- package/src/engine/skills/meeting-lifecycle.ts +0 -20
- package/src/engine/skills/messaging.ts +0 -46
- package/src/engine/skills/visual-memory.ts +0 -25
- package/src/engine/skills.ts +0 -688
- package/src/engine/soul-library.ts +0 -142
- package/src/engine/soul-templates.json +0 -1525
- package/src/engine/storage-manager.ts +0 -252
- package/src/engine/storage-routes.ts +0 -113
- package/src/engine/storage.ts +0 -528
- package/src/engine/task-poller.ts +0 -394
- package/src/engine/task-queue-after-spawn.ts +0 -66
- package/src/engine/task-queue-before-spawn.ts +0 -113
- package/src/engine/task-queue-routes.ts +0 -161
- package/src/engine/task-queue.ts +0 -664
- package/src/engine/tenant.ts +0 -409
- package/src/engine/tool-catalog.ts +0 -354
- package/src/engine/vault-routes.ts +0 -134
- package/src/engine/vault.ts +0 -601
- package/src/engine/workforce-routes.ts +0 -331
- package/src/engine/workforce.ts +0 -1161
- package/src/index.ts +0 -77
- package/src/lib/cidr.ts +0 -122
- package/src/lib/config-store.ts +0 -86
- package/src/lib/resilience.ts +0 -326
- package/src/lib/text-search.ts +0 -358
- package/src/mcp/adapters/activecampaign.adapter.ts +0 -391
- package/src/mcp/adapters/adobe-sign.adapter.ts +0 -469
- package/src/mcp/adapters/adp.adapter.ts +0 -358
- package/src/mcp/adapters/airtable.adapter.ts +0 -273
- package/src/mcp/adapters/apollo.adapter.ts +0 -420
- package/src/mcp/adapters/asana.adapter.ts +0 -315
- package/src/mcp/adapters/auth0.adapter.ts +0 -386
- package/src/mcp/adapters/aws.adapter.ts +0 -345
- package/src/mcp/adapters/azure-devops.adapter.ts +0 -389
- package/src/mcp/adapters/bamboohr.adapter.ts +0 -376
- package/src/mcp/adapters/basecamp.adapter.ts +0 -366
- package/src/mcp/adapters/bigcommerce.adapter.ts +0 -429
- package/src/mcp/adapters/bitbucket.adapter.ts +0 -260
- package/src/mcp/adapters/box.adapter.ts +0 -350
- package/src/mcp/adapters/brex.adapter.ts +0 -367
- package/src/mcp/adapters/buffer.adapter.ts +0 -303
- package/src/mcp/adapters/calendly.adapter.ts +0 -262
- package/src/mcp/adapters/canva.adapter.ts +0 -256
- package/src/mcp/adapters/chargebee.adapter.ts +0 -448
- package/src/mcp/adapters/circleci.adapter.ts +0 -216
- package/src/mcp/adapters/clickup.adapter.ts +0 -335
- package/src/mcp/adapters/close.adapter.ts +0 -390
- package/src/mcp/adapters/cloudflare.adapter.ts +0 -378
- package/src/mcp/adapters/confluence.adapter.ts +0 -301
- package/src/mcp/adapters/contentful.adapter.ts +0 -355
- package/src/mcp/adapters/copper.adapter.ts +0 -468
- package/src/mcp/adapters/crisp.adapter.ts +0 -415
- package/src/mcp/adapters/crowdstrike.adapter.ts +0 -413
- package/src/mcp/adapters/datadog.adapter.ts +0 -373
- package/src/mcp/adapters/digitalocean.adapter.ts +0 -336
- package/src/mcp/adapters/discord.adapter.ts +0 -248
- package/src/mcp/adapters/docker.adapter.ts +0 -238
- package/src/mcp/adapters/docusign.adapter.ts +0 -431
- package/src/mcp/adapters/drift.adapter.ts +0 -386
- package/src/mcp/adapters/dropbox.adapter.ts +0 -315
- package/src/mcp/adapters/figma.adapter.ts +0 -302
- package/src/mcp/adapters/firebase.adapter.ts +0 -446
- package/src/mcp/adapters/flyio.adapter.ts +0 -302
- package/src/mcp/adapters/freshbooks.adapter.ts +0 -474
- package/src/mcp/adapters/freshdesk.adapter.ts +0 -441
- package/src/mcp/adapters/freshsales.adapter.ts +0 -457
- package/src/mcp/adapters/freshservice.adapter.ts +0 -481
- package/src/mcp/adapters/front.adapter.ts +0 -357
- package/src/mcp/adapters/github-actions.adapter.ts +0 -329
- package/src/mcp/adapters/github.adapter.ts +0 -387
- package/src/mcp/adapters/gitlab.adapter.ts +0 -368
- package/src/mcp/adapters/gong.adapter.ts +0 -386
- package/src/mcp/adapters/google-ads.adapter.ts +0 -363
- package/src/mcp/adapters/google-analytics.adapter.ts +0 -316
- package/src/mcp/adapters/google-cloud.adapter.ts +0 -312
- package/src/mcp/adapters/gotomeeting.adapter.ts +0 -255
- package/src/mcp/adapters/grafana.adapter.ts +0 -361
- package/src/mcp/adapters/greenhouse.adapter.ts +0 -354
- package/src/mcp/adapters/gusto.adapter.ts +0 -329
- package/src/mcp/adapters/hashicorp-vault.adapter.ts +0 -355
- package/src/mcp/adapters/heroku.adapter.ts +0 -291
- package/src/mcp/adapters/hibob.adapter.ts +0 -334
- package/src/mcp/adapters/hootsuite.adapter.ts +0 -322
- package/src/mcp/adapters/hubspot.adapter.ts +0 -400
- package/src/mcp/adapters/huggingface.adapter.ts +0 -349
- package/src/mcp/adapters/index.ts +0 -524
- package/src/mcp/adapters/intercom.adapter.ts +0 -269
- package/src/mcp/adapters/jira.adapter.ts +0 -482
- package/src/mcp/adapters/klaviyo.adapter.ts +0 -353
- package/src/mcp/adapters/kubernetes.adapter.ts +0 -431
- package/src/mcp/adapters/lattice.adapter.ts +0 -339
- package/src/mcp/adapters/launchdarkly.adapter.ts +0 -368
- package/src/mcp/adapters/lever.adapter.ts +0 -347
- package/src/mcp/adapters/linear.adapter.ts +0 -300
- package/src/mcp/adapters/linkedin.adapter.ts +0 -331
- package/src/mcp/adapters/livechat.adapter.ts +0 -259
- package/src/mcp/adapters/loom.adapter.ts +0 -230
- package/src/mcp/adapters/mailchimp.adapter.ts +0 -394
- package/src/mcp/adapters/mailgun.adapter.ts +0 -425
- package/src/mcp/adapters/miro.adapter.ts +0 -274
- package/src/mcp/adapters/mixpanel.adapter.ts +0 -324
- package/src/mcp/adapters/monday.adapter.ts +0 -308
- package/src/mcp/adapters/mongodb-atlas.adapter.ts +0 -345
- package/src/mcp/adapters/neon.adapter.ts +0 -312
- package/src/mcp/adapters/netlify.adapter.ts +0 -324
- package/src/mcp/adapters/netsuite.adapter.ts +0 -411
- package/src/mcp/adapters/newrelic.adapter.ts +0 -339
- package/src/mcp/adapters/notion.adapter.ts +0 -338
- package/src/mcp/adapters/okta.adapter.ts +0 -394
- package/src/mcp/adapters/openai.adapter.ts +0 -315
- package/src/mcp/adapters/opsgenie.adapter.ts +0 -375
- package/src/mcp/adapters/outreach.adapter.ts +0 -372
- package/src/mcp/adapters/paddle.adapter.ts +0 -467
- package/src/mcp/adapters/pagerduty.adapter.ts +0 -412
- package/src/mcp/adapters/pandadoc.adapter.ts +0 -389
- package/src/mcp/adapters/paypal.adapter.ts +0 -465
- package/src/mcp/adapters/personio.adapter.ts +0 -401
- package/src/mcp/adapters/pinecone.adapter.ts +0 -340
- package/src/mcp/adapters/pipedrive.adapter.ts +0 -324
- package/src/mcp/adapters/plaid.adapter.ts +0 -444
- package/src/mcp/adapters/postmark.adapter.ts +0 -387
- package/src/mcp/adapters/power-automate.adapter.ts +0 -388
- package/src/mcp/adapters/quickbooks.adapter.ts +0 -431
- package/src/mcp/adapters/recurly.adapter.ts +0 -433
- package/src/mcp/adapters/reddit.adapter.ts +0 -371
- package/src/mcp/adapters/render.adapter.ts +0 -332
- package/src/mcp/adapters/ringcentral.adapter.ts +0 -281
- package/src/mcp/adapters/rippling.adapter.ts +0 -287
- package/src/mcp/adapters/salesforce.adapter.ts +0 -321
- package/src/mcp/adapters/salesloft.adapter.ts +0 -413
- package/src/mcp/adapters/sanity.adapter.ts +0 -363
- package/src/mcp/adapters/sap.adapter.ts +0 -483
- package/src/mcp/adapters/segment.adapter.ts +0 -260
- package/src/mcp/adapters/sendgrid.adapter.ts +0 -265
- package/src/mcp/adapters/sentry.adapter.ts +0 -331
- package/src/mcp/adapters/servicenow.adapter.ts +0 -468
- package/src/mcp/adapters/shopify.adapter.ts +0 -451
- package/src/mcp/adapters/shortcut.adapter.ts +0 -290
- package/src/mcp/adapters/slack.adapter.ts +0 -380
- package/src/mcp/adapters/smartsheet.adapter.ts +0 -326
- package/src/mcp/adapters/snowflake.adapter.ts +0 -347
- package/src/mcp/adapters/snyk.adapter.ts +0 -394
- package/src/mcp/adapters/splunk.adapter.ts +0 -403
- package/src/mcp/adapters/square.adapter.ts +0 -467
- package/src/mcp/adapters/statuspage.adapter.ts +0 -401
- package/src/mcp/adapters/stripe.adapter.ts +0 -380
- package/src/mcp/adapters/supabase.adapter.ts +0 -334
- package/src/mcp/adapters/teamwork.adapter.ts +0 -404
- package/src/mcp/adapters/telegram.adapter.ts +0 -299
- package/src/mcp/adapters/terraform.adapter.ts +0 -300
- package/src/mcp/adapters/todoist.adapter.ts +0 -239
- package/src/mcp/adapters/trello.adapter.ts +0 -316
- package/src/mcp/adapters/twilio.adapter.ts +0 -233
- package/src/mcp/adapters/twitter.adapter.ts +0 -348
- package/src/mcp/adapters/vercel.adapter.ts +0 -219
- package/src/mcp/adapters/weaviate.adapter.ts +0 -371
- package/src/mcp/adapters/webex.adapter.ts +0 -237
- package/src/mcp/adapters/webflow.adapter.ts +0 -287
- package/src/mcp/adapters/whatsapp.adapter.ts +0 -273
- package/src/mcp/adapters/whereby.adapter.ts +0 -240
- package/src/mcp/adapters/woocommerce.adapter.ts +0 -454
- package/src/mcp/adapters/wordpress.adapter.ts +0 -455
- package/src/mcp/adapters/workday.adapter.ts +0 -354
- package/src/mcp/adapters/wrike.adapter.ts +0 -349
- package/src/mcp/adapters/xero.adapter.ts +0 -472
- package/src/mcp/adapters/youtube.adapter.ts +0 -401
- package/src/mcp/adapters/zendesk.adapter.ts +0 -399
- package/src/mcp/adapters/zoho-crm.adapter.ts +0 -410
- package/src/mcp/adapters/zoom.adapter.ts +0 -241
- package/src/mcp/adapters/zuora.adapter.ts +0 -476
- package/src/mcp/framework/api-executor.ts +0 -192
- package/src/mcp/framework/aws-sigv4.ts +0 -216
- package/src/mcp/framework/credential-resolver.ts +0 -128
- package/src/mcp/framework/oauth-token-manager.ts +0 -22
- package/src/mcp/framework/skill-mcp-framework.ts +0 -226
- package/src/mcp/framework/types.ts +0 -130
- package/src/mcp/index.ts +0 -124
- package/src/mcp/integration-catalog.ts +0 -178
- package/src/middleware/dns-rebinding.ts +0 -44
- package/src/middleware/egress-filter.ts +0 -104
- package/src/middleware/firewall.ts +0 -192
- package/src/middleware/geo-ip.ts +0 -156
- package/src/middleware/index.ts +0 -390
- package/src/middleware/network-config.ts +0 -90
- package/src/middleware/proxy-config.ts +0 -71
- package/src/middleware/request-limits.ts +0 -59
- package/src/middleware/transport-encryption.ts +0 -398
- package/src/registry/cli.ts +0 -63
- package/src/registry/server.ts +0 -504
- package/src/runtime/agent-loop.ts +0 -779
- package/src/runtime/compaction.ts +0 -638
- package/src/runtime/email-channel.ts +0 -120
- package/src/runtime/environment.ts +0 -300
- package/src/runtime/followup.ts +0 -211
- package/src/runtime/gateway.ts +0 -260
- package/src/runtime/hooks.ts +0 -564
- package/src/runtime/index.ts +0 -1110
- package/src/runtime/llm-client.ts +0 -1056
- package/src/runtime/model-router.ts +0 -97
- package/src/runtime/providers.ts +0 -228
- package/src/runtime/session-manager.ts +0 -345
- package/src/runtime/subagent.ts +0 -153
- package/src/runtime/tool-executor.ts +0 -208
- package/src/runtime/types.ts +0 -255
- package/src/security/brute-force.ts +0 -423
- package/src/security/config.ts +0 -159
- package/src/security/csp.ts +0 -407
- package/src/security/external-content.ts +0 -299
- package/src/security/index.ts +0 -557
- package/src/security/input-sanitizer.ts +0 -452
- package/src/security/output-filter.ts +0 -575
- package/src/security/port-scanner.ts +0 -342
- package/src/security/prompt-guard.ts +0 -387
- package/src/security/sql-guard.ts +0 -338
- package/src/security/threat-logger.ts +0 -484
- package/src/server.ts +0 -828
- package/src/setup/company.ts +0 -183
- package/src/setup/database.ts +0 -153
- package/src/setup/deployment.ts +0 -561
- package/src/setup/domain.ts +0 -112
- package/src/setup/index.ts +0 -171
- package/src/setup/provision.ts +0 -532
- package/src/setup/registration.ts +0 -302
- package/src/system-prompts/catchup.ts +0 -48
- package/src/system-prompts/google/calendar.ts +0 -37
- package/src/system-prompts/google/chat.ts +0 -92
- package/src/system-prompts/google/contacts.ts +0 -25
- package/src/system-prompts/google/docs.ts +0 -29
- package/src/system-prompts/google/drive.ts +0 -34
- package/src/system-prompts/google/forms.ts +0 -25
- package/src/system-prompts/google/gmail.ts +0 -50
- package/src/system-prompts/google/index.ts +0 -23
- package/src/system-prompts/google/maps.ts +0 -20
- package/src/system-prompts/google/meet.ts +0 -130
- package/src/system-prompts/google/sheets.ts +0 -32
- package/src/system-prompts/google/slides.ts +0 -26
- package/src/system-prompts/google/tasks.ts +0 -27
- package/src/system-prompts/index.ts +0 -88
- package/src/system-prompts/microsoft/contacts.ts +0 -34
- package/src/system-prompts/microsoft/excel.ts +0 -52
- package/src/system-prompts/microsoft/index.ts +0 -31
- package/src/system-prompts/microsoft/onedrive.ts +0 -41
- package/src/system-prompts/microsoft/onenote.ts +0 -36
- package/src/system-prompts/microsoft/outlook-calendar.ts +0 -37
- package/src/system-prompts/microsoft/outlook-mail.ts +0 -46
- package/src/system-prompts/microsoft/planner.ts +0 -37
- package/src/system-prompts/microsoft/powerbi.ts +0 -38
- package/src/system-prompts/microsoft/powerpoint.ts +0 -35
- package/src/system-prompts/microsoft/sharepoint.ts +0 -44
- package/src/system-prompts/microsoft/teams.ts +0 -49
- package/src/system-prompts/microsoft/todo.ts +0 -37
- package/src/system-prompts/shared-blocks.ts +0 -87
- package/src/system-prompts/task.ts +0 -21
- package/src/system-prompts/triage.ts +0 -34
- package/src/types/hono-env.ts +0 -18
- package/src/types/optional-deps.d.ts +0 -10
package/src/admin/routes.ts
DELETED
|
@@ -1,2968 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Admin API Routes
|
|
3
|
-
*
|
|
4
|
-
* CRUD for agents, users, audit logs, rules, settings.
|
|
5
|
-
* All routes are protected by auth middleware (applied in server.ts).
|
|
6
|
-
* Input validation on all mutations. RBAC on sensitive operations.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { Hono } from 'hono';
|
|
10
|
-
import { configBus } from '../engine/config-bus.js';
|
|
11
|
-
import type { AppEnv } from '../types/hono-env.js';
|
|
12
|
-
import type { DatabaseAdapter } from '../db/adapter.js';
|
|
13
|
-
import { validate, requireRole, ValidationError, transportEncryptionMiddleware } from '../middleware/index.js';
|
|
14
|
-
import { PROVIDER_REGISTRY, type ProviderDef, type CustomProviderDef } from '../runtime/providers.js';
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Validate an API key by making a lightweight request to the provider.
|
|
18
|
-
* Each provider has a different validation endpoint.
|
|
19
|
-
*/
|
|
20
|
-
async function validateProviderApiKey(
|
|
21
|
-
providerId: string,
|
|
22
|
-
apiKey: string,
|
|
23
|
-
provider: ProviderDef,
|
|
24
|
-
): Promise<{ ok: boolean; error?: string }> {
|
|
25
|
-
const timeout = 10_000;
|
|
26
|
-
const ctrl = new AbortController();
|
|
27
|
-
const timer = setTimeout(() => ctrl.abort(), timeout);
|
|
28
|
-
|
|
29
|
-
try {
|
|
30
|
-
let resp: Response;
|
|
31
|
-
|
|
32
|
-
switch (providerId) {
|
|
33
|
-
case 'anthropic': {
|
|
34
|
-
// POST /v1/messages with a tiny request — Anthropic returns 401 for bad keys
|
|
35
|
-
resp = await fetch('https://api.anthropic.com/v1/messages', {
|
|
36
|
-
method: 'POST',
|
|
37
|
-
headers: { 'x-api-key': apiKey, 'anthropic-version': '2023-06-01', 'content-type': 'application/json' },
|
|
38
|
-
body: JSON.stringify({ model: 'claude-haiku-4-20250414', max_tokens: 1, messages: [{ role: 'user', content: 'hi' }] }),
|
|
39
|
-
signal: ctrl.signal,
|
|
40
|
-
});
|
|
41
|
-
// 200 or 400 (valid key, bad request) = key works; 401/403 = bad key
|
|
42
|
-
if (resp.status === 401 || resp.status === 403) {
|
|
43
|
-
return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };
|
|
44
|
-
}
|
|
45
|
-
return { ok: true };
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
case 'openai': {
|
|
49
|
-
// GET /v1/models — lightweight, just lists models
|
|
50
|
-
resp = await fetch('https://api.openai.com/v1/models', {
|
|
51
|
-
headers: { Authorization: 'Bearer ' + apiKey },
|
|
52
|
-
signal: ctrl.signal,
|
|
53
|
-
});
|
|
54
|
-
if (resp.status === 401 || resp.status === 403) {
|
|
55
|
-
return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };
|
|
56
|
-
}
|
|
57
|
-
return { ok: true };
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
case 'google': {
|
|
61
|
-
// GET /v1beta/models — list Gemini models
|
|
62
|
-
resp = await fetch('https://generativelanguage.googleapis.com/v1beta/models?key=' + apiKey, {
|
|
63
|
-
signal: ctrl.signal,
|
|
64
|
-
});
|
|
65
|
-
if (resp.status === 400 || resp.status === 401 || resp.status === 403) {
|
|
66
|
-
return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };
|
|
67
|
-
}
|
|
68
|
-
return { ok: true };
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
case 'xai': {
|
|
72
|
-
resp = await fetch('https://api.x.ai/v1/models', {
|
|
73
|
-
headers: { Authorization: 'Bearer ' + apiKey },
|
|
74
|
-
signal: ctrl.signal,
|
|
75
|
-
});
|
|
76
|
-
if (resp.status === 401 || resp.status === 403) {
|
|
77
|
-
return { ok: false, error: 'Invalid API key' };
|
|
78
|
-
}
|
|
79
|
-
return { ok: true };
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
case 'deepseek': {
|
|
83
|
-
resp = await fetch('https://api.deepseek.com/models', {
|
|
84
|
-
headers: { Authorization: 'Bearer ' + apiKey },
|
|
85
|
-
signal: ctrl.signal,
|
|
86
|
-
});
|
|
87
|
-
if (resp.status === 401 || resp.status === 403) {
|
|
88
|
-
return { ok: false, error: 'Invalid API key' };
|
|
89
|
-
}
|
|
90
|
-
return { ok: true };
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
case 'mistral': {
|
|
94
|
-
resp = await fetch('https://api.mistral.ai/v1/models', {
|
|
95
|
-
headers: { Authorization: 'Bearer ' + apiKey },
|
|
96
|
-
signal: ctrl.signal,
|
|
97
|
-
});
|
|
98
|
-
if (resp.status === 401 || resp.status === 403) {
|
|
99
|
-
return { ok: false, error: 'Invalid API key' };
|
|
100
|
-
}
|
|
101
|
-
return { ok: true };
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
case 'groq': {
|
|
105
|
-
resp = await fetch('https://api.groq.com/openai/v1/models', {
|
|
106
|
-
headers: { Authorization: 'Bearer ' + apiKey },
|
|
107
|
-
signal: ctrl.signal,
|
|
108
|
-
});
|
|
109
|
-
if (resp.status === 401 || resp.status === 403) {
|
|
110
|
-
return { ok: false, error: 'Invalid API key' };
|
|
111
|
-
}
|
|
112
|
-
return { ok: true };
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
case 'together': {
|
|
116
|
-
resp = await fetch('https://api.together.xyz/v1/models', {
|
|
117
|
-
headers: { Authorization: 'Bearer ' + apiKey },
|
|
118
|
-
signal: ctrl.signal,
|
|
119
|
-
});
|
|
120
|
-
if (resp.status === 401 || resp.status === 403) {
|
|
121
|
-
return { ok: false, error: 'Invalid API key' };
|
|
122
|
-
}
|
|
123
|
-
return { ok: true };
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
case 'openrouter': {
|
|
127
|
-
resp = await fetch('https://openrouter.ai/api/v1/models', {
|
|
128
|
-
headers: { Authorization: 'Bearer ' + apiKey },
|
|
129
|
-
signal: ctrl.signal,
|
|
130
|
-
});
|
|
131
|
-
if (resp.status === 401 || resp.status === 403) {
|
|
132
|
-
return { ok: false, error: 'Invalid API key' };
|
|
133
|
-
}
|
|
134
|
-
return { ok: true };
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
case 'fireworks': {
|
|
138
|
-
resp = await fetch('https://api.fireworks.ai/inference/v1/models', {
|
|
139
|
-
headers: { Authorization: 'Bearer ' + apiKey },
|
|
140
|
-
signal: ctrl.signal,
|
|
141
|
-
});
|
|
142
|
-
if (resp.status === 401 || resp.status === 403) {
|
|
143
|
-
return { ok: false, error: 'Invalid API key' };
|
|
144
|
-
}
|
|
145
|
-
return { ok: true };
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
case 'cerebras': {
|
|
149
|
-
resp = await fetch('https://api.cerebras.ai/v1/models', {
|
|
150
|
-
headers: { Authorization: 'Bearer ' + apiKey },
|
|
151
|
-
signal: ctrl.signal,
|
|
152
|
-
});
|
|
153
|
-
if (resp.status === 401 || resp.status === 403) {
|
|
154
|
-
return { ok: false, error: 'Invalid API key' };
|
|
155
|
-
}
|
|
156
|
-
return { ok: true };
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// Local providers (ollama, vllm, lmstudio, litellm) — skip validation
|
|
160
|
-
case 'ollama':
|
|
161
|
-
case 'vllm':
|
|
162
|
-
case 'lmstudio':
|
|
163
|
-
case 'litellm':
|
|
164
|
-
return { ok: true };
|
|
165
|
-
|
|
166
|
-
default: {
|
|
167
|
-
// For unknown/custom providers, try GET /models with Bearer auth
|
|
168
|
-
try {
|
|
169
|
-
resp = await fetch(provider.baseUrl + '/models', {
|
|
170
|
-
headers: { Authorization: 'Bearer ' + apiKey },
|
|
171
|
-
signal: ctrl.signal,
|
|
172
|
-
});
|
|
173
|
-
if (resp.status === 401 || resp.status === 403) {
|
|
174
|
-
return { ok: false, error: 'Invalid API key' };
|
|
175
|
-
}
|
|
176
|
-
return { ok: true };
|
|
177
|
-
} catch {
|
|
178
|
-
// Can't reach — skip validation for custom providers
|
|
179
|
-
return { ok: true };
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
} catch (e: any) {
|
|
184
|
-
if (e.name === 'AbortError') {
|
|
185
|
-
return { ok: false, error: 'Validation timed out — provider not reachable' };
|
|
186
|
-
}
|
|
187
|
-
return { ok: false, error: e.message || 'Connection failed' };
|
|
188
|
-
} finally {
|
|
189
|
-
clearTimeout(timer);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
import { deployToFly, getAppStatus, destroyApp, type FlyConfig, type AppConfig } from '../deploy/fly.js';
|
|
193
|
-
import { SecureVault } from '../engine/vault.js';
|
|
194
|
-
|
|
195
|
-
// Shared vault instance for encrypting/decrypting provider API keys
|
|
196
|
-
const vault = new SecureVault();
|
|
197
|
-
|
|
198
|
-
export function createAdminRoutes(db: DatabaseAdapter) {
|
|
199
|
-
const api = new Hono<AppEnv>();
|
|
200
|
-
|
|
201
|
-
// Transport encryption middleware — decrypts incoming, encrypts outgoing
|
|
202
|
-
api.use('*', transportEncryptionMiddleware());
|
|
203
|
-
|
|
204
|
-
// Wrapper: updateSettings + auto-emit config change events
|
|
205
|
-
const updateSettingsAndEmit = async (updates: any) => {
|
|
206
|
-
const result = await db.updateSettings(updates);
|
|
207
|
-
configBus.emitSettings(Object.keys(updates));
|
|
208
|
-
return result;
|
|
209
|
-
};
|
|
210
|
-
|
|
211
|
-
// ─── Dashboard Stats ────────────────────────────────
|
|
212
|
-
|
|
213
|
-
api.get('/stats', async (c) => {
|
|
214
|
-
const clientOrgId = c.req.query('clientOrgId') || '';
|
|
215
|
-
if (clientOrgId) {
|
|
216
|
-
// Scoped stats for client org users
|
|
217
|
-
const allAgents = await db.listAgents({ limit: 1000, offset: 0 });
|
|
218
|
-
const orgAgents = allAgents.filter((a: any) => a.client_org_id === clientOrgId);
|
|
219
|
-
const activeOrgAgents = orgAgents.filter((a: any) => a.status === 'active');
|
|
220
|
-
// Count users in this client org
|
|
221
|
-
let orgUsers = 0;
|
|
222
|
-
try {
|
|
223
|
-
const allUsers = await db.listUsers();
|
|
224
|
-
orgUsers = allUsers.filter((u: any) => u.client_org_id === clientOrgId).length;
|
|
225
|
-
} catch { orgUsers = 0; }
|
|
226
|
-
// Count audit events for this org's agents
|
|
227
|
-
let orgAudit = 0;
|
|
228
|
-
try {
|
|
229
|
-
const agentIds = orgAgents.map((a: any) => a.id);
|
|
230
|
-
if (agentIds.length > 0) {
|
|
231
|
-
const result = await (db as any).pool?.query?.(
|
|
232
|
-
`SELECT COUNT(*) FROM audit_log WHERE org_id = $1`,
|
|
233
|
-
[clientOrgId]
|
|
234
|
-
);
|
|
235
|
-
orgAudit = result?.rows?.[0]?.count ? parseInt(result.rows[0].count, 10) : 0;
|
|
236
|
-
}
|
|
237
|
-
} catch { orgAudit = 0; }
|
|
238
|
-
return c.json({
|
|
239
|
-
totalAgents: orgAgents.length,
|
|
240
|
-
activeAgents: activeOrgAgents.length,
|
|
241
|
-
totalUsers: orgUsers,
|
|
242
|
-
totalEmails: 0,
|
|
243
|
-
totalAuditEvents: orgAudit,
|
|
244
|
-
});
|
|
245
|
-
}
|
|
246
|
-
const stats = await db.getStats();
|
|
247
|
-
return c.json(stats);
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
// ─── Agents ─────────────────────────────────────────
|
|
251
|
-
|
|
252
|
-
api.get('/agents', async (c) => {
|
|
253
|
-
const status = c.req.query('status') as any;
|
|
254
|
-
const clientOrgId = c.req.query('clientOrgId') || '';
|
|
255
|
-
const limit = Math.min(parseInt(c.req.query('limit') || '50'), 200);
|
|
256
|
-
const offset = Math.max(parseInt(c.req.query('offset') || '0'), 0);
|
|
257
|
-
let agents = await db.listAgents({ status, limit, offset });
|
|
258
|
-
let total = await db.countAgents(status);
|
|
259
|
-
// Filter by client org if requested
|
|
260
|
-
if (clientOrgId) {
|
|
261
|
-
agents = agents.filter((a: any) => a.client_org_id === clientOrgId);
|
|
262
|
-
total = agents.length;
|
|
263
|
-
}
|
|
264
|
-
return c.json({ agents, total, limit, offset });
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
api.get('/agents/:id', async (c) => {
|
|
268
|
-
const agent = await db.getAgent(c.req.param('id'));
|
|
269
|
-
if (!agent) return c.json({ error: 'Agent not found' }, 404);
|
|
270
|
-
return c.json(agent);
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
api.post('/agents', async (c) => {
|
|
274
|
-
const body = await c.req.json();
|
|
275
|
-
validate(body, [
|
|
276
|
-
{ field: 'name', type: 'string', required: true, minLength: 1, maxLength: 64, pattern: /^[a-zA-Z0-9_-]+$/ },
|
|
277
|
-
{ field: 'email', type: 'email' },
|
|
278
|
-
{ field: 'role', type: 'string', maxLength: 32 },
|
|
279
|
-
]);
|
|
280
|
-
|
|
281
|
-
// Check for duplicate name
|
|
282
|
-
const existing = await db.getAgentByName(body.name);
|
|
283
|
-
if (existing) {
|
|
284
|
-
return c.json({ error: 'Agent name already exists' }, 409);
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
const userId = c.get('userId') || 'system';
|
|
288
|
-
const agent = await db.createAgent({ ...body, createdBy: userId });
|
|
289
|
-
return c.json(agent, 201);
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
api.patch('/agents/:id', async (c) => {
|
|
293
|
-
const id = c.req.param('id');
|
|
294
|
-
const existing = await db.getAgent(id);
|
|
295
|
-
if (!existing) return c.json({ error: 'Agent not found' }, 404);
|
|
296
|
-
|
|
297
|
-
const body = await c.req.json();
|
|
298
|
-
validate(body, [
|
|
299
|
-
{ field: 'name', type: 'string', minLength: 1, maxLength: 64 },
|
|
300
|
-
{ field: 'email', type: 'email' },
|
|
301
|
-
{ field: 'role', type: 'string', maxLength: 32 },
|
|
302
|
-
{ field: 'status', type: 'string', pattern: /^(active|archived|suspended)$/ },
|
|
303
|
-
]);
|
|
304
|
-
|
|
305
|
-
// If renaming, check for conflicts
|
|
306
|
-
if (body.name && body.name !== existing.name) {
|
|
307
|
-
const conflict = await db.getAgentByName(body.name);
|
|
308
|
-
if (conflict) return c.json({ error: 'Agent name already exists' }, 409);
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
const agent = await db.updateAgent(id, body);
|
|
312
|
-
|
|
313
|
-
// Update billing_rate if provided
|
|
314
|
-
if ('billingRate' in body || 'billing_rate' in body) {
|
|
315
|
-
const rate = body.billingRate ?? body.billing_rate ?? 0;
|
|
316
|
-
try {
|
|
317
|
-
await (db as any).pool.query('UPDATE agents SET billing_rate = $1 WHERE id = $2', [rate, id]);
|
|
318
|
-
} catch {
|
|
319
|
-
try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE agents SET billing_rate = ? WHERE id = ?').run(rate, id); } catch { /* ignore */ }
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
configBus.emitAgentUpdate(id, Object.keys(body));
|
|
324
|
-
return c.json(agent);
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
api.post('/agents/:id/archive', async (c) => {
|
|
328
|
-
const existing = await db.getAgent(c.req.param('id'));
|
|
329
|
-
if (!existing) return c.json({ error: 'Agent not found' }, 404);
|
|
330
|
-
if (existing.status === 'archived') return c.json({ error: 'Agent already archived' }, 400);
|
|
331
|
-
|
|
332
|
-
await db.archiveAgent(c.req.param('id'));
|
|
333
|
-
return c.json({ ok: true, status: 'archived' });
|
|
334
|
-
});
|
|
335
|
-
|
|
336
|
-
api.post('/agents/:id/restore', async (c) => {
|
|
337
|
-
const existing = await db.getAgent(c.req.param('id'));
|
|
338
|
-
if (!existing) return c.json({ error: 'Agent not found' }, 404);
|
|
339
|
-
if (existing.status !== 'archived') return c.json({ error: 'Agent is not archived' }, 400);
|
|
340
|
-
|
|
341
|
-
await db.updateAgent(c.req.param('id'), { status: 'active' } as any);
|
|
342
|
-
return c.json({ ok: true, status: 'active' });
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
// Permanent delete — owner/admin only
|
|
346
|
-
api.delete('/agents/:id', requireRole('admin'), async (c) => {
|
|
347
|
-
const existing = await db.getAgent(c.req.param('id'));
|
|
348
|
-
if (!existing) return c.json({ error: 'Agent not found' }, 404);
|
|
349
|
-
|
|
350
|
-
await db.deleteAgent(c.req.param('id'));
|
|
351
|
-
return c.json({ ok: true });
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
// ─── Agent Deployment ─────────────────────────────────
|
|
355
|
-
|
|
356
|
-
api.post('/agents/:id/deploy', requireRole('admin'), async (c) => {
|
|
357
|
-
const agentId = c.req.param('id');
|
|
358
|
-
const agent = await db.getAgent(agentId);
|
|
359
|
-
if (!agent) return c.json({ error: 'Agent not found' }, 404);
|
|
360
|
-
|
|
361
|
-
const body = await c.req.json();
|
|
362
|
-
const targetType = body.targetType || 'fly';
|
|
363
|
-
const config = body.config || {};
|
|
364
|
-
|
|
365
|
-
// Get deployment credentials
|
|
366
|
-
const settings = await db.getSettings();
|
|
367
|
-
const pricingConfig = (settings as any)?.modelPricingConfig || {};
|
|
368
|
-
const _providerApiKeys = pricingConfig.providerApiKeys || {};
|
|
369
|
-
|
|
370
|
-
if (targetType === 'fly') {
|
|
371
|
-
// Get Fly.io API token from deploy credentials or config
|
|
372
|
-
let flyToken = config.flyApiToken || process.env.FLY_API_TOKEN;
|
|
373
|
-
if (!flyToken && body.credentialId) {
|
|
374
|
-
// Look up stored credential
|
|
375
|
-
try {
|
|
376
|
-
const creds = await (db as any).query?.('SELECT config FROM deploy_credentials WHERE id = $1', [body.credentialId]);
|
|
377
|
-
if (creds?.rows?.[0]?.config) {
|
|
378
|
-
const credConfig = typeof creds.rows[0].config === 'string' ? JSON.parse(creds.rows[0].config) : creds.rows[0].config;
|
|
379
|
-
flyToken = credConfig.apiToken;
|
|
380
|
-
}
|
|
381
|
-
} catch { /* ignore */ }
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
if (!flyToken) {
|
|
385
|
-
return c.json({ error: 'Fly.io API token required. Add it in Settings → Deployments or pass flyApiToken in config.' }, 400);
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
const flyConfig: FlyConfig = {
|
|
389
|
-
apiToken: flyToken,
|
|
390
|
-
org: config.flyOrg || 'personal',
|
|
391
|
-
image: config.image || 'node:22-slim',
|
|
392
|
-
regions: config.regions || ['iad'],
|
|
393
|
-
};
|
|
394
|
-
|
|
395
|
-
const agentName = (agent as any).name || agentId;
|
|
396
|
-
const appConfig: AppConfig = {
|
|
397
|
-
subdomain: agentName.toLowerCase().replace(/[^a-z0-9-]/g, '-'),
|
|
398
|
-
dbType: 'postgres',
|
|
399
|
-
dbConnectionString: process.env.DATABASE_URL || '',
|
|
400
|
-
jwtSecret: process.env.JWT_SECRET || 'agent-' + agentId,
|
|
401
|
-
smtpHost: (settings as any)?.smtpHost,
|
|
402
|
-
smtpPort: (settings as any)?.smtpPort,
|
|
403
|
-
smtpUser: (settings as any)?.smtpUser,
|
|
404
|
-
smtpPass: (settings as any)?.smtpPass,
|
|
405
|
-
memoryMb: config.memoryMb || 256,
|
|
406
|
-
cpuKind: config.cpuKind || 'shared',
|
|
407
|
-
cpus: config.cpus || 1,
|
|
408
|
-
};
|
|
409
|
-
|
|
410
|
-
try {
|
|
411
|
-
const result = await deployToFly(appConfig, flyConfig);
|
|
412
|
-
|
|
413
|
-
// Update agent record with deployment info (stored in metadata)
|
|
414
|
-
const existingAgent = await db.getAgent(agentId);
|
|
415
|
-
const existingMeta = (existingAgent as any)?.metadata || {};
|
|
416
|
-
await db.updateAgent(agentId, {
|
|
417
|
-
status: result.status === 'started' ? 'active' : 'error',
|
|
418
|
-
metadata: {
|
|
419
|
-
...existingMeta,
|
|
420
|
-
deployment: {
|
|
421
|
-
target: 'fly',
|
|
422
|
-
appName: result.appName,
|
|
423
|
-
url: result.url,
|
|
424
|
-
region: result.region,
|
|
425
|
-
machineId: result.machineId,
|
|
426
|
-
deployedAt: new Date().toISOString(),
|
|
427
|
-
deployedBy: body.deployedBy || 'dashboard',
|
|
428
|
-
status: result.status,
|
|
429
|
-
},
|
|
430
|
-
},
|
|
431
|
-
} as any);
|
|
432
|
-
|
|
433
|
-
return c.json({
|
|
434
|
-
success: result.status === 'started',
|
|
435
|
-
deployment: result,
|
|
436
|
-
});
|
|
437
|
-
} catch (err: any) {
|
|
438
|
-
return c.json({ error: 'Deployment failed: ' + err.message }, 500);
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
if (targetType === 'local') {
|
|
443
|
-
const existingAgent = await db.getAgent(agentId);
|
|
444
|
-
const existingMeta = (existingAgent as any)?.metadata || {};
|
|
445
|
-
await db.updateAgent(agentId, {
|
|
446
|
-
status: 'active',
|
|
447
|
-
metadata: {
|
|
448
|
-
...existingMeta,
|
|
449
|
-
deployment: {
|
|
450
|
-
target: 'local',
|
|
451
|
-
url: `http://localhost:${3000 + Math.floor(Math.random() * 1000)}`,
|
|
452
|
-
deployedAt: new Date().toISOString(),
|
|
453
|
-
deployedBy: body.deployedBy || 'dashboard',
|
|
454
|
-
status: 'started',
|
|
455
|
-
},
|
|
456
|
-
},
|
|
457
|
-
} as any);
|
|
458
|
-
return c.json({ success: true, deployment: { status: 'started', target: 'local' } });
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
return c.json({ error: 'Unsupported deploy target: ' + targetType + '. Supported: fly, docker, vps, local' }, 400);
|
|
462
|
-
});
|
|
463
|
-
|
|
464
|
-
// Get deployment status
|
|
465
|
-
api.get('/agents/:id/deploy', requireRole('admin'), async (c) => {
|
|
466
|
-
const agent = await db.getAgent(c.req.param('id'));
|
|
467
|
-
if (!agent) return c.json({ error: 'Agent not found' }, 404);
|
|
468
|
-
|
|
469
|
-
const meta = (agent as any).metadata || {};
|
|
470
|
-
const info = meta.deployment;
|
|
471
|
-
if (!info) return c.json({ deployed: false });
|
|
472
|
-
|
|
473
|
-
if (info.target === 'fly' && info.appName) {
|
|
474
|
-
const flyToken = process.env.FLY_API_TOKEN;
|
|
475
|
-
if (flyToken) {
|
|
476
|
-
try {
|
|
477
|
-
const status = await getAppStatus(info.appName, { apiToken: flyToken });
|
|
478
|
-
return c.json({ deployed: true, ...info, live: status });
|
|
479
|
-
} catch { /* fall through */ }
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
return c.json({ deployed: true, ...info });
|
|
484
|
-
});
|
|
485
|
-
|
|
486
|
-
// Destroy deployment
|
|
487
|
-
api.delete('/agents/:id/deploy', requireRole('admin'), async (c) => {
|
|
488
|
-
const agent = await db.getAgent(c.req.param('id'));
|
|
489
|
-
if (!agent) return c.json({ error: 'Agent not found' }, 404);
|
|
490
|
-
|
|
491
|
-
const meta = (agent as any).metadata || {};
|
|
492
|
-
const info = meta.deployment;
|
|
493
|
-
if (!info) return c.json({ error: 'Agent not deployed' }, 400);
|
|
494
|
-
|
|
495
|
-
if (info.target === 'fly' && info.appName) {
|
|
496
|
-
const flyToken = process.env.FLY_API_TOKEN;
|
|
497
|
-
if (flyToken) {
|
|
498
|
-
try {
|
|
499
|
-
await destroyApp(info.appName, { apiToken: flyToken });
|
|
500
|
-
} catch (err: any) {
|
|
501
|
-
return c.json({ error: 'Failed to destroy: ' + err.message }, 500);
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
delete meta.deployment;
|
|
507
|
-
await db.updateAgent(c.req.param('id'), { status: 'inactive', metadata: meta } as any);
|
|
508
|
-
return c.json({ ok: true, message: 'Deployment destroyed' });
|
|
509
|
-
});
|
|
510
|
-
|
|
511
|
-
// ─── Users ──────────────────────────────────────────
|
|
512
|
-
|
|
513
|
-
api.get('/users', requireRole('admin'), async (c) => {
|
|
514
|
-
const limit = Math.min(parseInt(c.req.query('limit') || '50'), 200);
|
|
515
|
-
const offset = Math.max(parseInt(c.req.query('offset') || '0'), 0);
|
|
516
|
-
const users = await db.listUsers({ limit, offset });
|
|
517
|
-
// Strip sensitive fields
|
|
518
|
-
const safe = users.map(({ passwordHash, totpSecret, totpBackupCodes, ...u }) => u);
|
|
519
|
-
return c.json({ users: safe, limit, offset });
|
|
520
|
-
});
|
|
521
|
-
|
|
522
|
-
api.post('/users', requireRole('admin'), async (c) => {
|
|
523
|
-
const body = await c.req.json();
|
|
524
|
-
validate(body, [
|
|
525
|
-
{ field: 'email', type: 'email', required: true },
|
|
526
|
-
{ field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },
|
|
527
|
-
{ field: 'role', type: 'string', required: true, pattern: /^(owner|admin|member|viewer)$/ },
|
|
528
|
-
{ field: 'password', type: 'string', minLength: 8, maxLength: 128 },
|
|
529
|
-
]);
|
|
530
|
-
|
|
531
|
-
// Check duplicate email
|
|
532
|
-
const existing = await db.getUserByEmail(body.email);
|
|
533
|
-
if (existing) return c.json({ error: 'Email already registered' }, 409);
|
|
534
|
-
|
|
535
|
-
const user = await db.createUser(body);
|
|
536
|
-
|
|
537
|
-
// Mark as must-reset-password (admin-created accounts)
|
|
538
|
-
try {
|
|
539
|
-
await (db as any).pool.query(
|
|
540
|
-
'UPDATE users SET must_reset_password = TRUE WHERE id = $1',
|
|
541
|
-
[user.id]
|
|
542
|
-
);
|
|
543
|
-
} catch {
|
|
544
|
-
try {
|
|
545
|
-
const edb = (db as any).db;
|
|
546
|
-
if (edb?.prepare) edb.prepare('UPDATE users SET must_reset_password = 1 WHERE id = ?').run(user.id);
|
|
547
|
-
} catch { /* ignore */ }
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
// Set client org if provided
|
|
551
|
-
if (body.clientOrgId) {
|
|
552
|
-
try {
|
|
553
|
-
await (db as any).pool.query('UPDATE users SET client_org_id = $1 WHERE id = $2', [body.clientOrgId, user.id]);
|
|
554
|
-
} catch {
|
|
555
|
-
try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE users SET client_org_id = ? WHERE id = ?').run(body.clientOrgId, user.id); } catch { /* ignore */ }
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
// Set initial permissions if provided
|
|
560
|
-
if (body.permissions && body.permissions !== '*') {
|
|
561
|
-
try {
|
|
562
|
-
await (db as any).pool.query(
|
|
563
|
-
'UPDATE users SET permissions = $1 WHERE id = $2',
|
|
564
|
-
[JSON.stringify(body.permissions), user.id]
|
|
565
|
-
);
|
|
566
|
-
} catch {
|
|
567
|
-
try {
|
|
568
|
-
const edb = (db as any).db;
|
|
569
|
-
if (edb?.prepare) edb.prepare('UPDATE users SET permissions = ? WHERE id = ?').run(JSON.stringify(body.permissions), user.id);
|
|
570
|
-
} catch { /* ignore */ }
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
const { passwordHash, ...safe } = user;
|
|
575
|
-
return c.json(safe, 201);
|
|
576
|
-
});
|
|
577
|
-
|
|
578
|
-
api.patch('/users/:id', requireRole('admin'), async (c) => {
|
|
579
|
-
const existing = await db.getUser(c.req.param('id'));
|
|
580
|
-
if (!existing) return c.json({ error: 'User not found' }, 404);
|
|
581
|
-
|
|
582
|
-
const body = await c.req.json();
|
|
583
|
-
validate(body, [
|
|
584
|
-
{ field: 'email', type: 'email' },
|
|
585
|
-
{ field: 'name', type: 'string', minLength: 1, maxLength: 128 },
|
|
586
|
-
{ field: 'role', type: 'string', pattern: /^(owner|admin|member|viewer)$/ },
|
|
587
|
-
]);
|
|
588
|
-
|
|
589
|
-
const user = await db.updateUser(c.req.param('id'), body);
|
|
590
|
-
|
|
591
|
-
// Update client_org_id if provided
|
|
592
|
-
if ('clientOrgId' in body) {
|
|
593
|
-
const orgVal = body.clientOrgId || null;
|
|
594
|
-
try {
|
|
595
|
-
await (db as any).pool.query('UPDATE users SET client_org_id = $1 WHERE id = $2', [orgVal, user.id]);
|
|
596
|
-
} catch {
|
|
597
|
-
try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE users SET client_org_id = ? WHERE id = ?').run(orgVal, user.id); } catch { /* ignore */ }
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
const { passwordHash, ...safe } = user;
|
|
602
|
-
return c.json(safe);
|
|
603
|
-
});
|
|
604
|
-
|
|
605
|
-
// ─── Reset Password (admin/owner can reset any user's password) ──
|
|
606
|
-
|
|
607
|
-
api.post('/users/:id/reset-password', requireRole('admin'), async (c) => {
|
|
608
|
-
const existing = await db.getUser(c.req.param('id'));
|
|
609
|
-
if (!existing) return c.json({ error: 'User not found' }, 404);
|
|
610
|
-
|
|
611
|
-
const body = await c.req.json();
|
|
612
|
-
const newPassword = body.password;
|
|
613
|
-
|
|
614
|
-
if (!newPassword || typeof newPassword !== 'string' || newPassword.length < 8) {
|
|
615
|
-
return c.json({ error: 'Password must be at least 8 characters' }, 400);
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
const { default: bcrypt } = await import('bcryptjs');
|
|
619
|
-
const passwordHash = await bcrypt.hash(newPassword, 12);
|
|
620
|
-
|
|
621
|
-
// Use raw SQL via the pool — updateUser doesn't handle password_hash
|
|
622
|
-
await (db as any).pool.query(
|
|
623
|
-
'UPDATE users SET password_hash = $1, updated_at = NOW() WHERE id = $2',
|
|
624
|
-
[passwordHash, c.req.param('id')]
|
|
625
|
-
);
|
|
626
|
-
|
|
627
|
-
await db.logEvent({
|
|
628
|
-
actor: c.get('userId') || 'system',
|
|
629
|
-
actorType: 'user',
|
|
630
|
-
action: 'user.password_reset',
|
|
631
|
-
resource: `user:${c.req.param('id')}`,
|
|
632
|
-
details: { targetEmail: existing.email, resetBy: 'admin' },
|
|
633
|
-
ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip'),
|
|
634
|
-
orgId: c.get('userOrgId' as any) || undefined,
|
|
635
|
-
}).catch(() => {});
|
|
636
|
-
|
|
637
|
-
return c.json({ ok: true, message: 'Password reset successfully' });
|
|
638
|
-
});
|
|
639
|
-
|
|
640
|
-
// ─── Deactivate / Reactivate User ──────────────────
|
|
641
|
-
|
|
642
|
-
api.post('/users/:id/deactivate', requireRole('admin'), async (c) => {
|
|
643
|
-
const existing = await db.getUser(c.req.param('id'));
|
|
644
|
-
if (!existing) return c.json({ error: 'User not found' }, 404);
|
|
645
|
-
const requesterId = c.get('userId');
|
|
646
|
-
if (requesterId === c.req.param('id')) return c.json({ error: 'Cannot deactivate your own account' }, 400);
|
|
647
|
-
|
|
648
|
-
try {
|
|
649
|
-
await (db as any).pool.query('UPDATE users SET is_active = FALSE, updated_at = NOW() WHERE id = $1', [c.req.param('id')]);
|
|
650
|
-
} catch {
|
|
651
|
-
const edb = (db as any).db;
|
|
652
|
-
if (edb?.prepare) edb.prepare('UPDATE users SET is_active = 0, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(c.req.param('id'));
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
await db.logEvent({
|
|
656
|
-
actor: c.get('userId') || 'system', actorType: 'user', action: 'user.deactivated',
|
|
657
|
-
resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },
|
|
658
|
-
ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),
|
|
659
|
-
orgId: c.get('userOrgId' as any) || undefined,
|
|
660
|
-
}).catch(() => {});
|
|
661
|
-
|
|
662
|
-
return c.json({ ok: true, message: 'User deactivated' });
|
|
663
|
-
});
|
|
664
|
-
|
|
665
|
-
api.post('/users/:id/reactivate', requireRole('admin'), async (c) => {
|
|
666
|
-
const existing = await db.getUser(c.req.param('id'));
|
|
667
|
-
if (!existing) return c.json({ error: 'User not found' }, 404);
|
|
668
|
-
|
|
669
|
-
try {
|
|
670
|
-
await (db as any).pool.query('UPDATE users SET is_active = TRUE, updated_at = NOW() WHERE id = $1', [c.req.param('id')]);
|
|
671
|
-
} catch {
|
|
672
|
-
const edb = (db as any).db;
|
|
673
|
-
if (edb?.prepare) edb.prepare('UPDATE users SET is_active = 1, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(c.req.param('id'));
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
await db.logEvent({
|
|
677
|
-
actor: c.get('userId') || 'system', actorType: 'user', action: 'user.reactivated',
|
|
678
|
-
resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },
|
|
679
|
-
ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),
|
|
680
|
-
orgId: c.get('userOrgId' as any) || undefined,
|
|
681
|
-
}).catch(() => {});
|
|
682
|
-
|
|
683
|
-
return c.json({ ok: true, message: 'User reactivated' });
|
|
684
|
-
});
|
|
685
|
-
|
|
686
|
-
// ─── Delete User (owner only, requires confirmation token) ──
|
|
687
|
-
|
|
688
|
-
api.delete('/users/:id', requireRole('owner'), async (c) => {
|
|
689
|
-
const existing = await db.getUser(c.req.param('id'));
|
|
690
|
-
if (!existing) return c.json({ error: 'User not found' }, 404);
|
|
691
|
-
|
|
692
|
-
const requesterId = c.get('userId');
|
|
693
|
-
if (requesterId === c.req.param('id')) return c.json({ error: 'Cannot delete your own account' }, 400);
|
|
694
|
-
|
|
695
|
-
// Require confirmation token from frontend (5-step modal flow)
|
|
696
|
-
const body = await c.req.json().catch(() => ({}));
|
|
697
|
-
if (body.confirmationToken !== 'DELETE_USER_' + existing.email) {
|
|
698
|
-
return c.json({ error: 'Invalid confirmation. Delete requires 5-step confirmation from the dashboard.' }, 400);
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
await db.deleteUser(c.req.param('id'));
|
|
702
|
-
|
|
703
|
-
await db.logEvent({
|
|
704
|
-
actor: c.get('userId') || 'system', actorType: 'user', action: 'user.deleted',
|
|
705
|
-
resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },
|
|
706
|
-
ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),
|
|
707
|
-
orgId: c.get('userOrgId' as any) || undefined,
|
|
708
|
-
}).catch(() => {});
|
|
709
|
-
|
|
710
|
-
return c.json({ ok: true });
|
|
711
|
-
});
|
|
712
|
-
|
|
713
|
-
// ─── Page Registry (for permission UI) ──────────────
|
|
714
|
-
|
|
715
|
-
api.get('/page-registry', requireRole('admin'), async (c) => {
|
|
716
|
-
const { PAGE_REGISTRY } = await import('./page-registry.js');
|
|
717
|
-
return c.json(PAGE_REGISTRY);
|
|
718
|
-
});
|
|
719
|
-
|
|
720
|
-
// ─── User Permissions ──────────────────────────────
|
|
721
|
-
|
|
722
|
-
api.get('/users/:id/permissions', requireRole('admin'), async (c) => {
|
|
723
|
-
const user = await db.getUser(c.req.param('id'));
|
|
724
|
-
if (!user) return c.json({ error: 'User not found' }, 404);
|
|
725
|
-
return c.json({ userId: c.req.param('id'), permissions: user.permissions ?? '*' });
|
|
726
|
-
});
|
|
727
|
-
|
|
728
|
-
api.put('/users/:id/permissions', requireRole('admin'), async (c) => {
|
|
729
|
-
const user = await db.getUser(c.req.param('id'));
|
|
730
|
-
if (!user) return c.json({ error: 'User not found' }, 404);
|
|
731
|
-
|
|
732
|
-
const body = await c.req.json();
|
|
733
|
-
const permissions = body.permissions;
|
|
734
|
-
|
|
735
|
-
// Validate: must be '*' or an object of pageId → true | string[]
|
|
736
|
-
if (permissions !== '*') {
|
|
737
|
-
if (typeof permissions !== 'object' || permissions === null || Array.isArray(permissions)) {
|
|
738
|
-
return c.json({ error: 'permissions must be "*" or an object mapping pageId to true or string[]' }, 400);
|
|
739
|
-
}
|
|
740
|
-
const { PAGE_REGISTRY } = await import('./page-registry.js');
|
|
741
|
-
for (const [pageId, grant] of Object.entries(permissions)) {
|
|
742
|
-
if (pageId === '_allowedAgents') {
|
|
743
|
-
// Validate: must be '*' or string[]
|
|
744
|
-
if (grant !== '*' && !Array.isArray(grant)) {
|
|
745
|
-
return c.json({ error: '_allowedAgents must be "*" or string[]' }, 400);
|
|
746
|
-
}
|
|
747
|
-
continue;
|
|
748
|
-
}
|
|
749
|
-
if (!(pageId in PAGE_REGISTRY)) {
|
|
750
|
-
return c.json({ error: `Unknown page: ${pageId}` }, 400);
|
|
751
|
-
}
|
|
752
|
-
if (grant !== true && !Array.isArray(grant)) {
|
|
753
|
-
return c.json({ error: `Permission for "${pageId}" must be true or string[]` }, 400);
|
|
754
|
-
}
|
|
755
|
-
}
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
const serialized = JSON.stringify(permissions);
|
|
759
|
-
try {
|
|
760
|
-
await (db as any).pool.query(
|
|
761
|
-
'UPDATE users SET permissions = $1, updated_at = NOW() WHERE id = $2',
|
|
762
|
-
[serialized, c.req.param('id')]
|
|
763
|
-
);
|
|
764
|
-
} catch {
|
|
765
|
-
// SQLite/other fallback
|
|
766
|
-
const edb = (db as any).db || (db as any).pool;
|
|
767
|
-
if (edb?.prepare) {
|
|
768
|
-
edb.prepare('UPDATE users SET permissions = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(serialized, c.req.param('id'));
|
|
769
|
-
}
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
await db.logEvent({
|
|
773
|
-
actor: c.get('userId') || 'system',
|
|
774
|
-
actorType: 'user',
|
|
775
|
-
action: 'user.permissions_updated',
|
|
776
|
-
resource: `user:${c.req.param('id')}`,
|
|
777
|
-
details: { permissions, targetEmail: user.email },
|
|
778
|
-
ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),
|
|
779
|
-
orgId: c.get('userOrgId' as any) || undefined,
|
|
780
|
-
}).catch(() => {});
|
|
781
|
-
|
|
782
|
-
return c.json({ ok: true, permissions });
|
|
783
|
-
});
|
|
784
|
-
|
|
785
|
-
// ─── Current User Permissions (for frontend filtering) ──
|
|
786
|
-
|
|
787
|
-
api.get('/me/permissions', async (c) => {
|
|
788
|
-
const userId = c.get('userId' as any);
|
|
789
|
-
const userRole = c.get('userRole' as any);
|
|
790
|
-
if (!userId) return c.json({ error: 'Not authenticated' }, 401);
|
|
791
|
-
|
|
792
|
-
const user = await db.getUser(userId);
|
|
793
|
-
const clientOrgId = user?.clientOrgId || c.get('clientOrgId' as any) || null;
|
|
794
|
-
|
|
795
|
-
// Owner and admin always get full access
|
|
796
|
-
if (userRole === 'owner' || userRole === 'admin') {
|
|
797
|
-
return c.json({ permissions: '*', role: userRole, clientOrgId });
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
// Client org users get restricted page access by default
|
|
801
|
-
if (clientOrgId) {
|
|
802
|
-
const userPerms = user?.permissions;
|
|
803
|
-
if (!userPerms || userPerms === '*') {
|
|
804
|
-
// Default client org pages — hide internal-only pages
|
|
805
|
-
const clientPages: Record<string, boolean> = {
|
|
806
|
-
dashboard: true, agents: true, roles: true, skills: true,
|
|
807
|
-
'community-skills': true, 'skill-connections': true, 'database-access': true,
|
|
808
|
-
knowledge: true, 'knowledge-contributions': true, 'memory-transfer': true,
|
|
809
|
-
approvals: true, 'org-chart': true, 'task-pipeline': true, workforce: true,
|
|
810
|
-
messages: true, guardrails: true, journal: true, activity: true,
|
|
811
|
-
dlp: true, compliance: true, vault: true, audit: true, settings: true,
|
|
812
|
-
};
|
|
813
|
-
return c.json({ permissions: clientPages, role: userRole, clientOrgId });
|
|
814
|
-
}
|
|
815
|
-
return c.json({ permissions: userPerms, role: userRole, clientOrgId });
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
return c.json({ permissions: user?.permissions ?? '*', role: userRole, clientOrgId });
|
|
819
|
-
});
|
|
820
|
-
|
|
821
|
-
// ─── Platform Capabilities ──────────────────────────
|
|
822
|
-
|
|
823
|
-
api.get('/platform-capabilities', requireRole('admin'), async (c) => {
|
|
824
|
-
const os = await import('node:os');
|
|
825
|
-
const settings = await db.getSettings();
|
|
826
|
-
return c.json({ capabilities: settings?.platformCapabilities || {}, serverOS: os.platform() });
|
|
827
|
-
});
|
|
828
|
-
|
|
829
|
-
api.put('/platform-capabilities', requireRole('owner'), async (c) => {
|
|
830
|
-
const body = await c.req.json();
|
|
831
|
-
const userId = c.get('userId') || 'system';
|
|
832
|
-
const capabilities = {
|
|
833
|
-
localSystemAccess: !!body.localSystemAccess,
|
|
834
|
-
telegram: !!body.telegram,
|
|
835
|
-
whatsapp: !!body.whatsapp,
|
|
836
|
-
enabledAt: new Date().toISOString(),
|
|
837
|
-
enabledBy: userId,
|
|
838
|
-
};
|
|
839
|
-
|
|
840
|
-
await updateSettingsAndEmit({ platformCapabilities: capabilities } as any);
|
|
841
|
-
|
|
842
|
-
// Also emit per-capability events for services that listen specifically
|
|
843
|
-
for (const [cap, enabled] of Object.entries(body)) {
|
|
844
|
-
if (cap === 'enabledAt' || cap === 'enabledBy') continue;
|
|
845
|
-
configBus.emitCapability(cap, !!enabled);
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
await db.logEvent({
|
|
849
|
-
actor: userId,
|
|
850
|
-
actorType: 'user',
|
|
851
|
-
action: 'platform.capabilities_updated',
|
|
852
|
-
resource: 'company_settings',
|
|
853
|
-
details: capabilities,
|
|
854
|
-
ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip'),
|
|
855
|
-
orgId: c.get('userOrgId' as any) || undefined,
|
|
856
|
-
}).catch(() => {});
|
|
857
|
-
|
|
858
|
-
return c.json({ ok: true, capabilities });
|
|
859
|
-
});
|
|
860
|
-
|
|
861
|
-
// ─── WhatsApp QR Code ────────────────────────────────
|
|
862
|
-
|
|
863
|
-
api.get('/whatsapp/qr/:agentId', requireRole('admin'), async (c) => {
|
|
864
|
-
try {
|
|
865
|
-
var { getWhatsAppQR, isWhatsAppConnected } = await import('../agent-tools/tools/messaging/whatsapp.js');
|
|
866
|
-
var agentId = c.req.param('agentId');
|
|
867
|
-
if (isWhatsAppConnected(agentId)) {
|
|
868
|
-
return c.json({ status: 'connected' });
|
|
869
|
-
}
|
|
870
|
-
var qr = getWhatsAppQR(agentId);
|
|
871
|
-
if (qr) {
|
|
872
|
-
return c.json({ status: 'awaiting_scan', qr });
|
|
873
|
-
}
|
|
874
|
-
return c.json({ status: 'not_initialized', message: 'Agent has not started WhatsApp connection yet.' });
|
|
875
|
-
} catch (err: any) {
|
|
876
|
-
return c.json({ error: err.message }, 500);
|
|
877
|
-
}
|
|
878
|
-
});
|
|
879
|
-
|
|
880
|
-
// ─── Audit Log ──────────────────────────────────────
|
|
881
|
-
|
|
882
|
-
api.get('/audit', requireRole('admin'), async (c) => {
|
|
883
|
-
const filters = {
|
|
884
|
-
actor: c.req.query('actor') || undefined,
|
|
885
|
-
action: c.req.query('action') || undefined,
|
|
886
|
-
resource: c.req.query('resource') || undefined,
|
|
887
|
-
orgId: c.req.query('orgId') || undefined,
|
|
888
|
-
from: c.req.query('from') ? new Date(c.req.query('from')!) : undefined,
|
|
889
|
-
to: c.req.query('to') ? new Date(c.req.query('to')!) : undefined,
|
|
890
|
-
limit: Math.min(parseInt(c.req.query('limit') || '50'), 500),
|
|
891
|
-
offset: Math.max(parseInt(c.req.query('offset') || '0'), 0),
|
|
892
|
-
};
|
|
893
|
-
|
|
894
|
-
// Validate date params
|
|
895
|
-
if (filters.from && isNaN(filters.from.getTime())) {
|
|
896
|
-
return c.json({ error: 'Invalid "from" date' }, 400);
|
|
897
|
-
}
|
|
898
|
-
if (filters.to && isNaN(filters.to.getTime())) {
|
|
899
|
-
return c.json({ error: 'Invalid "to" date' }, 400);
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
const result = await db.queryAudit(filters);
|
|
903
|
-
return c.json(result);
|
|
904
|
-
});
|
|
905
|
-
|
|
906
|
-
// ─── API Keys ───────────────────────────────────────
|
|
907
|
-
|
|
908
|
-
api.get('/api-keys', requireRole('admin'), async (c) => {
|
|
909
|
-
const keys = await db.listApiKeys();
|
|
910
|
-
// Never expose key hashes
|
|
911
|
-
const safe = keys.map(({ keyHash, ...k }) => k);
|
|
912
|
-
return c.json({ keys: safe });
|
|
913
|
-
});
|
|
914
|
-
|
|
915
|
-
api.post('/api-keys', requireRole('admin'), async (c) => {
|
|
916
|
-
const body = await c.req.json();
|
|
917
|
-
validate(body, [
|
|
918
|
-
{ field: 'name', type: 'string', required: true, minLength: 1, maxLength: 64 },
|
|
919
|
-
]);
|
|
920
|
-
|
|
921
|
-
const userId = c.get('userId') || 'system';
|
|
922
|
-
const scopes = Array.isArray(body.scopes) ? body.scopes : ['*'];
|
|
923
|
-
const expiresAt = body.expiresAt ? new Date(body.expiresAt) : undefined;
|
|
924
|
-
|
|
925
|
-
const { key, plaintext } = await db.createApiKey({
|
|
926
|
-
name: body.name,
|
|
927
|
-
scopes,
|
|
928
|
-
createdBy: userId,
|
|
929
|
-
expiresAt,
|
|
930
|
-
});
|
|
931
|
-
|
|
932
|
-
// Only time the plaintext key is returned — emphasize this
|
|
933
|
-
const { keyHash, ...safeKey } = key;
|
|
934
|
-
return c.json({
|
|
935
|
-
key: safeKey,
|
|
936
|
-
plaintext,
|
|
937
|
-
warning: 'Store this key securely. It will not be shown again.',
|
|
938
|
-
}, 201);
|
|
939
|
-
});
|
|
940
|
-
|
|
941
|
-
api.delete('/api-keys/:id', requireRole('admin'), async (c) => {
|
|
942
|
-
const existing = await db.getApiKey(c.req.param('id'));
|
|
943
|
-
if (!existing) return c.json({ error: 'API key not found' }, 404);
|
|
944
|
-
|
|
945
|
-
await db.revokeApiKey(c.req.param('id'));
|
|
946
|
-
return c.json({ ok: true, revoked: true });
|
|
947
|
-
});
|
|
948
|
-
|
|
949
|
-
// ─── Email Rules ────────────────────────────────────
|
|
950
|
-
|
|
951
|
-
api.get('/rules', async (c) => {
|
|
952
|
-
const agentId = c.req.query('agentId') || undefined;
|
|
953
|
-
const rules = await db.getRules(agentId);
|
|
954
|
-
return c.json({ rules });
|
|
955
|
-
});
|
|
956
|
-
|
|
957
|
-
api.post('/rules', async (c) => {
|
|
958
|
-
const body = await c.req.json();
|
|
959
|
-
validate(body, [
|
|
960
|
-
{ field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },
|
|
961
|
-
]);
|
|
962
|
-
|
|
963
|
-
// Validate conditions/actions are objects
|
|
964
|
-
if (body.conditions && typeof body.conditions !== 'object') {
|
|
965
|
-
return c.json({ error: 'conditions must be an object' }, 400);
|
|
966
|
-
}
|
|
967
|
-
if (body.actions && typeof body.actions !== 'object') {
|
|
968
|
-
return c.json({ error: 'actions must be an object' }, 400);
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
const rule = await db.createRule({
|
|
972
|
-
name: body.name,
|
|
973
|
-
agentId: body.agentId,
|
|
974
|
-
conditions: body.conditions || {},
|
|
975
|
-
actions: body.actions || {},
|
|
976
|
-
priority: body.priority ?? 0,
|
|
977
|
-
enabled: body.enabled ?? true,
|
|
978
|
-
});
|
|
979
|
-
return c.json(rule, 201);
|
|
980
|
-
});
|
|
981
|
-
|
|
982
|
-
api.patch('/rules/:id', async (c) => {
|
|
983
|
-
const body = await c.req.json();
|
|
984
|
-
const rule = await db.updateRule(c.req.param('id'), body);
|
|
985
|
-
return c.json(rule);
|
|
986
|
-
});
|
|
987
|
-
|
|
988
|
-
api.delete('/rules/:id', async (c) => {
|
|
989
|
-
await db.deleteRule(c.req.param('id'));
|
|
990
|
-
return c.json({ ok: true });
|
|
991
|
-
});
|
|
992
|
-
|
|
993
|
-
// ─── Settings ───────────────────────────────────────
|
|
994
|
-
|
|
995
|
-
api.get('/settings', async (c) => {
|
|
996
|
-
const settings = await db.getSettings();
|
|
997
|
-
if (!settings) return c.json({ error: 'Not configured' }, 404);
|
|
998
|
-
|
|
999
|
-
// Redact sensitive fields
|
|
1000
|
-
const safe = { ...settings } as any;
|
|
1001
|
-
if (safe.smtpPass) safe.smtpPass = '***';
|
|
1002
|
-
if (safe.dkimPrivateKey) safe.dkimPrivateKey = '***';
|
|
1003
|
-
// Redact SSO secrets
|
|
1004
|
-
if (safe.ssoConfig?.oidc?.clientSecret) {
|
|
1005
|
-
safe.ssoConfig = { ...safe.ssoConfig, oidc: { ...safe.ssoConfig.oidc, clientSecret: '***' } };
|
|
1006
|
-
}
|
|
1007
|
-
return c.json(safe);
|
|
1008
|
-
});
|
|
1009
|
-
|
|
1010
|
-
api.patch('/settings', requireRole('admin'), async (c) => {
|
|
1011
|
-
const body = await c.req.json();
|
|
1012
|
-
validate(body, [
|
|
1013
|
-
{ field: 'name', type: 'string', minLength: 1, maxLength: 128 },
|
|
1014
|
-
{ field: 'domain', type: 'string', maxLength: 253 },
|
|
1015
|
-
{ field: 'subdomain', type: 'string', maxLength: 64 },
|
|
1016
|
-
{ field: 'primaryColor', type: 'string', pattern: /^#[0-9a-fA-F]{6}$/ },
|
|
1017
|
-
{ field: 'logoUrl', type: 'url' },
|
|
1018
|
-
{ field: 'smtpHost', type: 'string', maxLength: 253 },
|
|
1019
|
-
{ field: 'smtpPort', type: 'number' },
|
|
1020
|
-
{ field: 'smtpUser', type: 'string', maxLength: 253 },
|
|
1021
|
-
{ field: 'smtpPass', type: 'string', maxLength: 253 },
|
|
1022
|
-
{ field: 'dkimPrivateKey', type: 'string' },
|
|
1023
|
-
{ field: 'cfApiToken', type: 'string', maxLength: 500 },
|
|
1024
|
-
{ field: 'cfAccountId', type: 'string', maxLength: 100 },
|
|
1025
|
-
{ field: 'plan', type: 'string', maxLength: 32 },
|
|
1026
|
-
{ field: 'signatureTemplate', type: 'string', maxLength: 10000 },
|
|
1027
|
-
{ field: 'branding', type: 'object' },
|
|
1028
|
-
]);
|
|
1029
|
-
|
|
1030
|
-
const settings = await updateSettingsAndEmit(body);
|
|
1031
|
-
return c.json(settings);
|
|
1032
|
-
});
|
|
1033
|
-
|
|
1034
|
-
// ─── Branding Asset Upload ──────────────────────────
|
|
1035
|
-
|
|
1036
|
-
api.post('/settings/branding', requireRole('admin'), async (c) => {
|
|
1037
|
-
const body = await c.req.json();
|
|
1038
|
-
const { type, data, filename } = body; // type: 'logo' | 'favicon' | 'login_bg', data: base64 string
|
|
1039
|
-
if (!type || !data) return c.json({ error: 'type and data are required' }, 400);
|
|
1040
|
-
if (!['logo', 'favicon', 'login_bg', 'login_logo'].includes(type)) return c.json({ error: 'Invalid type' }, 400);
|
|
1041
|
-
|
|
1042
|
-
const os = await import('node:os');
|
|
1043
|
-
const fs = await import('node:fs');
|
|
1044
|
-
const path = await import('node:path');
|
|
1045
|
-
|
|
1046
|
-
const brandDir = path.join(os.homedir(), '.agenticmail', 'branding');
|
|
1047
|
-
if (!fs.existsSync(brandDir)) fs.mkdirSync(brandDir, { recursive: true });
|
|
1048
|
-
|
|
1049
|
-
// Decode base64 (strip data URL prefix if present)
|
|
1050
|
-
const base64 = data.replace(/^data:[^;]+;base64,/, '');
|
|
1051
|
-
const buffer = Buffer.from(base64, 'base64');
|
|
1052
|
-
|
|
1053
|
-
// Determine extension from filename or data URL
|
|
1054
|
-
const ext = filename ? path.extname(filename).toLowerCase() : '.png';
|
|
1055
|
-
const validExts = ['.png', '.jpg', '.jpeg', '.svg', '.ico', '.gif', '.webp'];
|
|
1056
|
-
if (!validExts.includes(ext)) return c.json({ error: 'Invalid file type. Supported: ' + validExts.join(', ') }, 400);
|
|
1057
|
-
|
|
1058
|
-
// Save original
|
|
1059
|
-
const savedName = type + ext;
|
|
1060
|
-
fs.writeFileSync(path.join(brandDir, savedName), buffer);
|
|
1061
|
-
|
|
1062
|
-
// Auto-generate favicon from logo upload
|
|
1063
|
-
if (type === 'logo' || type === 'favicon') {
|
|
1064
|
-
try {
|
|
1065
|
-
// Generate multiple sizes for favicon
|
|
1066
|
-
const sharp = (await import('sharp')).default;
|
|
1067
|
-
const sizes = [16, 32, 48, 64, 180, 192, 512];
|
|
1068
|
-
for (const size of sizes) {
|
|
1069
|
-
await sharp(buffer).resize(size, size, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, `icon-${size}.png`));
|
|
1070
|
-
}
|
|
1071
|
-
// Generate ICO (just use 32px PNG as simple favicon)
|
|
1072
|
-
await sharp(buffer).resize(32, 32, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, 'favicon.png'));
|
|
1073
|
-
// Apple touch icon
|
|
1074
|
-
await sharp(buffer).resize(180, 180, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, 'apple-touch-icon.png'));
|
|
1075
|
-
} catch (e: any) {
|
|
1076
|
-
console.warn('[branding] Sharp not available, skipping icon generation:', e.message);
|
|
1077
|
-
// Still save the original — just won't have auto-generated sizes
|
|
1078
|
-
}
|
|
1079
|
-
}
|
|
1080
|
-
|
|
1081
|
-
// Save branding config to settings (with cache-busting timestamp)
|
|
1082
|
-
const settings = await db.getSettings();
|
|
1083
|
-
const branding = settings?.branding || {};
|
|
1084
|
-
const v = Date.now();
|
|
1085
|
-
(branding as any)[type] = `/branding/${savedName}?v=${v}`;
|
|
1086
|
-
if (type === 'logo' || type === 'favicon') {
|
|
1087
|
-
(branding as any).favicon = `/branding/favicon.png?v=${v}`;
|
|
1088
|
-
(branding as any).appleTouchIcon = `/branding/apple-touch-icon.png?v=${v}`;
|
|
1089
|
-
(branding as any).icon192 = `/branding/icon-192.png?v=${v}`;
|
|
1090
|
-
(branding as any).icon512 = `/branding/icon-512.png?v=${v}`;
|
|
1091
|
-
}
|
|
1092
|
-
await updateSettingsAndEmit({ branding });
|
|
1093
|
-
|
|
1094
|
-
return c.json({ success: true, branding, message: 'Branding assets saved. Refresh to see changes.' });
|
|
1095
|
-
});
|
|
1096
|
-
|
|
1097
|
-
api.delete('/settings/branding/:type', requireRole('admin'), async (c) => {
|
|
1098
|
-
const type = c.req.param('type');
|
|
1099
|
-
if (!['logo', 'favicon', 'login_bg', 'login_logo'].includes(type)) return c.json({ error: 'Invalid type' }, 400);
|
|
1100
|
-
|
|
1101
|
-
const settings = await db.getSettings();
|
|
1102
|
-
const branding = settings?.branding || {};
|
|
1103
|
-
delete (branding as any)[type];
|
|
1104
|
-
// If removing logo, also remove auto-generated icons
|
|
1105
|
-
if (type === 'logo') {
|
|
1106
|
-
delete (branding as any).favicon;
|
|
1107
|
-
delete (branding as any).appleTouchIcon;
|
|
1108
|
-
delete (branding as any).icon192;
|
|
1109
|
-
delete (branding as any).icon512;
|
|
1110
|
-
}
|
|
1111
|
-
await updateSettingsAndEmit({ branding });
|
|
1112
|
-
return c.json({ success: true, branding });
|
|
1113
|
-
});
|
|
1114
|
-
|
|
1115
|
-
// ─── SSO Configuration ────────────────────────────
|
|
1116
|
-
|
|
1117
|
-
api.get('/settings/sso', requireRole('admin'), async (c) => {
|
|
1118
|
-
const settings = await db.getSettings();
|
|
1119
|
-
if (!settings) return c.json({ ssoConfig: null });
|
|
1120
|
-
|
|
1121
|
-
const sso = settings.ssoConfig || {};
|
|
1122
|
-
// Redact secrets for display
|
|
1123
|
-
const safe = { ...sso } as any;
|
|
1124
|
-
if (safe.oidc?.clientSecret) {
|
|
1125
|
-
safe.oidc = { ...safe.oidc, clientSecret: '***' };
|
|
1126
|
-
}
|
|
1127
|
-
if (safe.saml?.certificate) {
|
|
1128
|
-
// Show first/last 20 chars of cert
|
|
1129
|
-
const cert = safe.saml.certificate;
|
|
1130
|
-
safe.saml = {
|
|
1131
|
-
...safe.saml,
|
|
1132
|
-
certificate: cert.length > 50
|
|
1133
|
-
? cert.substring(0, 20) + '...' + cert.substring(cert.length - 20)
|
|
1134
|
-
: cert,
|
|
1135
|
-
certificateConfigured: true,
|
|
1136
|
-
};
|
|
1137
|
-
}
|
|
1138
|
-
return c.json({ ssoConfig: safe });
|
|
1139
|
-
});
|
|
1140
|
-
|
|
1141
|
-
api.put('/settings/sso/saml', requireRole('admin'), async (c) => {
|
|
1142
|
-
const body = await c.req.json();
|
|
1143
|
-
validate(body, [
|
|
1144
|
-
{ field: 'entityId', type: 'string', required: true, minLength: 1, maxLength: 512 },
|
|
1145
|
-
{ field: 'ssoUrl', type: 'url', required: true },
|
|
1146
|
-
{ field: 'certificate', type: 'string', required: true, minLength: 10 },
|
|
1147
|
-
]);
|
|
1148
|
-
|
|
1149
|
-
const settings = await db.getSettings();
|
|
1150
|
-
const current = settings?.ssoConfig || {};
|
|
1151
|
-
const ssoConfig = {
|
|
1152
|
-
...current,
|
|
1153
|
-
saml: {
|
|
1154
|
-
entityId: body.entityId,
|
|
1155
|
-
ssoUrl: body.ssoUrl,
|
|
1156
|
-
certificate: body.certificate,
|
|
1157
|
-
signatureAlgorithm: body.signatureAlgorithm || 'RSA-SHA256',
|
|
1158
|
-
autoProvision: body.autoProvision ?? true,
|
|
1159
|
-
defaultRole: body.defaultRole || 'member',
|
|
1160
|
-
allowedDomains: body.allowedDomains || [],
|
|
1161
|
-
},
|
|
1162
|
-
};
|
|
1163
|
-
|
|
1164
|
-
await updateSettingsAndEmit({ ssoConfig } as any);
|
|
1165
|
-
return c.json({ ok: true, provider: 'saml', configured: true });
|
|
1166
|
-
});
|
|
1167
|
-
|
|
1168
|
-
api.put('/settings/sso/oidc', requireRole('admin'), async (c) => {
|
|
1169
|
-
const body = await c.req.json();
|
|
1170
|
-
validate(body, [
|
|
1171
|
-
{ field: 'clientId', type: 'string', required: true, minLength: 1, maxLength: 256 },
|
|
1172
|
-
{ field: 'clientSecret', type: 'string', required: true, minLength: 1, maxLength: 512 },
|
|
1173
|
-
{ field: 'discoveryUrl', type: 'url', required: true },
|
|
1174
|
-
]);
|
|
1175
|
-
|
|
1176
|
-
const settings = await db.getSettings();
|
|
1177
|
-
const current = settings?.ssoConfig || {};
|
|
1178
|
-
|
|
1179
|
-
// If clientSecret is '***', keep the existing one
|
|
1180
|
-
let clientSecret = body.clientSecret;
|
|
1181
|
-
if (clientSecret === '***' && current.oidc?.clientSecret) {
|
|
1182
|
-
clientSecret = current.oidc.clientSecret;
|
|
1183
|
-
}
|
|
1184
|
-
|
|
1185
|
-
const ssoConfig = {
|
|
1186
|
-
...current,
|
|
1187
|
-
oidc: {
|
|
1188
|
-
clientId: body.clientId,
|
|
1189
|
-
clientSecret,
|
|
1190
|
-
discoveryUrl: body.discoveryUrl,
|
|
1191
|
-
scopes: body.scopes || ['openid', 'email', 'profile'],
|
|
1192
|
-
autoProvision: body.autoProvision ?? true,
|
|
1193
|
-
defaultRole: body.defaultRole || 'member',
|
|
1194
|
-
allowedDomains: body.allowedDomains || [],
|
|
1195
|
-
},
|
|
1196
|
-
};
|
|
1197
|
-
|
|
1198
|
-
await updateSettingsAndEmit({ ssoConfig } as any);
|
|
1199
|
-
return c.json({ ok: true, provider: 'oidc', configured: true });
|
|
1200
|
-
});
|
|
1201
|
-
|
|
1202
|
-
api.delete('/settings/sso/:provider', requireRole('admin'), async (c) => {
|
|
1203
|
-
const provider = c.req.param('provider');
|
|
1204
|
-
if (provider !== 'saml' && provider !== 'oidc') {
|
|
1205
|
-
return c.json({ error: 'Invalid provider. Use "saml" or "oidc".' }, 400);
|
|
1206
|
-
}
|
|
1207
|
-
|
|
1208
|
-
const settings = await db.getSettings();
|
|
1209
|
-
const current = settings?.ssoConfig || {};
|
|
1210
|
-
const ssoConfig = { ...current };
|
|
1211
|
-
delete (ssoConfig as any)[provider];
|
|
1212
|
-
|
|
1213
|
-
await updateSettingsAndEmit({ ssoConfig } as any);
|
|
1214
|
-
return c.json({ ok: true, provider, removed: true });
|
|
1215
|
-
});
|
|
1216
|
-
|
|
1217
|
-
// Test OIDC discovery URL
|
|
1218
|
-
api.post('/settings/sso/oidc/test', requireRole('admin'), async (c) => {
|
|
1219
|
-
const { discoveryUrl } = await c.req.json();
|
|
1220
|
-
if (!discoveryUrl) return c.json({ error: 'discoveryUrl required' }, 400);
|
|
1221
|
-
|
|
1222
|
-
try {
|
|
1223
|
-
const res = await fetch(discoveryUrl);
|
|
1224
|
-
if (!res.ok) return c.json({ ok: false, error: `HTTP ${res.status}` });
|
|
1225
|
-
const doc = await res.json();
|
|
1226
|
-
|
|
1227
|
-
return c.json({
|
|
1228
|
-
ok: true,
|
|
1229
|
-
issuer: doc.issuer,
|
|
1230
|
-
hasAuthorizationEndpoint: !!doc.authorization_endpoint,
|
|
1231
|
-
hasTokenEndpoint: !!doc.token_endpoint,
|
|
1232
|
-
hasUserinfoEndpoint: !!doc.userinfo_endpoint,
|
|
1233
|
-
hasJwksUri: !!doc.jwks_uri,
|
|
1234
|
-
supportedScopes: doc.scopes_supported,
|
|
1235
|
-
});
|
|
1236
|
-
} catch (e: any) {
|
|
1237
|
-
return c.json({ ok: false, error: e.message });
|
|
1238
|
-
}
|
|
1239
|
-
});
|
|
1240
|
-
|
|
1241
|
-
// ─── Organization Email Config ─────────────────────
|
|
1242
|
-
|
|
1243
|
-
api.get('/settings/org-email', requireRole('admin'), async (c) => {
|
|
1244
|
-
const settings = await db.getSettings();
|
|
1245
|
-
const cfg = settings?.orgEmailConfig;
|
|
1246
|
-
if (!cfg) return c.json({ configured: false });
|
|
1247
|
-
return c.json({
|
|
1248
|
-
configured: cfg.configured || false,
|
|
1249
|
-
provider: cfg.provider,
|
|
1250
|
-
label: cfg.label,
|
|
1251
|
-
oauthClientId: cfg.oauthClientId,
|
|
1252
|
-
oauthTenantId: cfg.oauthTenantId,
|
|
1253
|
-
});
|
|
1254
|
-
});
|
|
1255
|
-
|
|
1256
|
-
api.put('/settings/org-email', requireRole('admin'), async (c) => {
|
|
1257
|
-
const body = await c.req.json();
|
|
1258
|
-
const { provider, oauthClientId, oauthClientSecret, oauthTenantId } = body;
|
|
1259
|
-
if (!provider || !['google', 'microsoft'].includes(provider)) {
|
|
1260
|
-
return c.json({ error: 'provider must be "google" or "microsoft"' }, 400);
|
|
1261
|
-
}
|
|
1262
|
-
if (!oauthClientId || !oauthClientSecret) {
|
|
1263
|
-
return c.json({ error: 'oauthClientId and oauthClientSecret are required' }, 400);
|
|
1264
|
-
}
|
|
1265
|
-
const label = provider === 'google' ? 'Google Workspace' : 'Microsoft 365';
|
|
1266
|
-
const orgEmailConfig = {
|
|
1267
|
-
provider,
|
|
1268
|
-
oauthClientId,
|
|
1269
|
-
oauthClientSecret,
|
|
1270
|
-
oauthTenantId: provider === 'microsoft' ? (oauthTenantId || 'common') : undefined,
|
|
1271
|
-
oauthRedirectUri: '', // Will be set per-agent at OAuth time
|
|
1272
|
-
configured: true,
|
|
1273
|
-
label,
|
|
1274
|
-
};
|
|
1275
|
-
await updateSettingsAndEmit({ orgEmailConfig } as any);
|
|
1276
|
-
return c.json({ success: true, orgEmailConfig: { configured: true, provider, label, oauthClientId, oauthTenantId: orgEmailConfig.oauthTenantId } });
|
|
1277
|
-
});
|
|
1278
|
-
|
|
1279
|
-
api.delete('/settings/org-email', requireRole('admin'), async (c) => {
|
|
1280
|
-
await updateSettingsAndEmit({ orgEmailConfig: null } as any);
|
|
1281
|
-
return c.json({ success: true });
|
|
1282
|
-
});
|
|
1283
|
-
|
|
1284
|
-
// ─── Tool Security Config ─────────────────────────
|
|
1285
|
-
|
|
1286
|
-
api.get('/settings/tool-security', requireRole('admin'), async (c) => {
|
|
1287
|
-
const settings = await db.getSettings();
|
|
1288
|
-
return c.json({ toolSecurityConfig: settings?.toolSecurityConfig || {} });
|
|
1289
|
-
});
|
|
1290
|
-
|
|
1291
|
-
api.put('/settings/tool-security', requireRole('admin'), async (c) => {
|
|
1292
|
-
const body = await c.req.json();
|
|
1293
|
-
// Validate top-level shape
|
|
1294
|
-
if (body && typeof body !== 'object') {
|
|
1295
|
-
return c.json({ error: 'Body must be a JSON object' }, 400);
|
|
1296
|
-
}
|
|
1297
|
-
await updateSettingsAndEmit({ toolSecurityConfig: body } as any);
|
|
1298
|
-
const settings = await db.getSettings();
|
|
1299
|
-
return c.json({ toolSecurityConfig: settings?.toolSecurityConfig || {} });
|
|
1300
|
-
});
|
|
1301
|
-
|
|
1302
|
-
// ─── Firewall Config ──────────────────────────────────
|
|
1303
|
-
|
|
1304
|
-
api.get('/settings/firewall', requireRole('admin'), async (c) => {
|
|
1305
|
-
const settings = await db.getSettings();
|
|
1306
|
-
return c.json({ firewallConfig: settings?.firewallConfig || {} });
|
|
1307
|
-
});
|
|
1308
|
-
|
|
1309
|
-
api.put('/settings/firewall', requireRole('admin'), async (c) => {
|
|
1310
|
-
const body = await c.req.json();
|
|
1311
|
-
if (body && typeof body !== 'object') {
|
|
1312
|
-
return c.json({ error: 'Body must be a JSON object' }, 400);
|
|
1313
|
-
}
|
|
1314
|
-
// Validate mode fields
|
|
1315
|
-
if (body.ipAccess?.mode && !['allowlist', 'blocklist'].includes(body.ipAccess.mode)) {
|
|
1316
|
-
return c.json({ error: 'ipAccess.mode must be "allowlist" or "blocklist"' }, 400);
|
|
1317
|
-
}
|
|
1318
|
-
if (body.egress?.mode && !['allowlist', 'blocklist'].includes(body.egress.mode)) {
|
|
1319
|
-
return c.json({ error: 'egress.mode must be "allowlist" or "blocklist"' }, 400);
|
|
1320
|
-
}
|
|
1321
|
-
// Validate CIDR entries
|
|
1322
|
-
const { isValidIpOrCidr } = await import('../lib/cidr.js');
|
|
1323
|
-
for (const entry of (body.ipAccess?.allowlist || [])) {
|
|
1324
|
-
if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in allowlist: ' + entry }, 400);
|
|
1325
|
-
}
|
|
1326
|
-
for (const entry of (body.ipAccess?.blocklist || [])) {
|
|
1327
|
-
if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in blocklist: ' + entry }, 400);
|
|
1328
|
-
}
|
|
1329
|
-
for (const entry of (body.trustedProxies?.ips || [])) {
|
|
1330
|
-
if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in trusted proxies: ' + entry }, 400);
|
|
1331
|
-
}
|
|
1332
|
-
// Self-lockout protection for allowlist mode
|
|
1333
|
-
if (body.ipAccess?.enabled && body.ipAccess?.mode === 'allowlist' && body.ipAccess?.allowlist?.length > 0) {
|
|
1334
|
-
const clientIp = c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip') || '';
|
|
1335
|
-
if (clientIp && clientIp !== 'unknown') {
|
|
1336
|
-
const { compileIpMatcher } = await import('../lib/cidr.js');
|
|
1337
|
-
const matcher = compileIpMatcher(body.ipAccess.allowlist);
|
|
1338
|
-
if (!matcher(clientIp)) {
|
|
1339
|
-
return c.json({ error: 'Your current IP (' + clientIp + ') is not in the allowlist. Add it first to avoid lockout.' }, 400);
|
|
1340
|
-
}
|
|
1341
|
-
}
|
|
1342
|
-
}
|
|
1343
|
-
await updateSettingsAndEmit({ firewallConfig: body } as any);
|
|
1344
|
-
// Hot-reload ALL network middleware (firewall, security headers, rate limiting, HTTPS, egress, proxy)
|
|
1345
|
-
try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}
|
|
1346
|
-
const settings = await db.getSettings();
|
|
1347
|
-
return c.json({ firewallConfig: settings?.firewallConfig || {} });
|
|
1348
|
-
});
|
|
1349
|
-
|
|
1350
|
-
api.post('/settings/firewall/test-ip', requireRole('admin'), async (c) => {
|
|
1351
|
-
const { ip } = await c.req.json();
|
|
1352
|
-
if (!ip) return c.json({ error: 'ip is required' }, 400);
|
|
1353
|
-
const { isValidIpOrCidr, compileIpMatcher } = await import('../lib/cidr.js');
|
|
1354
|
-
if (!isValidIpOrCidr(ip)) return c.json({ error: 'Invalid IP address' }, 400);
|
|
1355
|
-
const settings = await db.getSettings();
|
|
1356
|
-
const ipAccess = settings?.firewallConfig?.ipAccess;
|
|
1357
|
-
if (!ipAccess?.enabled) {
|
|
1358
|
-
return c.json({ ip, allowed: true, reason: 'IP access control is disabled' });
|
|
1359
|
-
}
|
|
1360
|
-
if (ipAccess.mode === 'allowlist') {
|
|
1361
|
-
const matcher = compileIpMatcher(ipAccess.allowlist || []);
|
|
1362
|
-
const allowed = matcher(ip);
|
|
1363
|
-
return c.json({ ip, allowed, reason: allowed ? 'IP matches allowlist' : 'IP not in allowlist' });
|
|
1364
|
-
} else {
|
|
1365
|
-
const matcher = compileIpMatcher(ipAccess.blocklist || []);
|
|
1366
|
-
const blocked = matcher(ip);
|
|
1367
|
-
return c.json({ ip, allowed: !blocked, reason: blocked ? 'IP matches blocklist' : 'IP not in blocklist' });
|
|
1368
|
-
}
|
|
1369
|
-
});
|
|
1370
|
-
|
|
1371
|
-
// ─── Model Pricing Config ──────────────────────────────
|
|
1372
|
-
|
|
1373
|
-
api.get('/settings/model-pricing', requireRole('admin'), async (c) => {
|
|
1374
|
-
const settings = await db.getSettings();
|
|
1375
|
-
var config = settings?.modelPricingConfig || { models: [], currency: 'USD' };
|
|
1376
|
-
// Pre-seed with defaults if empty
|
|
1377
|
-
if (!config.models || config.models.length === 0) {
|
|
1378
|
-
config.models = getDefaultModelPricing();
|
|
1379
|
-
}
|
|
1380
|
-
return c.json({ modelPricingConfig: config });
|
|
1381
|
-
});
|
|
1382
|
-
|
|
1383
|
-
api.put('/settings/model-pricing', requireRole('admin'), async (c) => {
|
|
1384
|
-
const body = await c.req.json();
|
|
1385
|
-
if (!body || typeof body !== 'object') {
|
|
1386
|
-
return c.json({ error: 'Body must be a JSON object' }, 400);
|
|
1387
|
-
}
|
|
1388
|
-
// Validate models array
|
|
1389
|
-
if (body.models && Array.isArray(body.models)) {
|
|
1390
|
-
for (const m of body.models) {
|
|
1391
|
-
if (!m.provider || !m.modelId) {
|
|
1392
|
-
return c.json({ error: 'Each model must have provider and modelId' }, 400);
|
|
1393
|
-
}
|
|
1394
|
-
if (typeof m.inputCostPerMillion !== 'number' || m.inputCostPerMillion < 0) {
|
|
1395
|
-
return c.json({ error: `Invalid inputCostPerMillion for ${m.modelId}` }, 400);
|
|
1396
|
-
}
|
|
1397
|
-
if (typeof m.outputCostPerMillion !== 'number' || m.outputCostPerMillion < 0) {
|
|
1398
|
-
return c.json({ error: `Invalid outputCostPerMillion for ${m.modelId}` }, 400);
|
|
1399
|
-
}
|
|
1400
|
-
}
|
|
1401
|
-
}
|
|
1402
|
-
body.updatedAt = new Date().toISOString();
|
|
1403
|
-
await updateSettingsAndEmit({ modelPricingConfig: body } as any);
|
|
1404
|
-
const settings = await db.getSettings();
|
|
1405
|
-
return c.json({ modelPricingConfig: settings?.modelPricingConfig || {} });
|
|
1406
|
-
});
|
|
1407
|
-
|
|
1408
|
-
// ─── Provider Management ─────────────────────────────
|
|
1409
|
-
|
|
1410
|
-
api.get('/providers', requireRole('admin'), async (c) => {
|
|
1411
|
-
var settings = await db.getSettings();
|
|
1412
|
-
var pricingConfig = (settings as any)?.modelPricingConfig;
|
|
1413
|
-
var savedApiKeys = pricingConfig?.providerApiKeys || {};
|
|
1414
|
-
var builtIn = Object.values(PROVIDER_REGISTRY).map(function(p) {
|
|
1415
|
-
var configured = !p.requiresApiKey || !!savedApiKeys[p.id];
|
|
1416
|
-
return {
|
|
1417
|
-
id: p.id,
|
|
1418
|
-
name: p.name,
|
|
1419
|
-
baseUrl: p.baseUrl,
|
|
1420
|
-
apiType: p.apiType,
|
|
1421
|
-
isLocal: p.isLocal,
|
|
1422
|
-
requiresApiKey: p.requiresApiKey,
|
|
1423
|
-
configured: configured,
|
|
1424
|
-
source: 'built-in' as const,
|
|
1425
|
-
defaultModels: p.defaultModels || [],
|
|
1426
|
-
};
|
|
1427
|
-
});
|
|
1428
|
-
|
|
1429
|
-
var customProviders = pricingConfig?.customProviders || [];
|
|
1430
|
-
var custom = customProviders.map(function(p: any) {
|
|
1431
|
-
return { ...p, configured: true, source: 'custom' as const };
|
|
1432
|
-
});
|
|
1433
|
-
|
|
1434
|
-
return c.json({ providers: [...builtIn, ...custom] });
|
|
1435
|
-
});
|
|
1436
|
-
|
|
1437
|
-
api.post('/providers', requireRole('admin'), async (c) => {
|
|
1438
|
-
var body = await c.req.json();
|
|
1439
|
-
if (!body.id || !body.name || !body.baseUrl || !body.apiType) {
|
|
1440
|
-
return c.json({ error: 'id, name, baseUrl, and apiType are required' }, 400);
|
|
1441
|
-
}
|
|
1442
|
-
if (PROVIDER_REGISTRY[body.id]) {
|
|
1443
|
-
return c.json({ error: 'Cannot override built-in provider' }, 409);
|
|
1444
|
-
}
|
|
1445
|
-
var validTypes = ['anthropic', 'openai-compatible', 'google', 'ollama'];
|
|
1446
|
-
if (!validTypes.includes(body.apiType)) {
|
|
1447
|
-
return c.json({ error: 'apiType must be one of: ' + validTypes.join(', ') }, 400);
|
|
1448
|
-
}
|
|
1449
|
-
|
|
1450
|
-
var settings = await db.getSettings();
|
|
1451
|
-
var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };
|
|
1452
|
-
config.customProviders = config.customProviders || [];
|
|
1453
|
-
|
|
1454
|
-
if (config.customProviders.find(function(p: any) { return p.id === body.id; })) {
|
|
1455
|
-
return c.json({ error: 'Custom provider with this ID already exists' }, 409);
|
|
1456
|
-
}
|
|
1457
|
-
|
|
1458
|
-
config.customProviders.push({
|
|
1459
|
-
id: body.id,
|
|
1460
|
-
name: body.name,
|
|
1461
|
-
baseUrl: body.baseUrl,
|
|
1462
|
-
apiType: body.apiType,
|
|
1463
|
-
apiKeyEnvVar: body.apiKeyEnvVar || '',
|
|
1464
|
-
headers: body.headers || {},
|
|
1465
|
-
models: body.models || [],
|
|
1466
|
-
});
|
|
1467
|
-
|
|
1468
|
-
await updateSettingsAndEmit({ modelPricingConfig: config } as any);
|
|
1469
|
-
return c.json({ ok: true, provider: body });
|
|
1470
|
-
});
|
|
1471
|
-
|
|
1472
|
-
// ─── Provider API Key Management ────────────────────────
|
|
1473
|
-
api.post('/providers/:id/api-key', requireRole('admin'), async (c) => {
|
|
1474
|
-
var id = c.req.param('id');
|
|
1475
|
-
var provider = PROVIDER_REGISTRY[id];
|
|
1476
|
-
if (!provider) {
|
|
1477
|
-
return c.json({ error: 'Unknown provider' }, 404);
|
|
1478
|
-
}
|
|
1479
|
-
var body = await c.req.json();
|
|
1480
|
-
var apiKey = body.apiKey?.trim();
|
|
1481
|
-
if (!apiKey || typeof apiKey !== 'string' || apiKey.length < 5) {
|
|
1482
|
-
return c.json({ error: 'Valid API key required' }, 400);
|
|
1483
|
-
}
|
|
1484
|
-
var skipValidation = body.skipValidation === true;
|
|
1485
|
-
|
|
1486
|
-
// Validate the API key against the provider before saving
|
|
1487
|
-
if (!skipValidation) {
|
|
1488
|
-
try {
|
|
1489
|
-
var valid = await validateProviderApiKey(id, apiKey, provider);
|
|
1490
|
-
if (!valid.ok) {
|
|
1491
|
-
return c.json({ error: 'API key validation failed: ' + valid.error, validationFailed: true }, 400);
|
|
1492
|
-
}
|
|
1493
|
-
} catch (e: any) {
|
|
1494
|
-
return c.json({ error: 'API key validation failed: ' + (e.message || 'Unknown error'), validationFailed: true }, 400);
|
|
1495
|
-
}
|
|
1496
|
-
}
|
|
1497
|
-
|
|
1498
|
-
// Store API key encrypted via vault
|
|
1499
|
-
var settings = await db.getSettings();
|
|
1500
|
-
var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };
|
|
1501
|
-
config.providerApiKeys = config.providerApiKeys || {};
|
|
1502
|
-
config.providerApiKeys[id] = vault.encrypt(apiKey);
|
|
1503
|
-
await updateSettingsAndEmit({ modelPricingConfig: config } as any);
|
|
1504
|
-
|
|
1505
|
-
return c.json({ ok: true, message: 'API key saved for ' + provider.name, validated: !skipValidation });
|
|
1506
|
-
});
|
|
1507
|
-
|
|
1508
|
-
api.put('/providers/:id', requireRole('admin'), async (c) => {
|
|
1509
|
-
var id = c.req.param('id');
|
|
1510
|
-
if (PROVIDER_REGISTRY[id]) {
|
|
1511
|
-
return c.json({ error: 'Cannot modify built-in provider' }, 400);
|
|
1512
|
-
}
|
|
1513
|
-
|
|
1514
|
-
var body = await c.req.json();
|
|
1515
|
-
var settings = await db.getSettings();
|
|
1516
|
-
var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };
|
|
1517
|
-
config.customProviders = config.customProviders || [];
|
|
1518
|
-
|
|
1519
|
-
var idx = config.customProviders.findIndex(function(p: any) { return p.id === id; });
|
|
1520
|
-
if (idx === -1) {
|
|
1521
|
-
return c.json({ error: 'Custom provider not found' }, 404);
|
|
1522
|
-
}
|
|
1523
|
-
|
|
1524
|
-
config.customProviders[idx] = Object.assign({}, config.customProviders[idx], body, { id: id });
|
|
1525
|
-
await updateSettingsAndEmit({ modelPricingConfig: config } as any);
|
|
1526
|
-
return c.json({ ok: true, provider: config.customProviders[idx] });
|
|
1527
|
-
});
|
|
1528
|
-
|
|
1529
|
-
api.delete('/providers/:id', requireRole('admin'), async (c) => {
|
|
1530
|
-
var id = c.req.param('id');
|
|
1531
|
-
if (PROVIDER_REGISTRY[id]) {
|
|
1532
|
-
return c.json({ error: 'Cannot delete built-in provider' }, 400);
|
|
1533
|
-
}
|
|
1534
|
-
|
|
1535
|
-
var settings = await db.getSettings();
|
|
1536
|
-
var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };
|
|
1537
|
-
config.customProviders = config.customProviders || [];
|
|
1538
|
-
|
|
1539
|
-
var before = config.customProviders.length;
|
|
1540
|
-
config.customProviders = config.customProviders.filter(function(p: any) { return p.id !== id; });
|
|
1541
|
-
|
|
1542
|
-
if (config.customProviders.length === before) {
|
|
1543
|
-
return c.json({ error: 'Custom provider not found' }, 404);
|
|
1544
|
-
}
|
|
1545
|
-
|
|
1546
|
-
await updateSettingsAndEmit({ modelPricingConfig: config } as any);
|
|
1547
|
-
return c.json({ ok: true });
|
|
1548
|
-
});
|
|
1549
|
-
|
|
1550
|
-
api.get('/providers/:id/models', requireRole('admin'), async (c) => {
|
|
1551
|
-
var id = c.req.param('id');
|
|
1552
|
-
var provider = PROVIDER_REGISTRY[id];
|
|
1553
|
-
|
|
1554
|
-
// Ollama auto-discovery
|
|
1555
|
-
if (id === 'ollama' || (provider && provider.apiType === 'ollama')) {
|
|
1556
|
-
var ollamaHost = process.env.OLLAMA_HOST || (provider ? provider.baseUrl : 'http://localhost:11434');
|
|
1557
|
-
try {
|
|
1558
|
-
var resp = await fetch(ollamaHost + '/api/tags', { signal: AbortSignal.timeout(3000) });
|
|
1559
|
-
var data = await resp.json() as any;
|
|
1560
|
-
return c.json({ models: (data.models || []).map(function(m: any) { return { id: m.name, name: m.name, size: m.size }; }) });
|
|
1561
|
-
} catch (err: any) {
|
|
1562
|
-
return c.json({ models: [], error: 'Cannot connect to Ollama: ' + err.message });
|
|
1563
|
-
}
|
|
1564
|
-
}
|
|
1565
|
-
|
|
1566
|
-
// OpenAI-compatible local auto-discovery (vLLM, LM Studio, LiteLLM)
|
|
1567
|
-
if (provider && provider.isLocal && provider.apiType === 'openai-compatible') {
|
|
1568
|
-
try {
|
|
1569
|
-
var resp = await fetch(provider.baseUrl + '/models', { signal: AbortSignal.timeout(3000) });
|
|
1570
|
-
var data = await resp.json() as any;
|
|
1571
|
-
return c.json({ models: (data.data || []).map(function(m: any) { return { id: m.id, name: m.id }; }) });
|
|
1572
|
-
} catch (err: any) {
|
|
1573
|
-
return c.json({ models: [], error: 'Cannot connect to ' + provider.name + ': ' + err.message });
|
|
1574
|
-
}
|
|
1575
|
-
}
|
|
1576
|
-
|
|
1577
|
-
// Cloud providers — return default models from registry
|
|
1578
|
-
if (provider && provider.defaultModels) {
|
|
1579
|
-
return c.json({ models: provider.defaultModels.map(function(mid: string) { return { id: mid, name: mid }; }) });
|
|
1580
|
-
}
|
|
1581
|
-
|
|
1582
|
-
// Custom providers — check DB
|
|
1583
|
-
var settings = await db.getSettings();
|
|
1584
|
-
var pricingConfig = (settings as any)?.modelPricingConfig;
|
|
1585
|
-
var customProviders = pricingConfig?.customProviders || [];
|
|
1586
|
-
var customProvider = customProviders.find(function(p: any) { return p.id === id; });
|
|
1587
|
-
if (customProvider && customProvider.models) {
|
|
1588
|
-
return c.json({ models: customProvider.models });
|
|
1589
|
-
}
|
|
1590
|
-
|
|
1591
|
-
return c.json({ models: [] });
|
|
1592
|
-
});
|
|
1593
|
-
|
|
1594
|
-
// ─── Retention ──────────────────────────────────────
|
|
1595
|
-
|
|
1596
|
-
api.get('/retention', requireRole('admin'), async (c) => {
|
|
1597
|
-
const policy = await db.getRetentionPolicy();
|
|
1598
|
-
return c.json(policy);
|
|
1599
|
-
});
|
|
1600
|
-
|
|
1601
|
-
api.put('/retention', requireRole('owner'), async (c) => {
|
|
1602
|
-
const body = await c.req.json();
|
|
1603
|
-
validate(body, [
|
|
1604
|
-
{ field: 'enabled', type: 'boolean', required: true },
|
|
1605
|
-
{ field: 'retainDays', type: 'number', required: true, min: 1, max: 3650 },
|
|
1606
|
-
{ field: 'archiveFirst', type: 'boolean' },
|
|
1607
|
-
]);
|
|
1608
|
-
|
|
1609
|
-
await db.setRetentionPolicy({
|
|
1610
|
-
enabled: body.enabled,
|
|
1611
|
-
retainDays: body.retainDays,
|
|
1612
|
-
excludeTags: body.excludeTags || [],
|
|
1613
|
-
archiveFirst: body.archiveFirst ?? true,
|
|
1614
|
-
});
|
|
1615
|
-
return c.json({ ok: true });
|
|
1616
|
-
});
|
|
1617
|
-
|
|
1618
|
-
// ─── Security ────────────────────────────────────────
|
|
1619
|
-
|
|
1620
|
-
api.get('/settings/security', requireRole('admin'), async (c) => {
|
|
1621
|
-
try {
|
|
1622
|
-
const settings = await db.getSettings();
|
|
1623
|
-
const securityConfig = (settings as any)?.securityConfig || {};
|
|
1624
|
-
return c.json({ securityConfig });
|
|
1625
|
-
} catch (err: any) {
|
|
1626
|
-
return c.json({ error: err.message }, 500);
|
|
1627
|
-
}
|
|
1628
|
-
});
|
|
1629
|
-
|
|
1630
|
-
api.put('/settings/security', requireRole('admin'), async (c) => {
|
|
1631
|
-
try {
|
|
1632
|
-
const body = await c.req.json();
|
|
1633
|
-
const { securityConfig } = body;
|
|
1634
|
-
|
|
1635
|
-
if (!securityConfig || typeof securityConfig !== 'object') {
|
|
1636
|
-
return c.json({ error: 'securityConfig is required and must be an object' }, 400);
|
|
1637
|
-
}
|
|
1638
|
-
|
|
1639
|
-
await updateSettingsAndEmit({ securityConfig } as any);
|
|
1640
|
-
|
|
1641
|
-
// Sync transport encryption config to middleware
|
|
1642
|
-
if (securityConfig.transportEncryption) {
|
|
1643
|
-
try {
|
|
1644
|
-
const { setTransportEncryptionConfig } = await import('../middleware/transport-encryption.js');
|
|
1645
|
-
setTransportEncryptionConfig(securityConfig.transportEncryption);
|
|
1646
|
-
} catch {}
|
|
1647
|
-
}
|
|
1648
|
-
|
|
1649
|
-
return c.json({ ok: true });
|
|
1650
|
-
} catch (err: any) {
|
|
1651
|
-
return c.json({ error: err.message }, 500);
|
|
1652
|
-
}
|
|
1653
|
-
});
|
|
1654
|
-
|
|
1655
|
-
api.get('/settings/security/events', requireRole('admin'), async (c) => {
|
|
1656
|
-
try {
|
|
1657
|
-
const query = c.req.query();
|
|
1658
|
-
const filter = {
|
|
1659
|
-
eventType: query.eventType ? query.eventType.split(',') : undefined,
|
|
1660
|
-
severity: query.severity ? query.severity.split(',') : undefined,
|
|
1661
|
-
agentId: query.agentId,
|
|
1662
|
-
sourceIp: query.sourceIp,
|
|
1663
|
-
fromDate: query.fromDate,
|
|
1664
|
-
toDate: query.toDate,
|
|
1665
|
-
limit: query.limit ? parseInt(query.limit) : 50,
|
|
1666
|
-
offset: query.offset ? parseInt(query.offset) : 0
|
|
1667
|
-
};
|
|
1668
|
-
|
|
1669
|
-
const events = await (db as any).getSecurityEvents(filter);
|
|
1670
|
-
return c.json({ events });
|
|
1671
|
-
} catch (err: any) {
|
|
1672
|
-
return c.json({ error: err.message }, 500);
|
|
1673
|
-
}
|
|
1674
|
-
});
|
|
1675
|
-
|
|
1676
|
-
api.get('/settings/security/port-scan', requireRole('admin'), async (c) => {
|
|
1677
|
-
try {
|
|
1678
|
-
const { scanPorts } = await import('../security/port-scanner.js');
|
|
1679
|
-
const result = await scanPorts();
|
|
1680
|
-
return c.json({ scanResult: result });
|
|
1681
|
-
} catch (err: any) {
|
|
1682
|
-
return c.json({ error: err.message }, 500);
|
|
1683
|
-
}
|
|
1684
|
-
});
|
|
1685
|
-
|
|
1686
|
-
api.get('/agents/:id/security', requireRole('admin'), async (c) => {
|
|
1687
|
-
try {
|
|
1688
|
-
const agentId = c.req.param('id');
|
|
1689
|
-
const agent = await db.getAgent(agentId);
|
|
1690
|
-
|
|
1691
|
-
if (!agent) {
|
|
1692
|
-
return c.json({ error: 'Agent not found' }, 404);
|
|
1693
|
-
}
|
|
1694
|
-
|
|
1695
|
-
const securityOverrides = (agent as any)?.securityOverrides || {};
|
|
1696
|
-
return c.json({ securityOverrides });
|
|
1697
|
-
} catch (err: any) {
|
|
1698
|
-
return c.json({ error: err.message }, 500);
|
|
1699
|
-
}
|
|
1700
|
-
});
|
|
1701
|
-
|
|
1702
|
-
api.put('/agents/:id/security', requireRole('admin'), async (c) => {
|
|
1703
|
-
try {
|
|
1704
|
-
const agentId = c.req.param('id');
|
|
1705
|
-
const body = await c.req.json();
|
|
1706
|
-
const { securityOverrides } = body;
|
|
1707
|
-
|
|
1708
|
-
const agent = await db.getAgent(agentId);
|
|
1709
|
-
if (!agent) {
|
|
1710
|
-
return c.json({ error: 'Agent not found' }, 404);
|
|
1711
|
-
}
|
|
1712
|
-
|
|
1713
|
-
await db.updateAgent(agentId, { securityOverrides } as any);
|
|
1714
|
-
return c.json({ ok: true });
|
|
1715
|
-
} catch (err: any) {
|
|
1716
|
-
return c.json({ error: err.message }, 500);
|
|
1717
|
-
}
|
|
1718
|
-
});
|
|
1719
|
-
|
|
1720
|
-
// ─── CORS Helper ────────────────────────────────────
|
|
1721
|
-
/** Add an origin to CORS list, optionally removing an old one */
|
|
1722
|
-
async function updateCorsOrigin(newOrigin: string, oldOrigin?: string) {
|
|
1723
|
-
try {
|
|
1724
|
-
var settings = await db.getSettings();
|
|
1725
|
-
var fw = settings.firewallConfig || {};
|
|
1726
|
-
var net = fw.network || {};
|
|
1727
|
-
var origins: string[] = Array.isArray(net.corsOrigins) ? [...net.corsOrigins] : [];
|
|
1728
|
-
// Remove old if present
|
|
1729
|
-
if (oldOrigin) origins = origins.filter((o: string) => o !== oldOrigin);
|
|
1730
|
-
// Add new if not already present
|
|
1731
|
-
if (!origins.includes(newOrigin)) origins.push(newOrigin);
|
|
1732
|
-
await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: origins } } } as any);
|
|
1733
|
-
try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}
|
|
1734
|
-
} catch { /* non-critical */ }
|
|
1735
|
-
}
|
|
1736
|
-
|
|
1737
|
-
// ─── Get CORS Origins ─────────────────────────────────
|
|
1738
|
-
api.get('/domain/cors', requireRole('admin'), async (c) => {
|
|
1739
|
-
try {
|
|
1740
|
-
var settings = await db.getSettings();
|
|
1741
|
-
var origins = settings?.firewallConfig?.network?.corsOrigins || [];
|
|
1742
|
-
return c.json({ origins });
|
|
1743
|
-
} catch (err: any) {
|
|
1744
|
-
return c.json({ error: err.message }, 500);
|
|
1745
|
-
}
|
|
1746
|
-
});
|
|
1747
|
-
|
|
1748
|
-
// ─── Update CORS Origins ──────────────────────────────
|
|
1749
|
-
api.post('/domain/cors', requireRole('admin'), async (c) => {
|
|
1750
|
-
var body = await c.req.json();
|
|
1751
|
-
if (!Array.isArray(body.origins)) {
|
|
1752
|
-
return c.json({ error: 'origins must be an array of URLs' }, 400);
|
|
1753
|
-
}
|
|
1754
|
-
// Validate each origin
|
|
1755
|
-
for (var o of body.origins) {
|
|
1756
|
-
if (typeof o !== 'string') return c.json({ error: 'Each origin must be a string' }, 400);
|
|
1757
|
-
if (o !== '*' && !o.startsWith('http://') && !o.startsWith('https://')) {
|
|
1758
|
-
return c.json({ error: 'Origin "' + o + '" must start with http:// or https://' }, 400);
|
|
1759
|
-
}
|
|
1760
|
-
}
|
|
1761
|
-
try {
|
|
1762
|
-
var settings = await db.getSettings();
|
|
1763
|
-
var fw = settings.firewallConfig || {};
|
|
1764
|
-
var net = fw.network || {};
|
|
1765
|
-
await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: body.origins } } } as any);
|
|
1766
|
-
try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}
|
|
1767
|
-
return c.json({ success: true, origins: body.origins });
|
|
1768
|
-
} catch (err: any) {
|
|
1769
|
-
return c.json({ error: err.message }, 500);
|
|
1770
|
-
}
|
|
1771
|
-
});
|
|
1772
|
-
|
|
1773
|
-
// ─── Domain Registration ────────────────────────────
|
|
1774
|
-
|
|
1775
|
-
api.post('/domain/register', requireRole('admin'), async (c) => {
|
|
1776
|
-
var body = await c.req.json();
|
|
1777
|
-
if (!body.domain) {
|
|
1778
|
-
return c.json({ error: 'domain is required' }, 400);
|
|
1779
|
-
}
|
|
1780
|
-
|
|
1781
|
-
var domain = String(body.domain).toLowerCase().trim();
|
|
1782
|
-
if (!/^[a-zA-Z0-9][a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(domain)) {
|
|
1783
|
-
return c.json({ error: 'Invalid domain format' }, 400);
|
|
1784
|
-
}
|
|
1785
|
-
|
|
1786
|
-
try {
|
|
1787
|
-
var { DomainLock } = await import('../domain-lock/index.js');
|
|
1788
|
-
var lock = new DomainLock();
|
|
1789
|
-
|
|
1790
|
-
// Generate deployment key
|
|
1791
|
-
var keyPair = await lock.generateDeploymentKey();
|
|
1792
|
-
|
|
1793
|
-
// Get company info for registration
|
|
1794
|
-
var settings = await db.getSettings();
|
|
1795
|
-
|
|
1796
|
-
// Register with central registry
|
|
1797
|
-
var result = await lock.register(domain, keyPair.hash, {
|
|
1798
|
-
orgName: settings?.name,
|
|
1799
|
-
contactEmail: body.contactEmail,
|
|
1800
|
-
});
|
|
1801
|
-
|
|
1802
|
-
if (!result.success) {
|
|
1803
|
-
return c.json({ error: result.error, statusCode: result.statusCode }, 400);
|
|
1804
|
-
}
|
|
1805
|
-
|
|
1806
|
-
// Store in settings
|
|
1807
|
-
await updateSettingsAndEmit({
|
|
1808
|
-
domain: domain,
|
|
1809
|
-
deploymentKeyHash: keyPair.hash,
|
|
1810
|
-
domainRegistrationId: result.registrationId,
|
|
1811
|
-
domainDnsChallenge: result.dnsChallenge,
|
|
1812
|
-
domainRegisteredAt: new Date().toISOString(),
|
|
1813
|
-
domainStatus: 'pending_dns',
|
|
1814
|
-
} as any);
|
|
1815
|
-
|
|
1816
|
-
return c.json({
|
|
1817
|
-
deploymentKey: keyPair.plaintext,
|
|
1818
|
-
dnsChallenge: result.dnsChallenge,
|
|
1819
|
-
registrationId: result.registrationId,
|
|
1820
|
-
});
|
|
1821
|
-
} catch (err: any) {
|
|
1822
|
-
return c.json({ error: err.message || 'Domain registration failed' }, 500);
|
|
1823
|
-
}
|
|
1824
|
-
});
|
|
1825
|
-
|
|
1826
|
-
api.post('/domain/verify', requireRole('admin'), async (c) => {
|
|
1827
|
-
var body = await c.req.json();
|
|
1828
|
-
if (!body.domain) {
|
|
1829
|
-
return c.json({ error: 'domain is required' }, 400);
|
|
1830
|
-
}
|
|
1831
|
-
|
|
1832
|
-
var domain = String(body.domain).toLowerCase().trim();
|
|
1833
|
-
|
|
1834
|
-
try {
|
|
1835
|
-
var { DomainLock } = await import('../domain-lock/index.js');
|
|
1836
|
-
var lock = new DomainLock();
|
|
1837
|
-
|
|
1838
|
-
var result = await lock.checkVerification(domain);
|
|
1839
|
-
|
|
1840
|
-
if (result.verified) {
|
|
1841
|
-
await updateSettingsAndEmit({
|
|
1842
|
-
domainStatus: 'verified',
|
|
1843
|
-
domainVerifiedAt: new Date().toISOString(),
|
|
1844
|
-
} as any);
|
|
1845
|
-
return c.json({ verified: true });
|
|
1846
|
-
}
|
|
1847
|
-
|
|
1848
|
-
return c.json({ verified: false, error: result.error });
|
|
1849
|
-
} catch (err: any) {
|
|
1850
|
-
return c.json({ error: err.message || 'Verification check failed' }, 500);
|
|
1851
|
-
}
|
|
1852
|
-
});
|
|
1853
|
-
|
|
1854
|
-
// ─── Domain Status (GET) ──────────────────────────────
|
|
1855
|
-
api.get('/domain/status', requireRole('admin'), async (c) => {
|
|
1856
|
-
try {
|
|
1857
|
-
var settings = await db.getSettings();
|
|
1858
|
-
return c.json({
|
|
1859
|
-
domain: settings.domain || null,
|
|
1860
|
-
subdomain: settings.subdomain || null,
|
|
1861
|
-
status: settings.domainStatus || 'unregistered',
|
|
1862
|
-
registeredAt: settings.domainRegisteredAt || null,
|
|
1863
|
-
verifiedAt: settings.domainVerifiedAt || null,
|
|
1864
|
-
dnsChallenge: settings.domainDnsChallenge || null,
|
|
1865
|
-
useRootDomain: settings.useRootDomain || false,
|
|
1866
|
-
plan: settings.plan || 'self-hosted',
|
|
1867
|
-
});
|
|
1868
|
-
} catch (err: any) {
|
|
1869
|
-
return c.json({ error: err.message }, 500);
|
|
1870
|
-
}
|
|
1871
|
-
});
|
|
1872
|
-
|
|
1873
|
-
// ─── Domain Change ────────────────────────────────────
|
|
1874
|
-
api.post('/domain/change', requireRole('admin'), async (c) => {
|
|
1875
|
-
var body = await c.req.json();
|
|
1876
|
-
if (!body.domain) {
|
|
1877
|
-
return c.json({ error: 'domain is required' }, 400);
|
|
1878
|
-
}
|
|
1879
|
-
|
|
1880
|
-
var domain = String(body.domain).toLowerCase().trim();
|
|
1881
|
-
if (!/^[a-zA-Z0-9][a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(domain)) {
|
|
1882
|
-
return c.json({ error: 'Invalid domain format' }, 400);
|
|
1883
|
-
}
|
|
1884
|
-
|
|
1885
|
-
try {
|
|
1886
|
-
var { DomainLock } = await import('../domain-lock/index.js');
|
|
1887
|
-
var lock = new DomainLock();
|
|
1888
|
-
var keyPair = await lock.generateDeploymentKey();
|
|
1889
|
-
var settings = await db.getSettings();
|
|
1890
|
-
|
|
1891
|
-
var result = await lock.register(domain, keyPair.hash, {
|
|
1892
|
-
orgName: settings?.name,
|
|
1893
|
-
contactEmail: body.contactEmail,
|
|
1894
|
-
});
|
|
1895
|
-
|
|
1896
|
-
if (!result.success) {
|
|
1897
|
-
return c.json({ error: result.error, statusCode: result.statusCode }, 400);
|
|
1898
|
-
}
|
|
1899
|
-
|
|
1900
|
-
var oldDomain = settings.domain;
|
|
1901
|
-
await updateSettingsAndEmit({
|
|
1902
|
-
domain: domain,
|
|
1903
|
-
useRootDomain: body.useRootDomain || false,
|
|
1904
|
-
deploymentKeyHash: keyPair.hash,
|
|
1905
|
-
domainRegistrationId: result.registrationId,
|
|
1906
|
-
domainDnsChallenge: result.dnsChallenge,
|
|
1907
|
-
domainRegisteredAt: new Date().toISOString(),
|
|
1908
|
-
domainStatus: 'pending_dns',
|
|
1909
|
-
domainVerifiedAt: undefined,
|
|
1910
|
-
} as any);
|
|
1911
|
-
|
|
1912
|
-
// Auto-update CORS
|
|
1913
|
-
await updateCorsOrigin('https://' + domain, oldDomain ? 'https://' + oldDomain : undefined);
|
|
1914
|
-
|
|
1915
|
-
return c.json({
|
|
1916
|
-
success: true,
|
|
1917
|
-
deploymentKey: keyPair.plaintext,
|
|
1918
|
-
dnsChallenge: result.dnsChallenge,
|
|
1919
|
-
registrationId: result.registrationId,
|
|
1920
|
-
});
|
|
1921
|
-
} catch (err: any) {
|
|
1922
|
-
return c.json({ error: err.message || 'Domain change failed' }, 500);
|
|
1923
|
-
}
|
|
1924
|
-
});
|
|
1925
|
-
|
|
1926
|
-
// ─── Subdomain Update ─────────────────────────────────
|
|
1927
|
-
api.post('/domain/subdomain', requireRole('admin'), async (c) => {
|
|
1928
|
-
var body = await c.req.json();
|
|
1929
|
-
if (!body.subdomain) {
|
|
1930
|
-
return c.json({ error: 'subdomain is required' }, 400);
|
|
1931
|
-
}
|
|
1932
|
-
var sub = String(body.subdomain).toLowerCase().trim().replace(/\.agenticmail\.io$/, '');
|
|
1933
|
-
if (sub.length < 2) {
|
|
1934
|
-
return c.json({ error: 'Subdomain must be at least 2 characters.' }, 400);
|
|
1935
|
-
}
|
|
1936
|
-
if (sub.length > 63) {
|
|
1937
|
-
return c.json({ error: 'Subdomain must be 63 characters or fewer.' }, 400);
|
|
1938
|
-
}
|
|
1939
|
-
if (/^-|-$/.test(sub)) {
|
|
1940
|
-
return c.json({ error: 'Subdomain cannot start or end with a hyphen.' }, 400);
|
|
1941
|
-
}
|
|
1942
|
-
if (!/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/.test(sub)) {
|
|
1943
|
-
return c.json({ error: 'Subdomain can only contain lowercase letters, numbers, and hyphens.' }, 400);
|
|
1944
|
-
}
|
|
1945
|
-
// Reserved subdomains
|
|
1946
|
-
var reserved = ['www', 'mail', 'api', 'app', 'admin', 'dashboard', 'help', 'support', 'docs', 'status', 'blog', 'cdn', 'static', 'assets', 'ns1', 'ns2'];
|
|
1947
|
-
if (reserved.includes(sub)) {
|
|
1948
|
-
return c.json({ error: '"' + sub + '" is a reserved subdomain. Please choose a different one.' }, 400);
|
|
1949
|
-
}
|
|
1950
|
-
try {
|
|
1951
|
-
var settings = await db.getSettings();
|
|
1952
|
-
var oldSub = settings.subdomain || null;
|
|
1953
|
-
await updateSettingsAndEmit({ subdomain: sub } as any);
|
|
1954
|
-
// Auto-update CORS
|
|
1955
|
-
await updateCorsOrigin(
|
|
1956
|
-
'https://' + sub + '.agenticmail.io',
|
|
1957
|
-
oldSub ? 'https://' + oldSub + '.agenticmail.io' : undefined,
|
|
1958
|
-
);
|
|
1959
|
-
return c.json({ success: true, subdomain: sub, oldSubdomain: oldSub, plan: settings.plan || 'self-hosted' });
|
|
1960
|
-
} catch (err: any) {
|
|
1961
|
-
return c.json({ error: err.message || 'Subdomain update failed' }, 500);
|
|
1962
|
-
}
|
|
1963
|
-
});
|
|
1964
|
-
|
|
1965
|
-
// ─── Remove Custom Domain ─────────────────────────────
|
|
1966
|
-
api.delete('/domain', requireRole('admin'), async (c) => {
|
|
1967
|
-
try {
|
|
1968
|
-
var settings = await db.getSettings();
|
|
1969
|
-
var oldDomain = settings.domain;
|
|
1970
|
-
await updateSettingsAndEmit({
|
|
1971
|
-
domain: undefined,
|
|
1972
|
-
domainStatus: undefined,
|
|
1973
|
-
domainDnsChallenge: undefined,
|
|
1974
|
-
domainRegisteredAt: undefined,
|
|
1975
|
-
domainVerifiedAt: undefined,
|
|
1976
|
-
domainRegistrationId: undefined,
|
|
1977
|
-
deploymentKeyHash: undefined,
|
|
1978
|
-
} as any);
|
|
1979
|
-
// Remove old domain from CORS
|
|
1980
|
-
if (oldDomain) {
|
|
1981
|
-
try {
|
|
1982
|
-
var fw = settings.firewallConfig || {};
|
|
1983
|
-
var net = fw.network || {};
|
|
1984
|
-
var origins: string[] = Array.isArray(net.corsOrigins) ? net.corsOrigins.filter((o: string) => o !== 'https://' + oldDomain) : [];
|
|
1985
|
-
await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: origins } } } as any);
|
|
1986
|
-
try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}
|
|
1987
|
-
} catch {}
|
|
1988
|
-
}
|
|
1989
|
-
return c.json({ success: true });
|
|
1990
|
-
} catch (err: any) {
|
|
1991
|
-
return c.json({ error: err.message || 'Failed to remove domain' }, 500);
|
|
1992
|
-
}
|
|
1993
|
-
});
|
|
1994
|
-
|
|
1995
|
-
function getDefaultModelPricing() {
|
|
1996
|
-
return [
|
|
1997
|
-
// Anthropic (Feb 2026 — 1M context window)
|
|
1998
|
-
{ provider: 'anthropic', modelId: 'claude-opus-4-6', displayName: 'Claude Opus 4.6', inputCostPerMillion: 5, outputCostPerMillion: 25, contextWindow: 1000000 },
|
|
1999
|
-
{ provider: 'anthropic', modelId: 'claude-sonnet-4-6', displayName: 'Claude Sonnet 4.6', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 1000000 },
|
|
2000
|
-
{ provider: 'anthropic', modelId: 'claude-sonnet-4-5-20250929', displayName: 'Claude Sonnet 4.5', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 1000000 },
|
|
2001
|
-
{ provider: 'anthropic', modelId: 'claude-haiku-4-5-20251001', displayName: 'Claude Haiku 4.5', inputCostPerMillion: 0.8, outputCostPerMillion: 4, contextWindow: 200000 },
|
|
2002
|
-
// OpenAI
|
|
2003
|
-
{ provider: 'openai', modelId: 'gpt-4o', displayName: 'GPT-4o', inputCostPerMillion: 2.5, outputCostPerMillion: 10, contextWindow: 128000 },
|
|
2004
|
-
{ provider: 'openai', modelId: 'gpt-4o-mini', displayName: 'GPT-4o Mini', inputCostPerMillion: 0.15, outputCostPerMillion: 0.6, contextWindow: 128000 },
|
|
2005
|
-
{ provider: 'openai', modelId: 'gpt-4.1', displayName: 'GPT-4.1', inputCostPerMillion: 2, outputCostPerMillion: 8, contextWindow: 1000000 },
|
|
2006
|
-
{ provider: 'openai', modelId: 'gpt-4.1-mini', displayName: 'GPT-4.1 Mini', inputCostPerMillion: 0.4, outputCostPerMillion: 1.6, contextWindow: 1000000 },
|
|
2007
|
-
{ provider: 'openai', modelId: 'gpt-4.1-nano', displayName: 'GPT-4.1 Nano', inputCostPerMillion: 0.1, outputCostPerMillion: 0.4, contextWindow: 1000000 },
|
|
2008
|
-
{ provider: 'openai', modelId: 'o3', displayName: 'o3', inputCostPerMillion: 10, outputCostPerMillion: 40, contextWindow: 200000 },
|
|
2009
|
-
{ provider: 'openai', modelId: 'o4-mini', displayName: 'o4-mini', inputCostPerMillion: 1.1, outputCostPerMillion: 4.4, contextWindow: 200000 },
|
|
2010
|
-
// Google Gemini (up to 2M context)
|
|
2011
|
-
{ provider: 'google', modelId: 'gemini-2.5-pro', displayName: 'Gemini 2.5 Pro', inputCostPerMillion: 2.5, outputCostPerMillion: 15, contextWindow: 1000000 },
|
|
2012
|
-
{ provider: 'google', modelId: 'gemini-2.5-flash', displayName: 'Gemini 2.5 Flash', inputCostPerMillion: 0.15, outputCostPerMillion: 0.6, contextWindow: 1000000 },
|
|
2013
|
-
{ provider: 'google', modelId: 'gemini-2.0-flash', displayName: 'Gemini 2.0 Flash', inputCostPerMillion: 0.1, outputCostPerMillion: 0.4, contextWindow: 1000000 },
|
|
2014
|
-
{ provider: 'google', modelId: 'gemini-3-pro', displayName: 'Gemini 3 Pro', inputCostPerMillion: 2.5, outputCostPerMillion: 15, contextWindow: 1000000 },
|
|
2015
|
-
// DeepSeek (128K context)
|
|
2016
|
-
{ provider: 'deepseek', modelId: 'deepseek-chat', displayName: 'DeepSeek Chat (V3)', inputCostPerMillion: 0.14, outputCostPerMillion: 0.28, contextWindow: 128000 },
|
|
2017
|
-
{ provider: 'deepseek', modelId: 'deepseek-reasoner', displayName: 'DeepSeek Reasoner (R1)', inputCostPerMillion: 0.55, outputCostPerMillion: 2.19, contextWindow: 128000 },
|
|
2018
|
-
// xAI Grok (2M context window)
|
|
2019
|
-
{ provider: 'xai', modelId: 'grok-4', displayName: 'Grok 4', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 2000000 },
|
|
2020
|
-
{ provider: 'xai', modelId: 'grok-4-fast', displayName: 'Grok 4 Fast', inputCostPerMillion: 0.2, outputCostPerMillion: 0.5, contextWindow: 2000000 },
|
|
2021
|
-
{ provider: 'xai', modelId: 'grok-3', displayName: 'Grok 3', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 131072 },
|
|
2022
|
-
{ provider: 'xai', modelId: 'grok-3-mini', displayName: 'Grok 3 Mini', inputCostPerMillion: 0.3, outputCostPerMillion: 0.5, contextWindow: 131072 },
|
|
2023
|
-
// Mistral
|
|
2024
|
-
{ provider: 'mistral', modelId: 'mistral-large-latest', displayName: 'Mistral Large', inputCostPerMillion: 2, outputCostPerMillion: 6, contextWindow: 128000 },
|
|
2025
|
-
{ provider: 'mistral', modelId: 'mistral-small-latest', displayName: 'Mistral Small', inputCostPerMillion: 0.1, outputCostPerMillion: 0.3, contextWindow: 128000 },
|
|
2026
|
-
// Groq (inference provider)
|
|
2027
|
-
{ provider: 'groq', modelId: 'llama-3.3-70b-versatile', displayName: 'Llama 3.3 70B (Groq)', inputCostPerMillion: 0.59, outputCostPerMillion: 0.79, contextWindow: 128000 },
|
|
2028
|
-
// Together (inference provider)
|
|
2029
|
-
{ provider: 'together', modelId: 'meta-llama/Llama-3.3-70B-Instruct-Turbo', displayName: 'Llama 3.3 70B (Together)', inputCostPerMillion: 0.88, outputCostPerMillion: 0.88, contextWindow: 128000 },
|
|
2030
|
-
];
|
|
2031
|
-
}
|
|
2032
|
-
|
|
2033
|
-
// ─── Cloudflare Tunnel Deployment ───────────────────
|
|
2034
|
-
|
|
2035
|
-
/** Check if cloudflared is installed and tunnel status */
|
|
2036
|
-
api.get('/tunnel/status', requireRole('admin'), async (c) => {
|
|
2037
|
-
try {
|
|
2038
|
-
const { execSync } = await import('child_process');
|
|
2039
|
-
let installed = false;
|
|
2040
|
-
let version = '';
|
|
2041
|
-
let running = false;
|
|
2042
|
-
let config: any = null;
|
|
2043
|
-
|
|
2044
|
-
try {
|
|
2045
|
-
version = execSync('cloudflared --version 2>&1', { encoding: 'utf8', timeout: 5000 }).trim();
|
|
2046
|
-
installed = true;
|
|
2047
|
-
} catch { /* not installed */ }
|
|
2048
|
-
|
|
2049
|
-
// Check if running via pm2
|
|
2050
|
-
try {
|
|
2051
|
-
const pm2List = execSync('pm2 jlist 2>/dev/null', { encoding: 'utf8', timeout: 5000 });
|
|
2052
|
-
const procs = JSON.parse(pm2List);
|
|
2053
|
-
const cf = procs.find((p: any) => p.name === 'cloudflared');
|
|
2054
|
-
running = cf?.pm2_env?.status === 'online';
|
|
2055
|
-
} catch { /* pm2 not available */ }
|
|
2056
|
-
|
|
2057
|
-
// Read config
|
|
2058
|
-
const os = await import('os');
|
|
2059
|
-
const fs = await import('fs');
|
|
2060
|
-
const path = await import('path');
|
|
2061
|
-
const cfDir = path.join(os.default.homedir(), '.cloudflared');
|
|
2062
|
-
const cfgPath = path.join(cfDir, 'config.yml');
|
|
2063
|
-
if (fs.existsSync(cfgPath)) {
|
|
2064
|
-
const raw = fs.readFileSync(cfgPath, 'utf8');
|
|
2065
|
-
// Parse simple YAML
|
|
2066
|
-
const tunnelMatch = raw.match(/^tunnel:\s*(.+)$/m);
|
|
2067
|
-
const hostnameMatch = raw.match(/hostname:\s*(.+)$/m);
|
|
2068
|
-
const serviceMatch = raw.match(/service:\s*(http.+)$/m);
|
|
2069
|
-
config = {
|
|
2070
|
-
tunnelId: tunnelMatch?.[1]?.trim(),
|
|
2071
|
-
hostname: hostnameMatch?.[1]?.trim(),
|
|
2072
|
-
service: serviceMatch?.[1]?.trim(),
|
|
2073
|
-
raw,
|
|
2074
|
-
};
|
|
2075
|
-
}
|
|
2076
|
-
|
|
2077
|
-
return c.json({ installed, version, running, config });
|
|
2078
|
-
} catch (e: any) {
|
|
2079
|
-
return c.json({ error: e.message }, 500);
|
|
2080
|
-
}
|
|
2081
|
-
});
|
|
2082
|
-
|
|
2083
|
-
/** Install cloudflared */
|
|
2084
|
-
api.post('/tunnel/install', requireRole('admin'), async (c) => {
|
|
2085
|
-
try {
|
|
2086
|
-
const { execSync } = await import('child_process');
|
|
2087
|
-
const os = await import('os');
|
|
2088
|
-
const platform = os.default.platform();
|
|
2089
|
-
|
|
2090
|
-
if (platform === 'darwin') {
|
|
2091
|
-
// macOS — try brew first
|
|
2092
|
-
try {
|
|
2093
|
-
execSync('which brew', { timeout: 3000 });
|
|
2094
|
-
execSync('brew install cloudflared 2>&1', { encoding: 'utf8', timeout: 120000 });
|
|
2095
|
-
} catch {
|
|
2096
|
-
// Direct download
|
|
2097
|
-
const arch = os.default.arch() === 'arm64' ? 'arm64' : 'amd64';
|
|
2098
|
-
execSync(`curl -L -o /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-${arch} && chmod +x /usr/local/bin/cloudflared`, { timeout: 60000 });
|
|
2099
|
-
}
|
|
2100
|
-
} else if (platform === 'linux') {
|
|
2101
|
-
const arch = os.default.arch() === 'arm64' ? 'arm64' : 'amd64';
|
|
2102
|
-
execSync(`curl -L -o /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${arch} && chmod +x /usr/local/bin/cloudflared`, { timeout: 60000 });
|
|
2103
|
-
} else {
|
|
2104
|
-
return c.json({ error: 'Unsupported platform: ' + platform }, 400);
|
|
2105
|
-
}
|
|
2106
|
-
|
|
2107
|
-
const version = execSync('cloudflared --version 2>&1', { encoding: 'utf8', timeout: 5000 }).trim();
|
|
2108
|
-
return c.json({ success: true, version });
|
|
2109
|
-
} catch (e: any) {
|
|
2110
|
-
return c.json({ error: e.message }, 500);
|
|
2111
|
-
}
|
|
2112
|
-
});
|
|
2113
|
-
|
|
2114
|
-
/** Authenticate with Cloudflare (opens browser for login) */
|
|
2115
|
-
api.post('/tunnel/login', requireRole('admin'), async (c) => {
|
|
2116
|
-
try {
|
|
2117
|
-
const { exec: execCb } = await import('child_process');
|
|
2118
|
-
const { promisify } = await import('util');
|
|
2119
|
-
const execP = promisify(execCb);
|
|
2120
|
-
// This opens the browser for CF login — cert.pem is saved to ~/.cloudflared/
|
|
2121
|
-
await execP('cloudflared tunnel login', { timeout: 120000 });
|
|
2122
|
-
return c.json({ success: true });
|
|
2123
|
-
} catch (e: any) {
|
|
2124
|
-
return c.json({ error: 'Login failed or timed out. Make sure to complete the browser authorization. ' + e.message }, 500);
|
|
2125
|
-
}
|
|
2126
|
-
});
|
|
2127
|
-
|
|
2128
|
-
/** Create tunnel, configure DNS, and start */
|
|
2129
|
-
api.post('/tunnel/deploy', requireRole('admin'), async (c) => {
|
|
2130
|
-
const body = await c.req.json();
|
|
2131
|
-
const { domain, tunnelName, port } = body;
|
|
2132
|
-
if (!domain) return c.json({ error: 'domain is required' }, 400);
|
|
2133
|
-
|
|
2134
|
-
const localPort = port || 3200;
|
|
2135
|
-
const name = tunnelName || 'agenticmail-enterprise';
|
|
2136
|
-
|
|
2137
|
-
try {
|
|
2138
|
-
const { execSync } = await import('child_process');
|
|
2139
|
-
const os = await import('os');
|
|
2140
|
-
const fs = await import('fs');
|
|
2141
|
-
const path = await import('path');
|
|
2142
|
-
const cfDir = path.join(os.default.homedir(), '.cloudflared');
|
|
2143
|
-
const steps: string[] = [];
|
|
2144
|
-
|
|
2145
|
-
// Check cert exists (user must have logged in)
|
|
2146
|
-
if (!fs.existsSync(path.join(cfDir, 'cert.pem'))) {
|
|
2147
|
-
return c.json({ error: 'Not authenticated with Cloudflare. Click "Login to Cloudflare" first.' }, 400);
|
|
2148
|
-
}
|
|
2149
|
-
|
|
2150
|
-
// 1. Create tunnel
|
|
2151
|
-
let tunnelId = '';
|
|
2152
|
-
try {
|
|
2153
|
-
const out = execSync(`cloudflared tunnel create ${name} 2>&1`, { encoding: 'utf8', timeout: 30000 });
|
|
2154
|
-
const match = out.match(/Created tunnel .+ with id ([a-f0-9-]+)/);
|
|
2155
|
-
tunnelId = match?.[1] || '';
|
|
2156
|
-
steps.push('Created tunnel: ' + name + ' (' + tunnelId + ')');
|
|
2157
|
-
} catch (e: any) {
|
|
2158
|
-
// Tunnel might already exist
|
|
2159
|
-
if (e.message?.includes('already exists')) {
|
|
2160
|
-
const listOut = execSync('cloudflared tunnel list --output json 2>&1', { encoding: 'utf8', timeout: 15000 });
|
|
2161
|
-
const tunnels = JSON.parse(listOut);
|
|
2162
|
-
const existing = tunnels.find((t: any) => t.name === name);
|
|
2163
|
-
if (existing) {
|
|
2164
|
-
tunnelId = existing.id;
|
|
2165
|
-
steps.push('Using existing tunnel: ' + name + ' (' + tunnelId + ')');
|
|
2166
|
-
} else {
|
|
2167
|
-
throw e;
|
|
2168
|
-
}
|
|
2169
|
-
} else {
|
|
2170
|
-
throw e;
|
|
2171
|
-
}
|
|
2172
|
-
}
|
|
2173
|
-
|
|
2174
|
-
if (!tunnelId) return c.json({ error: 'Failed to get tunnel ID' }, 500);
|
|
2175
|
-
|
|
2176
|
-
// 2. Write config
|
|
2177
|
-
const config = [
|
|
2178
|
-
`tunnel: ${tunnelId}`,
|
|
2179
|
-
`credentials-file: ${path.join(cfDir, tunnelId + '.json')}`,
|
|
2180
|
-
'',
|
|
2181
|
-
'ingress:',
|
|
2182
|
-
` - hostname: ${domain}`,
|
|
2183
|
-
` service: http://localhost:${localPort}`,
|
|
2184
|
-
' - service: http_status:404',
|
|
2185
|
-
].join('\n');
|
|
2186
|
-
|
|
2187
|
-
fs.writeFileSync(path.join(cfDir, 'config.yml'), config);
|
|
2188
|
-
steps.push('Wrote config: ' + domain + ' → localhost:' + localPort);
|
|
2189
|
-
|
|
2190
|
-
// 3. Route DNS
|
|
2191
|
-
try {
|
|
2192
|
-
execSync(`cloudflared tunnel route dns ${tunnelId} ${domain} 2>&1`, { encoding: 'utf8', timeout: 30000 });
|
|
2193
|
-
steps.push('DNS CNAME created: ' + domain + ' → ' + tunnelId + '.cfargotunnel.com');
|
|
2194
|
-
} catch (e: any) {
|
|
2195
|
-
if (e.message?.includes('already exists')) {
|
|
2196
|
-
steps.push('DNS CNAME already exists for ' + domain);
|
|
2197
|
-
} else {
|
|
2198
|
-
steps.push('DNS routing failed (you may need to add CNAME manually): ' + e.message);
|
|
2199
|
-
}
|
|
2200
|
-
}
|
|
2201
|
-
|
|
2202
|
-
// 4. Start with PM2
|
|
2203
|
-
try {
|
|
2204
|
-
execSync('which pm2', { timeout: 3000 });
|
|
2205
|
-
// Stop existing if any
|
|
2206
|
-
try { execSync('pm2 delete cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }
|
|
2207
|
-
execSync(`pm2 start cloudflared --name cloudflared -- tunnel run`, { encoding: 'utf8', timeout: 15000 });
|
|
2208
|
-
execSync('pm2 save 2>/dev/null', { timeout: 5000 });
|
|
2209
|
-
steps.push('Started cloudflared via PM2 (auto-restarts on crash)');
|
|
2210
|
-
} catch {
|
|
2211
|
-
// No PM2 — try running directly in background
|
|
2212
|
-
try {
|
|
2213
|
-
const { spawn } = await import('child_process');
|
|
2214
|
-
const child = spawn('cloudflared', ['tunnel', 'run'], { detached: true, stdio: 'ignore' });
|
|
2215
|
-
child.unref();
|
|
2216
|
-
steps.push('Started cloudflared in background (install PM2 for auto-restart)');
|
|
2217
|
-
} catch (e2: any) {
|
|
2218
|
-
steps.push('Could not start tunnel automatically: ' + e2.message);
|
|
2219
|
-
}
|
|
2220
|
-
}
|
|
2221
|
-
|
|
2222
|
-
// 5. Update CORS to allow the new domain
|
|
2223
|
-
try {
|
|
2224
|
-
if (db) {
|
|
2225
|
-
const corsRows = await (db as any).query(`SELECT value FROM admin_settings WHERE key = 'cors_origins'`);
|
|
2226
|
-
let origins: string[] = [];
|
|
2227
|
-
if (corsRows?.[0]) {
|
|
2228
|
-
try { origins = JSON.parse((corsRows[0] as any).value); } catch { origins = []; }
|
|
2229
|
-
}
|
|
2230
|
-
const newOrigin = 'https://' + domain;
|
|
2231
|
-
if (!origins.includes(newOrigin)) {
|
|
2232
|
-
origins.push(newOrigin);
|
|
2233
|
-
await (db as any).execute(
|
|
2234
|
-
`INSERT INTO admin_settings (key, value) VALUES ('cors_origins', $1) ON CONFLICT (key) DO UPDATE SET value = $1`,
|
|
2235
|
-
[JSON.stringify(origins)]
|
|
2236
|
-
);
|
|
2237
|
-
steps.push('Added ' + newOrigin + ' to CORS allowed origins');
|
|
2238
|
-
}
|
|
2239
|
-
}
|
|
2240
|
-
} catch { /* non-critical */ }
|
|
2241
|
-
|
|
2242
|
-
return c.json({ success: true, tunnelId, domain, steps });
|
|
2243
|
-
} catch (e: any) {
|
|
2244
|
-
return c.json({ error: e.message }, 500);
|
|
2245
|
-
}
|
|
2246
|
-
});
|
|
2247
|
-
|
|
2248
|
-
// ─── Client Organizations ─────────────────────────────
|
|
2249
|
-
|
|
2250
|
-
api.get('/organizations', async (c) => {
|
|
2251
|
-
try {
|
|
2252
|
-
// Org-bound users can only see their own organization
|
|
2253
|
-
const clientOrgId = c.get('clientOrgId' as any);
|
|
2254
|
-
if (clientOrgId) {
|
|
2255
|
-
const isPostgres = (db as any).pool;
|
|
2256
|
-
if (isPostgres) {
|
|
2257
|
-
const { rows } = await (db as any)._query(
|
|
2258
|
-
`SELECT o.*, COUNT(a.id) as agent_count FROM client_organizations o LEFT JOIN agents a ON a.client_org_id = o.id WHERE o.id = $1 GROUP BY o.id`, [clientOrgId]);
|
|
2259
|
-
return c.json({ organizations: rows });
|
|
2260
|
-
}
|
|
2261
|
-
return c.json({ organizations: [] });
|
|
2262
|
-
}
|
|
2263
|
-
|
|
2264
|
-
// Full access for admins/owners
|
|
2265
|
-
const userRole = c.get('userRole' as any);
|
|
2266
|
-
if (userRole !== 'admin' && userRole !== 'owner') {
|
|
2267
|
-
return c.json({ error: 'Insufficient permissions' }, 403);
|
|
2268
|
-
}
|
|
2269
|
-
|
|
2270
|
-
const isPostgres = (db as any).pool;
|
|
2271
|
-
if (isPostgres) {
|
|
2272
|
-
const { rows } = await (db as any)._query(`
|
|
2273
|
-
SELECT o.*, COUNT(a.id) as agent_count
|
|
2274
|
-
FROM client_organizations o
|
|
2275
|
-
LEFT JOIN agents a ON a.client_org_id = o.id
|
|
2276
|
-
GROUP BY o.id
|
|
2277
|
-
ORDER BY o.created_at DESC
|
|
2278
|
-
`);
|
|
2279
|
-
return c.json({ organizations: rows });
|
|
2280
|
-
} else {
|
|
2281
|
-
const engineDb = db.getEngineDB();
|
|
2282
|
-
const rows = await engineDb!.all(`
|
|
2283
|
-
SELECT o.*, COUNT(a.id) as agent_count
|
|
2284
|
-
FROM client_organizations o
|
|
2285
|
-
LEFT JOIN agents a ON a.client_org_id = o.id
|
|
2286
|
-
GROUP BY o.id
|
|
2287
|
-
ORDER BY o.created_at DESC
|
|
2288
|
-
`);
|
|
2289
|
-
return c.json({ organizations: rows });
|
|
2290
|
-
}
|
|
2291
|
-
} catch (e: any) {
|
|
2292
|
-
return c.json({ error: e.message }, 500);
|
|
2293
|
-
}
|
|
2294
|
-
});
|
|
2295
|
-
|
|
2296
|
-
api.post('/organizations', requireRole('admin'), async (c) => {
|
|
2297
|
-
const body = await c.req.json();
|
|
2298
|
-
validate(body, [
|
|
2299
|
-
{ field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },
|
|
2300
|
-
{ field: 'slug', type: 'string', required: true, minLength: 1, maxLength: 64, pattern: /^[a-z0-9-]+$/ },
|
|
2301
|
-
{ field: 'contact_name', type: 'string', maxLength: 128 },
|
|
2302
|
-
{ field: 'contact_email', type: 'email' },
|
|
2303
|
-
{ field: 'description', type: 'string', maxLength: 512 },
|
|
2304
|
-
]);
|
|
2305
|
-
const id = (await import('crypto')).randomUUID();
|
|
2306
|
-
try {
|
|
2307
|
-
const isPostgres = (db as any).pool;
|
|
2308
|
-
if (isPostgres) {
|
|
2309
|
-
await (db as any)._query(
|
|
2310
|
-
`INSERT INTO client_organizations (id, name, slug, contact_name, contact_email, description, billing_rate_per_agent, currency) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
|
|
2311
|
-
[id, body.name, body.slug, body.contact_name || null, body.contact_email || null, body.description || null, body.billing_rate_per_agent || 0, body.currency || 'USD']
|
|
2312
|
-
);
|
|
2313
|
-
const { rows } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);
|
|
2314
|
-
return c.json(rows[0], 201);
|
|
2315
|
-
} else {
|
|
2316
|
-
const engineDb = db.getEngineDB();
|
|
2317
|
-
await engineDb!.run(
|
|
2318
|
-
`INSERT INTO client_organizations (id, name, slug, contact_name, contact_email, description, billing_rate_per_agent, currency) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
2319
|
-
[id, body.name, body.slug, body.contact_name || null, body.contact_email || null, body.description || null, body.billing_rate_per_agent || 0, body.currency || 'USD']
|
|
2320
|
-
);
|
|
2321
|
-
const row = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);
|
|
2322
|
-
return c.json(row, 201);
|
|
2323
|
-
}
|
|
2324
|
-
} catch (e: any) {
|
|
2325
|
-
if (e.message?.includes('UNIQUE') || e.code === '23505') return c.json({ error: 'Slug already exists' }, 409);
|
|
2326
|
-
return c.json({ error: e.message }, 500);
|
|
2327
|
-
}
|
|
2328
|
-
});
|
|
2329
|
-
|
|
2330
|
-
api.get('/organizations/:id', async (c) => {
|
|
2331
|
-
const id = c.req.param('id');
|
|
2332
|
-
const userRole = c.get('userRole' as any);
|
|
2333
|
-
const userClientOrgId = c.get('clientOrgId' as any);
|
|
2334
|
-
// Non-admins can only access their own client org
|
|
2335
|
-
if (userRole !== 'owner' && userRole !== 'admin') {
|
|
2336
|
-
if (!userClientOrgId || userClientOrgId !== id) return c.json({ error: 'Forbidden' }, 403);
|
|
2337
|
-
}
|
|
2338
|
-
try {
|
|
2339
|
-
const isPostgres = (db as any).pool;
|
|
2340
|
-
if (isPostgres) {
|
|
2341
|
-
const { rows: orgs } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);
|
|
2342
|
-
if (!orgs[0]) return c.json({ error: 'Organization not found' }, 404);
|
|
2343
|
-
const { rows: agents } = await (db as any)._query(`SELECT id, name, email, role, status FROM agents WHERE client_org_id = $1`, [id]);
|
|
2344
|
-
return c.json({ ...orgs[0], agents });
|
|
2345
|
-
} else {
|
|
2346
|
-
const engineDb = db.getEngineDB();
|
|
2347
|
-
const org = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);
|
|
2348
|
-
if (!org) return c.json({ error: 'Organization not found' }, 404);
|
|
2349
|
-
const agents = await engineDb!.all(`SELECT id, name, email, role, status FROM agents WHERE client_org_id = ?`, [id]);
|
|
2350
|
-
return c.json({ ...(org as any), agents });
|
|
2351
|
-
}
|
|
2352
|
-
} catch (e: any) {
|
|
2353
|
-
return c.json({ error: e.message }, 500);
|
|
2354
|
-
}
|
|
2355
|
-
});
|
|
2356
|
-
|
|
2357
|
-
api.patch('/organizations/:id', requireRole('admin'), async (c) => {
|
|
2358
|
-
const id = c.req.param('id');
|
|
2359
|
-
const body = await c.req.json();
|
|
2360
|
-
validate(body, [
|
|
2361
|
-
{ field: 'name', type: 'string', minLength: 1, maxLength: 128 },
|
|
2362
|
-
{ field: 'contact_name', type: 'string', maxLength: 128 },
|
|
2363
|
-
{ field: 'contact_email', type: 'email' },
|
|
2364
|
-
{ field: 'description', type: 'string', maxLength: 512 },
|
|
2365
|
-
]);
|
|
2366
|
-
try {
|
|
2367
|
-
const fields: string[] = [];
|
|
2368
|
-
const values: any[] = [];
|
|
2369
|
-
const isPostgres = (db as any).pool;
|
|
2370
|
-
let idx = 1;
|
|
2371
|
-
for (const key of ['name', 'contact_name', 'contact_email', 'description', 'billing_rate_per_agent', 'currency']) {
|
|
2372
|
-
if (body[key] !== undefined) {
|
|
2373
|
-
fields.push(isPostgres ? `${key} = $${idx++}` : `${key} = ?`);
|
|
2374
|
-
values.push(body[key]);
|
|
2375
|
-
}
|
|
2376
|
-
}
|
|
2377
|
-
// JSON fields
|
|
2378
|
-
if (body.allowed_roles !== undefined) {
|
|
2379
|
-
fields.push(isPostgres ? `allowed_roles = $${idx++}` : `allowed_roles = ?`);
|
|
2380
|
-
values.push(JSON.stringify(body.allowed_roles));
|
|
2381
|
-
}
|
|
2382
|
-
if (body.allowed_skills !== undefined) {
|
|
2383
|
-
fields.push(isPostgres ? `allowed_skills = $${idx++}` : `allowed_skills = ?`);
|
|
2384
|
-
values.push(JSON.stringify(body.allowed_skills));
|
|
2385
|
-
}
|
|
2386
|
-
if (fields.length === 0) return c.json({ error: 'No fields to update' }, 400);
|
|
2387
|
-
fields.push(isPostgres ? `updated_at = NOW()` : `updated_at = datetime('now')`);
|
|
2388
|
-
values.push(id);
|
|
2389
|
-
const where = isPostgres ? `$${idx}` : '?';
|
|
2390
|
-
const sql = `UPDATE client_organizations SET ${fields.join(', ')} WHERE id = ${where}`;
|
|
2391
|
-
if (isPostgres) {
|
|
2392
|
-
await (db as any)._query(sql, values);
|
|
2393
|
-
const { rows } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);
|
|
2394
|
-
return c.json(rows[0]);
|
|
2395
|
-
} else {
|
|
2396
|
-
const engineDb = db.getEngineDB();
|
|
2397
|
-
await engineDb!.run(sql, values);
|
|
2398
|
-
const row = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);
|
|
2399
|
-
return c.json(row);
|
|
2400
|
-
}
|
|
2401
|
-
} catch (e: any) {
|
|
2402
|
-
return c.json({ error: e.message }, 500);
|
|
2403
|
-
}
|
|
2404
|
-
});
|
|
2405
|
-
|
|
2406
|
-
api.post('/organizations/:id/toggle', requireRole('admin'), async (c) => {
|
|
2407
|
-
const id = c.req.param('id');
|
|
2408
|
-
try {
|
|
2409
|
-
const isPostgres = (db as any).pool;
|
|
2410
|
-
if (isPostgres) {
|
|
2411
|
-
const { rows } = await (db as any)._query(`SELECT is_active FROM client_organizations WHERE id = $1`, [id]);
|
|
2412
|
-
if (!rows[0]) return c.json({ error: 'Organization not found' }, 404);
|
|
2413
|
-
const newActive = !rows[0].is_active;
|
|
2414
|
-
await (db as any)._query(`UPDATE client_organizations SET is_active = $1, updated_at = NOW() WHERE id = $2`, [newActive, id]);
|
|
2415
|
-
const newStatus = newActive ? 'active' : 'suspended';
|
|
2416
|
-
await (db as any)._query(`UPDATE agents SET status = $1 WHERE client_org_id = $2`, [newStatus, id]);
|
|
2417
|
-
return c.json({ is_active: newActive });
|
|
2418
|
-
} else {
|
|
2419
|
-
const engineDb = db.getEngineDB();
|
|
2420
|
-
const org = await engineDb!.get<any>(`SELECT is_active FROM client_organizations WHERE id = ?`, [id]);
|
|
2421
|
-
if (!org) return c.json({ error: 'Organization not found' }, 404);
|
|
2422
|
-
const newActive = !(org.is_active);
|
|
2423
|
-
await engineDb!.run(`UPDATE client_organizations SET is_active = ?, updated_at = datetime('now') WHERE id = ?`, [newActive ? 1 : 0, id]);
|
|
2424
|
-
const newStatus = newActive ? 'active' : 'suspended';
|
|
2425
|
-
await engineDb!.run(`UPDATE agents SET status = ? WHERE client_org_id = ?`, [newStatus, id]);
|
|
2426
|
-
return c.json({ is_active: newActive });
|
|
2427
|
-
}
|
|
2428
|
-
} catch (e: any) {
|
|
2429
|
-
return c.json({ error: e.message }, 500);
|
|
2430
|
-
}
|
|
2431
|
-
});
|
|
2432
|
-
|
|
2433
|
-
api.delete('/organizations/:id', requireRole('admin'), async (c) => {
|
|
2434
|
-
const id = c.req.param('id');
|
|
2435
|
-
try {
|
|
2436
|
-
const isPostgres = (db as any).pool;
|
|
2437
|
-
if (isPostgres) {
|
|
2438
|
-
const { rows: agents } = await (db as any)._query(`SELECT id FROM agents WHERE client_org_id = $1`, [id]);
|
|
2439
|
-
if (agents.length > 0) return c.json({ error: 'Cannot delete organization with linked agents. Unassign all agents first.' }, 400);
|
|
2440
|
-
await (db as any)._query(`DELETE FROM client_organizations WHERE id = $1`, [id]);
|
|
2441
|
-
} else {
|
|
2442
|
-
const engineDb = db.getEngineDB();
|
|
2443
|
-
const agents = await engineDb!.all(`SELECT id FROM agents WHERE client_org_id = ?`, [id]);
|
|
2444
|
-
if (agents.length > 0) return c.json({ error: 'Cannot delete organization with linked agents. Unassign all agents first.' }, 400);
|
|
2445
|
-
await engineDb!.run(`DELETE FROM client_organizations WHERE id = ?`, [id]);
|
|
2446
|
-
}
|
|
2447
|
-
return c.json({ success: true });
|
|
2448
|
-
} catch (e: any) {
|
|
2449
|
-
return c.json({ error: e.message }, 500);
|
|
2450
|
-
}
|
|
2451
|
-
});
|
|
2452
|
-
|
|
2453
|
-
// ─── Agent-Org Linking ──────────────────────────────────
|
|
2454
|
-
|
|
2455
|
-
api.post('/agents/:id/assign-org', requireRole('admin'), async (c) => {
|
|
2456
|
-
const agentId = c.req.param('id');
|
|
2457
|
-
const { orgId, clearCredentials } = await c.req.json();
|
|
2458
|
-
if (!orgId) return c.json({ error: 'orgId is required' }, 400);
|
|
2459
|
-
try {
|
|
2460
|
-
const isPostgres = (db as any).pool;
|
|
2461
|
-
|
|
2462
|
-
// Get current org to detect reassignment
|
|
2463
|
-
let previousOrgId: string | null = null;
|
|
2464
|
-
if (isPostgres) {
|
|
2465
|
-
const { rows } = await (db as any)._query(`SELECT client_org_id FROM agents WHERE id = $1`, [agentId]);
|
|
2466
|
-
previousOrgId = rows[0]?.client_org_id || null;
|
|
2467
|
-
} else {
|
|
2468
|
-
const row = await db.getEngineDB()!.get(`SELECT client_org_id FROM agents WHERE id = ?`, [agentId]);
|
|
2469
|
-
previousOrgId = (row as any)?.client_org_id || null;
|
|
2470
|
-
}
|
|
2471
|
-
|
|
2472
|
-
const isReassignment = previousOrgId && previousOrgId !== orgId;
|
|
2473
|
-
|
|
2474
|
-
// Update admin agents table
|
|
2475
|
-
if (isPostgres) {
|
|
2476
|
-
await (db as any)._query(`UPDATE agents SET client_org_id = $1 WHERE id = $2`, [orgId, agentId]);
|
|
2477
|
-
} else {
|
|
2478
|
-
await db.getEngineDB()!.run(`UPDATE agents SET client_org_id = ? WHERE id = ?`, [orgId, agentId]);
|
|
2479
|
-
}
|
|
2480
|
-
// Also update engine managed_agents table
|
|
2481
|
-
const engineDb = db.getEngineDB();
|
|
2482
|
-
if (engineDb) {
|
|
2483
|
-
try {
|
|
2484
|
-
if (isPostgres) {
|
|
2485
|
-
await (db as any)._query(`UPDATE managed_agents SET client_org_id = $1, updated_at = NOW() WHERE id = $2`, [orgId, agentId]);
|
|
2486
|
-
} else {
|
|
2487
|
-
await engineDb.run(`UPDATE managed_agents SET client_org_id = ?, updated_at = datetime('now') WHERE id = ?`, [orgId, agentId]);
|
|
2488
|
-
}
|
|
2489
|
-
} catch { /* column may not exist yet before migration */ }
|
|
2490
|
-
}
|
|
2491
|
-
|
|
2492
|
-
// ALWAYS clear agent credentials when assigning to an org
|
|
2493
|
-
// Agent should start fresh with the new org's inherited credentials
|
|
2494
|
-
let credentialsCleared = 0;
|
|
2495
|
-
if (clearCredentials !== false) {
|
|
2496
|
-
try {
|
|
2497
|
-
// Clear agent-level email config from DB
|
|
2498
|
-
if (isPostgres) {
|
|
2499
|
-
await (db as any)._query(
|
|
2500
|
-
`UPDATE managed_agents SET config = config - 'emailConfig' - 'email', updated_at = NOW() WHERE id = $1`,
|
|
2501
|
-
[agentId]
|
|
2502
|
-
).catch(() => {});
|
|
2503
|
-
} else {
|
|
2504
|
-
// SQLite: read-modify-write
|
|
2505
|
-
const row = await db.getEngineDB()!.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);
|
|
2506
|
-
if (row) {
|
|
2507
|
-
const cfg = JSON.parse((row as any).config || '{}');
|
|
2508
|
-
delete cfg.emailConfig;
|
|
2509
|
-
delete cfg.email;
|
|
2510
|
-
await db.getEngineDB()!.run(`UPDATE managed_agents SET config = ?, updated_at = datetime('now') WHERE id = ?`, [JSON.stringify(cfg), agentId]);
|
|
2511
|
-
}
|
|
2512
|
-
}
|
|
2513
|
-
// Clear per-agent vault secrets from previous org (if reassignment)
|
|
2514
|
-
if (previousOrgId && (globalThis as any).__vault) {
|
|
2515
|
-
const vault = (globalThis as any).__vault;
|
|
2516
|
-
try {
|
|
2517
|
-
const secrets = await vault.getSecretsByOrg(previousOrgId, 'skill_credential');
|
|
2518
|
-
for (const secret of secrets) {
|
|
2519
|
-
if (secret.name?.includes(':agent:' + agentId)) {
|
|
2520
|
-
await vault.deleteSecret(secret.id);
|
|
2521
|
-
credentialsCleared++;
|
|
2522
|
-
}
|
|
2523
|
-
}
|
|
2524
|
-
} catch { /* vault may not support this query */ }
|
|
2525
|
-
}
|
|
2526
|
-
} catch { /* best effort */ }
|
|
2527
|
-
}
|
|
2528
|
-
|
|
2529
|
-
// Clear in-memory + push new org's credentials to the running agent
|
|
2530
|
-
let credentialsPushed = false;
|
|
2531
|
-
try {
|
|
2532
|
-
const oi = (globalThis as any).__orgIntegrations;
|
|
2533
|
-
if (oi) {
|
|
2534
|
-
// Force-clear ALL email config from running agent (in-memory)
|
|
2535
|
-
const agent = oi.lifecycle?.getAgent?.(agentId);
|
|
2536
|
-
if (agent?.config) {
|
|
2537
|
-
agent.config.emailConfig = null;
|
|
2538
|
-
if (agent.config.email) agent.config.email = null;
|
|
2539
|
-
}
|
|
2540
|
-
// Update the agent's client_org_id in-memory
|
|
2541
|
-
if (agent) {
|
|
2542
|
-
agent.client_org_id = orgId;
|
|
2543
|
-
agent.clientOrgId = orgId;
|
|
2544
|
-
}
|
|
2545
|
-
// Push new org's credentials
|
|
2546
|
-
credentialsPushed = await oi.pushCredentialsToAgent(agentId, orgId);
|
|
2547
|
-
}
|
|
2548
|
-
} catch { /* best effort */ }
|
|
2549
|
-
|
|
2550
|
-
return c.json({ success: true, reassigned: !!isReassignment, previousOrgId, credentialsCleared, credentialsPushed });
|
|
2551
|
-
} catch (e: any) {
|
|
2552
|
-
return c.json({ error: e.message }, 500);
|
|
2553
|
-
}
|
|
2554
|
-
});
|
|
2555
|
-
|
|
2556
|
-
api.post('/agents/:id/unassign-org', requireRole('admin'), async (c) => {
|
|
2557
|
-
const agentId = c.req.param('id');
|
|
2558
|
-
try {
|
|
2559
|
-
const isPostgres = (db as any).pool;
|
|
2560
|
-
|
|
2561
|
-
// Get current org before clearing
|
|
2562
|
-
let previousOrgId: string | null = null;
|
|
2563
|
-
if (isPostgres) {
|
|
2564
|
-
const { rows } = await (db as any)._query(`SELECT client_org_id FROM agents WHERE id = $1`, [agentId]);
|
|
2565
|
-
previousOrgId = rows[0]?.client_org_id || null;
|
|
2566
|
-
} else {
|
|
2567
|
-
const row = await db.getEngineDB()!.get(`SELECT client_org_id FROM agents WHERE id = ?`, [agentId]);
|
|
2568
|
-
previousOrgId = (row as any)?.client_org_id || null;
|
|
2569
|
-
}
|
|
2570
|
-
|
|
2571
|
-
// Update admin agents table
|
|
2572
|
-
if (isPostgres) {
|
|
2573
|
-
await (db as any)._query(`UPDATE agents SET client_org_id = NULL WHERE id = $1`, [agentId]);
|
|
2574
|
-
} else {
|
|
2575
|
-
await db.getEngineDB()!.run(`UPDATE agents SET client_org_id = NULL WHERE id = ?`, [agentId]);
|
|
2576
|
-
}
|
|
2577
|
-
// Also update engine managed_agents table
|
|
2578
|
-
const engineDb = db.getEngineDB();
|
|
2579
|
-
if (engineDb) {
|
|
2580
|
-
try {
|
|
2581
|
-
if (isPostgres) {
|
|
2582
|
-
await (db as any)._query(`UPDATE managed_agents SET client_org_id = NULL, updated_at = NOW() WHERE id = $1`, [agentId]);
|
|
2583
|
-
} else {
|
|
2584
|
-
await engineDb.run(`UPDATE managed_agents SET client_org_id = NULL, updated_at = datetime('now') WHERE id = ?`, [agentId]);
|
|
2585
|
-
}
|
|
2586
|
-
} catch { /* column may not exist yet before migration */ }
|
|
2587
|
-
}
|
|
2588
|
-
|
|
2589
|
-
// Clear org-inherited credentials from DB
|
|
2590
|
-
let credentialsCleared = 0;
|
|
2591
|
-
if (previousOrgId) {
|
|
2592
|
-
try {
|
|
2593
|
-
if (isPostgres) {
|
|
2594
|
-
await (db as any)._query(
|
|
2595
|
-
`UPDATE managed_agents SET config = config - 'emailConfig' - 'email', updated_at = NOW() WHERE id = $1`,
|
|
2596
|
-
[agentId]
|
|
2597
|
-
).catch(() => {});
|
|
2598
|
-
} else {
|
|
2599
|
-
const row = await db.getEngineDB()!.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);
|
|
2600
|
-
if (row) {
|
|
2601
|
-
const cfg = JSON.parse((row as any).config || '{}');
|
|
2602
|
-
delete cfg.emailConfig;
|
|
2603
|
-
delete cfg.email;
|
|
2604
|
-
await db.getEngineDB()!.run(`UPDATE managed_agents SET config = ?, updated_at = datetime('now') WHERE id = ?`, [JSON.stringify(cfg), agentId]);
|
|
2605
|
-
}
|
|
2606
|
-
}
|
|
2607
|
-
if ((globalThis as any).__vault) {
|
|
2608
|
-
const vault = (globalThis as any).__vault;
|
|
2609
|
-
try {
|
|
2610
|
-
const secrets = await vault.getSecretsByOrg(previousOrgId, 'skill_credential');
|
|
2611
|
-
for (const secret of secrets) {
|
|
2612
|
-
if (secret.name?.includes(':agent:' + agentId)) {
|
|
2613
|
-
await vault.deleteSecret(secret.id);
|
|
2614
|
-
credentialsCleared++;
|
|
2615
|
-
}
|
|
2616
|
-
}
|
|
2617
|
-
} catch { /* best effort */ }
|
|
2618
|
-
}
|
|
2619
|
-
} catch { /* best effort */ }
|
|
2620
|
-
}
|
|
2621
|
-
|
|
2622
|
-
// Clear ALL credentials from the running agent (in-memory)
|
|
2623
|
-
try {
|
|
2624
|
-
const oi = (globalThis as any).__orgIntegrations;
|
|
2625
|
-
if (oi) {
|
|
2626
|
-
const agent = oi.lifecycle?.getAgent?.(agentId);
|
|
2627
|
-
if (agent?.config) {
|
|
2628
|
-
agent.config.emailConfig = null;
|
|
2629
|
-
if (agent.config.email) agent.config.email = null;
|
|
2630
|
-
}
|
|
2631
|
-
if (agent) {
|
|
2632
|
-
agent.client_org_id = null;
|
|
2633
|
-
agent.clientOrgId = null;
|
|
2634
|
-
}
|
|
2635
|
-
}
|
|
2636
|
-
} catch { /* best effort */ }
|
|
2637
|
-
|
|
2638
|
-
return c.json({ success: true, previousOrgId, credentialsCleared });
|
|
2639
|
-
} catch (e: any) {
|
|
2640
|
-
return c.json({ error: e.message }, 500);
|
|
2641
|
-
}
|
|
2642
|
-
});
|
|
2643
|
-
|
|
2644
|
-
// ─── Agent Knowledge Access ─────────────────────────────
|
|
2645
|
-
|
|
2646
|
-
api.get('/agents/:id/knowledge-access', requireRole('admin'), async (c) => {
|
|
2647
|
-
const agentId = c.req.param('id');
|
|
2648
|
-
try {
|
|
2649
|
-
const isPostgres = (db as any).pool;
|
|
2650
|
-
if (isPostgres) {
|
|
2651
|
-
const { rows } = await (db as any)._query(`SELECT * FROM agent_knowledge_access WHERE agent_id = $1`, [agentId]);
|
|
2652
|
-
return c.json({ grants: rows });
|
|
2653
|
-
} else {
|
|
2654
|
-
const rows = await db.getEngineDB()!.all(`SELECT * FROM agent_knowledge_access WHERE agent_id = ?`, [agentId]);
|
|
2655
|
-
return c.json({ grants: rows });
|
|
2656
|
-
}
|
|
2657
|
-
} catch (e: any) {
|
|
2658
|
-
return c.json({ error: e.message }, 500);
|
|
2659
|
-
}
|
|
2660
|
-
});
|
|
2661
|
-
|
|
2662
|
-
api.put('/agents/:id/knowledge-access', requireRole('admin'), async (c) => {
|
|
2663
|
-
const agentId = c.req.param('id');
|
|
2664
|
-
const { grants } = await c.req.json();
|
|
2665
|
-
if (!Array.isArray(grants)) return c.json({ error: 'grants must be an array' }, 400);
|
|
2666
|
-
try {
|
|
2667
|
-
const isPostgres = (db as any).pool;
|
|
2668
|
-
if (isPostgres) {
|
|
2669
|
-
await (db as any)._query(`DELETE FROM agent_knowledge_access WHERE agent_id = $1`, [agentId]);
|
|
2670
|
-
for (const g of grants) {
|
|
2671
|
-
const id = (await import('crypto')).randomUUID();
|
|
2672
|
-
await (db as any)._query(
|
|
2673
|
-
`INSERT INTO agent_knowledge_access (id, agent_id, knowledge_base_id, access_type) VALUES ($1, $2, $3, $4)`,
|
|
2674
|
-
[id, agentId, g.knowledgeBaseId, g.accessType || 'read']
|
|
2675
|
-
);
|
|
2676
|
-
}
|
|
2677
|
-
} else {
|
|
2678
|
-
const engineDb = db.getEngineDB()!;
|
|
2679
|
-
await engineDb.run(`DELETE FROM agent_knowledge_access WHERE agent_id = ?`, [agentId]);
|
|
2680
|
-
for (const g of grants) {
|
|
2681
|
-
const id = (await import('crypto')).randomUUID();
|
|
2682
|
-
await engineDb.run(
|
|
2683
|
-
`INSERT INTO agent_knowledge_access (id, agent_id, knowledge_base_id, access_type) VALUES (?, ?, ?, ?)`,
|
|
2684
|
-
[id, agentId, g.knowledgeBaseId, g.accessType || 'read']
|
|
2685
|
-
);
|
|
2686
|
-
}
|
|
2687
|
-
}
|
|
2688
|
-
return c.json({ success: true });
|
|
2689
|
-
} catch (e: any) {
|
|
2690
|
-
return c.json({ error: e.message }, 500);
|
|
2691
|
-
}
|
|
2692
|
-
});
|
|
2693
|
-
|
|
2694
|
-
// ─── Organization Billing ────────────────────────────
|
|
2695
|
-
|
|
2696
|
-
api.get('/organizations/:id/billing', requireRole('admin'), async (c) => {
|
|
2697
|
-
const orgId = c.req.param('id');
|
|
2698
|
-
const months = parseInt(c.req.query('months') || '12');
|
|
2699
|
-
try {
|
|
2700
|
-
const isPostgres = (db as any).pool;
|
|
2701
|
-
let records: any[];
|
|
2702
|
-
if (isPostgres) {
|
|
2703
|
-
const { rows } = await (db as any)._query(
|
|
2704
|
-
`SELECT * FROM org_billing_records WHERE org_id = $1 ORDER BY month DESC LIMIT $2`,
|
|
2705
|
-
[orgId, months]
|
|
2706
|
-
);
|
|
2707
|
-
records = rows;
|
|
2708
|
-
} else {
|
|
2709
|
-
records = await db.getEngineDB()!.all(
|
|
2710
|
-
`SELECT * FROM org_billing_records WHERE org_id = ? ORDER BY month DESC LIMIT ?`,
|
|
2711
|
-
[orgId, months]
|
|
2712
|
-
);
|
|
2713
|
-
}
|
|
2714
|
-
return c.json({ records });
|
|
2715
|
-
} catch (e: any) {
|
|
2716
|
-
return c.json({ error: e.message }, 500);
|
|
2717
|
-
}
|
|
2718
|
-
});
|
|
2719
|
-
|
|
2720
|
-
api.put('/organizations/:id/billing', requireRole('admin'), async (c) => {
|
|
2721
|
-
const orgId = c.req.param('id');
|
|
2722
|
-
const { records } = await c.req.json();
|
|
2723
|
-
if (!Array.isArray(records)) return c.json({ error: 'records must be an array' }, 400);
|
|
2724
|
-
try {
|
|
2725
|
-
const isPostgres = (db as any).pool;
|
|
2726
|
-
const { randomUUID } = await import('crypto');
|
|
2727
|
-
for (const r of records) {
|
|
2728
|
-
if (!r.month) continue;
|
|
2729
|
-
if (isPostgres) {
|
|
2730
|
-
await (db as any)._query(
|
|
2731
|
-
`INSERT INTO org_billing_records (id, org_id, agent_id, month, revenue, token_cost, input_tokens, output_tokens, notes)
|
|
2732
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
|
2733
|
-
ON CONFLICT (org_id, agent_id, month) DO UPDATE SET
|
|
2734
|
-
revenue = EXCLUDED.revenue, token_cost = EXCLUDED.token_cost,
|
|
2735
|
-
input_tokens = EXCLUDED.input_tokens, output_tokens = EXCLUDED.output_tokens,
|
|
2736
|
-
notes = EXCLUDED.notes, updated_at = NOW()`,
|
|
2737
|
-
[randomUUID(), orgId, r.agentId || null, r.month, r.revenue || 0, r.tokenCost || 0, r.inputTokens || 0, r.outputTokens || 0, r.notes || null]
|
|
2738
|
-
);
|
|
2739
|
-
} else {
|
|
2740
|
-
const id = randomUUID();
|
|
2741
|
-
await db.getEngineDB()!.run(
|
|
2742
|
-
`INSERT OR REPLACE INTO org_billing_records (id, org_id, agent_id, month, revenue, token_cost, input_tokens, output_tokens, notes)
|
|
2743
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
2744
|
-
[id, orgId, r.agentId || null, r.month, r.revenue || 0, r.tokenCost || 0, r.inputTokens || 0, r.outputTokens || 0, r.notes || null]
|
|
2745
|
-
);
|
|
2746
|
-
}
|
|
2747
|
-
}
|
|
2748
|
-
return c.json({ success: true });
|
|
2749
|
-
} catch (e: any) {
|
|
2750
|
-
return c.json({ error: e.message }, 500);
|
|
2751
|
-
}
|
|
2752
|
-
});
|
|
2753
|
-
|
|
2754
|
-
// ─── Organization Billing Summary ─────────────────────
|
|
2755
|
-
|
|
2756
|
-
api.get('/organizations/:id/billing-summary', requireRole('admin'), async (c) => {
|
|
2757
|
-
const orgId = c.req.param('id');
|
|
2758
|
-
try {
|
|
2759
|
-
const isPostgres = (db as any).pool;
|
|
2760
|
-
let rows: any[];
|
|
2761
|
-
if (isPostgres) {
|
|
2762
|
-
const result = await (db as any)._query(
|
|
2763
|
-
`SELECT month, SUM(revenue) as total_revenue, SUM(token_cost) as total_cost,
|
|
2764
|
-
SUM(input_tokens) as total_input_tokens, SUM(output_tokens) as total_output_tokens
|
|
2765
|
-
FROM org_billing_records WHERE org_id = $1
|
|
2766
|
-
GROUP BY month ORDER BY month ASC`, [orgId]
|
|
2767
|
-
);
|
|
2768
|
-
rows = result.rows;
|
|
2769
|
-
} else {
|
|
2770
|
-
rows = await db.getEngineDB()!.all(
|
|
2771
|
-
`SELECT month, SUM(revenue) as total_revenue, SUM(token_cost) as total_cost,
|
|
2772
|
-
SUM(input_tokens) as total_input_tokens, SUM(output_tokens) as total_output_tokens
|
|
2773
|
-
FROM org_billing_records WHERE org_id = ?
|
|
2774
|
-
GROUP BY month ORDER BY month ASC`, [orgId]
|
|
2775
|
-
);
|
|
2776
|
-
}
|
|
2777
|
-
return c.json({ summary: rows });
|
|
2778
|
-
} catch (e: any) {
|
|
2779
|
-
return c.json({ error: e.message }, 500);
|
|
2780
|
-
}
|
|
2781
|
-
});
|
|
2782
|
-
|
|
2783
|
-
/** Stop and optionally delete tunnel */
|
|
2784
|
-
api.post('/tunnel/stop', requireRole('admin'), async (c) => {
|
|
2785
|
-
try {
|
|
2786
|
-
const { execSync } = await import('child_process');
|
|
2787
|
-
try { execSync('pm2 stop cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }
|
|
2788
|
-
try { execSync('pm2 delete cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }
|
|
2789
|
-
return c.json({ success: true });
|
|
2790
|
-
} catch (e: any) {
|
|
2791
|
-
return c.json({ error: e.message }, 500);
|
|
2792
|
-
}
|
|
2793
|
-
});
|
|
2794
|
-
|
|
2795
|
-
// ─── Custom Agent Roles (Soul Templates) ──────────────
|
|
2796
|
-
|
|
2797
|
-
const rolesQuery = async (sql: string, params: any[] = []) => {
|
|
2798
|
-
const isPostgres = (db as any).pool;
|
|
2799
|
-
if (isPostgres) {
|
|
2800
|
-
const { rows } = await (db as any)._query(sql, params);
|
|
2801
|
-
return rows;
|
|
2802
|
-
} else {
|
|
2803
|
-
const engineDb = db.getEngineDB();
|
|
2804
|
-
return await engineDb!.all(sql.replace(/\$(\d+)/g, '?'), params);
|
|
2805
|
-
}
|
|
2806
|
-
};
|
|
2807
|
-
|
|
2808
|
-
const rolesExec = async (sql: string, params: any[] = []) => {
|
|
2809
|
-
const isPostgres = (db as any).pool;
|
|
2810
|
-
if (isPostgres) {
|
|
2811
|
-
await (db as any)._query(sql, params);
|
|
2812
|
-
} else {
|
|
2813
|
-
const engineDb = db.getEngineDB();
|
|
2814
|
-
await engineDb!.run(sql.replace(/\$(\d+)/g, '?'), params);
|
|
2815
|
-
}
|
|
2816
|
-
};
|
|
2817
|
-
|
|
2818
|
-
const rolesGet = async (sql: string, params: any[] = []) => {
|
|
2819
|
-
const isPostgres = (db as any).pool;
|
|
2820
|
-
if (isPostgres) {
|
|
2821
|
-
const { rows } = await (db as any)._query(sql, params);
|
|
2822
|
-
return rows[0] || null;
|
|
2823
|
-
} else {
|
|
2824
|
-
const engineDb = db.getEngineDB();
|
|
2825
|
-
return await engineDb!.get(sql.replace(/\$(\d+)/g, '?'), params);
|
|
2826
|
-
}
|
|
2827
|
-
};
|
|
2828
|
-
|
|
2829
|
-
const mapRole = (r: any) => {
|
|
2830
|
-
if (!r) return null;
|
|
2831
|
-
const parse = (v: any) => { try { return typeof v === 'string' ? JSON.parse(v) : v; } catch { return v; } };
|
|
2832
|
-
return {
|
|
2833
|
-
id: r.id, name: r.name, slug: r.slug, category: r.category || 'operations',
|
|
2834
|
-
description: r.description, personality: r.personality || '',
|
|
2835
|
-
identity: parse(r.identity) || {}, suggestedSkills: parse(r.suggested_skills) || [],
|
|
2836
|
-
suggestedPreset: r.suggested_preset || null, tags: parse(r.tags) || [],
|
|
2837
|
-
orgId: r.org_id, isActive: r.is_active !== false && r.is_active !== 0,
|
|
2838
|
-
isCustom: true, metadata: parse(r.metadata),
|
|
2839
|
-
createdBy: r.created_by, createdAt: r.created_at, updatedAt: r.updated_at,
|
|
2840
|
-
};
|
|
2841
|
-
};
|
|
2842
|
-
|
|
2843
|
-
// List custom agent roles
|
|
2844
|
-
api.get('/roles', requireRole('admin'), async (c) => {
|
|
2845
|
-
try {
|
|
2846
|
-
const orgId = c.req.query('orgId');
|
|
2847
|
-
let sql: string, params: any[];
|
|
2848
|
-
if (orgId) {
|
|
2849
|
-
sql = 'SELECT * FROM custom_roles WHERE (org_id = $1 OR org_id IS NULL) AND is_active = $2 ORDER BY category, name';
|
|
2850
|
-
params = [orgId, (db as any).pool ? true : 1];
|
|
2851
|
-
} else {
|
|
2852
|
-
sql = 'SELECT * FROM custom_roles ORDER BY category, name';
|
|
2853
|
-
params = [];
|
|
2854
|
-
}
|
|
2855
|
-
const rows = await rolesQuery(sql, params);
|
|
2856
|
-
return c.json({ roles: rows.map(mapRole) });
|
|
2857
|
-
} catch (e: any) { return c.json({ error: e.message }, 500); }
|
|
2858
|
-
});
|
|
2859
|
-
|
|
2860
|
-
// Get single role
|
|
2861
|
-
api.get('/roles/:id', requireRole('admin'), async (c) => {
|
|
2862
|
-
try {
|
|
2863
|
-
const role = mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]));
|
|
2864
|
-
if (!role) return c.json({ error: 'Role not found' }, 404);
|
|
2865
|
-
return c.json(role);
|
|
2866
|
-
} catch (e: any) { return c.json({ error: e.message }, 500); }
|
|
2867
|
-
});
|
|
2868
|
-
|
|
2869
|
-
// Create role
|
|
2870
|
-
api.post('/roles', requireRole('admin'), async (c) => {
|
|
2871
|
-
try {
|
|
2872
|
-
const body = await c.req.json();
|
|
2873
|
-
validate(body, [
|
|
2874
|
-
{ field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },
|
|
2875
|
-
{ field: 'category', type: 'string', required: true },
|
|
2876
|
-
{ field: 'description', type: 'string', maxLength: 1024 },
|
|
2877
|
-
]);
|
|
2878
|
-
const slug = (body.slug || body.name).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
2879
|
-
const existing = body.orgId
|
|
2880
|
-
? await rolesGet('SELECT id FROM custom_roles WHERE slug = $1 AND org_id = $2', [slug, body.orgId])
|
|
2881
|
-
: await rolesGet('SELECT id FROM custom_roles WHERE slug = $1 AND org_id IS NULL', [slug]);
|
|
2882
|
-
if (existing) return c.json({ error: 'A role with this name already exists' }, 409);
|
|
2883
|
-
|
|
2884
|
-
const id = crypto.randomUUID();
|
|
2885
|
-
const isPostgres = (db as any).pool;
|
|
2886
|
-
await rolesExec(
|
|
2887
|
-
`INSERT INTO custom_roles (id, name, slug, category, description, personality, identity, suggested_skills, suggested_preset, tags, org_id, is_active, metadata, created_by) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`,
|
|
2888
|
-
[
|
|
2889
|
-
id, body.name, slug, body.category || 'operations', body.description || null,
|
|
2890
|
-
body.personality || null, JSON.stringify(body.identity || {}),
|
|
2891
|
-
JSON.stringify(body.suggestedSkills || []), body.suggestedPreset || null,
|
|
2892
|
-
JSON.stringify(body.tags || []), body.orgId || null,
|
|
2893
|
-
isPostgres ? true : 1, JSON.stringify(body.metadata || {}),
|
|
2894
|
-
(c as any).get?.('userId') || 'system',
|
|
2895
|
-
]
|
|
2896
|
-
);
|
|
2897
|
-
return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [id])), 201);
|
|
2898
|
-
} catch (e: any) { return c.json({ error: e.message }, e instanceof ValidationError ? 400 : 500); }
|
|
2899
|
-
});
|
|
2900
|
-
|
|
2901
|
-
// Update role
|
|
2902
|
-
api.put('/roles/:id', requireRole('admin'), async (c) => {
|
|
2903
|
-
try {
|
|
2904
|
-
const existing = await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]);
|
|
2905
|
-
if (!existing) return c.json({ error: 'Role not found' }, 404);
|
|
2906
|
-
const body = await c.req.json();
|
|
2907
|
-
const isPostgres = (db as any).pool;
|
|
2908
|
-
const fields: string[] = [];
|
|
2909
|
-
const values: any[] = [];
|
|
2910
|
-
let i = 1;
|
|
2911
|
-
const strMap: Record<string, string> = { name: 'name', category: 'category', description: 'description', personality: 'personality', suggestedPreset: 'suggested_preset' };
|
|
2912
|
-
for (const [key, col] of Object.entries(strMap)) {
|
|
2913
|
-
if (body[key] !== undefined) { fields.push(`${col} = $${i++}`); values.push(body[key]); }
|
|
2914
|
-
}
|
|
2915
|
-
if (body.identity !== undefined) { fields.push(`identity = $${i++}`); values.push(JSON.stringify(body.identity)); }
|
|
2916
|
-
if (body.suggestedSkills !== undefined) { fields.push(`suggested_skills = $${i++}`); values.push(JSON.stringify(body.suggestedSkills)); }
|
|
2917
|
-
if (body.tags !== undefined) { fields.push(`tags = $${i++}`); values.push(JSON.stringify(body.tags)); }
|
|
2918
|
-
if (body.orgId !== undefined) { fields.push(`org_id = $${i++}`); values.push(body.orgId || null); }
|
|
2919
|
-
if (body.isActive !== undefined) { fields.push(`is_active = $${i++}`); values.push(isPostgres ? !!body.isActive : (body.isActive ? 1 : 0)); }
|
|
2920
|
-
if (body.metadata !== undefined) { fields.push(`metadata = $${i++}`); values.push(JSON.stringify(body.metadata)); }
|
|
2921
|
-
if (body.name && body.name !== existing.name) {
|
|
2922
|
-
const slug = body.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
2923
|
-
fields.push(`slug = $${i++}`); values.push(slug);
|
|
2924
|
-
}
|
|
2925
|
-
if (fields.length === 0) return c.json({ error: 'No fields to update' }, 400);
|
|
2926
|
-
fields.push(`updated_at = $${i++}`); values.push(new Date().toISOString());
|
|
2927
|
-
values.push(c.req.param('id'));
|
|
2928
|
-
await rolesExec(`UPDATE custom_roles SET ${fields.join(', ')} WHERE id = $${i}`, values);
|
|
2929
|
-
return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')])));
|
|
2930
|
-
} catch (e: any) { return c.json({ error: e.message }, 500); }
|
|
2931
|
-
});
|
|
2932
|
-
|
|
2933
|
-
// Delete role
|
|
2934
|
-
api.delete('/roles/:id', requireRole('admin'), async (c) => {
|
|
2935
|
-
try {
|
|
2936
|
-
const existing = await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]);
|
|
2937
|
-
if (!existing) return c.json({ error: 'Role not found' }, 404);
|
|
2938
|
-
await rolesExec('DELETE FROM custom_roles WHERE id = $1', [c.req.param('id')]);
|
|
2939
|
-
return c.json({ success: true });
|
|
2940
|
-
} catch (e: any) { return c.json({ error: e.message }, 500); }
|
|
2941
|
-
});
|
|
2942
|
-
|
|
2943
|
-
// Duplicate role
|
|
2944
|
-
api.post('/roles/:id/duplicate', requireRole('admin'), async (c) => {
|
|
2945
|
-
try {
|
|
2946
|
-
const source = mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]));
|
|
2947
|
-
if (!source) return c.json({ error: 'Role not found' }, 404);
|
|
2948
|
-
const body = await c.req.json().catch(() => ({}));
|
|
2949
|
-
const newName = body.name || source.name + ' (Copy)';
|
|
2950
|
-
const slug = newName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
2951
|
-
const id = crypto.randomUUID();
|
|
2952
|
-
const isPostgres = (db as any).pool;
|
|
2953
|
-
await rolesExec(
|
|
2954
|
-
`INSERT INTO custom_roles (id, name, slug, category, description, personality, identity, suggested_skills, suggested_preset, tags, org_id, is_active, metadata, created_by) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`,
|
|
2955
|
-
[
|
|
2956
|
-
id, newName, slug, source.category, source.description, source.personality,
|
|
2957
|
-
JSON.stringify(source.identity), JSON.stringify(source.suggestedSkills),
|
|
2958
|
-
source.suggestedPreset, JSON.stringify(source.tags), source.orgId || null,
|
|
2959
|
-
isPostgres ? true : 1, JSON.stringify(source.metadata),
|
|
2960
|
-
(c as any).get?.('userId') || 'system',
|
|
2961
|
-
]
|
|
2962
|
-
);
|
|
2963
|
-
return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [id])), 201);
|
|
2964
|
-
} catch (e: any) { return c.json({ error: e.message }, 500); }
|
|
2965
|
-
});
|
|
2966
|
-
|
|
2967
|
-
return api;
|
|
2968
|
-
}
|