@wopr-network/platform-ui-core 1.0.0
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/.env.paperclip +18 -0
- package/.env.wopr +18 -0
- package/README.md +36 -0
- package/biome.json +52 -0
- package/next.config.ts +45 -0
- package/package.json +84 -0
- package/postcss.config.mjs +7 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/window.svg +1 -0
- package/src/__tests__/__snapshots__/layout-snapshots.test.tsx.snap +741 -0
- package/src/__tests__/account-page-redirect.test.tsx +73 -0
- package/src/__tests__/account-switcher.test.tsx +85 -0
- package/src/__tests__/activity-page.test.tsx +176 -0
- package/src/__tests__/add-payment-method-dialog.test.tsx +160 -0
- package/src/__tests__/admin-api.test.ts +244 -0
- package/src/__tests__/admin-gpu-api.test.ts +188 -0
- package/src/__tests__/admin-guard.test.tsx +79 -0
- package/src/__tests__/admin-marketplace-api.test.ts +179 -0
- package/src/__tests__/admin-middleware.test.ts +157 -0
- package/src/__tests__/admin-tenant-table.test.tsx +95 -0
- package/src/__tests__/affiliate-dashboard.test.tsx +178 -0
- package/src/__tests__/api-401-redirect.test.ts +78 -0
- package/src/__tests__/api-client.test.ts +316 -0
- package/src/__tests__/api-config.test.ts +89 -0
- package/src/__tests__/api-control-instance.test.ts +69 -0
- package/src/__tests__/api-fleet-resources.test.ts +52 -0
- package/src/__tests__/api-fleet-trpc.test.ts +252 -0
- package/src/__tests__/api-get-instance-config.test.ts +41 -0
- package/src/__tests__/api-null-guards.test.ts +244 -0
- package/src/__tests__/api-rename-instance.test.ts +60 -0
- package/src/__tests__/api-update-instance-config.test.ts +60 -0
- package/src/__tests__/audit-log-table-pagination.test.tsx +136 -0
- package/src/__tests__/auth-client.test.ts +87 -0
- package/src/__tests__/auth-password-reset.test.tsx +435 -0
- package/src/__tests__/auth-redirect.test.tsx +60 -0
- package/src/__tests__/auth.test.tsx +269 -0
- package/src/__tests__/auto-topup-card.test.tsx +257 -0
- package/src/__tests__/backups-tab.test.tsx +221 -0
- package/src/__tests__/billing-byok-callout.test.tsx +76 -0
- package/src/__tests__/billing-layout-nav-hidden.test.tsx +47 -0
- package/src/__tests__/billing-payment-org-invoices.test.tsx +123 -0
- package/src/__tests__/billing.test.tsx +509 -0
- package/src/__tests__/bot-settings/resources-tab.test.tsx +119 -0
- package/src/__tests__/bot-settings/storage-tab.test.tsx +80 -0
- package/src/__tests__/bot-settings/vps-info-panel.test.tsx +108 -0
- package/src/__tests__/bot-settings/vps-upgrade-card.test.tsx +52 -0
- package/src/__tests__/bot-settings-data-control.test.ts +49 -0
- package/src/__tests__/bot-settings-restart.test.tsx +149 -0
- package/src/__tests__/bot-settings.test.tsx +678 -0
- package/src/__tests__/brand.test.ts +335 -0
- package/src/__tests__/buy-credits-panel.test.tsx +249 -0
- package/src/__tests__/buy-crypto-credits-panel.test.tsx +178 -0
- package/src/__tests__/capability-conflicts.test.ts +88 -0
- package/src/__tests__/capability-resolver.test.tsx +173 -0
- package/src/__tests__/changeset-detail.test.tsx +156 -0
- package/src/__tests__/channel-setup-logger.test.ts +22 -0
- package/src/__tests__/channel-setup-toast.test.tsx +60 -0
- package/src/__tests__/channel-wizard.test.tsx +505 -0
- package/src/__tests__/chat/ambient-dot.test.tsx +35 -0
- package/src/__tests__/chat/chat-input.test.tsx +78 -0
- package/src/__tests__/chat/chat-message.test.tsx +45 -0
- package/src/__tests__/chat/chat-panel.test.tsx +111 -0
- package/src/__tests__/chat/chat-widget.test.tsx +82 -0
- package/src/__tests__/chat-store.test.ts +87 -0
- package/src/__tests__/command-center.test.tsx +246 -0
- package/src/__tests__/compliance-retention-edit.test.tsx +134 -0
- package/src/__tests__/coupon-input.test.tsx +119 -0
- package/src/__tests__/create-instance.test.tsx +96 -0
- package/src/__tests__/create-org-wizard.test.tsx +200 -0
- package/src/__tests__/credit-balance.test.tsx +103 -0
- package/src/__tests__/credits.test.tsx +376 -0
- package/src/__tests__/csrf-middleware.test.ts +198 -0
- package/src/__tests__/degraded-state-banner.test.tsx +130 -0
- package/src/__tests__/dividend-calculator.test.tsx +20 -0
- package/src/__tests__/dividend-stats.test.tsx +64 -0
- package/src/__tests__/dividend.test.tsx +169 -0
- package/src/__tests__/dockerfile.test.ts +110 -0
- package/src/__tests__/email-verification-banner.test.tsx +64 -0
- package/src/__tests__/env-example.test.ts +25 -0
- package/src/__tests__/error-boundaries.test.tsx +64 -0
- package/src/__tests__/fetch-pricing.test.ts +121 -0
- package/src/__tests__/field-oauth.test.tsx +302 -0
- package/src/__tests__/fixtures/mock-manifests-data.js +372 -0
- package/src/__tests__/fixtures/mock-manifests.ts +24 -0
- package/src/__tests__/fleet-health-timestamp.test.tsx +101 -0
- package/src/__tests__/fleet-health-update.test.tsx +83 -0
- package/src/__tests__/format-credit.test.ts +58 -0
- package/src/__tests__/gpu-dashboard.test.tsx +236 -0
- package/src/__tests__/hosted-usage-date-range.test.tsx +54 -0
- package/src/__tests__/instance-detail.test.tsx +571 -0
- package/src/__tests__/instance-list.test.tsx +230 -0
- package/src/__tests__/landing-hero.test.tsx +27 -0
- package/src/__tests__/landing-nav.test.tsx +24 -0
- package/src/__tests__/layout-snapshots.test.tsx +167 -0
- package/src/__tests__/logger.test.ts +54 -0
- package/src/__tests__/login-page-redirect.test.tsx +142 -0
- package/src/__tests__/manifest-validation.test.ts +126 -0
- package/src/__tests__/marketplace-admin.test.tsx +151 -0
- package/src/__tests__/marketplace.test.tsx +609 -0
- package/src/__tests__/merge-api-rates.test.ts +70 -0
- package/src/__tests__/middleware.test.ts +690 -0
- package/src/__tests__/network-page.test.tsx +100 -0
- package/src/__tests__/next-config-headers.test.ts +28 -0
- package/src/__tests__/not-found.test.tsx +26 -0
- package/src/__tests__/notifications.test.tsx +128 -0
- package/src/__tests__/oauth-buttons.test.tsx +101 -0
- package/src/__tests__/oauth-error-mapping.test.tsx +97 -0
- package/src/__tests__/observability.test.tsx +541 -0
- package/src/__tests__/onboarding-data.test.ts +363 -0
- package/src/__tests__/onboarding-page.test.tsx +113 -0
- package/src/__tests__/onboarding-store.test.ts +121 -0
- package/src/__tests__/org-billing-api.test.tsx +70 -0
- package/src/__tests__/org-billing-null-guards.test.ts +64 -0
- package/src/__tests__/org-billing-page.test.tsx +124 -0
- package/src/__tests__/plugin-definition.test.ts +43 -0
- package/src/__tests__/plugin-install-flow.test.tsx +535 -0
- package/src/__tests__/plugin-registry.test.tsx +475 -0
- package/src/__tests__/plugin-setup/setup-chat-panel.test.ts +142 -0
- package/src/__tests__/plugin-setup/use-plugin-setup-chat.test.ts +49 -0
- package/src/__tests__/plugin-tool-definitions.test.ts +51 -0
- package/src/__tests__/plugin-tool-sync.test.ts +59 -0
- package/src/__tests__/portfolio-chart.test.tsx +24 -0
- package/src/__tests__/pricing.test.tsx +107 -0
- package/src/__tests__/promotion-form.test.tsx +180 -0
- package/src/__tests__/promotions-list.test.tsx +194 -0
- package/src/__tests__/provider-key-api.test.ts +134 -0
- package/src/__tests__/resend-verification-button.test.tsx +104 -0
- package/src/__tests__/sanitize-redirect-url.test.ts +47 -0
- package/src/__tests__/secrets-audit-pagination.test.tsx +139 -0
- package/src/__tests__/settings.test.tsx +937 -0
- package/src/__tests__/setup-checklist.test.tsx +274 -0
- package/src/__tests__/setup.ts +82 -0
- package/src/__tests__/smoke.test.tsx +10 -0
- package/src/__tests__/snapshot-api.test.ts +104 -0
- package/src/__tests__/status-api.test.ts +46 -0
- package/src/__tests__/status-badge.test.tsx +33 -0
- package/src/__tests__/status-colors.test.ts +83 -0
- package/src/__tests__/status-page.test.tsx +86 -0
- package/src/__tests__/step-superpowers.test.tsx +218 -0
- package/src/__tests__/story-sections.test.tsx +24 -0
- package/src/__tests__/superpower-content-sanitize.test.tsx +87 -0
- package/src/__tests__/superpower-content.test.tsx +44 -0
- package/src/__tests__/suspension-banner.test.tsx +140 -0
- package/src/__tests__/tenant-context.test.tsx +146 -0
- package/src/__tests__/tenant-keys-api.test.ts +114 -0
- package/src/__tests__/tenant-table-pagination.test.tsx +124 -0
- package/src/__tests__/terminal-log-cleanup.test.tsx +51 -0
- package/src/__tests__/terminal-sequence.test.tsx +28 -0
- package/src/__tests__/transaction-history.test.tsx +325 -0
- package/src/__tests__/trpc-types.test.ts +102 -0
- package/src/__tests__/use-capability-meta.test.ts +161 -0
- package/src/__tests__/use-chat.test.ts +616 -0
- package/src/__tests__/use-has-org.test.ts +44 -0
- package/src/__tests__/use-image-status.test.ts +77 -0
- package/src/__tests__/use-pagination-params.test.ts +88 -0
- package/src/__tests__/use-plugin-setup-chat-stale-closure.test.ts +53 -0
- package/src/__tests__/use-webmcp.test.ts +119 -0
- package/src/__tests__/validate-elevenlabs-key.test.ts +95 -0
- package/src/__tests__/validate-redirect-url.test.ts +61 -0
- package/src/__tests__/verify-page.test.tsx +140 -0
- package/src/__tests__/verify-redirect.test.tsx +41 -0
- package/src/__tests__/verify-result-banner.test.tsx +66 -0
- package/src/__tests__/webmcp-feature-detect.test.ts +54 -0
- package/src/__tests__/webmcp-hook.test.tsx +72 -0
- package/src/__tests__/webmcp-marketplace-onboarding-tools.test.ts +185 -0
- package/src/__tests__/webmcp-register.test.ts +103 -0
- package/src/__tests__/webmcp-set-provider.test.ts +47 -0
- package/src/__tests__/webmcp-tools.test.ts +348 -0
- package/src/app/(auth)/error.tsx +72 -0
- package/src/app/(auth)/forgot-password/page.tsx +137 -0
- package/src/app/(auth)/layout.tsx +14 -0
- package/src/app/(auth)/loading.tsx +26 -0
- package/src/app/(auth)/login/page.tsx +188 -0
- package/src/app/(auth)/reset-password/page.tsx +169 -0
- package/src/app/(auth)/signup/page.tsx +309 -0
- package/src/app/(dashboard)/billing/credits/page.tsx +209 -0
- package/src/app/(dashboard)/billing/error.tsx +72 -0
- package/src/app/(dashboard)/billing/layout.tsx +73 -0
- package/src/app/(dashboard)/billing/loading.tsx +41 -0
- package/src/app/(dashboard)/billing/payment/page.tsx +639 -0
- package/src/app/(dashboard)/billing/plans/page.tsx +58 -0
- package/src/app/(dashboard)/billing/referrals/page.tsx +7 -0
- package/src/app/(dashboard)/billing/usage/hosted/page.tsx +348 -0
- package/src/app/(dashboard)/billing/usage/page.tsx +663 -0
- package/src/app/(dashboard)/changesets/[id]/changeset-detail-client.tsx +400 -0
- package/src/app/(dashboard)/changesets/[id]/error.tsx +57 -0
- package/src/app/(dashboard)/changesets/[id]/loading.tsx +23 -0
- package/src/app/(dashboard)/changesets/[id]/page.tsx +10 -0
- package/src/app/(dashboard)/changesets/error.tsx +72 -0
- package/src/app/(dashboard)/changesets/page.tsx +10 -0
- package/src/app/(dashboard)/chat/page.tsx +74 -0
- package/src/app/(dashboard)/dashboard/bots/[id]/settings/page.tsx +10 -0
- package/src/app/(dashboard)/dashboard/network/page.tsx +97 -0
- package/src/app/(dashboard)/dashboard/page.tsx +13 -0
- package/src/app/(dashboard)/error.tsx +72 -0
- package/src/app/(dashboard)/layout.tsx +113 -0
- package/src/app/(dashboard)/loading.tsx +27 -0
- package/src/app/(dashboard)/marketplace/[plugin]/page.tsx +548 -0
- package/src/app/(dashboard)/marketplace/error.tsx +72 -0
- package/src/app/(dashboard)/marketplace/loading.tsx +27 -0
- package/src/app/(dashboard)/marketplace/page.tsx +268 -0
- package/src/app/(dashboard)/not-found.tsx +46 -0
- package/src/app/(dashboard)/onboarding/page.tsx +267 -0
- package/src/app/(dashboard)/settings/account/page.tsx +132 -0
- package/src/app/(dashboard)/settings/activity/page.tsx +280 -0
- package/src/app/(dashboard)/settings/api-keys/page.tsx +530 -0
- package/src/app/(dashboard)/settings/brain/page.tsx +412 -0
- package/src/app/(dashboard)/settings/error.tsx +72 -0
- package/src/app/(dashboard)/settings/layout.tsx +114 -0
- package/src/app/(dashboard)/settings/loading.tsx +31 -0
- package/src/app/(dashboard)/settings/notifications/page.tsx +216 -0
- package/src/app/(dashboard)/settings/org/page.tsx +617 -0
- package/src/app/(dashboard)/settings/profile/page.tsx +510 -0
- package/src/app/(dashboard)/settings/providers/page.tsx +842 -0
- package/src/app/(dashboard)/settings/secrets/page.tsx +658 -0
- package/src/app/(dashboard)/settings/security/page.tsx +1133 -0
- package/src/app/admin/accounting/loading.tsx +32 -0
- package/src/app/admin/accounting/page.tsx +5 -0
- package/src/app/admin/affiliates/loading.tsx +32 -0
- package/src/app/admin/affiliates/page.tsx +5 -0
- package/src/app/admin/audit/loading.tsx +32 -0
- package/src/app/admin/audit/page.tsx +5 -0
- package/src/app/admin/billing-health/loading.tsx +17 -0
- package/src/app/admin/billing-health/page.tsx +10 -0
- package/src/app/admin/compliance/page.tsx +5 -0
- package/src/app/admin/error.tsx +72 -0
- package/src/app/admin/gpu/loading.tsx +38 -0
- package/src/app/admin/gpu/page.tsx +5 -0
- package/src/app/admin/incidents/page.tsx +10 -0
- package/src/app/admin/inference/loading.tsx +32 -0
- package/src/app/admin/inference/page.tsx +5 -0
- package/src/app/admin/layout.tsx +44 -0
- package/src/app/admin/loading.tsx +32 -0
- package/src/app/admin/marketplace/loading.tsx +32 -0
- package/src/app/admin/marketplace/page.tsx +5 -0
- package/src/app/admin/migrations/loading.tsx +22 -0
- package/src/app/admin/migrations/page.tsx +5 -0
- package/src/app/admin/onboarding/loading.tsx +18 -0
- package/src/app/admin/onboarding/page.tsx +5 -0
- package/src/app/admin/promotions/[id]/edit/loading.tsx +16 -0
- package/src/app/admin/promotions/[id]/edit/page.tsx +56 -0
- package/src/app/admin/promotions/[id]/loading.tsx +15 -0
- package/src/app/admin/promotions/[id]/page.tsx +311 -0
- package/src/app/admin/promotions/loading.tsx +21 -0
- package/src/app/admin/promotions/new/loading.tsx +16 -0
- package/src/app/admin/promotions/new/page.tsx +12 -0
- package/src/app/admin/promotions/page.tsx +266 -0
- package/src/app/admin/rate-overrides/loading.tsx +17 -0
- package/src/app/admin/rate-overrides/page.tsx +290 -0
- package/src/app/admin/roles/loading.tsx +27 -0
- package/src/app/admin/roles/page.tsx +5 -0
- package/src/app/admin/tenants/loading.tsx +32 -0
- package/src/app/admin/tenants/page.tsx +5 -0
- package/src/app/apple-icon.tsx +32 -0
- package/src/app/auth/callback/[provider]/page.tsx +104 -0
- package/src/app/auth/verify/page.tsx +224 -0
- package/src/app/channels/error.tsx +72 -0
- package/src/app/channels/loading.tsx +29 -0
- package/src/app/channels/page.tsx +262 -0
- package/src/app/channels/setup/[plugin]/page.tsx +136 -0
- package/src/app/error.tsx +72 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/fleet/error.tsx +72 -0
- package/src/app/fleet/health/page.tsx +9 -0
- package/src/app/fleet/layout.tsx +14 -0
- package/src/app/fleet/loading.tsx +33 -0
- package/src/app/fleet/page.tsx +5 -0
- package/src/app/global-error.tsx +96 -0
- package/src/app/globals.css +251 -0
- package/src/app/icon.svg +4 -0
- package/src/app/instances/[id]/instance-detail-client.tsx +1298 -0
- package/src/app/instances/[id]/page.tsx +10 -0
- package/src/app/instances/error.tsx +72 -0
- package/src/app/instances/instance-list-client.tsx +540 -0
- package/src/app/instances/loading.tsx +33 -0
- package/src/app/instances/new/create-instance-client.tsx +377 -0
- package/src/app/instances/new/page.tsx +9 -0
- package/src/app/instances/page.tsx +9 -0
- package/src/app/layout.tsx +83 -0
- package/src/app/not-found.tsx +38 -0
- package/src/app/og/route.tsx +50 -0
- package/src/app/page.tsx +39 -0
- package/src/app/plugins/error.tsx +72 -0
- package/src/app/plugins/layout.tsx +14 -0
- package/src/app/plugins/loading.tsx +30 -0
- package/src/app/plugins/page.tsx +555 -0
- package/src/app/pricing/error.tsx +72 -0
- package/src/app/pricing/loading.tsx +25 -0
- package/src/app/pricing/page.tsx +20 -0
- package/src/app/privacy/page.tsx +406 -0
- package/src/app/robots.ts +9 -0
- package/src/app/sitemap.ts +11 -0
- package/src/app/status/error.tsx +72 -0
- package/src/app/status/loading.tsx +21 -0
- package/src/app/status/page.tsx +20 -0
- package/src/app/terms/page.tsx +414 -0
- package/src/components/account-switcher.tsx +82 -0
- package/src/components/admin/accounting-dashboard.tsx +190 -0
- package/src/components/admin/admin-guard.tsx +36 -0
- package/src/components/admin/admin-nav.tsx +71 -0
- package/src/components/admin/affiliate-dashboard.tsx +564 -0
- package/src/components/admin/audit-log-table.tsx +336 -0
- package/src/components/admin/billing-health-dashboard.test.tsx +40 -0
- package/src/components/admin/billing-health-dashboard.tsx +416 -0
- package/src/components/admin/bulk-actions-bar.test.tsx +92 -0
- package/src/components/admin/bulk-actions-bar.tsx +80 -0
- package/src/components/admin/bulk-export-dialog.test.tsx +75 -0
- package/src/components/admin/bulk-export-dialog.tsx +189 -0
- package/src/components/admin/bulk-grant-dialog.test.tsx +81 -0
- package/src/components/admin/bulk-grant-dialog.tsx +147 -0
- package/src/components/admin/bulk-preview-dialog.test.tsx +72 -0
- package/src/components/admin/bulk-preview-dialog.tsx +106 -0
- package/src/components/admin/bulk-reactivate-dialog.test.tsx +51 -0
- package/src/components/admin/bulk-reactivate-dialog.tsx +55 -0
- package/src/components/admin/bulk-select-all-banner.test.tsx +36 -0
- package/src/components/admin/bulk-select-all-banner.tsx +44 -0
- package/src/components/admin/bulk-suspend-dialog.test.tsx +77 -0
- package/src/components/admin/bulk-suspend-dialog.tsx +129 -0
- package/src/components/admin/bulk-undo-toast.test.tsx +66 -0
- package/src/components/admin/bulk-undo-toast.tsx +121 -0
- package/src/components/admin/compliance-dashboard.tsx +1341 -0
- package/src/components/admin/gpu-dashboard.tsx +552 -0
- package/src/components/admin/grant-credits-dialog.tsx +121 -0
- package/src/components/admin/incident-dashboard.test.tsx +44 -0
- package/src/components/admin/incident-dashboard.tsx +717 -0
- package/src/components/admin/inference-dashboard.tsx +415 -0
- package/src/components/admin/marketplace-admin.tsx +765 -0
- package/src/components/admin/migrations-dashboard.tsx +404 -0
- package/src/components/admin/onboarding-dashboard.tsx +422 -0
- package/src/components/admin/promotions/promotion-form.tsx +440 -0
- package/src/components/admin/roles-dashboard.tsx +278 -0
- package/src/components/admin/suspend-dialog.tsx +98 -0
- package/src/components/admin/tenant-notes-panel.tsx +134 -0
- package/src/components/admin/tenant-row-actions.tsx +78 -0
- package/src/components/admin/tenant-table.tsx +339 -0
- package/src/components/auth/auth-error.tsx +22 -0
- package/src/components/auth/auth-redirect.tsx +18 -0
- package/src/components/auth/auth-shell.tsx +25 -0
- package/src/components/auth/email-verification-banner.tsx +25 -0
- package/src/components/auth/email-verification-result-banner.tsx +70 -0
- package/src/components/auth/resend-verification-button.tsx +94 -0
- package/src/components/auth/wopr-wordmark.tsx +19 -0
- package/src/components/billing/add-payment-method-dialog.tsx +267 -0
- package/src/components/billing/affiliate-dashboard.tsx +300 -0
- package/src/components/billing/auto-topup-card.tsx +432 -0
- package/src/components/billing/buy-credits-panel.tsx +180 -0
- package/src/components/billing/buy-crypto-credits-panel.tsx +96 -0
- package/src/components/billing/byok-callout.tsx +87 -0
- package/src/components/billing/coupon-input.tsx +86 -0
- package/src/components/billing/credit-balance.tsx +79 -0
- package/src/components/billing/degraded-state-banner.tsx +95 -0
- package/src/components/billing/dividend-banner.tsx +97 -0
- package/src/components/billing/dividend-eligibility.tsx +86 -0
- package/src/components/billing/dividend-pool-stats.tsx +86 -0
- package/src/components/billing/first-dividend-dialog.tsx +109 -0
- package/src/components/billing/low-balance-banner.tsx +50 -0
- package/src/components/billing/org-billing-page.tsx +360 -0
- package/src/components/billing/suspension-banner.tsx +53 -0
- package/src/components/billing/transaction-history.tsx +239 -0
- package/src/components/bot-settings/__tests__/bot-settings-client.test.tsx +205 -0
- package/src/components/bot-settings/backups-tab.tsx +377 -0
- package/src/components/bot-settings/bot-settings-client.tsx +1712 -0
- package/src/components/bot-settings/resources-tab.tsx +203 -0
- package/src/components/bot-settings/storage-tab.tsx +248 -0
- package/src/components/bot-settings/vps-info-panel.tsx +132 -0
- package/src/components/bot-settings/vps-upgrade-card.tsx +110 -0
- package/src/components/capability/CapabilityResolver.tsx +113 -0
- package/src/components/channel-wizard/field-interactive.tsx +48 -0
- package/src/components/channel-wizard/field-oauth.tsx +181 -0
- package/src/components/channel-wizard/field-paste.tsx +47 -0
- package/src/components/channel-wizard/field-qr.tsx +302 -0
- package/src/components/channel-wizard/index.ts +6 -0
- package/src/components/channel-wizard/step-renderer.tsx +103 -0
- package/src/components/channel-wizard/wizard.tsx +200 -0
- package/src/components/chat/ambient-dot.tsx +32 -0
- package/src/components/chat/chat-input.tsx +56 -0
- package/src/components/chat/chat-message.tsx +36 -0
- package/src/components/chat/chat-panel.tsx +138 -0
- package/src/components/chat/chat-widget.tsx +41 -0
- package/src/components/chat/index.ts +5 -0
- package/src/components/dashboard/command-center.tsx +614 -0
- package/src/components/instances/friends-tab.test.tsx +265 -0
- package/src/components/instances/friends-tab.tsx +721 -0
- package/src/components/landing/hero.tsx +53 -0
- package/src/components/landing/landing-nav.tsx +21 -0
- package/src/components/landing/landing-page.tsx +71 -0
- package/src/components/landing/portfolio-chart.tsx +349 -0
- package/src/components/landing/story-sections.tsx +50 -0
- package/src/components/landing/terminal-lines.ts +99 -0
- package/src/components/landing/terminal-sequence.tsx +453 -0
- package/src/components/landing/typing-effect.tsx +43 -0
- package/src/components/marketplace/category-filter.tsx +61 -0
- package/src/components/marketplace/empty-state.tsx +61 -0
- package/src/components/marketplace/featured-heroes.tsx +84 -0
- package/src/components/marketplace/first-visit-hero.tsx +110 -0
- package/src/components/marketplace/index.ts +9 -0
- package/src/components/marketplace/install-wizard.tsx +782 -0
- package/src/components/marketplace/marketplace-tabs.tsx +54 -0
- package/src/components/marketplace/plugin-card.tsx +129 -0
- package/src/components/marketplace/superpower-card.tsx +104 -0
- package/src/components/marketplace/superpower-content.tsx +117 -0
- package/src/components/marketplace/terminal-search.tsx +67 -0
- package/src/components/oauth-buttons.tsx +75 -0
- package/src/components/observability/fleet-health.tsx +370 -0
- package/src/components/observability/health-overview.tsx +246 -0
- package/src/components/observability/logs-viewer.tsx +215 -0
- package/src/components/observability/metrics-dashboard.tsx +288 -0
- package/src/components/onboarding/fallback-setup.tsx +137 -0
- package/src/components/onboarding/index.ts +3 -0
- package/src/components/onboarding/setup-checklist.tsx +333 -0
- package/src/components/onboarding/step-superpowers.tsx +122 -0
- package/src/components/plugin-setup/index.ts +1 -0
- package/src/components/plugin-setup/setup-chat-panel.tsx +188 -0
- package/src/components/pricing/dividend-calculator.tsx +47 -0
- package/src/components/pricing/dividend-stats.tsx +117 -0
- package/src/components/pricing/pricing-page.tsx +229 -0
- package/src/components/settings/create-org-wizard.tsx +225 -0
- package/src/components/sidebar.tsx +202 -0
- package/src/components/status/status-page.tsx +209 -0
- package/src/components/status-badge.tsx +28 -0
- package/src/components/theme-provider.tsx +8 -0
- package/src/components/ui/alert-dialog.tsx +141 -0
- package/src/components/ui/badge.tsx +47 -0
- package/src/components/ui/banner.tsx +36 -0
- package/src/components/ui/button.tsx +64 -0
- package/src/components/ui/card.tsx +75 -0
- package/src/components/ui/checkbox.tsx +52 -0
- package/src/components/ui/collapsible.tsx +31 -0
- package/src/components/ui/credit-detailed.tsx +33 -0
- package/src/components/ui/dialog.tsx +143 -0
- package/src/components/ui/dropdown-menu.tsx +228 -0
- package/src/components/ui/form.tsx +151 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/label.tsx +21 -0
- package/src/components/ui/popover.tsx +74 -0
- package/src/components/ui/progress.tsx +28 -0
- package/src/components/ui/radio-group.tsx +45 -0
- package/src/components/ui/select.tsx +175 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/sheet.tsx +125 -0
- package/src/components/ui/skeleton.tsx +15 -0
- package/src/components/ui/switch.tsx +35 -0
- package/src/components/ui/table.tsx +92 -0
- package/src/components/ui/tabs.tsx +81 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/tooltip.tsx +44 -0
- package/src/config/provider-docs.ts +17 -0
- package/src/hooks/__tests__/use-async.test.ts +127 -0
- package/src/hooks/__tests__/use-count-up.test.ts +129 -0
- package/src/hooks/__tests__/use-debounce.test.ts +105 -0
- package/src/hooks/__tests__/use-fleet-sse.test.ts +216 -0
- package/src/hooks/__tests__/use-local-storage.test.ts +74 -0
- package/src/hooks/__tests__/use-mobile.test.ts +86 -0
- package/src/hooks/__tests__/use-save-queue.test.ts +159 -0
- package/src/hooks/use-async.ts +54 -0
- package/src/hooks/use-capability-meta.ts +99 -0
- package/src/hooks/use-count-up.ts +23 -0
- package/src/hooks/use-debounce.ts +12 -0
- package/src/hooks/use-fleet-sse.ts +47 -0
- package/src/hooks/use-has-org.ts +18 -0
- package/src/hooks/use-image-status.ts +36 -0
- package/src/hooks/use-local-storage.ts +36 -0
- package/src/hooks/use-mobile.ts +17 -0
- package/src/hooks/use-page-context.ts +24 -0
- package/src/hooks/use-pagination-params.ts +30 -0
- package/src/hooks/use-plugin-registry.ts +247 -0
- package/src/hooks/use-plugin-setup-chat.ts +211 -0
- package/src/hooks/use-save-queue.ts +54 -0
- package/src/hooks/use-webmcp.ts +40 -0
- package/src/lib/__tests__/__snapshots__/pricing-data.test.ts.snap +112 -0
- package/src/lib/__tests__/admin-api.test.ts +487 -0
- package/src/lib/__tests__/api-bot-crud.test.ts +391 -0
- package/src/lib/__tests__/api-fetch.test.ts +196 -0
- package/src/lib/__tests__/bot-settings-data.test.ts +352 -0
- package/src/lib/__tests__/org-api.test.ts +281 -0
- package/src/lib/__tests__/org-billing-api.test.ts +242 -0
- package/src/lib/__tests__/pricing-data.test.ts +32 -0
- package/src/lib/__tests__/settings-api.test.ts +272 -0
- package/src/lib/admin-affiliate-api.ts +51 -0
- package/src/lib/admin-api.ts +325 -0
- package/src/lib/admin-compliance-api.ts +127 -0
- package/src/lib/admin-gpu-api.ts +82 -0
- package/src/lib/admin-incident-api.ts +121 -0
- package/src/lib/admin-inference-api.ts +47 -0
- package/src/lib/admin-marketplace-api.ts +97 -0
- package/src/lib/api-config.test.ts +111 -0
- package/src/lib/api-config.ts +65 -0
- package/src/lib/api-errors.test.ts +43 -0
- package/src/lib/api.ts +2011 -0
- package/src/lib/auth-client.ts +11 -0
- package/src/lib/bot-settings-data.ts +342 -0
- package/src/lib/brand-config.ts +145 -0
- package/src/lib/brand.ts +669 -0
- package/src/lib/changeset-api.ts +29 -0
- package/src/lib/changeset-types.ts +56 -0
- package/src/lib/channel-manifests.ts +50 -0
- package/src/lib/chat/chat-context.tsx +70 -0
- package/src/lib/chat/chat-store.ts +62 -0
- package/src/lib/chat/types.ts +35 -0
- package/src/lib/chat/use-chat.ts +255 -0
- package/src/lib/cost-comparison-data.test.ts +95 -0
- package/src/lib/cost-comparison-data.ts +54 -0
- package/src/lib/errors.test.ts +64 -0
- package/src/lib/errors.ts +52 -0
- package/src/lib/fetch-utils.test.ts +57 -0
- package/src/lib/fetch-utils.ts +25 -0
- package/src/lib/format-credit.test.ts +66 -0
- package/src/lib/format-credit.ts +24 -0
- package/src/lib/format.test.ts +62 -0
- package/src/lib/format.ts +17 -0
- package/src/lib/logger.ts +28 -0
- package/src/lib/marketplace-data.ts +346 -0
- package/src/lib/oauth-errors.ts +19 -0
- package/src/lib/onboarding-data.ts +1265 -0
- package/src/lib/onboarding-store.ts +233 -0
- package/src/lib/org-api.ts +74 -0
- package/src/lib/org-billing-api.ts +81 -0
- package/src/lib/page-prompts.test.ts +32 -0
- package/src/lib/page-prompts.ts +23 -0
- package/src/lib/plugin/index.ts +32 -0
- package/src/lib/plugin/tool-definitions.ts +306 -0
- package/src/lib/pricing-data.ts +115 -0
- package/src/lib/promotions-types.ts +58 -0
- package/src/lib/settings-api.ts +63 -0
- package/src/lib/status-colors.ts +38 -0
- package/src/lib/tenant-context.tsx +134 -0
- package/src/lib/trpc-types.ts +173 -0
- package/src/lib/trpc.tsx +86 -0
- package/src/lib/utils.test.ts +55 -0
- package/src/lib/utils.ts +18 -0
- package/src/lib/validate-redirect-url.ts +39 -0
- package/src/lib/webmcp/feature-detect.ts +13 -0
- package/src/lib/webmcp/marketplace-onboarding-tools.ts +202 -0
- package/src/lib/webmcp/register.ts +44 -0
- package/src/lib/webmcp/tools.ts +422 -0
- package/src/proxy.ts +258 -0
- package/src/types/missing-deps.d.ts +160 -0
- package/src/types/motion-dom.d.ts +162 -0
- package/src/types/vitest-matchers.d.ts +40 -0
- package/src/types/web-mcp.d.ts +22 -0
- package/tsconfig.json +34 -0
- package/vitest.config.ts +26 -0
package/src/lib/api.ts
ADDED
|
@@ -0,0 +1,2011 @@
|
|
|
1
|
+
import { API_BASE_URL, PLATFORM_BASE_URL } from "./api-config";
|
|
2
|
+
import { envKey, getBrandConfig } from "./brand-config";
|
|
3
|
+
import { ApiError } from "./errors";
|
|
4
|
+
import { handleUnauthorized } from "./fetch-utils";
|
|
5
|
+
import { logger } from "./logger";
|
|
6
|
+
import type { ApiPricingResponse, DividendStats } from "./pricing-data";
|
|
7
|
+
import { getActiveTenantId } from "./tenant-context";
|
|
8
|
+
import { trpcVanilla } from "./trpc";
|
|
9
|
+
|
|
10
|
+
const log = logger("api");
|
|
11
|
+
|
|
12
|
+
export { ApiError, NetworkError, toUserMessage, ValidationError } from "./errors";
|
|
13
|
+
export { UnauthorizedError } from "./fetch-utils";
|
|
14
|
+
|
|
15
|
+
// --- Public pricing API (no auth required) ---
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Fetch live pricing rates from the backend.
|
|
19
|
+
* Returns null if the fetch fails (caller should fall back to static data).
|
|
20
|
+
* Uses ISR (revalidate: 60) so the pricing route can be statically rendered
|
|
21
|
+
* at build time — avoids Dynamic Server usage that blocks e2e webServer startup.
|
|
22
|
+
*/
|
|
23
|
+
export async function fetchPublicPricing(): Promise<ApiPricingResponse | null> {
|
|
24
|
+
try {
|
|
25
|
+
const res = await fetch(`${API_BASE_URL}/v1/pricing`, {
|
|
26
|
+
next: { revalidate: 60 },
|
|
27
|
+
});
|
|
28
|
+
if (!res.ok) return null;
|
|
29
|
+
return (await res.json()) as ApiPricingResponse;
|
|
30
|
+
} catch (e) {
|
|
31
|
+
log.warn("Failed to fetch pricing data", e);
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Fetch live community dividend pool stats.
|
|
38
|
+
* Returns null if the endpoint is unavailable (caller should fall back to static projections).
|
|
39
|
+
*/
|
|
40
|
+
export async function fetchDividendStats(): Promise<DividendStats | null> {
|
|
41
|
+
try {
|
|
42
|
+
const res = await fetch(`${API_BASE_URL}/v1/billing/dividend/stats`, {
|
|
43
|
+
next: { revalidate: 60 },
|
|
44
|
+
});
|
|
45
|
+
if (!res.ok) return null;
|
|
46
|
+
return (await res.json()) as DividendStats;
|
|
47
|
+
} catch (e) {
|
|
48
|
+
log.warn("Failed to fetch dividend stats", e);
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export type InstanceStatus = "running" | "stopped" | "degraded" | "error";
|
|
54
|
+
|
|
55
|
+
export interface Instance {
|
|
56
|
+
id: string;
|
|
57
|
+
name: string;
|
|
58
|
+
status: InstanceStatus;
|
|
59
|
+
provider: string;
|
|
60
|
+
channels: string[];
|
|
61
|
+
plugins: PluginInfo[];
|
|
62
|
+
uptime: number | null;
|
|
63
|
+
createdAt: string;
|
|
64
|
+
subdomain?: string;
|
|
65
|
+
nodeId?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface PluginInfo {
|
|
69
|
+
id: string;
|
|
70
|
+
name: string;
|
|
71
|
+
version: string;
|
|
72
|
+
enabled: boolean;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface ChannelInfo {
|
|
76
|
+
id: string;
|
|
77
|
+
name: string;
|
|
78
|
+
type: string;
|
|
79
|
+
status: "connected" | "disconnected" | "error";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface SessionInfo {
|
|
83
|
+
id: string;
|
|
84
|
+
userId: string;
|
|
85
|
+
messageCount: number;
|
|
86
|
+
startedAt: string;
|
|
87
|
+
lastActivityAt: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface InstanceDetail extends Instance {
|
|
91
|
+
config: Record<string, unknown>;
|
|
92
|
+
channelDetails: ChannelInfo[];
|
|
93
|
+
sessions: SessionInfo[];
|
|
94
|
+
resourceUsage: {
|
|
95
|
+
memoryMb: number;
|
|
96
|
+
cpuPercent: number;
|
|
97
|
+
};
|
|
98
|
+
budgetCents?: number;
|
|
99
|
+
perAgentCents?: number;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// --- API client ---
|
|
103
|
+
|
|
104
|
+
export async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
|
|
105
|
+
const tenantId = getActiveTenantId();
|
|
106
|
+
const res = await fetch(`${API_BASE_URL}${path}`, {
|
|
107
|
+
...init,
|
|
108
|
+
credentials: "include",
|
|
109
|
+
headers: {
|
|
110
|
+
"Content-Type": "application/json",
|
|
111
|
+
...(tenantId ? { "x-tenant-id": tenantId } : {}),
|
|
112
|
+
...init?.headers,
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
if (res.status === 401) {
|
|
116
|
+
handleUnauthorized();
|
|
117
|
+
}
|
|
118
|
+
if (!res.ok) {
|
|
119
|
+
const body = await res.json().catch(() => ({}));
|
|
120
|
+
throw new ApiError(res.status, res.statusText, (body as { error?: string }).error ?? undefined);
|
|
121
|
+
}
|
|
122
|
+
return res.json() as Promise<T>;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Like apiFetch but returns the raw Response (for streaming or status inspection). */
|
|
126
|
+
export async function apiFetchRaw(path: string, init?: RequestInit): Promise<Response> {
|
|
127
|
+
const tenantId = getActiveTenantId();
|
|
128
|
+
const res = await fetch(`${API_BASE_URL}${path}`, {
|
|
129
|
+
...init,
|
|
130
|
+
credentials: "include",
|
|
131
|
+
headers: {
|
|
132
|
+
"Content-Type": "application/json",
|
|
133
|
+
...(tenantId ? { "x-tenant-id": tenantId } : {}),
|
|
134
|
+
...init?.headers,
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
if (res.status === 401) {
|
|
138
|
+
handleUnauthorized();
|
|
139
|
+
}
|
|
140
|
+
return res;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Fetch against the PLATFORM_BASE_URL (not /api) — for Next.js API routes and OAuth endpoints. */
|
|
144
|
+
export async function platformFetch<T>(path: string, init?: RequestInit): Promise<T> {
|
|
145
|
+
const tenantId = getActiveTenantId();
|
|
146
|
+
const res = await fetch(`${PLATFORM_BASE_URL}${path}`, {
|
|
147
|
+
...init,
|
|
148
|
+
credentials: "include",
|
|
149
|
+
headers: {
|
|
150
|
+
"Content-Type": "application/json",
|
|
151
|
+
...(tenantId ? { "x-tenant-id": tenantId } : {}),
|
|
152
|
+
...init?.headers,
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
if (res.status === 401) {
|
|
156
|
+
handleUnauthorized();
|
|
157
|
+
}
|
|
158
|
+
if (!res.ok) {
|
|
159
|
+
throw new Error(`API error: ${res.status} ${res.statusText}`);
|
|
160
|
+
}
|
|
161
|
+
return res.json() as Promise<T>;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Like platformFetch but returns the raw Response. */
|
|
165
|
+
export async function platformFetchRaw(path: string, init?: RequestInit): Promise<Response> {
|
|
166
|
+
const tenantId = getActiveTenantId();
|
|
167
|
+
const res = await fetch(`${PLATFORM_BASE_URL}${path}`, {
|
|
168
|
+
...init,
|
|
169
|
+
credentials: "include",
|
|
170
|
+
headers: {
|
|
171
|
+
"Content-Type": "application/json",
|
|
172
|
+
...(tenantId ? { "x-tenant-id": tenantId } : {}),
|
|
173
|
+
...init?.headers,
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
if (res.status === 401) {
|
|
177
|
+
handleUnauthorized();
|
|
178
|
+
}
|
|
179
|
+
return res;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export async function fleetFetch<T>(path: string, init?: RequestInit): Promise<T> {
|
|
183
|
+
const tenantId = getActiveTenantId();
|
|
184
|
+
const res = await fetch(`${PLATFORM_BASE_URL}/fleet${path}`, {
|
|
185
|
+
...init,
|
|
186
|
+
credentials: "include",
|
|
187
|
+
headers: {
|
|
188
|
+
"Content-Type": "application/json",
|
|
189
|
+
...(tenantId ? { "x-tenant-id": tenantId } : {}),
|
|
190
|
+
...init?.headers,
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
if (res.status === 401) {
|
|
194
|
+
handleUnauthorized();
|
|
195
|
+
}
|
|
196
|
+
if (!res.ok) {
|
|
197
|
+
const body = await res.json().catch(() => ({}));
|
|
198
|
+
throw new ApiError(res.status, res.statusText, (body as { error?: string }).error ?? undefined);
|
|
199
|
+
}
|
|
200
|
+
if (res.status === 204 || res.headers.get("content-length") === "0") {
|
|
201
|
+
return undefined as T;
|
|
202
|
+
}
|
|
203
|
+
return res.json() as Promise<T>;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// --- Instance API ---
|
|
207
|
+
|
|
208
|
+
/** Shape returned by GET /fleet/bots on the backend */
|
|
209
|
+
export interface BotStatusResponse {
|
|
210
|
+
id: string;
|
|
211
|
+
name: string;
|
|
212
|
+
state: string;
|
|
213
|
+
health: string | null;
|
|
214
|
+
uptime: string | null;
|
|
215
|
+
startedAt: string | null;
|
|
216
|
+
createdAt?: string;
|
|
217
|
+
env?: Record<string, string>;
|
|
218
|
+
stats: {
|
|
219
|
+
cpuPercent: number;
|
|
220
|
+
memoryUsageMb: number;
|
|
221
|
+
memoryLimitMb: number;
|
|
222
|
+
memoryPercent: number;
|
|
223
|
+
} | null;
|
|
224
|
+
[key: string]: unknown;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Parse channel IDs from bot env vars ({PREFIX}_PLUGINS_CHANNELS is comma-separated). */
|
|
228
|
+
export function parseChannelsFromEnv(env: Record<string, string> | undefined): string[] {
|
|
229
|
+
const raw = env?.[envKey("PLUGINS_CHANNELS")];
|
|
230
|
+
if (!raw) return [];
|
|
231
|
+
return raw
|
|
232
|
+
.split(",")
|
|
233
|
+
.map((s) => s.trim())
|
|
234
|
+
.filter(Boolean);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Parse plugin IDs from bot env vars ({PREFIX}_PLUGINS_OTHER, {PREFIX}_PLUGINS_VOICE, {PREFIX}_PLUGINS_PROVIDERS are comma-separated). */
|
|
238
|
+
export function parsePluginsFromEnv(env: Record<string, string> | undefined): PluginInfo[] {
|
|
239
|
+
if (!env) return [];
|
|
240
|
+
const ids = new Set<string>();
|
|
241
|
+
for (const key of [
|
|
242
|
+
envKey("PLUGINS_OTHER"),
|
|
243
|
+
envKey("PLUGINS_VOICE"),
|
|
244
|
+
envKey("PLUGINS_PROVIDERS"),
|
|
245
|
+
]) {
|
|
246
|
+
const raw = env[key];
|
|
247
|
+
if (raw) {
|
|
248
|
+
for (const id of raw
|
|
249
|
+
.split(",")
|
|
250
|
+
.map((s) => s.trim())
|
|
251
|
+
.filter(Boolean)) {
|
|
252
|
+
ids.add(id);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return [...ids].map((id) => ({ id, name: id, version: "", enabled: true }));
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Extract the LLM provider from bot env vars. */
|
|
260
|
+
export function getProviderFromEnv(env?: Record<string, string>): string {
|
|
261
|
+
const val = env?.[envKey("LLM_PROVIDER")];
|
|
262
|
+
return typeof val === "string" ? val : "";
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export function mapBotState(state: string): InstanceStatus {
|
|
266
|
+
if (state === "running") return "running";
|
|
267
|
+
if (state === "error" || state === "dead") return "error";
|
|
268
|
+
return "stopped";
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function mapBotStatusToFleetInstance(bot: BotStatusResponse): FleetInstance {
|
|
272
|
+
const status = mapBotState(bot.state);
|
|
273
|
+
|
|
274
|
+
let health: HealthStatus;
|
|
275
|
+
if (bot.health === "healthy") health = "healthy";
|
|
276
|
+
else if (bot.health === "unhealthy") health = "unhealthy";
|
|
277
|
+
else if (bot.health === "degraded" || bot.health === "starting") health = "degraded";
|
|
278
|
+
else if (status === "running") health = "healthy";
|
|
279
|
+
else if (status === "stopped" || status === "error") health = "degraded";
|
|
280
|
+
else health = "healthy";
|
|
281
|
+
|
|
282
|
+
let uptime: number | null = null;
|
|
283
|
+
if (bot.uptime) {
|
|
284
|
+
const startedMs = new Date(bot.uptime).getTime();
|
|
285
|
+
if (!Number.isNaN(startedMs)) {
|
|
286
|
+
uptime = Math.floor((Date.now() - startedMs) / 1000);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
id: bot.id,
|
|
292
|
+
name: bot.name,
|
|
293
|
+
status,
|
|
294
|
+
health,
|
|
295
|
+
uptime,
|
|
296
|
+
pluginCount: 0,
|
|
297
|
+
sessionCount: 0,
|
|
298
|
+
provider: "",
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export async function listInstances(): Promise<Instance[]> {
|
|
303
|
+
const data = await trpcVanilla.fleet.listInstances.query(undefined);
|
|
304
|
+
const raw = (data as { bots?: BotStatusResponse[] | null }).bots;
|
|
305
|
+
const bots = Array.isArray(raw) ? raw : [];
|
|
306
|
+
return bots.map((bot) => ({
|
|
307
|
+
id: bot.id,
|
|
308
|
+
name: bot.name,
|
|
309
|
+
status: mapBotState(bot.state),
|
|
310
|
+
provider: getProviderFromEnv(bot.env as Record<string, string> | undefined),
|
|
311
|
+
channels: parseChannelsFromEnv(bot.env),
|
|
312
|
+
plugins: parsePluginsFromEnv(bot.env),
|
|
313
|
+
uptime: (() => {
|
|
314
|
+
const ms = bot.uptime ? new Date(bot.uptime).getTime() : NaN;
|
|
315
|
+
return Number.isNaN(ms) ? null : Math.floor((Date.now() - ms) / 1000);
|
|
316
|
+
})(),
|
|
317
|
+
createdAt: (bot.createdAt as string | undefined) ?? new Date().toISOString(),
|
|
318
|
+
}));
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export async function getInstance(id: string): Promise<InstanceDetail> {
|
|
322
|
+
const bot = (await trpcVanilla.fleet.getInstance.query({
|
|
323
|
+
id,
|
|
324
|
+
})) as BotStatusResponse;
|
|
325
|
+
const uptimeMs = bot.uptime ? new Date(bot.uptime).getTime() : NaN;
|
|
326
|
+
const extra = bot as Record<string, unknown>;
|
|
327
|
+
return {
|
|
328
|
+
id: bot.id,
|
|
329
|
+
name: bot.name,
|
|
330
|
+
status: mapBotState(bot.state),
|
|
331
|
+
provider: getProviderFromEnv(bot.env as Record<string, string> | undefined),
|
|
332
|
+
channels: parseChannelsFromEnv(bot.env as Record<string, string> | undefined),
|
|
333
|
+
plugins: parsePluginsFromEnv(bot.env as Record<string, string> | undefined),
|
|
334
|
+
uptime: Number.isNaN(uptimeMs) ? null : Math.floor((Date.now() - uptimeMs) / 1000),
|
|
335
|
+
createdAt: (bot.createdAt as string | undefined) ?? new Date().toISOString(),
|
|
336
|
+
subdomain: (extra.subdomain as string | undefined) ?? undefined,
|
|
337
|
+
nodeId: (extra.nodeId as string | undefined) ?? undefined,
|
|
338
|
+
config: bot.env ?? {},
|
|
339
|
+
channelDetails: [],
|
|
340
|
+
sessions: [],
|
|
341
|
+
resourceUsage: {
|
|
342
|
+
memoryMb: bot.stats?.memoryUsageMb ?? 0,
|
|
343
|
+
cpuPercent: bot.stats?.cpuPercent ?? 0,
|
|
344
|
+
},
|
|
345
|
+
budgetCents: typeof extra.budgetCents === "number" ? extra.budgetCents : undefined,
|
|
346
|
+
perAgentCents: typeof extra.perAgentCents === "number" ? extra.perAgentCents : undefined,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export async function createInstance(data: {
|
|
351
|
+
name: string;
|
|
352
|
+
template?: string;
|
|
353
|
+
provider: string;
|
|
354
|
+
channels: string[];
|
|
355
|
+
plugins: string[];
|
|
356
|
+
}): Promise<Instance> {
|
|
357
|
+
const result = await trpcVanilla.fleet.createInstance.mutate(data);
|
|
358
|
+
const profile = result as Record<string, unknown>;
|
|
359
|
+
return {
|
|
360
|
+
id: (profile.id as string) ?? "",
|
|
361
|
+
name: (profile.name as string) ?? data.name,
|
|
362
|
+
status: "stopped",
|
|
363
|
+
provider: data.provider,
|
|
364
|
+
channels: data.channels,
|
|
365
|
+
plugins: data.plugins.map((id) => ({ id, name: id, version: "", enabled: true })),
|
|
366
|
+
uptime: null,
|
|
367
|
+
createdAt: new Date().toISOString(),
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/** Payload for deploying a bot from onboarding. */
|
|
372
|
+
export interface DeployBotPayload {
|
|
373
|
+
name: string;
|
|
374
|
+
description?: string;
|
|
375
|
+
env?: Record<string, string>;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Deploy a new bot instance via tRPC fleet.createInstance.
|
|
380
|
+
* Uses the default stable image from brand config. tenantId is injected server-side.
|
|
381
|
+
*/
|
|
382
|
+
export async function deployInstance(payload: DeployBotPayload): Promise<Instance> {
|
|
383
|
+
const input = {
|
|
384
|
+
name: payload.name,
|
|
385
|
+
image: getBrandConfig().defaultImage || "ghcr.io/wopr-network/wopr:stable",
|
|
386
|
+
description: payload.description ?? "",
|
|
387
|
+
env: payload.env ?? {},
|
|
388
|
+
};
|
|
389
|
+
const result = await trpcVanilla.fleet.createInstance.mutate(input);
|
|
390
|
+
const profile = result as Record<string, unknown>;
|
|
391
|
+
return {
|
|
392
|
+
id: (profile.id as string) ?? "",
|
|
393
|
+
name: (profile.name as string) ?? payload.name,
|
|
394
|
+
status: "stopped",
|
|
395
|
+
provider: getProviderFromEnv(payload.env),
|
|
396
|
+
channels: parseChannelsFromEnv(payload.env),
|
|
397
|
+
plugins: parsePluginsFromEnv(payload.env),
|
|
398
|
+
uptime: null,
|
|
399
|
+
createdAt: new Date().toISOString(),
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// --- Channel QR polling API ---
|
|
404
|
+
|
|
405
|
+
export type ChannelQrStatus = "pending" | "connected" | "expired" | "no-session";
|
|
406
|
+
|
|
407
|
+
export interface ChannelQrResponse {
|
|
408
|
+
qrPng: string | null;
|
|
409
|
+
status: ChannelQrStatus;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** Poll the platform for the current QR code state for a given bot instance. */
|
|
413
|
+
export async function pollChannelQr(botId: string): Promise<ChannelQrResponse> {
|
|
414
|
+
const res = await fetch(`${PLATFORM_BASE_URL}/api/channels/${encodeURIComponent(botId)}/qr`, {
|
|
415
|
+
credentials: "include",
|
|
416
|
+
});
|
|
417
|
+
if (!res.ok) {
|
|
418
|
+
throw new Error(`API error: ${res.status} ${res.statusText}`);
|
|
419
|
+
}
|
|
420
|
+
return res.json() as Promise<ChannelQrResponse>;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// --- Channel API ---
|
|
424
|
+
|
|
425
|
+
/** Connect a channel (plugin) to a bot instance. */
|
|
426
|
+
export async function connectChannel(
|
|
427
|
+
botId: string,
|
|
428
|
+
pluginId: string,
|
|
429
|
+
credentials: Record<string, string>,
|
|
430
|
+
): Promise<ChannelInfo> {
|
|
431
|
+
return fleetFetch<ChannelInfo>(`/bots/${botId}/channels/${pluginId}`, {
|
|
432
|
+
method: "POST",
|
|
433
|
+
body: JSON.stringify(credentials),
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/** Test channel credentials against the provider API before connecting. */
|
|
438
|
+
export async function testChannelConnection(
|
|
439
|
+
pluginId: string,
|
|
440
|
+
credentials: Record<string, string>,
|
|
441
|
+
): Promise<{ success: boolean; error?: string }> {
|
|
442
|
+
const res = await fetch(`${PLATFORM_BASE_URL}/api/channels/${pluginId}/test`, {
|
|
443
|
+
method: "POST",
|
|
444
|
+
credentials: "include",
|
|
445
|
+
headers: { "Content-Type": "application/json" },
|
|
446
|
+
body: JSON.stringify({ credentials }),
|
|
447
|
+
});
|
|
448
|
+
if (!res.ok) {
|
|
449
|
+
throw new Error(`API error: ${res.status} ${res.statusText}`);
|
|
450
|
+
}
|
|
451
|
+
return res.json() as Promise<{ success: boolean; error?: string }>;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/** List channels connected to a bot instance. */
|
|
455
|
+
export async function listChannels(botId: string): Promise<ChannelInfo[]> {
|
|
456
|
+
return fleetFetch<ChannelInfo[]>(`/bots/${botId}/channels`);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
export async function controlInstance(
|
|
460
|
+
id: string,
|
|
461
|
+
action: "start" | "stop" | "restart" | "destroy",
|
|
462
|
+
): Promise<void> {
|
|
463
|
+
await trpcVanilla.fleet.controlInstance.mutate({ id, action });
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/** PATCH /fleet/bots/:id — Update bot env config. */
|
|
467
|
+
export async function updateInstanceConfig(id: string, env: Record<string, string>): Promise<void> {
|
|
468
|
+
await fleetFetch(`/bots/${id}`, {
|
|
469
|
+
method: "PATCH",
|
|
470
|
+
body: JSON.stringify({ env }),
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/** PUT /api/provision/budget — Update instance spending budget. */
|
|
475
|
+
export async function updateInstanceBudget(
|
|
476
|
+
id: string,
|
|
477
|
+
budgetCents: number,
|
|
478
|
+
perAgentCents?: number,
|
|
479
|
+
): Promise<void> {
|
|
480
|
+
await apiFetch("/provision/budget", {
|
|
481
|
+
method: "PUT",
|
|
482
|
+
body: JSON.stringify({ instanceId: id, budgetCents, perAgentCents }),
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/** PATCH /fleet/bots/:id — Rename a bot instance. */
|
|
487
|
+
export async function renameInstance(id: string, name: string): Promise<void> {
|
|
488
|
+
await fleetFetch(`/bots/${id}`, {
|
|
489
|
+
method: "PATCH",
|
|
490
|
+
body: JSON.stringify({ name }),
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/** GET /fleet/bots/:id/secrets — List secret key names (no values). */
|
|
495
|
+
export async function getInstanceSecretKeys(id: string): Promise<string[]> {
|
|
496
|
+
try {
|
|
497
|
+
const data = await fleetFetch<{ keys: string[] }>(`/bots/${id}/secrets`);
|
|
498
|
+
return data.keys;
|
|
499
|
+
} catch (err) {
|
|
500
|
+
// 404 means the endpoint doesn't exist yet on this bot — treat as empty.
|
|
501
|
+
// All other errors (network failure, 500, etc.) propagate so callers can show an error.
|
|
502
|
+
if (err instanceof Error && err.message.includes("404")) {
|
|
503
|
+
return [];
|
|
504
|
+
}
|
|
505
|
+
throw err;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/** PUT /fleet/bots/:id/secrets — Write secret values. */
|
|
510
|
+
export async function updateInstanceSecrets(
|
|
511
|
+
id: string,
|
|
512
|
+
secrets: Record<string, string>,
|
|
513
|
+
): Promise<void> {
|
|
514
|
+
await fleetFetch(`/bots/${id}/secrets`, {
|
|
515
|
+
method: "PUT",
|
|
516
|
+
body: JSON.stringify({ secrets }),
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/** Toggle a plugin's enabled/disabled state on a bot instance. */
|
|
521
|
+
export async function toggleInstancePlugin(
|
|
522
|
+
botId: string,
|
|
523
|
+
pluginId: string,
|
|
524
|
+
enabled: boolean,
|
|
525
|
+
): Promise<void> {
|
|
526
|
+
await fleetFetch(`/bots/${botId}/plugins/${pluginId}`, {
|
|
527
|
+
method: "PATCH",
|
|
528
|
+
body: JSON.stringify({ enabled }),
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// --- Image update API ---
|
|
533
|
+
|
|
534
|
+
export interface ImageStatusResponse {
|
|
535
|
+
currentDigest: string;
|
|
536
|
+
latestDigest: string;
|
|
537
|
+
updateAvailable: boolean;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
export async function getImageStatus(id: string): Promise<ImageStatusResponse | null> {
|
|
541
|
+
try {
|
|
542
|
+
return await fleetFetch<ImageStatusResponse>(`/bots/${id}/image-status`);
|
|
543
|
+
} catch (e) {
|
|
544
|
+
log.warn("Failed to fetch image status", e);
|
|
545
|
+
return null;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
export async function pullImageUpdate(id: string): Promise<void> {
|
|
550
|
+
await fleetFetch<unknown>(`/bots/${id}/update`, { method: "POST" });
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// --- Observability types ---
|
|
554
|
+
|
|
555
|
+
export type HealthStatus = "healthy" | "degraded" | "unhealthy";
|
|
556
|
+
export type LogLevel = "debug" | "info" | "warn" | "error";
|
|
557
|
+
|
|
558
|
+
export interface PluginHealth {
|
|
559
|
+
name: string;
|
|
560
|
+
status: HealthStatus;
|
|
561
|
+
latencyMs: number | null;
|
|
562
|
+
lastCheck: string;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
export interface ProviderHealth {
|
|
566
|
+
name: string;
|
|
567
|
+
available: boolean;
|
|
568
|
+
latencyMs: number | null;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
export interface HealthHistoryEntry {
|
|
572
|
+
timestamp: string;
|
|
573
|
+
status: HealthStatus;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
export interface InstanceHealth {
|
|
577
|
+
status: HealthStatus;
|
|
578
|
+
uptime: number;
|
|
579
|
+
activeSessions: number;
|
|
580
|
+
totalSessions: number;
|
|
581
|
+
plugins: PluginHealth[];
|
|
582
|
+
providers: ProviderHealth[];
|
|
583
|
+
history: HealthHistoryEntry[];
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
export interface LogEntry {
|
|
587
|
+
id: string;
|
|
588
|
+
timestamp: string;
|
|
589
|
+
level: LogLevel;
|
|
590
|
+
source: string;
|
|
591
|
+
message: string;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
export interface MetricsSnapshot {
|
|
595
|
+
timestamp: string;
|
|
596
|
+
requestCount: number;
|
|
597
|
+
latencyP50: number;
|
|
598
|
+
latencyP95: number;
|
|
599
|
+
latencyP99: number;
|
|
600
|
+
activeSessions: number;
|
|
601
|
+
memoryMb: number;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
export interface TokenUsage {
|
|
605
|
+
provider: string;
|
|
606
|
+
inputTokens: number;
|
|
607
|
+
outputTokens: number;
|
|
608
|
+
totalCost: number;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
export interface PluginEventCount {
|
|
612
|
+
plugin: string;
|
|
613
|
+
count: number;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
export interface InstanceMetrics {
|
|
617
|
+
timeseries: MetricsSnapshot[];
|
|
618
|
+
tokenUsage: TokenUsage[];
|
|
619
|
+
pluginEvents: PluginEventCount[];
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
export interface FleetInstance {
|
|
623
|
+
id: string;
|
|
624
|
+
name: string;
|
|
625
|
+
status: InstanceStatus;
|
|
626
|
+
health: HealthStatus;
|
|
627
|
+
uptime: number | null;
|
|
628
|
+
pluginCount: number;
|
|
629
|
+
sessionCount: number;
|
|
630
|
+
provider: string;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
export interface ActivityEvent {
|
|
634
|
+
id: string;
|
|
635
|
+
timestamp: string;
|
|
636
|
+
actor: string;
|
|
637
|
+
action: string;
|
|
638
|
+
target: string;
|
|
639
|
+
targetHref: string;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
export interface FleetResources {
|
|
643
|
+
totalCpuPercent: number;
|
|
644
|
+
totalMemoryMb: number;
|
|
645
|
+
memoryCapacityMb: number;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// --- Observability API ---
|
|
649
|
+
|
|
650
|
+
export async function getInstanceHealth(id: string): Promise<InstanceHealth> {
|
|
651
|
+
const data = await trpcVanilla.fleet.getInstanceHealth.query({ id });
|
|
652
|
+
const res = data as {
|
|
653
|
+
id: string;
|
|
654
|
+
state: string;
|
|
655
|
+
health: string | null;
|
|
656
|
+
uptime: string | null;
|
|
657
|
+
stats: { cpuPercent: number; memoryUsageMb: number } | null;
|
|
658
|
+
};
|
|
659
|
+
|
|
660
|
+
let healthStatus: HealthStatus;
|
|
661
|
+
if (res.health === "healthy") healthStatus = "healthy";
|
|
662
|
+
else if (res.health === "unhealthy") healthStatus = "unhealthy";
|
|
663
|
+
else healthStatus = "degraded";
|
|
664
|
+
|
|
665
|
+
let uptime = 0;
|
|
666
|
+
if (res.uptime) {
|
|
667
|
+
const ms = new Date(res.uptime).getTime();
|
|
668
|
+
if (!Number.isNaN(ms)) {
|
|
669
|
+
uptime = Math.floor((Date.now() - ms) / 1000);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
return {
|
|
674
|
+
status: healthStatus,
|
|
675
|
+
uptime,
|
|
676
|
+
activeSessions: 0,
|
|
677
|
+
totalSessions: 0,
|
|
678
|
+
plugins: [],
|
|
679
|
+
providers: [],
|
|
680
|
+
history: [],
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
export async function getInstanceLogs(
|
|
685
|
+
id: string,
|
|
686
|
+
params?: { level?: LogLevel; source?: string; search?: string },
|
|
687
|
+
): Promise<LogEntry[]> {
|
|
688
|
+
const data = await trpcVanilla.fleet.getInstanceLogs.query({
|
|
689
|
+
id,
|
|
690
|
+
tail: 100,
|
|
691
|
+
});
|
|
692
|
+
const rawLogs = (data as { logs?: string[] | null }).logs ?? [];
|
|
693
|
+
|
|
694
|
+
// Parse raw container log strings into structured LogEntry objects
|
|
695
|
+
// Format: "2026-02-20T10:00:00Z [LEVEL] message" or plain text
|
|
696
|
+
let entries: LogEntry[] = rawLogs.map((line, i) => {
|
|
697
|
+
const match = line.match(/^(\S+)\s+\[(\w+)]\s+(.*)$/);
|
|
698
|
+
if (match) {
|
|
699
|
+
const level = match[2].toLowerCase();
|
|
700
|
+
return {
|
|
701
|
+
id: `log-${i}`,
|
|
702
|
+
timestamp: match[1],
|
|
703
|
+
level: (["debug", "info", "warn", "error"].includes(level) ? level : "info") as LogLevel,
|
|
704
|
+
source: "container",
|
|
705
|
+
message: match[3],
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
return {
|
|
709
|
+
id: `log-${i}`,
|
|
710
|
+
timestamp: new Date().toISOString(),
|
|
711
|
+
level: "info" as LogLevel,
|
|
712
|
+
source: "container",
|
|
713
|
+
message: line,
|
|
714
|
+
};
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
// Apply client-side filters (server doesn't support them via tRPC)
|
|
718
|
+
if (params?.level) {
|
|
719
|
+
entries = entries.filter((e) => e.level === params.level);
|
|
720
|
+
}
|
|
721
|
+
if (params?.source) {
|
|
722
|
+
entries = entries.filter((e) => e.source === params.source);
|
|
723
|
+
}
|
|
724
|
+
if (params?.search) {
|
|
725
|
+
const q = params.search.toLowerCase();
|
|
726
|
+
entries = entries.filter((e) => e.message.toLowerCase().includes(q));
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
return entries;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
export async function getInstanceMetrics(id: string): Promise<InstanceMetrics> {
|
|
733
|
+
const data = await trpcVanilla.fleet.getInstanceMetrics.query({ id });
|
|
734
|
+
const res = data as {
|
|
735
|
+
id: string;
|
|
736
|
+
stats: {
|
|
737
|
+
cpuPercent: number;
|
|
738
|
+
memoryUsageMb: number;
|
|
739
|
+
memoryLimitMb: number;
|
|
740
|
+
memoryPercent: number;
|
|
741
|
+
} | null;
|
|
742
|
+
};
|
|
743
|
+
|
|
744
|
+
// Build a single-point timeseries from current stats
|
|
745
|
+
const snapshot: MetricsSnapshot = {
|
|
746
|
+
timestamp: new Date().toISOString(),
|
|
747
|
+
requestCount: 0,
|
|
748
|
+
latencyP50: 0,
|
|
749
|
+
latencyP95: 0,
|
|
750
|
+
latencyP99: 0,
|
|
751
|
+
activeSessions: 0,
|
|
752
|
+
memoryMb: res.stats?.memoryUsageMb ?? 0,
|
|
753
|
+
};
|
|
754
|
+
|
|
755
|
+
return {
|
|
756
|
+
timeseries: [snapshot],
|
|
757
|
+
tokenUsage: [],
|
|
758
|
+
pluginEvents: [],
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
export async function getFleetHealth(): Promise<FleetInstance[]> {
|
|
763
|
+
const data = await trpcVanilla.fleet.listInstances.query(undefined);
|
|
764
|
+
const raw = (data as { bots?: BotStatusResponse[] | null }).bots;
|
|
765
|
+
const bots = Array.isArray(raw) ? raw : [];
|
|
766
|
+
return bots.map(mapBotStatusToFleetInstance);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
export async function getActivityFeed(): Promise<ActivityEvent[]> {
|
|
770
|
+
return apiFetch<ActivityEvent[]>("/activity");
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
export async function getFleetResources(): Promise<FleetResources> {
|
|
774
|
+
return apiFetch<FleetResources>("/fleet/resources");
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// --- Settings types ---
|
|
778
|
+
|
|
779
|
+
export interface UserProfile {
|
|
780
|
+
id: string;
|
|
781
|
+
name: string;
|
|
782
|
+
email: string;
|
|
783
|
+
avatarUrl: string | null;
|
|
784
|
+
oauthConnections: { provider: string; connected: boolean }[];
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
export interface ProviderKey {
|
|
788
|
+
id: string;
|
|
789
|
+
provider: string;
|
|
790
|
+
maskedKey: string;
|
|
791
|
+
status: "valid" | "invalid" | "unchecked";
|
|
792
|
+
lastChecked: string | null;
|
|
793
|
+
defaultModel: string | null;
|
|
794
|
+
models: string[];
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
export interface PlatformApiKey {
|
|
798
|
+
id: string;
|
|
799
|
+
name: string;
|
|
800
|
+
prefix: string;
|
|
801
|
+
scope: "read-only" | "full" | "instances";
|
|
802
|
+
instanceIds?: string[];
|
|
803
|
+
createdAt: string;
|
|
804
|
+
lastUsedAt: string | null;
|
|
805
|
+
expiresAt: string | null;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
export interface OrgMember {
|
|
809
|
+
id: string;
|
|
810
|
+
userId: string;
|
|
811
|
+
name: string;
|
|
812
|
+
email: string;
|
|
813
|
+
role: "owner" | "admin" | "member";
|
|
814
|
+
joinedAt: string;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
export interface OrgInvite {
|
|
818
|
+
id: string;
|
|
819
|
+
email: string;
|
|
820
|
+
role: "admin" | "member";
|
|
821
|
+
invitedBy: string;
|
|
822
|
+
expiresAt: string;
|
|
823
|
+
createdAt: string;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
export interface Organization {
|
|
827
|
+
id: string;
|
|
828
|
+
name: string;
|
|
829
|
+
slug: string;
|
|
830
|
+
billingEmail: string;
|
|
831
|
+
members: OrgMember[];
|
|
832
|
+
invites: OrgInvite[];
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// --- Settings API ---
|
|
836
|
+
|
|
837
|
+
export async function getProfile(): Promise<UserProfile> {
|
|
838
|
+
// NOTE: add tRPC procedure
|
|
839
|
+
return apiFetch<UserProfile>("/settings/profile");
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
export async function updateProfile(
|
|
843
|
+
data: Partial<Pick<UserProfile, "name" | "email">>,
|
|
844
|
+
): Promise<UserProfile> {
|
|
845
|
+
// NOTE: add tRPC procedure
|
|
846
|
+
return apiFetch<UserProfile>("/settings/profile", {
|
|
847
|
+
method: "PATCH",
|
|
848
|
+
body: JSON.stringify(data),
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
export async function uploadAvatar(file: File): Promise<UserProfile> {
|
|
853
|
+
const tenantId = getActiveTenantId();
|
|
854
|
+
const form = new FormData();
|
|
855
|
+
form.append("avatar", file);
|
|
856
|
+
const res = await fetch(`${API_BASE_URL}/settings/profile/avatar`, {
|
|
857
|
+
method: "POST",
|
|
858
|
+
credentials: "include",
|
|
859
|
+
headers: {
|
|
860
|
+
...(tenantId ? { "x-tenant-id": tenantId } : {}),
|
|
861
|
+
},
|
|
862
|
+
body: form,
|
|
863
|
+
});
|
|
864
|
+
if (res.status === 401) {
|
|
865
|
+
handleUnauthorized();
|
|
866
|
+
}
|
|
867
|
+
if (!res.ok) {
|
|
868
|
+
const body = await res.json().catch(() => ({}));
|
|
869
|
+
throw new ApiError(res.status, res.statusText, (body as { error?: string }).error ?? undefined);
|
|
870
|
+
}
|
|
871
|
+
return res.json() as Promise<UserProfile>;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
export async function changePassword(data: {
|
|
875
|
+
currentPassword: string;
|
|
876
|
+
newPassword: string;
|
|
877
|
+
}): Promise<void> {
|
|
878
|
+
// NOTE: add tRPC procedure
|
|
879
|
+
await apiFetch("/settings/profile/password", { method: "POST", body: JSON.stringify(data) });
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
export async function deleteAccount(): Promise<void> {
|
|
883
|
+
// NOTE: add tRPC procedure
|
|
884
|
+
await apiFetch("/settings/profile", { method: "DELETE" });
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
export async function listProviderKeys(): Promise<ProviderKey[]> {
|
|
888
|
+
// NOTE: add tRPC procedure
|
|
889
|
+
return apiFetch<ProviderKey[]>("/settings/providers");
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
export async function testProviderKey(id: string): Promise<{ valid: boolean }> {
|
|
893
|
+
// NOTE: add tRPC procedure
|
|
894
|
+
return apiFetch<{ valid: boolean }>(`/settings/providers/${id}/test`, { method: "POST" });
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
export async function removeProviderKey(id: string, provider: string): Promise<void> {
|
|
898
|
+
try {
|
|
899
|
+
await deleteTenantKey(provider);
|
|
900
|
+
} catch {
|
|
901
|
+
// tenant-key may not exist if it was never stored there -- continue
|
|
902
|
+
}
|
|
903
|
+
await apiFetch(`/settings/providers/${id}`, { method: "DELETE" });
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
export async function saveProviderKey(provider: string, key: string): Promise<ProviderKey> {
|
|
907
|
+
await storeTenantKey(provider, key);
|
|
908
|
+
return apiFetch<ProviderKey>("/settings/providers", {
|
|
909
|
+
method: "POST",
|
|
910
|
+
body: JSON.stringify({ provider, key }),
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
export async function updateProviderModel(id: string, model: string): Promise<void> {
|
|
915
|
+
// NOTE: add tRPC procedure
|
|
916
|
+
await apiFetch(`/settings/providers/${id}/model`, {
|
|
917
|
+
method: "PATCH",
|
|
918
|
+
body: JSON.stringify({ model }),
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
export async function listApiKeys(): Promise<PlatformApiKey[]> {
|
|
923
|
+
// NOTE: add tRPC procedure
|
|
924
|
+
return apiFetch<PlatformApiKey[]>("/settings/api-keys");
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
export async function createApiKey(data: {
|
|
928
|
+
name: string;
|
|
929
|
+
scope: string;
|
|
930
|
+
expiration: string;
|
|
931
|
+
instanceIds?: string[];
|
|
932
|
+
}): Promise<{ key: PlatformApiKey; secret: string }> {
|
|
933
|
+
// NOTE: add tRPC procedure
|
|
934
|
+
return apiFetch<{ key: PlatformApiKey; secret: string }>("/settings/api-keys", {
|
|
935
|
+
method: "POST",
|
|
936
|
+
body: JSON.stringify(data),
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
export async function revokeApiKey(id: string): Promise<void> {
|
|
941
|
+
// NOTE: add tRPC procedure
|
|
942
|
+
await apiFetch(`/settings/api-keys/${id}`, { method: "DELETE" });
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// --- Secrets Vault types ---
|
|
946
|
+
|
|
947
|
+
/** A secret summary (value never returned after creation). */
|
|
948
|
+
export interface SecretSummary {
|
|
949
|
+
id: string;
|
|
950
|
+
name: string;
|
|
951
|
+
/** e.g. "webhook-signing", "api-token", "service-credential" */
|
|
952
|
+
type: string;
|
|
953
|
+
createdAt: string;
|
|
954
|
+
lastUsedAt: string | null;
|
|
955
|
+
expiresAt: string | null;
|
|
956
|
+
rotatedAt: string | null;
|
|
957
|
+
isActive: boolean;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
/** An audit log entry for secret access. */
|
|
961
|
+
export interface SecretAuditEntry {
|
|
962
|
+
id: string;
|
|
963
|
+
secretId: string;
|
|
964
|
+
action: "accessed" | "rotated" | "created" | "deleted";
|
|
965
|
+
actorType: "plugin" | "bot" | "user";
|
|
966
|
+
actorName: string;
|
|
967
|
+
timestamp: string;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// NOTE: migrate to tRPC when secrets router is extended
|
|
971
|
+
/** GET /settings/secrets — List all secrets for the tenant. */
|
|
972
|
+
export async function listSecrets(): Promise<SecretSummary[]> {
|
|
973
|
+
return apiFetch<SecretSummary[]>("/settings/secrets");
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// NOTE: migrate to tRPC when secrets router is extended
|
|
977
|
+
/** POST /settings/secrets — Create a new secret. */
|
|
978
|
+
export async function createSecret(data: {
|
|
979
|
+
name: string;
|
|
980
|
+
value: string;
|
|
981
|
+
type: string;
|
|
982
|
+
expiresIn?: string;
|
|
983
|
+
}): Promise<{ secret: SecretSummary; plaintextValue: string }> {
|
|
984
|
+
return apiFetch<{ secret: SecretSummary; plaintextValue: string }>("/settings/secrets", {
|
|
985
|
+
method: "POST",
|
|
986
|
+
body: JSON.stringify(data),
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
// NOTE: migrate to tRPC when secrets router is extended
|
|
991
|
+
/** POST /settings/secrets/:id/rotate — Rotate a secret. */
|
|
992
|
+
export async function rotateSecret(
|
|
993
|
+
id: string,
|
|
994
|
+
): Promise<{ secret: SecretSummary; plaintextValue: string }> {
|
|
995
|
+
return apiFetch<{ secret: SecretSummary; plaintextValue: string }>(
|
|
996
|
+
`/settings/secrets/${encodeURIComponent(id)}/rotate`,
|
|
997
|
+
{ method: "POST" },
|
|
998
|
+
);
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
// NOTE: migrate to tRPC when secrets router is extended
|
|
1002
|
+
/** DELETE /settings/secrets/:id — Delete a secret. */
|
|
1003
|
+
export async function deleteSecret(id: string): Promise<void> {
|
|
1004
|
+
await apiFetch(`/settings/secrets/${encodeURIComponent(id)}`, { method: "DELETE" });
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// NOTE: migrate to tRPC when secrets router is extended
|
|
1008
|
+
/** GET /settings/secrets/:id/audit — Fetch audit log for a secret. */
|
|
1009
|
+
export async function fetchSecretAudit(id: string): Promise<SecretAuditEntry[]> {
|
|
1010
|
+
return apiFetch<SecretAuditEntry[]>(`/settings/secrets/${encodeURIComponent(id)}/audit`);
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
// --- Billing types ---
|
|
1014
|
+
|
|
1015
|
+
export interface BillingUsage {
|
|
1016
|
+
plan: string;
|
|
1017
|
+
planName: string;
|
|
1018
|
+
billingPeriodStart: string;
|
|
1019
|
+
billingPeriodEnd: string;
|
|
1020
|
+
instancesRunning: number;
|
|
1021
|
+
instanceCap: number;
|
|
1022
|
+
storageUsedGb: number;
|
|
1023
|
+
storageCapGb: number;
|
|
1024
|
+
apiCalls: number;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
export type HostedCapability = "transcription" | "image_gen" | "text_gen" | "embeddings";
|
|
1028
|
+
|
|
1029
|
+
export interface HostedCapabilityUsage {
|
|
1030
|
+
capability: HostedCapability;
|
|
1031
|
+
label: string;
|
|
1032
|
+
units: number;
|
|
1033
|
+
unitLabel: string;
|
|
1034
|
+
cost: number;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
export interface HostedUsageSummary {
|
|
1038
|
+
periodStart: string;
|
|
1039
|
+
periodEnd: string;
|
|
1040
|
+
capabilities: HostedCapabilityUsage[];
|
|
1041
|
+
totalCost: number;
|
|
1042
|
+
includedCredit: number;
|
|
1043
|
+
amountDue: number;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
export interface BillingUsageSummary {
|
|
1047
|
+
periodStart: string;
|
|
1048
|
+
periodEnd: string;
|
|
1049
|
+
totalSpend: number;
|
|
1050
|
+
includedCredit: number;
|
|
1051
|
+
amountDue: number;
|
|
1052
|
+
planName: string;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
export interface HostedUsageEvent {
|
|
1056
|
+
id: string;
|
|
1057
|
+
date: string;
|
|
1058
|
+
capability: HostedCapability;
|
|
1059
|
+
provider: string;
|
|
1060
|
+
units: number;
|
|
1061
|
+
unitLabel: string;
|
|
1062
|
+
cost: number;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
export interface SpendingLimit {
|
|
1066
|
+
alertAt: number | null;
|
|
1067
|
+
hardCap: number | null;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
export interface SpendingLimits {
|
|
1071
|
+
global: SpendingLimit;
|
|
1072
|
+
perCapability: Record<HostedCapability, SpendingLimit>;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
export type InferenceMode = "byok" | "hosted";
|
|
1076
|
+
|
|
1077
|
+
export interface InvoiceLineItem {
|
|
1078
|
+
capability: string;
|
|
1079
|
+
units: number;
|
|
1080
|
+
unitPrice: number;
|
|
1081
|
+
total: number;
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
export interface ProviderCost {
|
|
1085
|
+
provider: string;
|
|
1086
|
+
estimatedCost: number;
|
|
1087
|
+
inputTokens: number;
|
|
1088
|
+
outputTokens: number;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
export interface UsageDataPoint {
|
|
1092
|
+
date: string;
|
|
1093
|
+
apiCalls: number;
|
|
1094
|
+
instances: number;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
export interface Invoice {
|
|
1098
|
+
id: string;
|
|
1099
|
+
date: string;
|
|
1100
|
+
amount: number;
|
|
1101
|
+
status: "paid" | "pending" | "failed";
|
|
1102
|
+
downloadUrl: string;
|
|
1103
|
+
hostedLineItems?: InvoiceLineItem[];
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
export interface PaymentMethod {
|
|
1107
|
+
id: string;
|
|
1108
|
+
brand: string;
|
|
1109
|
+
last4: string;
|
|
1110
|
+
expiryMonth: number;
|
|
1111
|
+
expiryYear: number;
|
|
1112
|
+
isDefault: boolean;
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
export interface BillingInfo {
|
|
1116
|
+
email: string;
|
|
1117
|
+
paymentMethods: PaymentMethod[];
|
|
1118
|
+
invoices: Invoice[];
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
export interface CreditOption {
|
|
1122
|
+
priceId: string;
|
|
1123
|
+
label: string;
|
|
1124
|
+
amountCents: number;
|
|
1125
|
+
creditCents: number;
|
|
1126
|
+
bonusPercent: number;
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
// --- Auto-topup types ---
|
|
1130
|
+
|
|
1131
|
+
export type AutoTopupInterval = "daily" | "weekly" | "monthly";
|
|
1132
|
+
|
|
1133
|
+
export interface AutoTopupSettings {
|
|
1134
|
+
usageBased: {
|
|
1135
|
+
enabled: boolean;
|
|
1136
|
+
thresholdCents: number;
|
|
1137
|
+
topupAmountCents: number;
|
|
1138
|
+
};
|
|
1139
|
+
scheduled: {
|
|
1140
|
+
enabled: boolean;
|
|
1141
|
+
amountCents: number;
|
|
1142
|
+
interval: AutoTopupInterval;
|
|
1143
|
+
nextChargeDate: string | null;
|
|
1144
|
+
};
|
|
1145
|
+
paymentMethodLast4: string | null;
|
|
1146
|
+
paymentMethodBrand: string | null;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
// --- Billing API (tRPC) ---
|
|
1150
|
+
|
|
1151
|
+
export async function getCurrentPlan(): Promise<string> {
|
|
1152
|
+
const res = await trpcVanilla.billing.currentPlan.query(undefined);
|
|
1153
|
+
return res.tier;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
export async function getBillingUsage(): Promise<BillingUsage> {
|
|
1157
|
+
// NOTE(WOP-687): align backend response shape with UI type
|
|
1158
|
+
const plan = await getCurrentPlan();
|
|
1159
|
+
return {
|
|
1160
|
+
plan,
|
|
1161
|
+
planName: plan.charAt(0).toUpperCase() + plan.slice(1),
|
|
1162
|
+
billingPeriodStart: new Date(Date.now() - 30 * 86400000).toISOString(),
|
|
1163
|
+
billingPeriodEnd: new Date().toISOString(),
|
|
1164
|
+
instancesRunning: 0,
|
|
1165
|
+
instanceCap: 1,
|
|
1166
|
+
storageUsedGb: 0,
|
|
1167
|
+
storageCapGb: 1,
|
|
1168
|
+
apiCalls: 0,
|
|
1169
|
+
};
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
export async function getProviderCosts(): Promise<ProviderCost[]> {
|
|
1173
|
+
return trpcVanilla.billing.providerCosts.query(undefined);
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
export async function getUsageHistory(_days?: number): Promise<UsageDataPoint[]> {
|
|
1177
|
+
// NOTE(WOP-687): backend usageHistory returns Stripe reports, not daily data points
|
|
1178
|
+
return [];
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
export async function getBillingInfo(): Promise<BillingInfo> {
|
|
1182
|
+
return trpcVanilla.billing.billingInfo.query(undefined);
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
export async function updateBillingEmail(email: string): Promise<void> {
|
|
1186
|
+
await trpcVanilla.billing.updateBillingEmail.mutate({ email });
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
export async function removePaymentMethod(id: string): Promise<void> {
|
|
1190
|
+
await trpcVanilla.billing.removePaymentMethod.mutate({ id });
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
export async function setDefaultPaymentMethod(id: string): Promise<void> {
|
|
1194
|
+
await trpcVanilla.billing.setDefaultPaymentMethod.mutate({ id });
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
export async function createSetupIntent(): Promise<{ clientSecret: string }> {
|
|
1198
|
+
return apiFetch<{ clientSecret: string }>("/billing/setup-intent", { method: "POST" });
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
export async function createBillingPortalSession(): Promise<{ url: string }> {
|
|
1202
|
+
return trpcVanilla.billing.portalSession.mutate({
|
|
1203
|
+
returnUrl: typeof window !== "undefined" ? window.location.href : "",
|
|
1204
|
+
});
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
// --- Capability settings types ---
|
|
1208
|
+
|
|
1209
|
+
export type CapabilityName = "transcription" | "image-gen" | "text-gen" | "embeddings";
|
|
1210
|
+
export type CapabilityMode = "hosted" | "byok";
|
|
1211
|
+
|
|
1212
|
+
export interface CapabilitySetting {
|
|
1213
|
+
capability: CapabilityName;
|
|
1214
|
+
mode: CapabilityMode;
|
|
1215
|
+
maskedKey: string | null;
|
|
1216
|
+
keyStatus: "valid" | "invalid" | "unchecked" | null;
|
|
1217
|
+
provider: string | null;
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
export interface CapabilityMetaEntry {
|
|
1221
|
+
capability: string;
|
|
1222
|
+
label: string;
|
|
1223
|
+
description: string;
|
|
1224
|
+
pricing: string;
|
|
1225
|
+
hostedProvider: string;
|
|
1226
|
+
icon: string;
|
|
1227
|
+
sortOrder: number;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
// --- Capability settings API ---
|
|
1231
|
+
|
|
1232
|
+
// --- Credits types ---
|
|
1233
|
+
|
|
1234
|
+
export interface CreditBalance {
|
|
1235
|
+
balance: number;
|
|
1236
|
+
dailyBurn: number;
|
|
1237
|
+
runway: number | null;
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
export type CreditTransactionType =
|
|
1241
|
+
| "purchase"
|
|
1242
|
+
| "signup_credit"
|
|
1243
|
+
| "bot_runtime"
|
|
1244
|
+
| "refund"
|
|
1245
|
+
| "bonus"
|
|
1246
|
+
| "adjustment"
|
|
1247
|
+
| "community_dividend";
|
|
1248
|
+
|
|
1249
|
+
export interface CreditTransaction {
|
|
1250
|
+
id: string;
|
|
1251
|
+
type: CreditTransactionType;
|
|
1252
|
+
description: string;
|
|
1253
|
+
amount: number;
|
|
1254
|
+
createdAt: string;
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
export interface CreditHistoryResponse {
|
|
1258
|
+
transactions: CreditTransaction[];
|
|
1259
|
+
nextCursor: string | null;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
// --- Affiliate types ---
|
|
1263
|
+
|
|
1264
|
+
export type ReferralStatus = "pending" | "matched";
|
|
1265
|
+
|
|
1266
|
+
export interface AffiliateStats {
|
|
1267
|
+
referralCode: string;
|
|
1268
|
+
referralUrl: string;
|
|
1269
|
+
totalReferred: number;
|
|
1270
|
+
totalConverted: number;
|
|
1271
|
+
totalEarnedCents: number;
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
export type Referral =
|
|
1275
|
+
| { id: string; maskedEmail: string; joinedAt: string; status: "pending"; matchAmountCents: null }
|
|
1276
|
+
| {
|
|
1277
|
+
id: string;
|
|
1278
|
+
maskedEmail: string;
|
|
1279
|
+
joinedAt: string;
|
|
1280
|
+
status: "matched";
|
|
1281
|
+
matchAmountCents: number;
|
|
1282
|
+
};
|
|
1283
|
+
|
|
1284
|
+
export interface AffiliateReferralsResponse {
|
|
1285
|
+
referrals: Referral[];
|
|
1286
|
+
total: number;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
export interface CheckoutResponse {
|
|
1290
|
+
checkoutUrl: string;
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
// --- Credits API (tRPC) ---
|
|
1294
|
+
|
|
1295
|
+
export async function getCreditBalance(): Promise<CreditBalance> {
|
|
1296
|
+
const res = await trpcVanilla.billing.creditsBalance.query({});
|
|
1297
|
+
return {
|
|
1298
|
+
balance: (res?.balance_cents ?? 0) / 100,
|
|
1299
|
+
dailyBurn: (res?.daily_burn_cents ?? 0) / 100,
|
|
1300
|
+
runway: res?.runway_days ?? null,
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
function mapTransactionType(backendType: string): CreditTransactionType {
|
|
1305
|
+
const map: Record<string, CreditTransactionType> = {
|
|
1306
|
+
grant: "purchase",
|
|
1307
|
+
refund: "refund",
|
|
1308
|
+
correction: "adjustment",
|
|
1309
|
+
community_dividend: "community_dividend",
|
|
1310
|
+
};
|
|
1311
|
+
return map[backendType] ?? "adjustment";
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
export async function getCreditHistory(_cursor?: string): Promise<CreditHistoryResponse> {
|
|
1315
|
+
const res = await trpcVanilla.billing.creditsHistory.query({});
|
|
1316
|
+
const entries = Array.isArray(res?.entries) ? res.entries : [];
|
|
1317
|
+
return {
|
|
1318
|
+
transactions: (
|
|
1319
|
+
entries as Array<{
|
|
1320
|
+
id?: string;
|
|
1321
|
+
type?: string;
|
|
1322
|
+
reason?: string;
|
|
1323
|
+
amount_cents?: number;
|
|
1324
|
+
created_at?: number;
|
|
1325
|
+
}>
|
|
1326
|
+
).map((e) => ({
|
|
1327
|
+
id: e.id ?? "",
|
|
1328
|
+
type: mapTransactionType(e.type ?? ""),
|
|
1329
|
+
description: e.reason ?? "",
|
|
1330
|
+
amount: (e.amount_cents ?? 0) / 100,
|
|
1331
|
+
createdAt: e.created_at
|
|
1332
|
+
? new Date(e.created_at * 1000).toISOString()
|
|
1333
|
+
: new Date().toISOString(),
|
|
1334
|
+
})),
|
|
1335
|
+
nextCursor: null, // NOTE(WOP-687): implement cursor-based pagination
|
|
1336
|
+
};
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
export async function getCreditOptions(): Promise<CreditOption[]> {
|
|
1340
|
+
return trpcVanilla.billing.creditOptions.query(undefined);
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
export async function createCreditCheckout(priceId: string): Promise<CheckoutResponse> {
|
|
1344
|
+
const res = await trpcVanilla.billing.creditsCheckout.mutate({
|
|
1345
|
+
priceId,
|
|
1346
|
+
successUrl: `${typeof window !== "undefined" ? window.location.origin : ""}/billing/credits?checkout=success`,
|
|
1347
|
+
cancelUrl: `${typeof window !== "undefined" ? window.location.origin : ""}/billing/credits?checkout=cancel`,
|
|
1348
|
+
});
|
|
1349
|
+
const url = res.url;
|
|
1350
|
+
if (!url) throw new Error("Portal URL unavailable");
|
|
1351
|
+
return { checkoutUrl: url };
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
export async function createCryptoCheckout(
|
|
1355
|
+
amountUsd: number,
|
|
1356
|
+
): Promise<{ url: string; referenceId: string }> {
|
|
1357
|
+
return trpcVanilla.billing.cryptoCheckout.mutate({ amountUsd });
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
// --- Dividend types ---
|
|
1361
|
+
|
|
1362
|
+
export interface DividendWalletStats {
|
|
1363
|
+
poolCents: number;
|
|
1364
|
+
activeUsers: number;
|
|
1365
|
+
perUserCents: number;
|
|
1366
|
+
nextDistributionAt: string;
|
|
1367
|
+
userEligible: boolean;
|
|
1368
|
+
userLastPurchaseAt: string | null;
|
|
1369
|
+
userWindowExpiresAt: string | null;
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
export interface DividendHistoryEntry {
|
|
1373
|
+
date: string;
|
|
1374
|
+
amountCents: number;
|
|
1375
|
+
poolCents: number;
|
|
1376
|
+
activeUsers: number;
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
export interface DividendHistoryResponse {
|
|
1380
|
+
dividends: DividendHistoryEntry[];
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
export interface DividendLifetime {
|
|
1384
|
+
totalCents: number;
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
// --- Dividend API ---
|
|
1388
|
+
|
|
1389
|
+
export async function getDividendStats(): Promise<DividendWalletStats> {
|
|
1390
|
+
const res = await apiFetch<{
|
|
1391
|
+
pool_cents?: number;
|
|
1392
|
+
active_users?: number;
|
|
1393
|
+
per_user_cents?: number;
|
|
1394
|
+
next_distribution_at?: string;
|
|
1395
|
+
user_eligible?: boolean;
|
|
1396
|
+
user_last_purchase_at?: string | null;
|
|
1397
|
+
user_window_expires_at?: string | null;
|
|
1398
|
+
}>("/billing/dividend/stats");
|
|
1399
|
+
return {
|
|
1400
|
+
poolCents: res?.pool_cents ?? 0,
|
|
1401
|
+
activeUsers: res?.active_users ?? 0,
|
|
1402
|
+
perUserCents: res?.per_user_cents ?? 0,
|
|
1403
|
+
nextDistributionAt: res?.next_distribution_at ?? "",
|
|
1404
|
+
userEligible: res?.user_eligible ?? false,
|
|
1405
|
+
userLastPurchaseAt: res?.user_last_purchase_at ?? null,
|
|
1406
|
+
userWindowExpiresAt: res?.user_window_expires_at ?? null,
|
|
1407
|
+
};
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
export async function getDividendHistory(): Promise<DividendHistoryResponse> {
|
|
1411
|
+
const res = await apiFetch<{
|
|
1412
|
+
dividends?: Array<{
|
|
1413
|
+
date: string;
|
|
1414
|
+
amount_cents: number;
|
|
1415
|
+
pool_cents: number;
|
|
1416
|
+
active_users: number;
|
|
1417
|
+
}>;
|
|
1418
|
+
}>("/billing/dividend/history");
|
|
1419
|
+
const dividends = Array.isArray(res?.dividends) ? res.dividends : [];
|
|
1420
|
+
return {
|
|
1421
|
+
dividends: dividends.map((d) => ({
|
|
1422
|
+
date: d.date ?? "",
|
|
1423
|
+
amountCents: d.amount_cents ?? 0,
|
|
1424
|
+
poolCents: d.pool_cents ?? 0,
|
|
1425
|
+
activeUsers: d.active_users ?? 0,
|
|
1426
|
+
})),
|
|
1427
|
+
};
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
export async function getDividendLifetime(): Promise<DividendLifetime> {
|
|
1431
|
+
const res = await apiFetch<{ total_cents?: number }>("/billing/dividend/lifetime");
|
|
1432
|
+
return { totalCents: res?.total_cents ?? 0 };
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
// --- Hosted usage API (tRPC) ---
|
|
1436
|
+
|
|
1437
|
+
export async function getInferenceMode(): Promise<InferenceMode> {
|
|
1438
|
+
const res = await trpcVanilla.billing.inferenceMode.query(undefined);
|
|
1439
|
+
return res.mode;
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
export async function getHostedUsageSummary(): Promise<HostedUsageSummary> {
|
|
1443
|
+
return trpcVanilla.billing.hostedUsageSummary.query(undefined);
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
export async function getBillingUsageSummary(): Promise<BillingUsageSummary> {
|
|
1447
|
+
const res = await trpcVanilla.billing.usageSummary.query(undefined);
|
|
1448
|
+
return {
|
|
1449
|
+
periodStart: res?.period_start ?? "",
|
|
1450
|
+
periodEnd: res?.period_end ?? "",
|
|
1451
|
+
totalSpend: (res?.total_spend_cents ?? 0) / 100,
|
|
1452
|
+
includedCredit: (res?.included_credit_cents ?? 0) / 100,
|
|
1453
|
+
amountDue: (res?.amount_due_cents ?? 0) / 100,
|
|
1454
|
+
planName: res?.plan_name ?? "",
|
|
1455
|
+
};
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
export async function getHostedUsageEvents(params?: {
|
|
1459
|
+
capability?: HostedCapability;
|
|
1460
|
+
from?: string;
|
|
1461
|
+
to?: string;
|
|
1462
|
+
}): Promise<HostedUsageEvent[]> {
|
|
1463
|
+
return trpcVanilla.billing.hostedUsageEvents.query(params ?? {});
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
export async function getSpendingLimits(): Promise<SpendingLimits> {
|
|
1467
|
+
return trpcVanilla.billing.spendingLimits.query(undefined);
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
export async function updateSpendingLimits(limits: SpendingLimits): Promise<void> {
|
|
1471
|
+
await trpcVanilla.billing.updateSpendingLimits.mutate({ ...limits });
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
// --- Affiliate API (tRPC) ---
|
|
1475
|
+
|
|
1476
|
+
export async function getAffiliateStats(): Promise<AffiliateStats> {
|
|
1477
|
+
const res = await trpcVanilla.billing.affiliateStats.query(undefined);
|
|
1478
|
+
return {
|
|
1479
|
+
referralCode: res?.referral_code ?? "",
|
|
1480
|
+
referralUrl: res?.referral_url ?? "",
|
|
1481
|
+
totalReferred: res?.total_referred ?? 0,
|
|
1482
|
+
totalConverted: res?.total_converted ?? 0,
|
|
1483
|
+
totalEarnedCents: res?.total_earned_cents ?? 0,
|
|
1484
|
+
};
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
export async function getAffiliateReferrals(params?: {
|
|
1488
|
+
limit?: number;
|
|
1489
|
+
offset?: number;
|
|
1490
|
+
}): Promise<AffiliateReferralsResponse> {
|
|
1491
|
+
const res = await trpcVanilla.billing.affiliateReferrals.query({
|
|
1492
|
+
limit: params?.limit ?? 20,
|
|
1493
|
+
offset: params?.offset ?? 0,
|
|
1494
|
+
});
|
|
1495
|
+
const referrals = Array.isArray(res?.referrals) ? res.referrals : [];
|
|
1496
|
+
return {
|
|
1497
|
+
referrals: (
|
|
1498
|
+
referrals as Array<{
|
|
1499
|
+
id?: string;
|
|
1500
|
+
masked_email?: string;
|
|
1501
|
+
joined_at?: string;
|
|
1502
|
+
status?: string;
|
|
1503
|
+
match_amount_cents?: number | null;
|
|
1504
|
+
}>
|
|
1505
|
+
).map((r) => ({
|
|
1506
|
+
id: r.id ?? "",
|
|
1507
|
+
maskedEmail: r.masked_email ?? "",
|
|
1508
|
+
joinedAt: r.joined_at ? new Date(r.joined_at).toISOString() : "",
|
|
1509
|
+
status: r.status ?? "pending",
|
|
1510
|
+
matchAmountCents: r.match_amount_cents ?? null,
|
|
1511
|
+
})) as Referral[],
|
|
1512
|
+
total: res?.total ?? 0,
|
|
1513
|
+
};
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
// --- Auto-topup API (tRPC) ---
|
|
1517
|
+
|
|
1518
|
+
export async function getAutoTopupSettings(): Promise<AutoTopupSettings> {
|
|
1519
|
+
return trpcVanilla.billing.autoTopupSettings.query(undefined);
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
export async function updateAutoTopupSettings(update: {
|
|
1523
|
+
usageBased?: { enabled: boolean; thresholdCents: number; topupAmountCents: number };
|
|
1524
|
+
scheduled?: { enabled: boolean; amountCents: number; interval: AutoTopupInterval };
|
|
1525
|
+
}): Promise<AutoTopupSettings> {
|
|
1526
|
+
return trpcVanilla.billing.updateAutoTopupSettings.mutate(update);
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
// --- Account status types ---
|
|
1530
|
+
|
|
1531
|
+
export type AccountStatusValue = "active" | "grace_period" | "suspended" | "banned";
|
|
1532
|
+
|
|
1533
|
+
export interface AccountStatus {
|
|
1534
|
+
status: AccountStatusValue;
|
|
1535
|
+
statusReason: string | null;
|
|
1536
|
+
graceDeadline: string | null;
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
export async function getAccountStatus(): Promise<AccountStatus | null> {
|
|
1540
|
+
try {
|
|
1541
|
+
const res = await trpcVanilla.billing.accountStatus.query(undefined);
|
|
1542
|
+
return {
|
|
1543
|
+
status: (res?.status as AccountStatusValue) ?? "active",
|
|
1544
|
+
statusReason: res?.status_reason ?? null,
|
|
1545
|
+
graceDeadline: res?.grace_deadline ?? null,
|
|
1546
|
+
};
|
|
1547
|
+
} catch {
|
|
1548
|
+
// Endpoint may not exist yet — non-critical
|
|
1549
|
+
return null;
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
// --- Model selection types ---
|
|
1554
|
+
|
|
1555
|
+
export interface ModelSelection {
|
|
1556
|
+
modelId: string;
|
|
1557
|
+
providerId: string;
|
|
1558
|
+
mode: "hosted" | "byok";
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
// --- Model selection API ---
|
|
1562
|
+
|
|
1563
|
+
export async function getModelSelection(): Promise<ModelSelection> {
|
|
1564
|
+
// NOTE: add tRPC procedure
|
|
1565
|
+
return apiFetch<ModelSelection>("/settings/model");
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
export async function updateModelSelection(data: ModelSelection): Promise<ModelSelection> {
|
|
1569
|
+
// NOTE: add tRPC procedure
|
|
1570
|
+
return apiFetch<ModelSelection>("/settings/model", {
|
|
1571
|
+
method: "PUT",
|
|
1572
|
+
body: JSON.stringify(data),
|
|
1573
|
+
});
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
// --- Tenant key store API (AES-256-GCM encrypted backend) ---
|
|
1577
|
+
|
|
1578
|
+
export interface TenantKeyMeta {
|
|
1579
|
+
provider: string;
|
|
1580
|
+
hasKey: boolean;
|
|
1581
|
+
maskedKey: string | null;
|
|
1582
|
+
createdAt: string | null;
|
|
1583
|
+
updatedAt: string | null;
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
export async function listTenantKeys(): Promise<TenantKeyMeta[]> {
|
|
1587
|
+
return apiFetch<TenantKeyMeta[]>("/tenant-keys");
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
export async function getTenantKey(provider: string): Promise<TenantKeyMeta> {
|
|
1591
|
+
return apiFetch<TenantKeyMeta>(`/tenant-keys/${encodeURIComponent(provider)}`);
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
export async function storeTenantKey(provider: string, key: string): Promise<TenantKeyMeta> {
|
|
1595
|
+
return apiFetch<TenantKeyMeta>(`/tenant-keys/${encodeURIComponent(provider)}`, {
|
|
1596
|
+
method: "PUT",
|
|
1597
|
+
body: JSON.stringify({ key }),
|
|
1598
|
+
});
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
export async function deleteTenantKey(provider: string): Promise<void> {
|
|
1602
|
+
await apiFetch(`/tenant-keys/${encodeURIComponent(provider)}`, { method: "DELETE" });
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
// --- BYOK key validation ---
|
|
1606
|
+
|
|
1607
|
+
export interface KeyValidationResult {
|
|
1608
|
+
valid: boolean;
|
|
1609
|
+
message?: string;
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
export async function validateDeepgramKey(key: string): Promise<KeyValidationResult> {
|
|
1613
|
+
const { testProviderKey: testProviderKeyViaTrpc } = await import("./settings-api");
|
|
1614
|
+
try {
|
|
1615
|
+
const result = await testProviderKeyViaTrpc("transcription", key);
|
|
1616
|
+
return result.valid
|
|
1617
|
+
? { valid: true }
|
|
1618
|
+
: { valid: false, message: "Invalid API key. Please check and try again." };
|
|
1619
|
+
} catch (e) {
|
|
1620
|
+
log.warn("Key validation request failed", e);
|
|
1621
|
+
return { valid: false, message: "Could not validate key. Please try again." };
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
export async function validateElevenLabsKey(key: string): Promise<KeyValidationResult> {
|
|
1626
|
+
const { testProviderKey, saveProviderKey: saveProviderKeyViaTrpc } = await import(
|
|
1627
|
+
"./settings-api"
|
|
1628
|
+
);
|
|
1629
|
+
try {
|
|
1630
|
+
const result = await testProviderKey("elevenlabs", key);
|
|
1631
|
+
if (result.valid) {
|
|
1632
|
+
await saveProviderKeyViaTrpc("elevenlabs", key);
|
|
1633
|
+
return { valid: true };
|
|
1634
|
+
}
|
|
1635
|
+
return {
|
|
1636
|
+
valid: false,
|
|
1637
|
+
message: result.error ?? "Invalid API key. Please check and try again.",
|
|
1638
|
+
};
|
|
1639
|
+
} catch (e) {
|
|
1640
|
+
log.warn("Key validation request failed", e);
|
|
1641
|
+
return { valid: false, message: "Could not validate key. Please try again." };
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
// --- Notification preferences types ---
|
|
1646
|
+
|
|
1647
|
+
export interface NotificationPreferences {
|
|
1648
|
+
billing_low_balance: boolean;
|
|
1649
|
+
billing_receipts: boolean;
|
|
1650
|
+
billing_auto_topup: boolean;
|
|
1651
|
+
agent_channel_disconnect: boolean;
|
|
1652
|
+
agent_status_changes: boolean;
|
|
1653
|
+
account_role_changes: boolean;
|
|
1654
|
+
account_team_invites: boolean;
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
// --- Public platform health (no auth required) ---
|
|
1658
|
+
|
|
1659
|
+
export interface PlatformServiceHealth {
|
|
1660
|
+
name: string;
|
|
1661
|
+
status: HealthStatus;
|
|
1662
|
+
latencyMs: number | null;
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
export interface PlatformHealthResponse {
|
|
1666
|
+
status: HealthStatus;
|
|
1667
|
+
services: PlatformServiceHealth[];
|
|
1668
|
+
version: string;
|
|
1669
|
+
uptime: number;
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
export async function fetchPlatformHealth(): Promise<PlatformHealthResponse | null> {
|
|
1673
|
+
try {
|
|
1674
|
+
const res = await fetch(`${API_BASE_URL}/health`, {
|
|
1675
|
+
cache: "no-store",
|
|
1676
|
+
});
|
|
1677
|
+
if (!res.ok) return null;
|
|
1678
|
+
return (await res.json()) as PlatformHealthResponse;
|
|
1679
|
+
} catch (e) {
|
|
1680
|
+
log.warn("Failed to fetch platform health", e);
|
|
1681
|
+
return null;
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
// --- Snapshot types ---
|
|
1686
|
+
|
|
1687
|
+
export type SnapshotType = "nightly" | "on-demand" | "pre-restore";
|
|
1688
|
+
export type SnapshotTrigger = "manual" | "scheduled" | "pre_update";
|
|
1689
|
+
|
|
1690
|
+
export interface Snapshot {
|
|
1691
|
+
id: string;
|
|
1692
|
+
instanceId: string;
|
|
1693
|
+
name: string | null;
|
|
1694
|
+
type: SnapshotType;
|
|
1695
|
+
trigger: SnapshotTrigger;
|
|
1696
|
+
sizeMb: number;
|
|
1697
|
+
createdAt: string;
|
|
1698
|
+
expiresAt: number | null;
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
/** List all snapshots for a bot instance. */
|
|
1702
|
+
export async function listSnapshots(instanceId: string): Promise<Snapshot[]> {
|
|
1703
|
+
const data = await apiFetch<{ snapshots: Snapshot[] }>(`/bots/${instanceId}/snapshots`, {
|
|
1704
|
+
method: "GET",
|
|
1705
|
+
});
|
|
1706
|
+
return data.snapshots;
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
/** Create an on-demand snapshot. */
|
|
1710
|
+
export async function createSnapshot(
|
|
1711
|
+
instanceId: string,
|
|
1712
|
+
name?: string,
|
|
1713
|
+
): Promise<{ snapshot: Snapshot; estimatedMonthlyCost: string }> {
|
|
1714
|
+
return apiFetch<{ snapshot: Snapshot; estimatedMonthlyCost: string }>(
|
|
1715
|
+
`/bots/${instanceId}/snapshots`,
|
|
1716
|
+
{
|
|
1717
|
+
method: "POST",
|
|
1718
|
+
body: JSON.stringify(name ? { name } : {}),
|
|
1719
|
+
},
|
|
1720
|
+
);
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
/** Restore an instance from a snapshot. Uses the /instances/ route. */
|
|
1724
|
+
export async function restoreSnapshot(instanceId: string, snapshotId: string): Promise<void> {
|
|
1725
|
+
await apiFetch<{ ok: boolean; restored: string }>(
|
|
1726
|
+
`/instances/${instanceId}/snapshots/${snapshotId}/restore`,
|
|
1727
|
+
{ method: "POST" },
|
|
1728
|
+
);
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
/** Delete a snapshot. Backend returns 204 no content. */
|
|
1732
|
+
export async function deleteSnapshot(instanceId: string, snapshotId: string): Promise<void> {
|
|
1733
|
+
const res = await apiFetchRaw(`/bots/${instanceId}/snapshots/${snapshotId}`, {
|
|
1734
|
+
method: "DELETE",
|
|
1735
|
+
});
|
|
1736
|
+
if (!res.ok && res.status !== 204) {
|
|
1737
|
+
const body = await res.json().catch(() => ({}));
|
|
1738
|
+
throw new ApiError(res.status, res.statusText, (body as { error?: string }).error ?? undefined);
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
// --- P2P Friends API types ---
|
|
1743
|
+
|
|
1744
|
+
export interface Friend {
|
|
1745
|
+
id: string;
|
|
1746
|
+
name: string;
|
|
1747
|
+
status: "online" | "offline" | "unknown";
|
|
1748
|
+
sharedCapabilities: string[];
|
|
1749
|
+
connectedAt: string;
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
export interface DiscoveredBot {
|
|
1753
|
+
id: string;
|
|
1754
|
+
name: string;
|
|
1755
|
+
capabilities: string[];
|
|
1756
|
+
discoveredAt: string;
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
export interface FriendRequest {
|
|
1760
|
+
id: string;
|
|
1761
|
+
fromId: string;
|
|
1762
|
+
fromName: string;
|
|
1763
|
+
toId: string;
|
|
1764
|
+
toName: string;
|
|
1765
|
+
direction: "inbound" | "outbound";
|
|
1766
|
+
status: "pending";
|
|
1767
|
+
createdAt: string;
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
export interface AutoAcceptConfig {
|
|
1771
|
+
enabled: boolean;
|
|
1772
|
+
rules: {
|
|
1773
|
+
requireCapabilities?: string[];
|
|
1774
|
+
maxFriends?: number;
|
|
1775
|
+
};
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
export interface CapabilityShareUpdate {
|
|
1779
|
+
capabilities: string[];
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
// --- P2P Friends API ---
|
|
1783
|
+
|
|
1784
|
+
export async function listFriends(instanceId: string): Promise<Friend[]> {
|
|
1785
|
+
const data = await apiFetch<{ friends: Friend[] }>(`/instances/${instanceId}/friends`, {
|
|
1786
|
+
method: "GET",
|
|
1787
|
+
});
|
|
1788
|
+
return data.friends;
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
export async function listDiscoveredBots(instanceId: string): Promise<DiscoveredBot[]> {
|
|
1792
|
+
const data = await apiFetch<{ discovered: DiscoveredBot[] }>(
|
|
1793
|
+
`/instances/${instanceId}/friends/discovered`,
|
|
1794
|
+
{ method: "GET" },
|
|
1795
|
+
);
|
|
1796
|
+
return data.discovered;
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
export async function sendFriendRequest(instanceId: string, targetBotId: string): Promise<void> {
|
|
1800
|
+
await apiFetch<{ ok: boolean }>(`/instances/${instanceId}/friends/requests`, {
|
|
1801
|
+
method: "POST",
|
|
1802
|
+
body: JSON.stringify({ targetBotId }),
|
|
1803
|
+
});
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
export async function listFriendRequests(instanceId: string): Promise<FriendRequest[]> {
|
|
1807
|
+
const data = await apiFetch<{ requests: FriendRequest[] }>(
|
|
1808
|
+
`/instances/${instanceId}/friends/requests`,
|
|
1809
|
+
{ method: "GET" },
|
|
1810
|
+
);
|
|
1811
|
+
return data.requests;
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
export async function acceptFriendRequest(instanceId: string, requestId: string): Promise<void> {
|
|
1815
|
+
await apiFetch<{ ok: boolean }>(`/instances/${instanceId}/friends/requests/${requestId}/accept`, {
|
|
1816
|
+
method: "POST",
|
|
1817
|
+
});
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
export async function rejectFriendRequest(instanceId: string, requestId: string): Promise<void> {
|
|
1821
|
+
await apiFetch<{ ok: boolean }>(`/instances/${instanceId}/friends/requests/${requestId}/reject`, {
|
|
1822
|
+
method: "POST",
|
|
1823
|
+
});
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
export async function removeFriend(instanceId: string, friendId: string): Promise<void> {
|
|
1827
|
+
await apiFetch<{ ok: boolean }>(`/instances/${instanceId}/friends/${friendId}`, {
|
|
1828
|
+
method: "DELETE",
|
|
1829
|
+
});
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
export async function updateFriendCapabilities(
|
|
1833
|
+
instanceId: string,
|
|
1834
|
+
friendId: string,
|
|
1835
|
+
capabilities: string[],
|
|
1836
|
+
): Promise<void> {
|
|
1837
|
+
await apiFetch<{ ok: boolean }>(`/instances/${instanceId}/friends/${friendId}/capabilities`, {
|
|
1838
|
+
method: "PATCH",
|
|
1839
|
+
body: JSON.stringify({ capabilities }),
|
|
1840
|
+
});
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
export async function getAutoAcceptConfig(instanceId: string): Promise<AutoAcceptConfig> {
|
|
1844
|
+
return apiFetch<AutoAcceptConfig>(`/instances/${instanceId}/friends/auto-accept`, {
|
|
1845
|
+
method: "GET",
|
|
1846
|
+
});
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
export async function updateAutoAcceptConfig(
|
|
1850
|
+
instanceId: string,
|
|
1851
|
+
config: AutoAcceptConfig,
|
|
1852
|
+
): Promise<void> {
|
|
1853
|
+
await apiFetch<{ ok: boolean }>(`/instances/${instanceId}/friends/auto-accept`, {
|
|
1854
|
+
method: "PUT",
|
|
1855
|
+
body: JSON.stringify(config),
|
|
1856
|
+
});
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
// --- Login History API ---
|
|
1860
|
+
|
|
1861
|
+
export interface LoginAttempt {
|
|
1862
|
+
id: string;
|
|
1863
|
+
timestamp: string;
|
|
1864
|
+
userAgent: string;
|
|
1865
|
+
ip: string;
|
|
1866
|
+
location: string | null;
|
|
1867
|
+
success: boolean;
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
export interface LoginHistoryResponse {
|
|
1871
|
+
attempts: LoginAttempt[];
|
|
1872
|
+
total: number;
|
|
1873
|
+
hasMore: boolean;
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
export async function fetchLoginHistory(params: {
|
|
1877
|
+
limit?: number;
|
|
1878
|
+
offset?: number;
|
|
1879
|
+
}): Promise<LoginHistoryResponse> {
|
|
1880
|
+
const query = new URLSearchParams();
|
|
1881
|
+
if (params.limit != null) query.set("limit", String(params.limit));
|
|
1882
|
+
if (params.offset != null) query.set("offset", String(params.offset));
|
|
1883
|
+
const qs = query.toString();
|
|
1884
|
+
return apiFetch<LoginHistoryResponse>(`/settings/login-history${qs ? `?${qs}` : ""}`);
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
// --- Audit Log API ---
|
|
1888
|
+
|
|
1889
|
+
export interface AuditEvent {
|
|
1890
|
+
id: string;
|
|
1891
|
+
action: string;
|
|
1892
|
+
resourceType: string;
|
|
1893
|
+
resourceId: string;
|
|
1894
|
+
resourceName: string | null;
|
|
1895
|
+
details: string | null;
|
|
1896
|
+
createdAt: string;
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
export interface AuditLogResponse {
|
|
1900
|
+
events: AuditEvent[];
|
|
1901
|
+
total: number;
|
|
1902
|
+
hasMore: boolean;
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
export async function fetchAuditLog(params: {
|
|
1906
|
+
limit?: number;
|
|
1907
|
+
offset?: number;
|
|
1908
|
+
since?: string;
|
|
1909
|
+
until?: string;
|
|
1910
|
+
action?: string;
|
|
1911
|
+
search?: string;
|
|
1912
|
+
}): Promise<AuditLogResponse> {
|
|
1913
|
+
const query = new URLSearchParams();
|
|
1914
|
+
if (params.limit != null) query.set("limit", String(params.limit));
|
|
1915
|
+
if (params.offset != null) query.set("offset", String(params.offset));
|
|
1916
|
+
if (params.since) query.set("since", params.since);
|
|
1917
|
+
if (params.until) query.set("until", params.until);
|
|
1918
|
+
if (params.action) query.set("action", params.action);
|
|
1919
|
+
if (params.search) query.set("search", params.search);
|
|
1920
|
+
const qs = query.toString();
|
|
1921
|
+
return apiFetch<AuditLogResponse>(`/audit${qs ? `?${qs}` : ""}`);
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
// --- Typed helpers used by components ---
|
|
1925
|
+
|
|
1926
|
+
export interface VpsInfo {
|
|
1927
|
+
botId: string;
|
|
1928
|
+
status: "active" | "canceling" | "canceled";
|
|
1929
|
+
hostname: string | null;
|
|
1930
|
+
sshConnectionString: string | null;
|
|
1931
|
+
diskSizeGb: number;
|
|
1932
|
+
createdAt: string;
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
/** Fetch VPS info for a bot. Returns null if not found or on error. */
|
|
1936
|
+
export async function getVpsInfo(botId: string): Promise<VpsInfo | null> {
|
|
1937
|
+
const res = await apiFetchRaw(`/fleet/bots/${botId}/vps-info`);
|
|
1938
|
+
if (!res.ok) return null;
|
|
1939
|
+
return res.json() as Promise<VpsInfo>;
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
export interface VpsUpgradeResult {
|
|
1943
|
+
url?: string;
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
/** Initiate a VPS upgrade for a bot. Returns the raw Response for status inspection (409, 402). */
|
|
1947
|
+
export async function upgradeToVps(
|
|
1948
|
+
botId: string,
|
|
1949
|
+
successUrl: string,
|
|
1950
|
+
cancelUrl: string,
|
|
1951
|
+
): Promise<Response> {
|
|
1952
|
+
return apiFetchRaw(`/fleet/bots/${botId}/upgrade-to-vps`, {
|
|
1953
|
+
method: "POST",
|
|
1954
|
+
body: JSON.stringify({ successUrl, cancelUrl }),
|
|
1955
|
+
});
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
export interface OAuthInitiateResult {
|
|
1959
|
+
authorizeUrl: string;
|
|
1960
|
+
state: string;
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
/** Initiate OAuth flow. Returns the raw Response so callers can inspect errors. */
|
|
1964
|
+
export async function initiateChannelOAuth(provider: string): Promise<Response> {
|
|
1965
|
+
return platformFetchRaw(`/api/channel-oauth/initiate`, {
|
|
1966
|
+
method: "POST",
|
|
1967
|
+
body: JSON.stringify({ provider }),
|
|
1968
|
+
});
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
export interface OAuthPollResult {
|
|
1972
|
+
status: "pending" | "completed" | "expired";
|
|
1973
|
+
token?: string;
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
/** Poll for OAuth token after the popup callback. */
|
|
1977
|
+
export async function pollChannelOAuth(state: string): Promise<OAuthPollResult> {
|
|
1978
|
+
const res = await platformFetchRaw(`/api/channel-oauth/poll?state=${encodeURIComponent(state)}`);
|
|
1979
|
+
if (!res.ok) throw new Error("Failed to retrieve token");
|
|
1980
|
+
return res.json() as Promise<OAuthPollResult>;
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
/** Post a message to the onboarding quick-setup endpoint. Returns the raw Response. */
|
|
1984
|
+
export async function quickSetup(apiKey: string, channel: string): Promise<Response> {
|
|
1985
|
+
return apiFetchRaw(`/onboarding/quick-setup`, {
|
|
1986
|
+
method: "POST",
|
|
1987
|
+
body: JSON.stringify({ apiKey, channel }),
|
|
1988
|
+
});
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
/** Send a chat message. Fire-and-forget; caller handles errors. */
|
|
1992
|
+
export async function sendChatMessage(sessionId: string, message: string): Promise<void> {
|
|
1993
|
+
await apiFetchRaw(`/chat`, {
|
|
1994
|
+
method: "POST",
|
|
1995
|
+
body: JSON.stringify({ sessionId, message }),
|
|
1996
|
+
});
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
/** Open an SSE stream for chat. Returns the raw Response for body reading. */
|
|
2000
|
+
export async function openChatStream(sessionId: string, signal: AbortSignal): Promise<Response> {
|
|
2001
|
+
const tenantId = getActiveTenantId();
|
|
2002
|
+
const res = await fetch(`${API_BASE_URL}/chat/stream`, {
|
|
2003
|
+
headers: {
|
|
2004
|
+
"X-Session-ID": sessionId,
|
|
2005
|
+
...(tenantId ? { "x-tenant-id": tenantId } : {}),
|
|
2006
|
+
},
|
|
2007
|
+
credentials: "include",
|
|
2008
|
+
signal,
|
|
2009
|
+
});
|
|
2010
|
+
return res;
|
|
2011
|
+
}
|