@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
|
@@ -0,0 +1,690 @@
|
|
|
1
|
+
import { NextRequest } from "next/server";
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import middleware, { config, validateCsrfOrigin } from "../proxy";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Build a real NextRequest for the given path with optional cookies and headers.
|
|
7
|
+
* Uses https://localhost as the base URL so `nextUrl.protocol` is "https:".
|
|
8
|
+
*/
|
|
9
|
+
function buildRequest(
|
|
10
|
+
path: string,
|
|
11
|
+
opts: {
|
|
12
|
+
method?: string;
|
|
13
|
+
cookies?: Record<string, string>;
|
|
14
|
+
headers?: Record<string, string>;
|
|
15
|
+
host?: string;
|
|
16
|
+
} = {},
|
|
17
|
+
): NextRequest {
|
|
18
|
+
const url = new URL(path, "https://localhost");
|
|
19
|
+
const headers = new Headers(opts.headers ?? {});
|
|
20
|
+
// opts.host takes explicit precedence; fall back to existing header or "localhost"
|
|
21
|
+
if (opts.host) {
|
|
22
|
+
headers.set("host", opts.host);
|
|
23
|
+
} else if (!headers.has("host")) {
|
|
24
|
+
headers.set("host", "localhost");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const req = new NextRequest(url, {
|
|
28
|
+
method: opts.method ?? "GET",
|
|
29
|
+
headers,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
if (opts.cookies) {
|
|
33
|
+
for (const [name, value] of Object.entries(opts.cookies)) {
|
|
34
|
+
req.cookies.set(name, value);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return req;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isRedirect(res: Response): boolean {
|
|
42
|
+
return res.status >= 300 && res.status < 400;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function redirectPath(res: Response): string {
|
|
46
|
+
const loc = res.headers.get("location");
|
|
47
|
+
if (!loc) return "";
|
|
48
|
+
try {
|
|
49
|
+
const u = new URL(loc);
|
|
50
|
+
return u.pathname + (u.search ? u.search : "");
|
|
51
|
+
} catch {
|
|
52
|
+
// Non-URL location string (e.g. relative path) — return as-is
|
|
53
|
+
return loc;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isPassThrough(res: Response): boolean {
|
|
58
|
+
return res.headers.get("x-middleware-next") === "1";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
describe("middleware", () => {
|
|
62
|
+
beforeEach(() => {
|
|
63
|
+
// Reset to a safe no-session mock by default.
|
|
64
|
+
// Tests that need fetch (admin routes) stub it themselves.
|
|
65
|
+
vi.stubGlobal("fetch", vi.fn().mockResolvedValue({ ok: false }));
|
|
66
|
+
delete process.env.NEXT_PUBLIC_APP_DOMAIN;
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
afterEach(() => {
|
|
70
|
+
vi.unstubAllGlobals();
|
|
71
|
+
delete process.env.NEXT_PUBLIC_APP_DOMAIN;
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// Unauthenticated redirects
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
describe("unauthenticated redirects", () => {
|
|
78
|
+
it("redirects /dashboard to /login with callbackUrl", async () => {
|
|
79
|
+
const req = buildRequest("/dashboard");
|
|
80
|
+
const res = await middleware(req);
|
|
81
|
+
expect(isRedirect(res)).toBe(true);
|
|
82
|
+
const loc = redirectPath(res);
|
|
83
|
+
expect(loc).toContain("/login");
|
|
84
|
+
expect(loc).toContain("callbackUrl=%2Fdashboard");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("redirects /marketplace to /login with callbackUrl", async () => {
|
|
88
|
+
const req = buildRequest("/marketplace");
|
|
89
|
+
const res = await middleware(req);
|
|
90
|
+
expect(isRedirect(res)).toBe(true);
|
|
91
|
+
expect(redirectPath(res)).toContain("/login");
|
|
92
|
+
expect(redirectPath(res)).toContain("callbackUrl=%2Fmarketplace");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("redirects /settings to /login", async () => {
|
|
96
|
+
const req = buildRequest("/settings");
|
|
97
|
+
const res = await middleware(req);
|
|
98
|
+
expect(isRedirect(res)).toBe(true);
|
|
99
|
+
expect(redirectPath(res)).toContain("/login");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("does not treat an empty session cookie as authenticated", async () => {
|
|
103
|
+
const req = buildRequest("/dashboard", {
|
|
104
|
+
cookies: { "better-auth.session_token": "" },
|
|
105
|
+
});
|
|
106
|
+
const res = await middleware(req);
|
|
107
|
+
expect(isRedirect(res)).toBe(true);
|
|
108
|
+
expect(redirectPath(res)).toContain("/login");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("does not treat a whitespace-only session cookie as authenticated", async () => {
|
|
112
|
+
const req = buildRequest("/dashboard", {
|
|
113
|
+
cookies: { "better-auth.session_token": " " },
|
|
114
|
+
});
|
|
115
|
+
const res = await middleware(req);
|
|
116
|
+
expect(isRedirect(res)).toBe(true);
|
|
117
|
+
expect(redirectPath(res)).toContain("/login");
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
// Authenticated pass-through
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
describe("authenticated pass-through", () => {
|
|
125
|
+
it("passes /dashboard through with a valid session cookie", async () => {
|
|
126
|
+
const req = buildRequest("/dashboard", {
|
|
127
|
+
cookies: { "better-auth.session_token": "valid-token" },
|
|
128
|
+
});
|
|
129
|
+
const res = await middleware(req);
|
|
130
|
+
expect(isPassThrough(res)).toBe(true);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("passes /marketplace through with a valid session cookie", async () => {
|
|
134
|
+
const req = buildRequest("/marketplace", {
|
|
135
|
+
cookies: { "better-auth.session_token": "valid-token" },
|
|
136
|
+
});
|
|
137
|
+
const res = await middleware(req);
|
|
138
|
+
expect(isPassThrough(res)).toBe(true);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("passes /settings/profile through with a valid session cookie", async () => {
|
|
142
|
+
const req = buildRequest("/settings/profile", {
|
|
143
|
+
cookies: { "better-auth.session_token": "valid-token" },
|
|
144
|
+
});
|
|
145
|
+
const res = await middleware(req);
|
|
146
|
+
expect(isPassThrough(res)).toBe(true);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("accepts __Secure- prefixed session cookie as authenticated", async () => {
|
|
150
|
+
const req = buildRequest("/dashboard", {
|
|
151
|
+
cookies: { "__Secure-better-auth.session_token": "valid-token" },
|
|
152
|
+
});
|
|
153
|
+
const res = await middleware(req);
|
|
154
|
+
expect(isPassThrough(res)).toBe(true);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("forwards x-nonce request header on authenticated non-admin routes", async () => {
|
|
158
|
+
const req = buildRequest("/dashboard", {
|
|
159
|
+
cookies: { "better-auth.session_token": "valid-token" },
|
|
160
|
+
});
|
|
161
|
+
const res = await middleware(req);
|
|
162
|
+
expect(isPassThrough(res)).toBe(true);
|
|
163
|
+
// CSP header must contain a nonce
|
|
164
|
+
const csp = res.headers.get("content-security-policy") ?? "";
|
|
165
|
+
const nonceMatch = csp.match(/'nonce-([^']+)'/);
|
|
166
|
+
expect(nonceMatch).not.toBeNull();
|
|
167
|
+
// The nonce must NOT be in the response headers (security)
|
|
168
|
+
expect(res.headers.get("x-nonce")).toBeNull();
|
|
169
|
+
// Verify the response was created via nextWithNonce() — it sets x-middleware-request-x-nonce
|
|
170
|
+
// NextResponse.next({ request: { headers } }) propagates custom headers with this prefix
|
|
171
|
+
const requestNonce = res.headers.get("x-middleware-request-x-nonce");
|
|
172
|
+
expect(requestNonce).toBe(nonceMatch?.[1]);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
// Public routes — prefix match
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
describe("public routes (prefix match)", () => {
|
|
180
|
+
it("allows /login without authentication", async () => {
|
|
181
|
+
const req = buildRequest("/login");
|
|
182
|
+
const res = await middleware(req);
|
|
183
|
+
expect(isPassThrough(res)).toBe(true);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("allows /signup without authentication", async () => {
|
|
187
|
+
const req = buildRequest("/signup");
|
|
188
|
+
const res = await middleware(req);
|
|
189
|
+
expect(isPassThrough(res)).toBe(true);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("allows /forgot-password without authentication", async () => {
|
|
193
|
+
const req = buildRequest("/forgot-password");
|
|
194
|
+
const res = await middleware(req);
|
|
195
|
+
expect(isPassThrough(res)).toBe(true);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("allows /reset-password without authentication", async () => {
|
|
199
|
+
const req = buildRequest("/reset-password");
|
|
200
|
+
const res = await middleware(req);
|
|
201
|
+
expect(isPassThrough(res)).toBe(true);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("allows /auth/callback without authentication", async () => {
|
|
205
|
+
const req = buildRequest("/auth/callback");
|
|
206
|
+
const res = await middleware(req);
|
|
207
|
+
expect(isPassThrough(res)).toBe(true);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("allows /auth/verify without authentication", async () => {
|
|
211
|
+
const req = buildRequest("/auth/verify");
|
|
212
|
+
const res = await middleware(req);
|
|
213
|
+
expect(isPassThrough(res)).toBe(true);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("allows /login/extra (prefix match extends beyond exact path)", async () => {
|
|
217
|
+
const req = buildRequest("/login/extra");
|
|
218
|
+
const res = await middleware(req);
|
|
219
|
+
expect(isPassThrough(res)).toBe(true);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// ---------------------------------------------------------------------------
|
|
224
|
+
// Public routes — exact match only
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
226
|
+
describe("public routes (exact match only)", () => {
|
|
227
|
+
it("allows /terms without authentication (exact)", async () => {
|
|
228
|
+
const req = buildRequest("/terms");
|
|
229
|
+
const res = await middleware(req);
|
|
230
|
+
expect(isPassThrough(res)).toBe(true);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("allows /privacy without authentication (exact)", async () => {
|
|
234
|
+
const req = buildRequest("/privacy");
|
|
235
|
+
const res = await middleware(req);
|
|
236
|
+
expect(isPassThrough(res)).toBe(true);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("allows /pricing without authentication (exact)", async () => {
|
|
240
|
+
const req = buildRequest("/pricing");
|
|
241
|
+
const res = await middleware(req);
|
|
242
|
+
expect(isPassThrough(res)).toBe(true);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("does NOT allow /terms/extra (exact match only, no prefix)", async () => {
|
|
246
|
+
const req = buildRequest("/terms/extra");
|
|
247
|
+
const res = await middleware(req);
|
|
248
|
+
expect(isRedirect(res)).toBe(true);
|
|
249
|
+
expect(redirectPath(res)).toContain("/login");
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it("does NOT allow /pricing/enterprise (exact match only)", async () => {
|
|
253
|
+
const req = buildRequest("/pricing/enterprise");
|
|
254
|
+
const res = await middleware(req);
|
|
255
|
+
expect(isRedirect(res)).toBe(true);
|
|
256
|
+
expect(redirectPath(res)).toContain("/login");
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it("does NOT allow /privacy/policy (exact match only)", async () => {
|
|
260
|
+
const req = buildRequest("/privacy/policy");
|
|
261
|
+
const res = await middleware(req);
|
|
262
|
+
expect(isRedirect(res)).toBe(true);
|
|
263
|
+
expect(redirectPath(res)).toContain("/login");
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// ---------------------------------------------------------------------------
|
|
268
|
+
// Root path (/) — special handling
|
|
269
|
+
// ---------------------------------------------------------------------------
|
|
270
|
+
describe("root path special handling", () => {
|
|
271
|
+
it("allows unauthenticated GET / (public exact path)", async () => {
|
|
272
|
+
const req = buildRequest("/");
|
|
273
|
+
const res = await middleware(req);
|
|
274
|
+
expect(isPassThrough(res)).toBe(true);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it("redirects authenticated GET / to /marketplace (no app domain configured)", async () => {
|
|
278
|
+
const req = buildRequest("/", {
|
|
279
|
+
cookies: { "better-auth.session_token": "valid-token" },
|
|
280
|
+
});
|
|
281
|
+
const res = await middleware(req);
|
|
282
|
+
expect(isRedirect(res)).toBe(true);
|
|
283
|
+
expect(redirectPath(res)).toBe("/marketplace");
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("redirects authenticated GET / to app domain when NEXT_PUBLIC_APP_DOMAIN is set and host is the marketing domain", async () => {
|
|
287
|
+
process.env.NEXT_PUBLIC_APP_DOMAIN = "app.localhost";
|
|
288
|
+
const req = buildRequest("/", {
|
|
289
|
+
cookies: { "better-auth.session_token": "valid-token" },
|
|
290
|
+
host: "localhost",
|
|
291
|
+
});
|
|
292
|
+
const res = await middleware(req);
|
|
293
|
+
expect(isRedirect(res)).toBe(true);
|
|
294
|
+
const loc = res.headers.get("location");
|
|
295
|
+
expect(loc).toBe("https://app.localhost/marketplace");
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it("redirects authenticated GET / to /marketplace when already on app subdomain", async () => {
|
|
299
|
+
process.env.NEXT_PUBLIC_APP_DOMAIN = "app.localhost";
|
|
300
|
+
const req = buildRequest("/", {
|
|
301
|
+
cookies: { "better-auth.session_token": "valid-token" },
|
|
302
|
+
host: "app.localhost",
|
|
303
|
+
});
|
|
304
|
+
const res = await middleware(req);
|
|
305
|
+
expect(isRedirect(res)).toBe(true);
|
|
306
|
+
expect(redirectPath(res)).toBe("/marketplace");
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// ---------------------------------------------------------------------------
|
|
311
|
+
// Static files and API routes — always pass through
|
|
312
|
+
// ---------------------------------------------------------------------------
|
|
313
|
+
describe("static files and API routes", () => {
|
|
314
|
+
it("allows /_next/static paths without authentication", async () => {
|
|
315
|
+
const req = buildRequest("/_next/static/chunk.js");
|
|
316
|
+
const res = await middleware(req);
|
|
317
|
+
expect(isPassThrough(res)).toBe(true);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it("allows paths with file extensions (favicon.ico)", async () => {
|
|
321
|
+
const req = buildRequest("/favicon.ico");
|
|
322
|
+
const res = await middleware(req);
|
|
323
|
+
expect(isPassThrough(res)).toBe(true);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it("allows /api/health without authentication (public exact path for infra probes)", async () => {
|
|
327
|
+
const req = buildRequest("/api/health");
|
|
328
|
+
const res = await middleware(req);
|
|
329
|
+
expect(isPassThrough(res)).toBe(true);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it("allows /api/auth/session without authentication", async () => {
|
|
333
|
+
const req = buildRequest("/api/auth/session");
|
|
334
|
+
const res = await middleware(req);
|
|
335
|
+
expect(isPassThrough(res)).toBe(true);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it("redirects unauthenticated GET /api/something to /login", async () => {
|
|
339
|
+
const req = buildRequest("/api/something");
|
|
340
|
+
const res = await middleware(req);
|
|
341
|
+
expect(isRedirect(res)).toBe(true);
|
|
342
|
+
expect(redirectPath(res)).toContain("/login");
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it("passes /api/something through with a valid session cookie", async () => {
|
|
346
|
+
const req = buildRequest("/api/something", {
|
|
347
|
+
cookies: { "better-auth.session_token": "valid-token" },
|
|
348
|
+
});
|
|
349
|
+
const res = await middleware(req);
|
|
350
|
+
expect(isPassThrough(res)).toBe(true);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it("allows /api/auth/callback without authentication (public)", async () => {
|
|
354
|
+
const req = buildRequest("/api/auth/callback");
|
|
355
|
+
const res = await middleware(req);
|
|
356
|
+
expect(isPassThrough(res)).toBe(true);
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
// ---------------------------------------------------------------------------
|
|
361
|
+
// Admin route authorization — combined with session check
|
|
362
|
+
// ---------------------------------------------------------------------------
|
|
363
|
+
describe("admin route authorization", () => {
|
|
364
|
+
it("redirects unauthenticated /admin to /login (not /marketplace)", async () => {
|
|
365
|
+
const req = buildRequest("/admin");
|
|
366
|
+
const res = await middleware(req);
|
|
367
|
+
expect(isRedirect(res)).toBe(true);
|
|
368
|
+
expect(redirectPath(res)).toContain("/login");
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it("redirects authenticated non-admin to /marketplace when accessing /admin", async () => {
|
|
372
|
+
vi.stubGlobal(
|
|
373
|
+
"fetch",
|
|
374
|
+
vi.fn().mockResolvedValue({
|
|
375
|
+
ok: true,
|
|
376
|
+
json: async () => ({ user: { role: "user" } }),
|
|
377
|
+
}),
|
|
378
|
+
);
|
|
379
|
+
const req = buildRequest("/admin", {
|
|
380
|
+
cookies: { "better-auth.session_token": "valid-token" },
|
|
381
|
+
});
|
|
382
|
+
const res = await middleware(req);
|
|
383
|
+
expect(isRedirect(res)).toBe(true);
|
|
384
|
+
expect(redirectPath(res)).toBe("/marketplace");
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
it("allows platform_admin through to /admin", async () => {
|
|
388
|
+
vi.stubGlobal(
|
|
389
|
+
"fetch",
|
|
390
|
+
vi.fn().mockResolvedValue({
|
|
391
|
+
ok: true,
|
|
392
|
+
json: async () => ({ user: { role: "platform_admin" } }),
|
|
393
|
+
}),
|
|
394
|
+
);
|
|
395
|
+
const req = buildRequest("/admin", {
|
|
396
|
+
cookies: { "better-auth.session_token": "valid-token" },
|
|
397
|
+
});
|
|
398
|
+
const res = await middleware(req);
|
|
399
|
+
expect(isPassThrough(res)).toBe(true);
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it("redirects non-admin from /admin/users to /marketplace", async () => {
|
|
403
|
+
vi.stubGlobal(
|
|
404
|
+
"fetch",
|
|
405
|
+
vi.fn().mockResolvedValue({
|
|
406
|
+
ok: true,
|
|
407
|
+
json: async () => ({ user: { role: "user" } }),
|
|
408
|
+
}),
|
|
409
|
+
);
|
|
410
|
+
const req = buildRequest("/admin/users", {
|
|
411
|
+
cookies: { "better-auth.session_token": "valid-token" },
|
|
412
|
+
});
|
|
413
|
+
const res = await middleware(req);
|
|
414
|
+
expect(isRedirect(res)).toBe(true);
|
|
415
|
+
expect(redirectPath(res)).toBe("/marketplace");
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it("redirects to /marketplace when session fetch fails (fail closed)", async () => {
|
|
419
|
+
vi.stubGlobal("fetch", vi.fn().mockRejectedValue(new Error("network error")));
|
|
420
|
+
const req = buildRequest("/admin", {
|
|
421
|
+
cookies: { "better-auth.session_token": "valid-token" },
|
|
422
|
+
});
|
|
423
|
+
const res = await middleware(req);
|
|
424
|
+
expect(isRedirect(res)).toBe(true);
|
|
425
|
+
expect(redirectPath(res)).toBe("/marketplace");
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
it("does not call fetch for non-admin routes", async () => {
|
|
429
|
+
const fetchSpy = vi.fn().mockResolvedValue({ ok: false });
|
|
430
|
+
vi.stubGlobal("fetch", fetchSpy);
|
|
431
|
+
const req = buildRequest("/marketplace", {
|
|
432
|
+
cookies: { "better-auth.session_token": "valid-token" },
|
|
433
|
+
});
|
|
434
|
+
await middleware(req);
|
|
435
|
+
expect(fetchSpy).not.toHaveBeenCalled();
|
|
436
|
+
});
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
// ---------------------------------------------------------------------------
|
|
440
|
+
// CSRF protection — integration-level (validateCsrfOrigin unit tests are in csrf-middleware.test.ts)
|
|
441
|
+
// ---------------------------------------------------------------------------
|
|
442
|
+
describe("CSRF protection (middleware integration)", () => {
|
|
443
|
+
it("blocks POST to /api/foo without Origin header", async () => {
|
|
444
|
+
const req = buildRequest("/api/foo", {
|
|
445
|
+
method: "POST",
|
|
446
|
+
host: "localhost",
|
|
447
|
+
});
|
|
448
|
+
const res = await middleware(req);
|
|
449
|
+
expect(res.status).toBe(403);
|
|
450
|
+
const body = await res.json();
|
|
451
|
+
expect(body.error).toBe("CSRF validation failed");
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
it("allows POST to /api/foo with matching Origin", async () => {
|
|
455
|
+
const req = buildRequest("/api/foo", {
|
|
456
|
+
method: "POST",
|
|
457
|
+
headers: {
|
|
458
|
+
host: "localhost",
|
|
459
|
+
origin: "https://localhost",
|
|
460
|
+
},
|
|
461
|
+
});
|
|
462
|
+
const res = await middleware(req);
|
|
463
|
+
expect(res.status).not.toBe(403);
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
it("exempts POST /api/auth routes from CSRF check", async () => {
|
|
467
|
+
const req = buildRequest("/api/auth/callback", {
|
|
468
|
+
method: "POST",
|
|
469
|
+
host: "localhost",
|
|
470
|
+
// No Origin header
|
|
471
|
+
});
|
|
472
|
+
const res = await middleware(req);
|
|
473
|
+
expect(res.status).not.toBe(403);
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
it("blocks DELETE to /api/auth routes (only POST is exempt)", async () => {
|
|
477
|
+
const req = buildRequest("/api/auth/callback", {
|
|
478
|
+
method: "DELETE",
|
|
479
|
+
host: "localhost",
|
|
480
|
+
});
|
|
481
|
+
const res = await middleware(req);
|
|
482
|
+
expect(res.status).toBe(403);
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
it("blocks PUT to /api/auth routes (only POST is exempt)", async () => {
|
|
486
|
+
const req = buildRequest("/api/auth/callback", {
|
|
487
|
+
method: "PUT",
|
|
488
|
+
host: "localhost",
|
|
489
|
+
});
|
|
490
|
+
const res = await middleware(req);
|
|
491
|
+
expect(res.status).toBe(403);
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
it("allows GET to /api/foo without Origin (GET is not a mutation)", async () => {
|
|
495
|
+
const req = buildRequest("/api/foo", {
|
|
496
|
+
method: "GET",
|
|
497
|
+
host: "localhost",
|
|
498
|
+
});
|
|
499
|
+
const res = await middleware(req);
|
|
500
|
+
expect(res.status).not.toBe(403);
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
it("blocks DELETE to /api/foo without Origin", async () => {
|
|
504
|
+
const req = buildRequest("/api/foo", {
|
|
505
|
+
method: "DELETE",
|
|
506
|
+
host: "localhost",
|
|
507
|
+
});
|
|
508
|
+
const res = await middleware(req);
|
|
509
|
+
expect(res.status).toBe(403);
|
|
510
|
+
});
|
|
511
|
+
});
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
// ---------------------------------------------------------------------------
|
|
515
|
+
// validateCsrfOrigin — additional edge cases not in csrf-middleware.test.ts
|
|
516
|
+
// ---------------------------------------------------------------------------
|
|
517
|
+
describe("validateCsrfOrigin", () => {
|
|
518
|
+
it("returns true when Origin matches host exactly", () => {
|
|
519
|
+
const req = buildRequest("/api/foo", {
|
|
520
|
+
method: "POST",
|
|
521
|
+
host: "example.com",
|
|
522
|
+
headers: { origin: "https://example.com" },
|
|
523
|
+
});
|
|
524
|
+
expect(validateCsrfOrigin(req)).toBe(true);
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
it("returns false when Origin mismatches host", () => {
|
|
528
|
+
const req = buildRequest("/api/foo", {
|
|
529
|
+
method: "POST",
|
|
530
|
+
host: "example.com",
|
|
531
|
+
headers: { origin: "https://evil.com" },
|
|
532
|
+
});
|
|
533
|
+
expect(validateCsrfOrigin(req)).toBe(false);
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
it("returns false when no Origin and no Referer", () => {
|
|
537
|
+
const req = buildRequest("/api/foo", {
|
|
538
|
+
method: "POST",
|
|
539
|
+
host: "example.com",
|
|
540
|
+
});
|
|
541
|
+
expect(validateCsrfOrigin(req)).toBe(false);
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
it("falls back to Referer when Origin is absent", () => {
|
|
545
|
+
const req = buildRequest("/api/foo", {
|
|
546
|
+
method: "POST",
|
|
547
|
+
host: "example.com",
|
|
548
|
+
headers: { referer: "https://example.com/some/page" },
|
|
549
|
+
});
|
|
550
|
+
expect(validateCsrfOrigin(req)).toBe(true);
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
it("returns false for an invalid Referer URL", () => {
|
|
554
|
+
const req = buildRequest("/api/foo", {
|
|
555
|
+
method: "POST",
|
|
556
|
+
host: "example.com",
|
|
557
|
+
headers: { referer: "not-a-url" },
|
|
558
|
+
});
|
|
559
|
+
expect(validateCsrfOrigin(req)).toBe(false);
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
it("returns false when no host header present", () => {
|
|
563
|
+
const url = new URL("/api/foo", "https://localhost");
|
|
564
|
+
const headers = new Headers(); // No host header
|
|
565
|
+
const req = new NextRequest(url, { method: "POST", headers });
|
|
566
|
+
expect(validateCsrfOrigin(req)).toBe(false);
|
|
567
|
+
});
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
// ---------------------------------------------------------------------------
|
|
571
|
+
// CSP nonce header
|
|
572
|
+
// ---------------------------------------------------------------------------
|
|
573
|
+
describe("CSP nonce in middleware", () => {
|
|
574
|
+
it("sets a Content-Security-Policy header with a nonce in script-src", async () => {
|
|
575
|
+
const req = buildRequest("/login");
|
|
576
|
+
const res = await middleware(req);
|
|
577
|
+
const csp = res.headers.get("content-security-policy");
|
|
578
|
+
expect(csp).not.toBeNull();
|
|
579
|
+
expect(csp).toMatch(/script-src [^;]*'nonce-[A-Za-z0-9+/=_-]+'[^;]*/);
|
|
580
|
+
// script-src must not use 'unsafe-inline' (nonce replaces it)
|
|
581
|
+
const scriptSrc = csp?.split(";").find((d) => d.trim().startsWith("script-src"));
|
|
582
|
+
expect(scriptSrc).not.toContain("'unsafe-inline'");
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
it("generates a different nonce per request", async () => {
|
|
586
|
+
const res1 = await middleware(buildRequest("/login"));
|
|
587
|
+
const res2 = await middleware(buildRequest("/login"));
|
|
588
|
+
const csp1 = res1.headers.get("content-security-policy") ?? "";
|
|
589
|
+
const csp2 = res2.headers.get("content-security-policy") ?? "";
|
|
590
|
+
const nonce1 = csp1.match(/'nonce-([^']+)'/)?.[1];
|
|
591
|
+
const nonce2 = csp2.match(/'nonce-([^']+)'/)?.[1];
|
|
592
|
+
expect(nonce1).toMatch(/^[A-Za-z0-9+/=_-]+$/);
|
|
593
|
+
expect(nonce2).toMatch(/^[A-Za-z0-9+/=_-]+$/);
|
|
594
|
+
expect(nonce1).not.toBe(nonce2);
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
it("includes 'strict-dynamic' in script-src", async () => {
|
|
598
|
+
const req = buildRequest("/login");
|
|
599
|
+
const res = await middleware(req);
|
|
600
|
+
const csp = res.headers.get("content-security-policy");
|
|
601
|
+
expect(csp).toContain("'strict-dynamic'");
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
it("keeps https://js.stripe.com in script-src for CSP Level 2 fallback", async () => {
|
|
605
|
+
const req = buildRequest("/login");
|
|
606
|
+
const res = await middleware(req);
|
|
607
|
+
const csp = res.headers.get("content-security-policy");
|
|
608
|
+
expect(csp).toContain("https://js.stripe.com");
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
it("does NOT expose the nonce via x-nonce response header", async () => {
|
|
612
|
+
const req = buildRequest("/login");
|
|
613
|
+
const res = await middleware(req);
|
|
614
|
+
// Nonce must not leak in response headers — it's forwarded via request headers only
|
|
615
|
+
expect(res.headers.get("x-nonce")).toBeNull();
|
|
616
|
+
// CSP header must still contain the nonce
|
|
617
|
+
const csp = res.headers.get("content-security-policy") ?? "";
|
|
618
|
+
expect(csp).toMatch(/'nonce-[A-Za-z0-9+/=_-]+'/);
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
it("preserves all other CSP directives (default-src, style-src, connect-src, etc.)", async () => {
|
|
622
|
+
const req = buildRequest("/login");
|
|
623
|
+
const res = await middleware(req);
|
|
624
|
+
const csp = res.headers.get("content-security-policy") ?? "";
|
|
625
|
+
expect(csp).toContain("default-src 'self'");
|
|
626
|
+
expect(csp).toMatch(/style-src-elem 'self' 'nonce-[A-Za-z0-9+/=_-]+'/);
|
|
627
|
+
|
|
628
|
+
expect(csp).toContain("img-src 'self' data: blob:");
|
|
629
|
+
expect(csp).toContain("frame-src https://js.stripe.com");
|
|
630
|
+
expect(csp).toContain("frame-ancestors 'none'");
|
|
631
|
+
expect(csp).toContain("object-src 'none'");
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
it("does not set Cache-Control on nonce-carrying non-admin responses", async () => {
|
|
635
|
+
const req = buildRequest("/login");
|
|
636
|
+
const res = await middleware(req);
|
|
637
|
+
expect(res.headers.get("cache-control")).toBeNull();
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
it("sets Vary: * on nonce-carrying responses", async () => {
|
|
641
|
+
const req = buildRequest("/login");
|
|
642
|
+
const res = await middleware(req);
|
|
643
|
+
expect(res.headers.get("vary")).toBe("*");
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
it("does not overwrite stricter Cache-Control on admin routes", async () => {
|
|
647
|
+
vi.stubGlobal(
|
|
648
|
+
"fetch",
|
|
649
|
+
vi
|
|
650
|
+
.fn()
|
|
651
|
+
.mockResolvedValue(
|
|
652
|
+
new Response(JSON.stringify({ user: { role: "platform_admin" } }), { status: 200 }),
|
|
653
|
+
),
|
|
654
|
+
);
|
|
655
|
+
const req = buildRequest("/admin/dashboard", {
|
|
656
|
+
cookies: { "better-auth.session_token": "valid-token" },
|
|
657
|
+
});
|
|
658
|
+
const res = await middleware(req);
|
|
659
|
+
// Admin route sets stricter cache headers; withCsp should not overwrite
|
|
660
|
+
expect(res.headers.get("cache-control")).toBe("no-store, no-cache, must-revalidate");
|
|
661
|
+
});
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
// ---------------------------------------------------------------------------
|
|
665
|
+
// CSP style-src directive
|
|
666
|
+
// ---------------------------------------------------------------------------
|
|
667
|
+
describe("CSP style-src directive", () => {
|
|
668
|
+
it("uses nonce-based style-src (NONCE_STYLES_ENABLED = true)", async () => {
|
|
669
|
+
const req = buildRequest("/login", {
|
|
670
|
+
cookies: { "better-auth.session_token": "tok" },
|
|
671
|
+
});
|
|
672
|
+
const res = await middleware(req);
|
|
673
|
+
const csp = res.headers.get("content-security-policy") ?? "";
|
|
674
|
+
// Must contain nonce in style-src-elem (split from style-src for finer-grained control)
|
|
675
|
+
expect(csp).toMatch(/style-src-elem 'self' 'nonce-[A-Za-z0-9+/=_-]+'/);
|
|
676
|
+
// Must NOT contain unsafe-inline in style-src-elem
|
|
677
|
+
expect(csp).not.toContain("style-src-elem 'self' 'unsafe-inline'");
|
|
678
|
+
});
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
// ---------------------------------------------------------------------------
|
|
682
|
+
// config export
|
|
683
|
+
// ---------------------------------------------------------------------------
|
|
684
|
+
describe("middleware config", () => {
|
|
685
|
+
it("exports a matcher that excludes _next/static, _next/image, and favicon.ico", () => {
|
|
686
|
+
const pattern = config.matcher[0];
|
|
687
|
+
expect(pattern).toContain("_next/static");
|
|
688
|
+
expect(pattern).toContain("favicon.ico");
|
|
689
|
+
});
|
|
690
|
+
});
|