@wopr-network/platform-ui-core 1.27.7 → 1.27.9
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/next.config.ts +1 -2
- package/package.json +17 -17
- package/src/__tests__/account-switcher.test.tsx +21 -20
- package/src/__tests__/activity-page.test.tsx +2 -6
- package/src/__tests__/add-payment-method-dialog.test.tsx +9 -32
- package/src/__tests__/admin-api.test.ts +1 -6
- package/src/__tests__/admin-gpu-api.test.ts +1 -3
- package/src/__tests__/admin-marketplace-api.test.ts +1 -4
- package/src/__tests__/admin-middleware.test.ts +76 -83
- package/src/__tests__/affiliate-dashboard.test.tsx +3 -3
- package/src/__tests__/api-401-redirect.test.ts +46 -9
- package/src/__tests__/api-client.test.ts +3 -5
- package/src/__tests__/api-config.test.ts +22 -42
- package/src/__tests__/api-fleet-resources.test.ts +1 -2
- package/src/__tests__/api-fleet-trpc.test.ts +2 -8
- package/src/__tests__/api-null-guards.test.ts +3 -1
- package/src/__tests__/audit-log-table-pagination.test.tsx +2 -6
- package/src/__tests__/auth-password-reset.test.tsx +7 -21
- package/src/__tests__/auth-redirect.test.tsx +8 -2
- package/src/__tests__/auth.test.tsx +25 -23
- package/src/__tests__/auto-topup-card.test.tsx +4 -12
- package/src/__tests__/backups-tab.test.tsx +3 -4
- package/src/__tests__/billing-layout-nav-hidden.test.tsx +5 -37
- package/src/__tests__/billing-payment-org-invoices.test.tsx +2 -18
- package/src/__tests__/billing.test.tsx +8 -39
- package/src/__tests__/bot-settings/resources-tab.test.tsx +1 -3
- package/src/__tests__/bot-settings/storage-tab.test.tsx +1 -3
- package/src/__tests__/bot-settings/vps-upgrade-card.test.tsx +1 -3
- package/src/__tests__/bot-settings-restart.test.tsx +1 -3
- package/src/__tests__/bot-settings.test.tsx +2 -6
- package/src/__tests__/brand.test.ts +6 -26
- package/src/__tests__/buy-credits-panel.test.tsx +1 -3
- package/src/__tests__/buy-crypto-credits-panel.test.tsx +101 -119
- package/src/__tests__/capability-conflicts.test.ts +2 -8
- package/src/__tests__/capability-resolver.test.tsx +2 -12
- package/src/__tests__/channel-wizard.test.tsx +4 -17
- package/src/__tests__/chat/chat-panel.test.tsx +1 -4
- package/src/__tests__/chat-store.test.ts +5 -15
- package/src/__tests__/command-center.test.tsx +10 -12
- package/src/__tests__/compliance-retention-edit.test.tsx +3 -6
- package/src/__tests__/confirmation-tracker.test.tsx +3 -18
- package/src/__tests__/coupon-input.test.tsx +1 -3
- package/src/__tests__/create-instance.test.tsx +1 -3
- package/src/__tests__/credit-balance.test.tsx +4 -12
- package/src/__tests__/credits.test.tsx +32 -85
- package/src/__tests__/email-verification-banner.test.tsx +2 -6
- package/src/__tests__/error-boundaries.test.tsx +0 -1
- package/src/__tests__/fetch-pricing.test.ts +2 -1
- package/src/__tests__/field-oauth.test.tsx +2 -6
- package/src/__tests__/fixtures/mock-manifests-data.js +1 -3
- package/src/__tests__/fixtures/mock-manifests.ts +2 -4
- package/src/__tests__/fleet-health-timestamp.test.tsx +1 -8
- package/src/__tests__/fleet-health-update.test.tsx +1 -8
- package/src/__tests__/gpu-dashboard.test.tsx +2 -6
- package/src/__tests__/instance-detail.test.tsx +3 -9
- package/src/__tests__/instance-list.test.tsx +1 -5
- package/src/__tests__/layout-snapshots.test.tsx +64 -11
- package/src/__tests__/marketplace-admin.test.tsx +2 -6
- package/src/__tests__/marketplace.test.tsx +11 -35
- package/src/__tests__/merge-api-rates.test.ts +1 -6
- package/src/__tests__/middleware.test.ts +32 -219
- package/src/__tests__/next-config-headers.test.ts +1 -3
- package/src/__tests__/notifications.test.tsx +4 -11
- package/src/__tests__/oauth-buttons.test.tsx +36 -59
- package/src/__tests__/oauth-error-mapping.test.tsx +2 -6
- package/src/__tests__/observability.test.tsx +23 -36
- package/src/__tests__/onboarding-page.test.tsx +4 -6
- package/src/__tests__/org-billing-api.test.tsx +1 -6
- package/src/__tests__/plugin-install-flow.test.tsx +28 -58
- package/src/__tests__/plugin-registry.test.tsx +3 -11
- package/src/__tests__/plugin-tool-sync.test.ts +1 -3
- package/src/__tests__/plugins-catalog-error.test.tsx +2 -6
- package/src/__tests__/plugins-toggle-race.test.tsx +3 -5
- package/src/__tests__/portfolio-chart.test.tsx +2 -6
- package/src/__tests__/promotion-form.test.tsx +2 -6
- package/src/__tests__/promotions-list.test.tsx +1 -3
- package/src/__tests__/provider-key-api.test.ts +2 -1
- package/src/__tests__/resend-verification-button.test.tsx +8 -24
- package/src/__tests__/secrets-audit-pagination.test.tsx +1 -3
- package/src/__tests__/settings.test.tsx +11 -21
- package/src/__tests__/setup-checklist.test.tsx +3 -9
- package/src/__tests__/setup.ts +25 -6
- package/src/__tests__/snapshot-api.test.ts +2 -1
- package/src/__tests__/step-superpowers.test.tsx +1 -3
- package/src/__tests__/tenant-context.test.tsx +1 -6
- package/src/__tests__/tenant-keys-api.test.ts +3 -4
- package/src/__tests__/tenant-table-pagination.test.tsx +2 -6
- package/src/__tests__/terminal-log-cleanup.test.tsx +0 -1
- package/src/__tests__/transaction-history.test.tsx +190 -238
- package/src/__tests__/trpc-types.test.ts +2 -6
- package/src/__tests__/use-chat.test.ts +1 -3
- package/src/__tests__/use-plugin-setup-chat-stale-closure.test.ts +1 -4
- package/src/__tests__/use-sidecar-bridge.test.tsx +105 -0
- package/src/__tests__/use-webmcp.test.ts +1 -3
- package/src/__tests__/validate-elevenlabs-key.test.ts +2 -1
- package/src/__tests__/verify-page.test.tsx +4 -13
- package/src/__tests__/verify-redirect.test.tsx +2 -6
- package/src/app/(auth)/error.tsx +1 -7
- package/src/app/(auth)/forgot-password/page.tsx +4 -18
- package/src/app/(auth)/login/page.tsx +5 -22
- package/src/app/(auth)/reset-password/page.tsx +2 -12
- package/src/app/(auth)/signup/page.tsx +10 -44
- package/src/app/(auth)/verify/page.tsx +47 -0
- package/src/app/(dashboard)/billing/credits/page.tsx +14 -67
- package/src/app/(dashboard)/billing/error.tsx +2 -10
- package/src/app/(dashboard)/billing/layout.tsx +12 -62
- package/src/app/(dashboard)/billing/payment/page.tsx +17 -68
- package/src/app/(dashboard)/billing/plans/page.tsx +3 -9
- package/src/app/(dashboard)/billing/usage/hosted/page.tsx +8 -25
- package/src/app/(dashboard)/billing/usage/page.tsx +63 -103
- package/src/app/(dashboard)/changesets/[id]/changeset-detail-client.tsx +9 -27
- package/src/app/(dashboard)/changesets/[id]/error.tsx +2 -6
- package/src/app/(dashboard)/changesets/error.tsx +1 -7
- package/src/app/(dashboard)/chat/page.tsx +2 -6
- package/src/app/(dashboard)/dashboard/network/page.tsx +5 -19
- package/src/app/(dashboard)/error.tsx +1 -7
- package/src/app/(dashboard)/layout.tsx +15 -36
- package/src/app/(dashboard)/marketplace/[plugin]/page.tsx +14 -51
- package/src/app/(dashboard)/marketplace/error.tsx +1 -7
- package/src/app/(dashboard)/marketplace/page.tsx +6 -27
- package/src/app/(dashboard)/not-found.tsx +2 -5
- package/src/app/(dashboard)/onboarding/page.tsx +5 -22
- package/src/app/(dashboard)/settings/account/page.tsx +1 -6
- package/src/app/(dashboard)/settings/activity/page.tsx +8 -34
- package/src/app/(dashboard)/settings/api-keys/page.tsx +15 -60
- package/src/app/(dashboard)/settings/brain/page.tsx +9 -31
- package/src/app/(dashboard)/settings/error.tsx +2 -10
- package/src/app/(dashboard)/settings/notifications/page.tsx +2 -6
- package/src/app/(dashboard)/settings/org/page.tsx +13 -56
- package/src/app/(dashboard)/settings/page.tsx +1 -0
- package/src/app/(dashboard)/settings/profile/page.tsx +126 -73
- package/src/app/(dashboard)/settings/providers/page.tsx +21 -78
- package/src/app/(dashboard)/settings/secrets/page.tsx +13 -58
- package/src/app/(dashboard)/settings/security/page.tsx +31 -111
- package/src/app/admin/email-templates/email-templates-client.tsx +15 -58
- package/src/app/admin/error.tsx +1 -7
- package/src/app/admin/fleet-updates/error.tsx +1 -7
- package/src/app/admin/fleet-updates/fleet-updates-client.tsx +10 -50
- package/src/app/admin/layout.tsx +4 -0
- package/src/app/admin/payment-methods/page.tsx +9 -38
- package/src/app/admin/products/error.tsx +2 -7
- package/src/app/admin/products/page.tsx +1 -4
- package/src/app/admin/promotions/[id]/page.tsx +9 -38
- package/src/app/admin/promotions/page.tsx +9 -36
- package/src/app/admin/rate-overrides/page.tsx +9 -45
- package/src/app/auth/callback/[provider]/page.tsx +1 -8
- package/src/app/auth/verify/page.tsx +9 -36
- package/src/app/channels/error.tsx +2 -10
- package/src/app/channels/layout.tsx +9 -0
- package/src/app/channels/page.tsx +8 -20
- package/src/app/channels/setup/[plugin]/page.tsx +3 -5
- package/src/app/error.tsx +1 -7
- package/src/app/fleet/error.tsx +1 -7
- package/src/app/fleet/layout.tsx +5 -0
- package/src/app/fleet/settings/page.tsx +1 -3
- package/src/app/global-error.tsx +2 -10
- package/src/app/globals.css +1 -4
- package/src/app/instances/[id]/instance-detail-client.tsx +51 -125
- package/src/app/instances/error.tsx +2 -10
- package/src/app/instances/instance-list-client.tsx +20 -69
- package/src/app/instances/layout.tsx +9 -0
- package/src/app/instances/new/create-instance-client.tsx +10 -31
- package/src/app/layout.tsx +2 -10
- package/src/app/not-found.tsx +1 -3
- package/src/app/page.tsx +1 -2
- package/src/app/plugins/error.tsx +2 -10
- package/src/app/plugins/layout.tsx +5 -0
- package/src/app/plugins/page.tsx +16 -48
- package/src/app/pricing/error.tsx +1 -7
- package/src/app/privacy/page.tsx +93 -150
- package/src/app/status/error.tsx +1 -7
- package/src/app/terms/page.tsx +89 -144
- package/src/components/account-switcher.tsx +25 -52
- package/src/components/admin/accounting-dashboard.tsx +1 -3
- package/src/components/admin/admin-guard.tsx +1 -3
- package/src/components/admin/admin-nav.tsx +1 -3
- package/src/components/admin/affiliate-dashboard.tsx +25 -94
- package/src/components/admin/audit-log-table.tsx +13 -49
- package/src/components/admin/billing-health-dashboard.tsx +7 -25
- package/src/components/admin/bulk-actions-bar.test.tsx +1 -7
- package/src/components/admin/bulk-actions-bar.tsx +1 -3
- package/src/components/admin/bulk-export-dialog.test.tsx +1 -7
- package/src/components/admin/bulk-export-dialog.tsx +6 -32
- package/src/components/admin/bulk-grant-dialog.test.tsx +2 -6
- package/src/components/admin/bulk-grant-dialog.tsx +4 -15
- package/src/components/admin/bulk-preview-dialog.tsx +3 -12
- package/src/components/admin/bulk-reactivate-dialog.tsx +1 -7
- package/src/components/admin/bulk-select-all-banner.tsx +1 -6
- package/src/components/admin/bulk-suspend-dialog.tsx +5 -12
- package/src/components/admin/bulk-undo-toast.tsx +1 -2
- package/src/components/admin/compliance-dashboard.tsx +31 -101
- package/src/components/admin/gpu-dashboard.tsx +21 -70
- package/src/components/admin/grant-credits-dialog.tsx +4 -17
- package/src/components/admin/incident-dashboard.tsx +10 -25
- package/src/components/admin/inference-dashboard.tsx +14 -54
- package/src/components/admin/marketplace-admin.tsx +18 -60
- package/src/components/admin/migrations-dashboard.tsx +9 -42
- package/src/components/admin/onboarding-dashboard.tsx +14 -64
- package/src/components/admin/pool-config-dashboard.tsx +4 -10
- package/src/components/admin/products/fleet-form.tsx +2 -11
- package/src/components/admin/products/nav-editor.tsx +3 -10
- package/src/components/admin/promotions/promotion-form.tsx +9 -42
- package/src/components/admin/roles-dashboard.tsx +7 -34
- package/src/components/admin/suspend-dialog.tsx +4 -11
- package/src/components/admin/tenant-notes-panel.tsx +1 -3
- package/src/components/admin/tenant-row-actions.tsx +4 -20
- package/src/components/admin/tenant-table.tsx +12 -49
- package/src/components/auth/auth-redirect.tsx +11 -3
- package/src/components/auth/email-verification-result-banner.tsx +1 -3
- package/src/components/auth/resend-verification-button.tsx +2 -10
- package/src/components/auth/wopr-wordmark.tsx +1 -3
- package/src/components/billing/add-payment-method-dialog.tsx +1 -2
- package/src/components/billing/affiliate-dashboard.tsx +4 -16
- package/src/components/billing/amount-selector.tsx +1 -3
- package/src/components/billing/auto-topup-card.tsx +2 -11
- package/src/components/billing/buy-credits-panel.tsx +14 -17
- package/src/components/billing/byok-callout.tsx +6 -8
- package/src/components/billing/confirmation-tracker.tsx +4 -14
- package/src/components/billing/credit-balance-badge.tsx +22 -0
- package/src/components/billing/credit-balance.tsx +3 -9
- package/src/components/billing/crypto-checkout.tsx +5 -24
- package/src/components/billing/degraded-state-banner.tsx +1 -3
- package/src/components/billing/deposit-view.tsx +301 -41
- package/src/components/billing/dividend-banner.tsx +1 -3
- package/src/components/billing/dividend-eligibility.tsx +3 -12
- package/src/components/billing/dividend-pool-stats.tsx +6 -20
- package/src/components/billing/first-dividend-dialog.tsx +2 -2
- package/src/components/billing/org-billing-page.tsx +8 -31
- package/src/components/billing/payment-method-picker.tsx +2 -10
- package/src/components/billing/suspension-banner.tsx +2 -7
- package/src/components/billing/transaction-history.tsx +10 -58
- package/src/components/billing/unified-checkout.tsx +547 -0
- package/src/components/bot-settings/backups-tab.tsx +9 -33
- package/src/components/bot-settings/bot-settings-client.tsx +32 -134
- package/src/components/bot-settings/resources-tab.tsx +2 -9
- package/src/components/bot-settings/storage-tab.tsx +19 -48
- package/src/components/bot-settings/vps-info-panel.tsx +3 -11
- package/src/components/bot-settings/vps-upgrade-card.tsx +3 -4
- package/src/components/brand-hydrator.tsx +13 -0
- package/src/components/channel-wizard/field-interactive.tsx +1 -3
- package/src/components/channel-wizard/field-qr.tsx +10 -39
- package/src/components/channel-wizard/step-renderer.tsx +5 -28
- package/src/components/channel-wizard/wizard.tsx +6 -31
- package/src/components/chat/chat-message.tsx +1 -4
- package/src/components/chat/chat-panel.tsx +4 -18
- package/src/components/chat/chat-widget.tsx +3 -14
- package/src/components/dashboard/command-center.tsx +15 -61
- package/src/components/fleet/update-settings-card.tsx +7 -23
- package/src/components/instance-update-banner.tsx +130 -0
- package/src/components/instances/friends-tab.test.tsx +2 -9
- package/src/components/instances/friends-tab.tsx +18 -74
- package/src/components/instances/update-available-badge.tsx +2 -11
- package/src/components/landing/hero.tsx +3 -9
- package/src/components/landing/landing-page.tsx +1 -3
- package/src/components/landing/portfolio-chart.tsx +4 -9
- package/src/components/landing/story-sections.tsx +1 -3
- package/src/components/landing/terminal-sequence.tsx +4 -17
- package/src/components/marketplace/empty-state.tsx +2 -6
- package/src/components/marketplace/first-visit-hero.tsx +1 -3
- package/src/components/marketplace/install-wizard.tsx +20 -77
- package/src/components/marketplace/marketplace-tabs.tsx +1 -4
- package/src/components/marketplace/plugin-card.tsx +2 -9
- package/src/components/marketplace/superpower-content.tsx +1 -3
- package/src/components/marketplace/terminal-search.tsx +2 -8
- package/src/components/oauth-buttons.tsx +29 -14
- package/src/components/observability/fleet-health.tsx +5 -18
- package/src/components/observability/health-overview.tsx +7 -20
- package/src/components/observability/logs-viewer.tsx +8 -32
- package/src/components/observability/metrics-dashboard.tsx +2 -15
- package/src/components/onboarding/fallback-setup.tsx +6 -25
- package/src/components/onboarding/setup-checklist.tsx +18 -51
- package/src/components/onboarding/step-superpowers.tsx +1 -4
- package/src/components/plugin-setup/setup-chat-panel.tsx +6 -22
- package/src/components/pricing/dividend-calculator.tsx +6 -12
- package/src/components/pricing/dividend-stats.tsx +5 -17
- package/src/components/pricing/pricing-page.tsx +17 -36
- package/src/components/settings/create-org-wizard.tsx +2 -5
- package/src/components/sidebar.tsx +7 -42
- package/src/components/sidecar-frame.tsx +78 -0
- package/src/components/status/status-page.tsx +6 -28
- package/src/components/ui/alert-dialog.tsx +8 -25
- package/src/components/ui/badge.tsx +2 -8
- package/src/components/ui/banner.tsx +1 -6
- package/src/components/ui/card.tsx +5 -24
- package/src/components/ui/checkbox.tsx +1 -5
- package/src/components/ui/collapsible.tsx +3 -8
- package/src/components/ui/dialog.tsx +4 -10
- package/src/components/ui/dropdown-menu.tsx +9 -18
- package/src/components/ui/form.tsx +2 -16
- package/src/components/ui/popover.tsx +3 -23
- package/src/components/ui/progress.tsx +1 -5
- package/src/components/ui/radio-group.tsx +3 -15
- package/src/components/ui/select.tsx +4 -17
- package/src/components/ui/sheet.tsx +5 -19
- package/src/components/ui/skeleton.tsx +1 -7
- package/src/components/ui/table.tsx +5 -22
- package/src/components/ui/tabs.tsx +3 -13
- package/src/components/ui/tooltip.tsx +1 -1
- package/src/components/unified-sidebar.tsx +493 -0
- package/src/hooks/__tests__/use-fleet-sse.test.ts +1 -4
- package/src/hooks/__tests__/use-save-queue.test.ts +2 -8
- package/src/hooks/use-credit-balance.ts +27 -0
- package/src/hooks/use-my-org-role.ts +1 -3
- package/src/hooks/use-plugin-registry.ts +8 -14
- package/src/hooks/use-plugin-setup-chat.ts +2 -5
- package/src/hooks/use-sidecar-bridge.tsx +148 -0
- package/src/hooks/use-webmcp.ts +1 -4
- package/src/lib/__tests__/admin-api.test.ts +1 -3
- package/src/lib/__tests__/api-bot-crud.test.ts +8 -18
- package/src/lib/__tests__/api-fetch.test.ts +4 -16
- package/src/lib/__tests__/org-billing-api.test.ts +1 -3
- package/src/lib/__tests__/pricing-data.test.ts +0 -8
- package/src/lib/__tests__/settings-api.test.ts +1 -3
- package/src/lib/admin-affiliate-api.ts +2 -7
- package/src/lib/admin-api.ts +6 -26
- package/src/lib/admin-incident-api.ts +11 -19
- package/src/lib/admin-marketplace-api.ts +1 -5
- package/src/lib/api-config.test.ts +5 -50
- package/src/lib/api.ts +143 -122
- package/src/lib/auth-client.ts +1 -2
- package/src/lib/bot-settings-data.ts +11 -36
- package/src/lib/brand-config.ts +56 -115
- package/src/lib/brand.ts +2 -15
- package/src/lib/chat/use-chat.ts +2 -7
- package/src/lib/cost-comparison-data.test.ts +1 -3
- package/src/lib/cost-comparison-data.ts +1 -4
- package/src/lib/errors.ts +1 -4
- package/src/lib/fetch-utils.test.ts +26 -9
- package/src/lib/fetch-utils.ts +40 -11
- package/src/lib/logger.ts +2 -0
- package/src/lib/marketplace-data.ts +3 -11
- package/src/lib/oauth-errors.ts +2 -4
- package/src/lib/onboarding-data.ts +3 -11
- package/src/lib/org-api.ts +2 -10
- package/src/lib/org-billing-api.ts +5 -19
- package/src/lib/plugin/tool-definitions.ts +1 -2
- package/src/lib/require-auth.ts +57 -0
- package/src/lib/settings-api.ts +1 -4
- package/src/lib/sidecar-routes.ts +43 -0
- package/src/lib/trpc-server.ts +49 -0
- package/src/lib/trpc-types.ts +4 -6
- package/src/lib/trpc.tsx +12 -4
- package/src/lib/validate-redirect-url.ts +1 -4
- package/src/lib/webmcp/marketplace-onboarding-tools.ts +6 -16
- package/src/lib/webmcp/register.ts +1 -4
- package/src/lib/webmcp/tools.ts +2 -9
- package/src/proxy.ts +35 -212
- package/src/types/missing-deps.d.ts +2 -8
- package/tsconfig.json +1 -8
- package/biome.json +0 -52
- package/src/__tests__/__snapshots__/layout-snapshots.test.tsx.snap +0 -741
- package/src/__tests__/billing-byok-callout.test.tsx +0 -76
- package/src/lib/__tests__/__snapshots__/pricing-data.test.ts.snap +0 -112
package/next.config.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { NextConfig } from "next";
|
|
2
2
|
|
|
3
|
-
const isSecureOrigin =
|
|
4
|
-
(process.env.NEXT_PUBLIC_API_URL ?? "").startsWith("https://");
|
|
3
|
+
const isSecureOrigin = (process.env.NEXT_PUBLIC_API_URL ?? "").startsWith("https://");
|
|
5
4
|
|
|
6
5
|
const nextConfig: NextConfig = {
|
|
7
6
|
output: "standalone",
|
package/package.json
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wopr-network/platform-ui-core",
|
|
3
|
-
"version": "1.27.
|
|
3
|
+
"version": "1.27.9",
|
|
4
4
|
"description": "Brand-agnostic AI agent platform UI — deploy as any brand via env vars",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "https://github.com/wopr-network/platform-ui-core.git"
|
|
8
8
|
},
|
|
9
9
|
"license": "UNLICENSED",
|
|
10
|
-
"packageManager": "pnpm@10.31.0",
|
|
11
10
|
"exports": {
|
|
12
11
|
".": "./src/lib/index.ts",
|
|
13
12
|
"./app/*": "./src/app/*",
|
|
@@ -32,22 +31,13 @@
|
|
|
32
31
|
"vitest.config.ts",
|
|
33
32
|
".env.wopr"
|
|
34
33
|
],
|
|
35
|
-
"scripts": {
|
|
36
|
-
"dev": "next dev",
|
|
37
|
-
"build": "next build",
|
|
38
|
-
"start": "next start",
|
|
39
|
-
"lint": "biome check src/",
|
|
40
|
-
"format": "biome format --write src/",
|
|
41
|
-
"check": "biome check src/ && tsc --noEmit",
|
|
42
|
-
"test": "vitest run",
|
|
43
|
-
"test:e2e": "playwright test"
|
|
44
|
-
},
|
|
45
34
|
"dependencies": {
|
|
46
35
|
"@hookform/resolvers": "^5.2.2",
|
|
47
36
|
"@noble/hashes": "^2.0.1",
|
|
37
|
+
"@radix-ui/react-portal": "^1.1.10",
|
|
48
38
|
"@scure/bip32": "^2.0.1",
|
|
49
39
|
"@scure/bip39": "^2.0.1",
|
|
50
|
-
"@stripe/react-stripe-js": "^
|
|
40
|
+
"@stripe/react-stripe-js": "^6.0.0",
|
|
51
41
|
"@stripe/stripe-js": "^8.7.0",
|
|
52
42
|
"@tanstack/react-query": "^5.90.21",
|
|
53
43
|
"@trpc/client": "^11.10.0",
|
|
@@ -56,7 +46,7 @@
|
|
|
56
46
|
"class-variance-authority": "^0.7.1",
|
|
57
47
|
"clsx": "^2.1.1",
|
|
58
48
|
"framer-motion": "^12.34.0",
|
|
59
|
-
"lucide-react": "^
|
|
49
|
+
"lucide-react": "^1.7.0",
|
|
60
50
|
"next": "16.1.6",
|
|
61
51
|
"next-themes": "^0.4.6",
|
|
62
52
|
"qrcode.react": "^4.2.0",
|
|
@@ -84,9 +74,9 @@
|
|
|
84
74
|
"@types/node": "^20",
|
|
85
75
|
"@types/react": "^19",
|
|
86
76
|
"@types/react-dom": "^19",
|
|
87
|
-
"@vitejs/plugin-react": "^
|
|
77
|
+
"@vitejs/plugin-react": "^6.0.0",
|
|
88
78
|
"@vitest/coverage-v8": "^4.0.18",
|
|
89
|
-
"jsdom": "^
|
|
79
|
+
"jsdom": "^29.0.1",
|
|
90
80
|
"shadcn": "^3.8.4",
|
|
91
81
|
"tailwindcss": "^4",
|
|
92
82
|
"tw-animate-css": "^1.4.0",
|
|
@@ -95,5 +85,15 @@
|
|
|
95
85
|
},
|
|
96
86
|
"release": {
|
|
97
87
|
"extends": "@wopr-network/semantic-release-config"
|
|
88
|
+
},
|
|
89
|
+
"scripts": {
|
|
90
|
+
"dev": "next dev",
|
|
91
|
+
"build": "next build",
|
|
92
|
+
"start": "next start",
|
|
93
|
+
"lint": "biome check src/",
|
|
94
|
+
"format": "biome format --write src/",
|
|
95
|
+
"check": "biome check src/ && tsc --noEmit",
|
|
96
|
+
"test": "vitest run",
|
|
97
|
+
"test:e2e": "playwright test"
|
|
98
98
|
}
|
|
99
|
-
}
|
|
99
|
+
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { render, screen } from "@testing-library/react";
|
|
2
|
-
import userEvent from "@testing-library/user-event";
|
|
3
2
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
3
|
|
|
5
4
|
const mockSwitchTenant = vi.fn();
|
|
@@ -39,36 +38,38 @@ describe("AccountSwitcher", () => {
|
|
|
39
38
|
expect(screen.getByText("Alice")).toBeInTheDocument();
|
|
40
39
|
});
|
|
41
40
|
|
|
42
|
-
it("
|
|
41
|
+
it("renders a fallback icon for tenants without images", () => {
|
|
43
42
|
render(<AccountSwitcher />);
|
|
44
|
-
|
|
43
|
+
// The component renders a span with bg-sidebar-accent class as the fallback icon
|
|
44
|
+
const icon = document.querySelector(".bg-sidebar-accent");
|
|
45
|
+
expect(icon).not.toBeNull();
|
|
45
46
|
});
|
|
46
47
|
|
|
47
|
-
it("
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
48
|
+
it("renders nothing when no tenants exist", () => {
|
|
49
|
+
mockUseTenant.mockReturnValue({
|
|
50
|
+
activeTenantId: "",
|
|
51
|
+
tenants: [],
|
|
52
|
+
isLoading: false,
|
|
53
|
+
switchTenant: mockSwitchTenant,
|
|
54
|
+
});
|
|
53
55
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
render(<AccountSwitcher />);
|
|
57
|
-
await user.click(screen.getByRole("button"));
|
|
58
|
-
await user.click(screen.getByText("My Team"));
|
|
59
|
-
expect(mockSwitchTenant).toHaveBeenCalledWith("org-1");
|
|
56
|
+
const { container } = render(<AccountSwitcher />);
|
|
57
|
+
expect(container.firstChild).toBeNull();
|
|
60
58
|
});
|
|
61
59
|
|
|
62
|
-
it("
|
|
60
|
+
it("falls back to first tenant when activeTenantId does not match", () => {
|
|
63
61
|
mockUseTenant.mockReturnValue({
|
|
64
|
-
activeTenantId: "
|
|
65
|
-
tenants: [
|
|
62
|
+
activeTenantId: "nonexistent",
|
|
63
|
+
tenants: [
|
|
64
|
+
{ id: "user-1", name: "Alice", type: "personal" as const, image: null },
|
|
65
|
+
{ id: "org-1", name: "My Team", type: "org" as const, image: null },
|
|
66
|
+
],
|
|
66
67
|
isLoading: false,
|
|
67
68
|
switchTenant: mockSwitchTenant,
|
|
68
69
|
});
|
|
69
70
|
|
|
70
|
-
|
|
71
|
-
expect(
|
|
71
|
+
render(<AccountSwitcher />);
|
|
72
|
+
expect(screen.getByText("Alice")).toBeInTheDocument();
|
|
72
73
|
});
|
|
73
74
|
|
|
74
75
|
it("renders nothing while loading", () => {
|
|
@@ -120,9 +120,7 @@ describe("ActivityPage", () => {
|
|
|
120
120
|
// Wait for debounce (300ms) + API call
|
|
121
121
|
await vi.waitFor(
|
|
122
122
|
() => {
|
|
123
|
-
expect(mockFetchAuditLog).toHaveBeenLastCalledWith(
|
|
124
|
-
expect.objectContaining({ search: "billing", offset: 0 }),
|
|
125
|
-
);
|
|
123
|
+
expect(mockFetchAuditLog).toHaveBeenLastCalledWith(expect.objectContaining({ search: "billing", offset: 0 }));
|
|
126
124
|
},
|
|
127
125
|
{ timeout: 1000 },
|
|
128
126
|
);
|
|
@@ -160,9 +158,7 @@ describe("ActivityPage", () => {
|
|
|
160
158
|
});
|
|
161
159
|
await user.click(screen.getByRole("button", { name: "Next" }));
|
|
162
160
|
|
|
163
|
-
expect(mockFetchAuditLog).toHaveBeenLastCalledWith(
|
|
164
|
-
expect.objectContaining({ offset: 50, limit: 50 }),
|
|
165
|
-
);
|
|
161
|
+
expect(mockFetchAuditLog).toHaveBeenLastCalledWith(expect.objectContaining({ offset: 50, limit: 50 }));
|
|
166
162
|
});
|
|
167
163
|
|
|
168
164
|
it("calls fetchAuditLog with correct params", async () => {
|
|
@@ -8,9 +8,7 @@ const mockUseStripe = vi.fn(() => ({ confirmSetup: mockConfirmSetup }));
|
|
|
8
8
|
const mockUseElements = vi.fn(() => ({}));
|
|
9
9
|
|
|
10
10
|
vi.mock("@stripe/react-stripe-js", () => ({
|
|
11
|
-
Elements: ({ children }: { children: React.ReactNode }) =>
|
|
12
|
-
<div data-testid="stripe-elements">{children}</div>
|
|
13
|
-
),
|
|
11
|
+
Elements: ({ children }: { children: React.ReactNode }) => <div data-testid="stripe-elements">{children}</div>,
|
|
14
12
|
PaymentElement: () => <div data-testid="payment-element" />,
|
|
15
13
|
useStripe: () => mockUseStripe(),
|
|
16
14
|
useElements: () => mockUseElements(),
|
|
@@ -44,9 +42,7 @@ describe("AddPaymentMethodDialog", () => {
|
|
|
44
42
|
});
|
|
45
43
|
|
|
46
44
|
it("fetches setup intent and renders PaymentElement when opened", async () => {
|
|
47
|
-
render(
|
|
48
|
-
<AddPaymentMethodDialog open={true} onOpenChange={onOpenChange} onSuccess={onSuccess} />,
|
|
49
|
-
);
|
|
45
|
+
render(<AddPaymentMethodDialog open={true} onOpenChange={onOpenChange} onSuccess={onSuccess} />);
|
|
50
46
|
|
|
51
47
|
await waitFor(() => {
|
|
52
48
|
expect(createSetupIntent).toHaveBeenCalledOnce();
|
|
@@ -56,13 +52,9 @@ describe("AddPaymentMethodDialog", () => {
|
|
|
56
52
|
});
|
|
57
53
|
|
|
58
54
|
it("shows error and retry button when setup intent fails", async () => {
|
|
59
|
-
(createSetupIntent as ReturnType<typeof vi.fn>).mockRejectedValueOnce(
|
|
60
|
-
new Error("Network error"),
|
|
61
|
-
);
|
|
55
|
+
(createSetupIntent as ReturnType<typeof vi.fn>).mockRejectedValueOnce(new Error("Network error"));
|
|
62
56
|
|
|
63
|
-
render(
|
|
64
|
-
<AddPaymentMethodDialog open={true} onOpenChange={onOpenChange} onSuccess={onSuccess} />,
|
|
65
|
-
);
|
|
57
|
+
render(<AddPaymentMethodDialog open={true} onOpenChange={onOpenChange} onSuccess={onSuccess} />);
|
|
66
58
|
|
|
67
59
|
await waitFor(() => {
|
|
68
60
|
expect(screen.getByText(/failed to initialize/i)).toBeInTheDocument();
|
|
@@ -74,9 +66,7 @@ describe("AddPaymentMethodDialog", () => {
|
|
|
74
66
|
it("calls confirmSetup on form submit and triggers onSuccess", async () => {
|
|
75
67
|
mockConfirmSetup.mockResolvedValueOnce({ setupIntent: { status: "succeeded" } });
|
|
76
68
|
|
|
77
|
-
render(
|
|
78
|
-
<AddPaymentMethodDialog open={true} onOpenChange={onOpenChange} onSuccess={onSuccess} />,
|
|
79
|
-
);
|
|
69
|
+
render(<AddPaymentMethodDialog open={true} onOpenChange={onOpenChange} onSuccess={onSuccess} />);
|
|
80
70
|
|
|
81
71
|
await waitFor(() => {
|
|
82
72
|
expect(screen.getByTestId("payment-element")).toBeInTheDocument();
|
|
@@ -96,9 +86,7 @@ describe("AddPaymentMethodDialog", () => {
|
|
|
96
86
|
error: { message: "Your card was declined." },
|
|
97
87
|
});
|
|
98
88
|
|
|
99
|
-
render(
|
|
100
|
-
<AddPaymentMethodDialog open={true} onOpenChange={onOpenChange} onSuccess={onSuccess} />,
|
|
101
|
-
);
|
|
89
|
+
render(<AddPaymentMethodDialog open={true} onOpenChange={onOpenChange} onSuccess={onSuccess} />);
|
|
102
90
|
|
|
103
91
|
await waitFor(() => {
|
|
104
92
|
expect(screen.getByTestId("payment-element")).toBeInTheDocument();
|
|
@@ -115,17 +103,13 @@ describe("AddPaymentMethodDialog", () => {
|
|
|
115
103
|
});
|
|
116
104
|
|
|
117
105
|
it("does not fetch setup intent when closed", () => {
|
|
118
|
-
render(
|
|
119
|
-
<AddPaymentMethodDialog open={false} onOpenChange={onOpenChange} onSuccess={onSuccess} />,
|
|
120
|
-
);
|
|
106
|
+
render(<AddPaymentMethodDialog open={false} onOpenChange={onOpenChange} onSuccess={onSuccess} />);
|
|
121
107
|
|
|
122
108
|
expect(createSetupIntent).not.toHaveBeenCalled();
|
|
123
109
|
});
|
|
124
110
|
|
|
125
111
|
it("closes dialog when cancel is clicked", async () => {
|
|
126
|
-
render(
|
|
127
|
-
<AddPaymentMethodDialog open={true} onOpenChange={onOpenChange} onSuccess={onSuccess} />,
|
|
128
|
-
);
|
|
112
|
+
render(<AddPaymentMethodDialog open={true} onOpenChange={onOpenChange} onSuccess={onSuccess} />);
|
|
129
113
|
|
|
130
114
|
await waitFor(() => {
|
|
131
115
|
expect(screen.getByTestId("payment-element")).toBeInTheDocument();
|
|
@@ -142,14 +126,7 @@ describe("AddPaymentMethodDialog", () => {
|
|
|
142
126
|
clientSecret: "seti_org_test_secret_456",
|
|
143
127
|
});
|
|
144
128
|
|
|
145
|
-
render(
|
|
146
|
-
<AddPaymentMethodDialog
|
|
147
|
-
open={true}
|
|
148
|
-
onOpenChange={onOpenChange}
|
|
149
|
-
onSuccess={onSuccess}
|
|
150
|
-
orgId="org-123"
|
|
151
|
-
/>,
|
|
152
|
-
);
|
|
129
|
+
render(<AddPaymentMethodDialog open={true} onOpenChange={onOpenChange} onSuccess={onSuccess} orgId="org-123" />);
|
|
153
130
|
|
|
154
131
|
await waitFor(() => {
|
|
155
132
|
expect(createOrgSetupIntent).toHaveBeenCalledWith("org-123");
|
|
@@ -48,12 +48,7 @@ import {
|
|
|
48
48
|
getAffiliateVelocity,
|
|
49
49
|
} from "@/lib/admin-affiliate-api";
|
|
50
50
|
|
|
51
|
-
import {
|
|
52
|
-
getCacheStats,
|
|
53
|
-
getDailyCost,
|
|
54
|
-
getPageCost,
|
|
55
|
-
getSessionCost,
|
|
56
|
-
} from "@/lib/admin-inference-api";
|
|
51
|
+
import { getCacheStats, getDailyCost, getPageCost, getSessionCost } from "@/lib/admin-inference-api";
|
|
57
52
|
|
|
58
53
|
beforeEach(() => {
|
|
59
54
|
vi.clearAllMocks();
|
|
@@ -112,9 +112,7 @@ describe("provisionGpuNode", () => {
|
|
|
112
112
|
|
|
113
113
|
it("throws when provisioning fails", async () => {
|
|
114
114
|
mockApiFetch.mockRejectedValue(new Error("Quota exceeded"));
|
|
115
|
-
await expect(
|
|
116
|
-
provisionGpuNode({ name: "x", region: "nyc3", size: "gpu-h100x1" }),
|
|
117
|
-
).rejects.toThrow("Quota exceeded");
|
|
115
|
+
await expect(provisionGpuNode({ name: "x", region: "nyc3", size: "gpu-h100x1" })).rejects.toThrow("Quota exceeded");
|
|
118
116
|
});
|
|
119
117
|
});
|
|
120
118
|
|
|
@@ -74,10 +74,7 @@ describe("getAllPlugins", () => {
|
|
|
74
74
|
|
|
75
75
|
describe("getDiscoveryQueue", () => {
|
|
76
76
|
it("returns only unreviewed plugins from tRPC", async () => {
|
|
77
|
-
const plugins = [
|
|
78
|
-
fakePlugin({ id: "a", reviewed: true }),
|
|
79
|
-
fakePlugin({ id: "b", reviewed: false }),
|
|
80
|
-
];
|
|
77
|
+
const plugins = [fakePlugin({ id: "a", reviewed: true }), fakePlugin({ id: "b", reviewed: false })];
|
|
81
78
|
mockListPlugins.query.mockResolvedValue(plugins);
|
|
82
79
|
|
|
83
80
|
const result = await getDiscoveryQueue();
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
2
|
|
|
3
3
|
// We test the middleware function directly by importing from the module.
|
|
4
|
-
|
|
5
|
-
import middleware from "../proxy";
|
|
4
|
+
import middleware, { validateCsrfOrigin } from "../proxy";
|
|
6
5
|
|
|
7
6
|
// Minimal NextRequest-compatible mock
|
|
8
7
|
function mockRequest(opts: {
|
|
@@ -16,9 +15,7 @@ function mockRequest(opts: {
|
|
|
16
15
|
if (!headers.has("host")) {
|
|
17
16
|
headers.set("host", url.host);
|
|
18
17
|
}
|
|
19
|
-
const cookieMap = new Map(
|
|
20
|
-
Object.entries(opts.cookies ?? {}).map(([k, v]) => [k, { name: k, value: v }]),
|
|
21
|
-
);
|
|
18
|
+
const cookieMap = new Map(Object.entries(opts.cookies ?? {}).map(([k, v]) => [k, { name: k, value: v }]));
|
|
22
19
|
return {
|
|
23
20
|
method: opts.method ?? "GET",
|
|
24
21
|
url: opts.url,
|
|
@@ -32,126 +29,122 @@ function mockRequest(opts: {
|
|
|
32
29
|
} as unknown as Parameters<typeof middleware>[0];
|
|
33
30
|
}
|
|
34
31
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
beforeEach(() => {
|
|
39
|
-
globalThis.fetch = originalFetch;
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
describe("Admin route middleware authorization", () => {
|
|
43
|
-
it("redirects non-admin users from /admin/tenants to /marketplace", async () => {
|
|
44
|
-
// Mock Better Auth get-session returning a regular user
|
|
45
|
-
globalThis.fetch = vi.fn().mockResolvedValue({
|
|
46
|
-
ok: true,
|
|
47
|
-
json: async () => ({
|
|
48
|
-
session: { id: "s1" },
|
|
49
|
-
user: { id: "u1", role: "user" },
|
|
50
|
-
}),
|
|
51
|
-
});
|
|
52
|
-
|
|
32
|
+
describe("Middleware — CSP, CSRF, nonce, tenant forwarding", () => {
|
|
33
|
+
it("sets Content-Security-Policy header on responses", async () => {
|
|
53
34
|
const req = mockRequest({
|
|
54
|
-
url: "https://localhost:3000/
|
|
55
|
-
cookies: { "better-auth.session_token": "valid-token" },
|
|
35
|
+
url: "https://localhost:3000/marketplace",
|
|
56
36
|
});
|
|
57
37
|
|
|
58
38
|
const res = await middleware(req);
|
|
59
|
-
|
|
60
|
-
expect(res.
|
|
61
|
-
expect(res.headers.get("location")).toContain("/marketplace");
|
|
39
|
+
expect(res.headers.get("content-security-policy")).not.toBeNull();
|
|
40
|
+
expect(res.headers.get("content-security-policy")).toContain("default-src 'self'");
|
|
62
41
|
});
|
|
63
42
|
|
|
64
|
-
it("
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
json: async () => ({
|
|
68
|
-
session: { id: "s1" },
|
|
69
|
-
user: { id: "u1", role: "platform_admin" },
|
|
70
|
-
}),
|
|
43
|
+
it("sets Vary header on responses", async () => {
|
|
44
|
+
const req = mockRequest({
|
|
45
|
+
url: "https://localhost:3000/marketplace",
|
|
71
46
|
});
|
|
72
47
|
|
|
48
|
+
const res = await middleware(req);
|
|
49
|
+
expect(res.headers.get("vary")).toBe("*");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("passes through GET requests to any route without auth check", async () => {
|
|
73
53
|
const req = mockRequest({
|
|
74
54
|
url: "https://localhost:3000/admin/tenants",
|
|
75
|
-
cookies: { "better-auth.session_token": "valid-token" },
|
|
76
55
|
});
|
|
77
56
|
|
|
78
57
|
const res = await middleware(req);
|
|
79
|
-
//
|
|
58
|
+
// Middleware no longer checks auth — just sets CSP headers and passes through
|
|
80
59
|
expect(res.status).not.toBe(307);
|
|
60
|
+
expect(res.headers.get("content-security-policy")).not.toBeNull();
|
|
81
61
|
});
|
|
82
62
|
|
|
83
|
-
it("
|
|
63
|
+
it("rejects CSRF-invalid POST to /api routes with 403", async () => {
|
|
84
64
|
const req = mockRequest({
|
|
85
|
-
|
|
86
|
-
|
|
65
|
+
method: "POST",
|
|
66
|
+
url: "https://localhost:3000/api/some-endpoint",
|
|
67
|
+
headers: {
|
|
68
|
+
host: "localhost:3000",
|
|
69
|
+
// No origin or referer — CSRF fails
|
|
70
|
+
},
|
|
87
71
|
});
|
|
88
72
|
|
|
89
73
|
const res = await middleware(req);
|
|
90
|
-
|
|
91
|
-
expect(res.status).toBe(307);
|
|
92
|
-
expect(res.headers.get("location")).toContain("/login");
|
|
74
|
+
expect(res.status).toBe(403);
|
|
93
75
|
});
|
|
94
76
|
|
|
95
|
-
it("
|
|
96
|
-
// Simulate network error or 500 from Better Auth
|
|
97
|
-
globalThis.fetch = vi.fn().mockResolvedValue({
|
|
98
|
-
ok: false,
|
|
99
|
-
status: 500,
|
|
100
|
-
json: async () => ({}),
|
|
101
|
-
});
|
|
102
|
-
|
|
77
|
+
it("allows CSRF-valid POST to /api routes", async () => {
|
|
103
78
|
const req = mockRequest({
|
|
104
|
-
|
|
105
|
-
|
|
79
|
+
method: "POST",
|
|
80
|
+
url: "https://localhost:3000/api/some-endpoint",
|
|
81
|
+
headers: {
|
|
82
|
+
host: "localhost:3000",
|
|
83
|
+
origin: "https://localhost:3000",
|
|
84
|
+
},
|
|
106
85
|
});
|
|
107
86
|
|
|
108
87
|
const res = await middleware(req);
|
|
109
|
-
|
|
110
|
-
expect(res.status).toBe(307);
|
|
111
|
-
expect(res.headers.get("location")).toContain("/marketplace");
|
|
88
|
+
expect(res.status).not.toBe(403);
|
|
112
89
|
});
|
|
113
90
|
|
|
114
|
-
it("
|
|
115
|
-
globalThis.fetch = vi.fn().mockResolvedValue({
|
|
116
|
-
ok: true,
|
|
117
|
-
json: async () => ({
|
|
118
|
-
session: { id: "s1" },
|
|
119
|
-
user: { id: "u1", role: "platform_admin" },
|
|
120
|
-
}),
|
|
121
|
-
});
|
|
122
|
-
|
|
91
|
+
it("exempts /api/auth/callback POST from CSRF checks", async () => {
|
|
123
92
|
const req = mockRequest({
|
|
124
|
-
|
|
125
|
-
|
|
93
|
+
method: "POST",
|
|
94
|
+
url: "https://localhost:3000/api/auth/callback",
|
|
95
|
+
headers: {
|
|
96
|
+
host: "localhost:3000",
|
|
97
|
+
// No origin — would normally fail CSRF
|
|
98
|
+
},
|
|
126
99
|
});
|
|
127
100
|
|
|
128
101
|
const res = await middleware(req);
|
|
129
|
-
expect(res.
|
|
130
|
-
expect(res.headers.get("pragma")).toBe("no-cache");
|
|
102
|
+
expect(res.status).not.toBe(403);
|
|
131
103
|
});
|
|
132
104
|
|
|
133
|
-
it("does not
|
|
105
|
+
it("does not perform CSRF checks on GET requests to /api", async () => {
|
|
134
106
|
const req = mockRequest({
|
|
135
|
-
|
|
136
|
-
|
|
107
|
+
method: "GET",
|
|
108
|
+
url: "https://localhost:3000/api/some-endpoint",
|
|
137
109
|
});
|
|
138
110
|
|
|
139
111
|
const res = await middleware(req);
|
|
140
|
-
expect(res.
|
|
141
|
-
expect(res.headers.get("pragma")).toBeNull();
|
|
112
|
+
expect(res.status).not.toBe(403);
|
|
142
113
|
});
|
|
114
|
+
});
|
|
143
115
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
116
|
+
describe("validateCsrfOrigin", () => {
|
|
117
|
+
it("returns true when origin matches host", () => {
|
|
118
|
+
const req = mockRequest({
|
|
119
|
+
url: "https://localhost:3000/api/test",
|
|
120
|
+
headers: {
|
|
121
|
+
host: "localhost:3000",
|
|
122
|
+
origin: "https://localhost:3000",
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
expect(validateCsrfOrigin(req)).toBe(true);
|
|
126
|
+
});
|
|
147
127
|
|
|
128
|
+
it("returns false when origin does not match host", () => {
|
|
148
129
|
const req = mockRequest({
|
|
149
|
-
url: "https://localhost:3000/
|
|
150
|
-
|
|
130
|
+
url: "https://localhost:3000/api/test",
|
|
131
|
+
headers: {
|
|
132
|
+
host: "localhost:3000",
|
|
133
|
+
origin: "https://evil.com",
|
|
134
|
+
},
|
|
151
135
|
});
|
|
136
|
+
expect(validateCsrfOrigin(req)).toBe(false);
|
|
137
|
+
});
|
|
152
138
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
139
|
+
it("returns false when no host header", () => {
|
|
140
|
+
const req = mockRequest({
|
|
141
|
+
url: "https://localhost:3000/api/test",
|
|
142
|
+
headers: {
|
|
143
|
+
origin: "https://localhost:3000",
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
// Override host to empty
|
|
147
|
+
req.headers.delete("host");
|
|
148
|
+
expect(validateCsrfOrigin(req)).toBe(false);
|
|
156
149
|
});
|
|
157
150
|
});
|
|
@@ -164,8 +164,8 @@ describe("Affiliate Dashboard error state", () => {
|
|
|
164
164
|
});
|
|
165
165
|
});
|
|
166
166
|
|
|
167
|
-
describe("Billing layout with Referrals
|
|
168
|
-
it("renders
|
|
167
|
+
describe("Billing layout with Referrals content", () => {
|
|
168
|
+
it("renders child content inside billing layout", async () => {
|
|
169
169
|
const { default: BillingLayout } = await import("../app/(dashboard)/billing/layout");
|
|
170
170
|
render(
|
|
171
171
|
<BillingLayout>
|
|
@@ -173,6 +173,6 @@ describe("Billing layout with Referrals nav", () => {
|
|
|
173
173
|
</BillingLayout>,
|
|
174
174
|
);
|
|
175
175
|
|
|
176
|
-
expect(screen.getByText("
|
|
176
|
+
expect(screen.getByText("child content")).toBeInTheDocument();
|
|
177
177
|
});
|
|
178
178
|
});
|
|
@@ -5,6 +5,17 @@ vi.mock("@/lib/api-config", () => ({
|
|
|
5
5
|
PLATFORM_BASE_URL: "https://api.test",
|
|
6
6
|
}));
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Helper: wait for all pending microtasks/promises to flush.
|
|
10
|
+
* handleUnauthorized fires an async session check internally;
|
|
11
|
+
* we need to let that settle before asserting on mockLocation.href.
|
|
12
|
+
*/
|
|
13
|
+
function flushPromises() {
|
|
14
|
+
return new Promise<void>((resolve) => {
|
|
15
|
+
setTimeout(resolve, 0);
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
8
19
|
describe("401 redirect handling", () => {
|
|
9
20
|
const mockLocation = { href: "", pathname: "/dashboard", search: "" };
|
|
10
21
|
const mockFetch = vi.fn();
|
|
@@ -23,9 +34,15 @@ describe("401 redirect handling", () => {
|
|
|
23
34
|
vi.unstubAllGlobals();
|
|
24
35
|
});
|
|
25
36
|
|
|
26
|
-
it("handleUnauthorized
|
|
37
|
+
it("handleUnauthorized throws UnauthorizedError and redirects when session is expired", async () => {
|
|
38
|
+
// Mock the internal session check to return no session (expired)
|
|
39
|
+
mockFetch.mockResolvedValueOnce({
|
|
40
|
+
json: () => Promise.resolve({ session: null }),
|
|
41
|
+
});
|
|
27
42
|
const { handleUnauthorized } = await import("@/lib/fetch-utils");
|
|
28
43
|
expect(() => handleUnauthorized()).toThrow("Session expired");
|
|
44
|
+
// Wait for the async session check to complete and trigger redirect
|
|
45
|
+
await flushPromises();
|
|
29
46
|
expect(mockLocation.href).toBe("/login?reason=expired&callbackUrl=%2Fdashboard");
|
|
30
47
|
});
|
|
31
48
|
|
|
@@ -37,27 +54,41 @@ describe("401 redirect handling", () => {
|
|
|
37
54
|
});
|
|
38
55
|
|
|
39
56
|
it("apiFetch redirects on 401", async () => {
|
|
57
|
+
// First call: the API request itself returns 401
|
|
40
58
|
mockFetch.mockResolvedValueOnce({ ok: false, status: 401, statusText: "Unauthorized" });
|
|
59
|
+
// Second call: the internal session check returns expired
|
|
60
|
+
mockFetch.mockResolvedValueOnce({
|
|
61
|
+
json: () => Promise.resolve({ session: null }),
|
|
62
|
+
});
|
|
41
63
|
const { getProfile } = await import("@/lib/api");
|
|
42
64
|
await expect(getProfile()).rejects.toThrow("Session expired");
|
|
65
|
+
await flushPromises();
|
|
43
66
|
expect(mockLocation.href).toContain("/login?reason=expired");
|
|
44
67
|
});
|
|
45
68
|
|
|
46
|
-
it("fleetFetch redirects on 401", async () => {
|
|
69
|
+
it("fleetFetch (updateInstanceConfig) redirects on 401", async () => {
|
|
47
70
|
mockFetch.mockResolvedValueOnce({ ok: false, status: 401, statusText: "Unauthorized" });
|
|
48
|
-
|
|
49
|
-
|
|
71
|
+
mockFetch.mockResolvedValueOnce({
|
|
72
|
+
json: () => Promise.resolve({ session: null }),
|
|
73
|
+
});
|
|
74
|
+
const { updateInstanceConfig } = await import("@/lib/api");
|
|
75
|
+
await expect(updateInstanceConfig("bot-1", {})).rejects.toThrow("Session expired");
|
|
76
|
+
await flushPromises();
|
|
50
77
|
expect(mockLocation.href).toContain("/login?reason=expired");
|
|
51
78
|
});
|
|
52
79
|
|
|
53
|
-
it("
|
|
80
|
+
it("apiFetch (listProviderKeys) redirects on 401", async () => {
|
|
54
81
|
mockFetch.mockResolvedValueOnce({ ok: false, status: 401, statusText: "Unauthorized" });
|
|
55
|
-
|
|
56
|
-
|
|
82
|
+
mockFetch.mockResolvedValueOnce({
|
|
83
|
+
json: () => Promise.resolve({ session: null }),
|
|
84
|
+
});
|
|
85
|
+
const { listProviderKeys } = await import("@/lib/api");
|
|
86
|
+
await expect(listProviderKeys()).rejects.toThrow("Session expired");
|
|
87
|
+
await flushPromises();
|
|
57
88
|
expect(mockLocation.href).toContain("/login?reason=expired");
|
|
58
89
|
});
|
|
59
90
|
|
|
60
|
-
it("non-401 errors still throw
|
|
91
|
+
it("non-401 errors still throw without redirecting", async () => {
|
|
61
92
|
mockFetch.mockResolvedValueOnce({
|
|
62
93
|
ok: false,
|
|
63
94
|
status: 500,
|
|
@@ -65,14 +96,20 @@ describe("401 redirect handling", () => {
|
|
|
65
96
|
json: () => Promise.resolve({}),
|
|
66
97
|
});
|
|
67
98
|
const { getProfile } = await import("@/lib/api");
|
|
68
|
-
|
|
99
|
+
// getProfile uses tRPC, so the error message comes from tRPC internals
|
|
100
|
+
await expect(getProfile()).rejects.toThrow();
|
|
101
|
+
// The key assertion: non-401 errors must NOT redirect to /login
|
|
69
102
|
expect(mockLocation.href).toBe("");
|
|
70
103
|
});
|
|
71
104
|
|
|
72
105
|
it("bot-settings-data apiFetch redirects on 401", async () => {
|
|
73
106
|
mockFetch.mockResolvedValueOnce({ ok: false, status: 401, statusText: "Unauthorized" });
|
|
107
|
+
mockFetch.mockResolvedValueOnce({
|
|
108
|
+
json: () => Promise.resolve({ session: null }),
|
|
109
|
+
});
|
|
74
110
|
const { getBotSettings } = await import("@/lib/bot-settings-data");
|
|
75
111
|
await expect(getBotSettings("bot-1")).rejects.toThrow("Session expired");
|
|
112
|
+
await flushPromises();
|
|
76
113
|
expect(mockLocation.href).toContain("/login?reason=expired");
|
|
77
114
|
});
|
|
78
115
|
});
|