@wopr-network/platform-ui-core 1.27.8 → 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 +4 -14
- 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
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useRouter } from "next/navigation";
|
|
4
|
+
import { useEffect } from "react";
|
|
5
|
+
import { useSession } from "@/lib/auth-client";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Hook for pages that require authentication.
|
|
9
|
+
* Redirects to /login if no session. Returns session data when authed.
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* const { user, session, isPending } = useRequireAuth();
|
|
13
|
+
* if (isPending) return <Loading />;
|
|
14
|
+
*/
|
|
15
|
+
export function useRequireAuth(callbackUrl?: string) {
|
|
16
|
+
const { data, isPending } = useSession();
|
|
17
|
+
const router = useRouter();
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
// Diagnostic logging — remove once redirect loop is resolved
|
|
21
|
+
if (!isPending) {
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!isPending && !data?.session) {
|
|
25
|
+
const callback = callbackUrl || window.location.pathname;
|
|
26
|
+
router.replace(`/login?reason=expired&callbackUrl=${encodeURIComponent(callback)}`);
|
|
27
|
+
}
|
|
28
|
+
}, [isPending, data, router, callbackUrl]);
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
user: data?.user ?? null,
|
|
32
|
+
session: data?.session ?? null,
|
|
33
|
+
isPending,
|
|
34
|
+
isAuthed: !!data?.session,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Hook for pages that require platform_admin role.
|
|
40
|
+
* Redirects to / if not admin.
|
|
41
|
+
*/
|
|
42
|
+
export function useRequireAdmin() {
|
|
43
|
+
const auth = useRequireAuth();
|
|
44
|
+
const router = useRouter();
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
if (!auth.isPending && auth.user && (auth.user as Record<string, unknown>).role !== "platform_admin") {
|
|
48
|
+
router.replace("/");
|
|
49
|
+
}
|
|
50
|
+
}, [auth.isPending, auth.user, router]);
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
...auth,
|
|
54
|
+
isAdmin: (auth.user as Record<string, unknown> | null)?.role === "platform_admin",
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
// retry
|
package/src/lib/settings-api.ts
CHANGED
|
@@ -33,10 +33,7 @@ export async function saveProviderKey(
|
|
|
33
33
|
});
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
export async function testProviderKey(
|
|
37
|
-
provider: string,
|
|
38
|
-
key?: string,
|
|
39
|
-
): Promise<{ valid: boolean; error?: string }> {
|
|
36
|
+
export async function testProviderKey(provider: string, key?: string): Promise<{ valid: boolean; error?: string }> {
|
|
40
37
|
return trpcVanilla.capabilities.testKey.mutate({
|
|
41
38
|
provider: provider as ProviderName,
|
|
42
39
|
key: key ?? "",
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// core/platform-ui-core/src/lib/sidecar-routes.ts
|
|
2
|
+
|
|
3
|
+
export type RouteType = "iframe" | "native";
|
|
4
|
+
|
|
5
|
+
export const IFRAME_PREFIXES = [
|
|
6
|
+
"/dashboard",
|
|
7
|
+
"/inbox",
|
|
8
|
+
"/issues",
|
|
9
|
+
"/routines",
|
|
10
|
+
"/goals",
|
|
11
|
+
"/projects",
|
|
12
|
+
"/agents",
|
|
13
|
+
"/org",
|
|
14
|
+
"/skills",
|
|
15
|
+
"/company",
|
|
16
|
+
"/approvals",
|
|
17
|
+
"/activity",
|
|
18
|
+
"/costs",
|
|
19
|
+
"/execution-workspaces",
|
|
20
|
+
"/plugins",
|
|
21
|
+
] as const;
|
|
22
|
+
|
|
23
|
+
export function getRouteType(pathname: string): RouteType {
|
|
24
|
+
for (const prefix of IFRAME_PREFIXES) {
|
|
25
|
+
if (pathname === prefix || pathname.startsWith(`${prefix}/`)) return "iframe";
|
|
26
|
+
}
|
|
27
|
+
return "native";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function fromSidecarPath(sidecarPath: string): string {
|
|
31
|
+
const segments = sidecarPath.split("/").filter(Boolean);
|
|
32
|
+
if (segments.length === 0) return "/dashboard";
|
|
33
|
+
|
|
34
|
+
const firstSegment = `/${segments[0]}`;
|
|
35
|
+
for (const prefix of IFRAME_PREFIXES) {
|
|
36
|
+
if (firstSegment === prefix || prefix.startsWith(`${firstSegment}/`)) {
|
|
37
|
+
return sidecarPath;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// First segment is a company prefix — strip it
|
|
42
|
+
return `/${segments.slice(1).join("/")}` || "/dashboard";
|
|
43
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side tRPC client for calling core from SSR / API routes / server actions.
|
|
3
|
+
*
|
|
4
|
+
* This client injects service-to-service auth headers so that core can identify
|
|
5
|
+
* the calling product, tenant, and user without relying on browser session cookies.
|
|
6
|
+
*
|
|
7
|
+
* Environment variables (server-only — NEVER prefix with NEXT_PUBLIC_):
|
|
8
|
+
* CORE_SERVICE_TOKEN — shared secret between this UI server and core
|
|
9
|
+
* INTERNAL_API_URL — internal network URL (e.g. http://core:3001)
|
|
10
|
+
* Falls back to NEXT_PUBLIC_API_URL → localhost:3001
|
|
11
|
+
*
|
|
12
|
+
* The browser-side tRPC client (trpc.tsx) still uses session cookies during the
|
|
13
|
+
* transition period. Eventually all calls will flow through SSR → this client → core.
|
|
14
|
+
*/
|
|
15
|
+
import { createTRPCClient, httpBatchLink } from "@trpc/client";
|
|
16
|
+
import type { AppRouter } from "./trpc-types";
|
|
17
|
+
|
|
18
|
+
export interface ServerTRPCContext {
|
|
19
|
+
tenantId: string;
|
|
20
|
+
userId: string;
|
|
21
|
+
product: string;
|
|
22
|
+
roles?: string[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Create a tRPC client for server-side calls to core.
|
|
27
|
+
*
|
|
28
|
+
* Each request context (SSR render, API route handler) should call this once
|
|
29
|
+
* with the current user/tenant context extracted from the incoming request.
|
|
30
|
+
*/
|
|
31
|
+
export function createServerTRPCClient(ctx: ServerTRPCContext) {
|
|
32
|
+
const serviceToken = process.env.CORE_SERVICE_TOKEN ?? "";
|
|
33
|
+
const apiUrl = process.env.INTERNAL_API_URL ?? process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:3001";
|
|
34
|
+
|
|
35
|
+
return createTRPCClient<AppRouter>({
|
|
36
|
+
links: [
|
|
37
|
+
httpBatchLink({
|
|
38
|
+
url: `${apiUrl}/trpc`,
|
|
39
|
+
headers: () => ({
|
|
40
|
+
...(serviceToken ? { Authorization: `Bearer ${serviceToken}` } : {}),
|
|
41
|
+
"X-Tenant-Id": ctx.tenantId,
|
|
42
|
+
"X-User-Id": ctx.userId,
|
|
43
|
+
"X-Product": ctx.product,
|
|
44
|
+
...(ctx.roles?.length ? { "X-User-Roles": ctx.roles.join(",") } : {}),
|
|
45
|
+
}),
|
|
46
|
+
}),
|
|
47
|
+
],
|
|
48
|
+
});
|
|
49
|
+
}
|
package/src/lib/trpc-types.ts
CHANGED
|
@@ -13,12 +13,7 @@
|
|
|
13
13
|
* The real AppRouter type lives at:
|
|
14
14
|
* wopr-network/wopr-platform/src/trpc/index.ts → `export type AppRouter = typeof appRouter;`
|
|
15
15
|
*/
|
|
16
|
-
import type {
|
|
17
|
-
AnyTRPCMutationProcedure,
|
|
18
|
-
AnyTRPCQueryProcedure,
|
|
19
|
-
AnyTRPCRootTypes,
|
|
20
|
-
TRPCBuiltRouter,
|
|
21
|
-
} from "@trpc/server";
|
|
16
|
+
import type { AnyTRPCMutationProcedure, AnyTRPCQueryProcedure, AnyTRPCRootTypes, TRPCBuiltRouter } from "@trpc/server";
|
|
22
17
|
|
|
23
18
|
/**
|
|
24
19
|
* Minimal router record for the procedures this UI consumes.
|
|
@@ -106,6 +101,7 @@ type AppRouterRecord = {
|
|
|
106
101
|
billingInfo: AnyTRPCQueryProcedure;
|
|
107
102
|
creditsBalance: AnyTRPCQueryProcedure;
|
|
108
103
|
creditsHistory: AnyTRPCQueryProcedure;
|
|
104
|
+
creditsDailySummary: AnyTRPCQueryProcedure;
|
|
109
105
|
creditsCheckout: AnyTRPCMutationProcedure;
|
|
110
106
|
creditOptions: AnyTRPCQueryProcedure;
|
|
111
107
|
inferenceMode: AnyTRPCQueryProcedure;
|
|
@@ -133,6 +129,7 @@ type AppRouterRecord = {
|
|
|
133
129
|
fleet: {
|
|
134
130
|
listInstances: AnyTRPCQueryProcedure;
|
|
135
131
|
getInstance: AnyTRPCQueryProcedure;
|
|
132
|
+
instanceVersionCheck: AnyTRPCQueryProcedure;
|
|
136
133
|
createInstance: AnyTRPCMutationProcedure;
|
|
137
134
|
controlInstance: AnyTRPCMutationProcedure;
|
|
138
135
|
getInstanceHealth: AnyTRPCQueryProcedure;
|
|
@@ -159,6 +156,7 @@ type AppRouterRecord = {
|
|
|
159
156
|
getProfile: AnyTRPCQueryProcedure;
|
|
160
157
|
updateProfile: AnyTRPCMutationProcedure;
|
|
161
158
|
changePassword: AnyTRPCMutationProcedure;
|
|
159
|
+
deleteAccount: AnyTRPCMutationProcedure;
|
|
162
160
|
};
|
|
163
161
|
org: {
|
|
164
162
|
getOrganization: AnyTRPCQueryProcedure;
|
package/src/lib/trpc.tsx
CHANGED
|
@@ -5,6 +5,7 @@ import { createTRPCClient, httpBatchLink } from "@trpc/client";
|
|
|
5
5
|
import { createTRPCReact } from "@trpc/react-query";
|
|
6
6
|
import { useState } from "react";
|
|
7
7
|
import { PLATFORM_BASE_URL } from "./api-config";
|
|
8
|
+
import { getBrandConfig } from "./brand-config";
|
|
8
9
|
import { handleUnauthorized } from "./fetch-utils";
|
|
9
10
|
import { getActiveTenantId, TenantProvider } from "./tenant-context";
|
|
10
11
|
import type { AppRouter } from "./trpc-types";
|
|
@@ -16,8 +17,7 @@ async function trpcFetchWithAuth(url: RequestInfo | URL, options?: RequestInit)
|
|
|
16
17
|
if (res.status === 401) {
|
|
17
18
|
// On login page, don't throw — let the batch continue so public queries
|
|
18
19
|
// (like enabledSocialProviders) resolve alongside failing auth queries.
|
|
19
|
-
const onLoginPage =
|
|
20
|
-
typeof window !== "undefined" && window.location.pathname.startsWith("/login");
|
|
20
|
+
const onLoginPage = typeof window !== "undefined" && window.location.pathname.startsWith("/login");
|
|
21
21
|
if (!onLoginPage) {
|
|
22
22
|
handleUnauthorized();
|
|
23
23
|
}
|
|
@@ -36,7 +36,11 @@ export const trpcVanilla = createTRPCClient<AppRouter>({
|
|
|
36
36
|
fetch: trpcFetchWithAuth,
|
|
37
37
|
headers() {
|
|
38
38
|
const tenantId = getActiveTenantId();
|
|
39
|
-
|
|
39
|
+
const product = process.env.NEXT_PUBLIC_PRODUCT_SLUG || getBrandConfig().storagePrefix;
|
|
40
|
+
return {
|
|
41
|
+
...(tenantId ? { "x-tenant-id": tenantId } : {}),
|
|
42
|
+
...(product ? { "x-product": product } : {}),
|
|
43
|
+
};
|
|
40
44
|
},
|
|
41
45
|
}),
|
|
42
46
|
],
|
|
@@ -87,7 +91,11 @@ export function TRPCProvider({
|
|
|
87
91
|
fetch: trpcFetchWithAuth,
|
|
88
92
|
headers() {
|
|
89
93
|
const tenantId = getActiveTenantId();
|
|
90
|
-
|
|
94
|
+
const product = process.env.NEXT_PUBLIC_PRODUCT_SLUG || getBrandConfig().storagePrefix;
|
|
95
|
+
return {
|
|
96
|
+
...(tenantId ? { "x-tenant-id": tenantId } : {}),
|
|
97
|
+
...(product ? { "x-product": product } : {}),
|
|
98
|
+
};
|
|
91
99
|
},
|
|
92
100
|
}),
|
|
93
101
|
],
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
/** Origins that are always allowed as redirect targets from checkout responses. */
|
|
2
|
-
const STATIC_ALLOWED_ORIGINS: readonly string[] = [
|
|
3
|
-
"https://checkout.stripe.com",
|
|
4
|
-
"https://billing.stripe.com",
|
|
5
|
-
];
|
|
2
|
+
const STATIC_ALLOWED_ORIGINS: readonly string[] = ["https://checkout.stripe.com", "https://billing.stripe.com"];
|
|
6
3
|
|
|
7
4
|
/**
|
|
8
5
|
* Build the full allowed origins set, including any configured BTCPay Server origin.
|
|
@@ -10,17 +10,14 @@ function isOnMarketplace(): boolean {
|
|
|
10
10
|
return typeof window !== "undefined" && window.location.pathname.startsWith("/marketplace");
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
export function getMarketplaceOnboardingTools(
|
|
14
|
-
deps: MarketplaceOnboardingToolDeps,
|
|
15
|
-
): ModelContextTool[] {
|
|
13
|
+
export function getMarketplaceOnboardingTools(deps: MarketplaceOnboardingToolDeps): ModelContextTool[] {
|
|
16
14
|
const { router } = deps;
|
|
17
15
|
|
|
18
16
|
return [
|
|
19
17
|
// ── Marketplace tools ───────────────────────────────────────────
|
|
20
18
|
{
|
|
21
19
|
name: "marketplace.showSuperpowers",
|
|
22
|
-
description:
|
|
23
|
-
"Filter the marketplace grid by a search query. Navigates to the marketplace page first if needed.",
|
|
20
|
+
description: "Filter the marketplace grid by a search query. Navigates to the marketplace page first if needed.",
|
|
24
21
|
inputSchema: {
|
|
25
22
|
type: "object",
|
|
26
23
|
properties: {
|
|
@@ -34,16 +31,13 @@ export function getMarketplaceOnboardingTools(
|
|
|
34
31
|
router.push(`/marketplace?q=${encodeURIComponent(query)}`);
|
|
35
32
|
return { ok: true, navigated: true };
|
|
36
33
|
}
|
|
37
|
-
window.dispatchEvent(
|
|
38
|
-
new CustomEvent(eventName("marketplace"), { detail: { type: "filter", query } }),
|
|
39
|
-
);
|
|
34
|
+
window.dispatchEvent(new CustomEvent(eventName("marketplace"), { detail: { type: "filter", query } }));
|
|
40
35
|
return { ok: true, navigated: false };
|
|
41
36
|
},
|
|
42
37
|
},
|
|
43
38
|
{
|
|
44
39
|
name: "marketplace.highlightCard",
|
|
45
|
-
description:
|
|
46
|
-
"Pulse/glow a specific plugin card and scroll it into view. Uses data-plugin-card-id attribute.",
|
|
40
|
+
description: "Pulse/glow a specific plugin card and scroll it into view. Uses data-plugin-card-id attribute.",
|
|
47
41
|
inputSchema: {
|
|
48
42
|
type: "object",
|
|
49
43
|
properties: {
|
|
@@ -86,9 +80,7 @@ export function getMarketplaceOnboardingTools(
|
|
|
86
80
|
properties: {},
|
|
87
81
|
},
|
|
88
82
|
handler: async () => {
|
|
89
|
-
window.dispatchEvent(
|
|
90
|
-
new CustomEvent(eventName("marketplace"), { detail: { type: "clearFilter" } }),
|
|
91
|
-
);
|
|
83
|
+
window.dispatchEvent(new CustomEvent(eventName("marketplace"), { detail: { type: "clearFilter" } }));
|
|
92
84
|
return { ok: true };
|
|
93
85
|
},
|
|
94
86
|
},
|
|
@@ -188,9 +180,7 @@ export function getMarketplaceOnboardingTools(
|
|
|
188
180
|
},
|
|
189
181
|
handler: async (params) => {
|
|
190
182
|
const elementId = params.elementId as string;
|
|
191
|
-
const el = document.querySelector(
|
|
192
|
-
`[data-onboarding-id="${CSS.escape(elementId)}"]`,
|
|
193
|
-
) as HTMLElement | null;
|
|
183
|
+
const el = document.querySelector(`[data-onboarding-id="${CSS.escape(elementId)}"]`) as HTMLElement | null;
|
|
194
184
|
if (!el) {
|
|
195
185
|
return { error: `Element with data-onboarding-id='${elementId}' not found` };
|
|
196
186
|
}
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import { isWebMCPAvailable } from "./feature-detect";
|
|
2
|
-
import {
|
|
3
|
-
getMarketplaceOnboardingTools,
|
|
4
|
-
type MarketplaceOnboardingToolDeps,
|
|
5
|
-
} from "./marketplace-onboarding-tools";
|
|
2
|
+
import { getMarketplaceOnboardingTools, type MarketplaceOnboardingToolDeps } from "./marketplace-onboarding-tools";
|
|
6
3
|
import { type ConfirmCallback, getChatWebMCPTools, getWebMCPTools } from "./tools";
|
|
7
4
|
|
|
8
5
|
/**
|
package/src/lib/webmcp/tools.ts
CHANGED
|
@@ -1,10 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
controlInstance,
|
|
3
|
-
createInstance,
|
|
4
|
-
getInstanceHealth,
|
|
5
|
-
getInstanceLogs,
|
|
6
|
-
listInstances,
|
|
7
|
-
} from "@/lib/api";
|
|
1
|
+
import { controlInstance, createInstance, getInstanceHealth, getInstanceLogs, listInstances } from "@/lib/api";
|
|
8
2
|
import { installPlugin } from "@/lib/bot-settings-data";
|
|
9
3
|
import { brandName, eventName, getBrandConfig, productName } from "@/lib/brand-config";
|
|
10
4
|
import { listMarketplacePlugins } from "@/lib/marketplace-data";
|
|
@@ -324,8 +318,7 @@ export function getChatWebMCPTools(): ModelContextTool[] {
|
|
|
324
318
|
},
|
|
325
319
|
{
|
|
326
320
|
name: "setup.begin",
|
|
327
|
-
description:
|
|
328
|
-
"Begin conversational setup for a plugin. Bot receives plugin ID and config schema.",
|
|
321
|
+
description: "Begin conversational setup for a plugin. Bot receives plugin ID and config schema.",
|
|
329
322
|
inputSchema: {
|
|
330
323
|
type: "object",
|
|
331
324
|
properties: {
|
package/src/proxy.ts
CHANGED
|
@@ -1,13 +1,26 @@
|
|
|
1
1
|
import { type NextRequest, NextResponse } from "next/server";
|
|
2
|
-
import { getBrandConfig } from "@/lib/brand-config";
|
|
3
|
-
import { logger } from "@/lib/logger";
|
|
4
|
-
import { sanitizeRedirectUrl } from "@/lib/utils";
|
|
5
2
|
|
|
6
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Middleware — CSP headers, CSRF protection, nonce generation, tenant cookie forwarding.
|
|
5
|
+
*
|
|
6
|
+
* NO auth checks. Pages that need auth use useRequireAuth() from @/lib/require-auth.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const MUTATION_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
10
|
+
|
|
11
|
+
const CSRF_EXEMPT_AUTH_PATHS = ["/api/auth/callback"];
|
|
12
|
+
|
|
13
|
+
// Middleware runs at edge before initBrandConfig(). Read cookie name from
|
|
14
|
+
// env var directly — this is the ONE place process.env is acceptable for
|
|
15
|
+
// brand config, because middleware can't await the core API.
|
|
16
|
+
const TENANT_COOKIE_NAME =
|
|
17
|
+
process.env.NEXT_PUBLIC_BRAND_TENANT_COOKIE ||
|
|
18
|
+
`${process.env.NEXT_PUBLIC_BRAND_STORAGE_PREFIX || "platform"}_tenant_id`;
|
|
19
|
+
|
|
20
|
+
const NONCE_STYLES_ENABLED = true;
|
|
7
21
|
|
|
8
22
|
/** Derive API origin from request hostname. Convention: api.<domain>. */
|
|
9
23
|
function getApiOrigin(host: string): string {
|
|
10
|
-
// Explicit override for local dev
|
|
11
24
|
if (process.env.NEXT_PUBLIC_API_URL) {
|
|
12
25
|
try {
|
|
13
26
|
return new URL(process.env.NEXT_PUBLIC_API_URL).origin;
|
|
@@ -24,21 +37,6 @@ function getApiOrigin(host: string): string {
|
|
|
24
37
|
return `https://api.${hostname}`;
|
|
25
38
|
}
|
|
26
39
|
|
|
27
|
-
/**
|
|
28
|
-
* Only add upgrade-insecure-requests when actually serving over HTTPS.
|
|
29
|
-
* Checking NODE_ENV breaks local dev in Docker (NODE_ENV=production but no TLS).
|
|
30
|
-
* Computed per-request in buildCsp() from the request URL protocol.
|
|
31
|
-
*/
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Nonce-based style-src toggle.
|
|
35
|
-
*
|
|
36
|
-
* Enabled — style-src uses nonce-based policy instead of 'unsafe-inline'.
|
|
37
|
-
* Tailwind v4 compiles styles at build time (no runtime injection).
|
|
38
|
-
* framer-motion nonce support is provided via MotionConfig in the root layout.
|
|
39
|
-
*/
|
|
40
|
-
const NONCE_STYLES_ENABLED = true;
|
|
41
|
-
|
|
42
40
|
/** Build the CSP header value with a per-request nonce. */
|
|
43
41
|
function buildCsp(nonce: string, requestUrl?: string, requestHost?: string): string {
|
|
44
42
|
const isHttps = requestUrl ? requestUrl.startsWith("https://") : false;
|
|
@@ -49,10 +47,10 @@ function buildCsp(nonce: string, requestUrl?: string, requestHost?: string): str
|
|
|
49
47
|
...(NONCE_STYLES_ENABLED
|
|
50
48
|
? [`style-src-elem 'self' 'unsafe-inline' 'nonce-${nonce}'`, "style-src-attr 'unsafe-inline'"]
|
|
51
49
|
: ["style-src 'self' 'unsafe-inline'"]),
|
|
52
|
-
"img-src 'self' data: blob:",
|
|
50
|
+
"img-src 'self' data: blob: https:",
|
|
53
51
|
"font-src 'self'",
|
|
54
52
|
`connect-src 'self' https://api.stripe.com${api ? ` ${api}` : ""}`,
|
|
55
|
-
"frame-src https://js.stripe.com",
|
|
53
|
+
"frame-src 'self' https://js.stripe.com",
|
|
56
54
|
"frame-ancestors 'none'",
|
|
57
55
|
"base-uri 'self'",
|
|
58
56
|
"form-action 'self'",
|
|
@@ -61,241 +59,66 @@ function buildCsp(nonce: string, requestUrl?: string, requestHost?: string): str
|
|
|
61
59
|
].join("; ");
|
|
62
60
|
}
|
|
63
61
|
|
|
64
|
-
const publicPaths = [
|
|
65
|
-
"/login",
|
|
66
|
-
"/signup",
|
|
67
|
-
"/forgot-password",
|
|
68
|
-
"/reset-password",
|
|
69
|
-
"/auth/callback",
|
|
70
|
-
"/auth/verify",
|
|
71
|
-
"/api/auth/",
|
|
72
|
-
];
|
|
73
|
-
|
|
74
|
-
/** Paths that are public only when matched exactly (not as a prefix). */
|
|
75
|
-
const publicExactPaths = new Set([
|
|
76
|
-
"/",
|
|
77
|
-
"/og",
|
|
78
|
-
"/terms",
|
|
79
|
-
"/privacy",
|
|
80
|
-
"/pricing",
|
|
81
|
-
"/status",
|
|
82
|
-
// Health endpoint must be publicly accessible for infra probes (uptime monitors,
|
|
83
|
-
// Kubernetes liveness/readiness, load balancers) that do not carry session cookies.
|
|
84
|
-
"/api/health",
|
|
85
|
-
// Better Auth root endpoint — sub-paths matched via publicPaths prefix list (/api/auth/).
|
|
86
|
-
"/api/auth",
|
|
87
|
-
]);
|
|
88
|
-
|
|
89
|
-
const MUTATION_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Mutation paths under /api/auth that are exempt from CSRF origin validation.
|
|
93
|
-
* OAuth identity providers POST to callback URLs cross-origin — these cannot
|
|
94
|
-
* carry a matching Origin header, so we must allow them through.
|
|
95
|
-
* All other /api/auth mutations (sign-in, sign-up, sign-out, etc.) are
|
|
96
|
-
* validated like any other /api route.
|
|
97
|
-
*/
|
|
98
|
-
const CSRF_EXEMPT_AUTH_PATHS = [
|
|
99
|
-
"/api/auth/callback", // e.g. /api/auth/callback/google, /api/auth/callback/github
|
|
100
|
-
];
|
|
101
|
-
|
|
102
|
-
const PLATFORM_BASE_URL = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:3001";
|
|
103
|
-
|
|
104
|
-
const TENANT_COOKIE_NAME = getBrandConfig().tenantCookieName;
|
|
105
|
-
|
|
106
|
-
/** Post-auth landing page — configurable per brand (default: /marketplace). */
|
|
107
|
-
const HOME_PATH = (() => {
|
|
108
|
-
const p = (process.env.NEXT_PUBLIC_BRAND_HOME_PATH || "/marketplace").trim();
|
|
109
|
-
if (!p || /^https?:\/\//i.test(p)) return "/marketplace";
|
|
110
|
-
return p.startsWith("/") ? p : `/${p}`;
|
|
111
|
-
})();
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Validate that a state-changing request originates from this application.
|
|
115
|
-
* Checks the Origin header (preferred) with Referer as fallback.
|
|
116
|
-
* Returns true if the request is safe, false if it should be blocked.
|
|
117
|
-
*/
|
|
118
62
|
export function validateCsrfOrigin(request: NextRequest): boolean {
|
|
119
63
|
const origin = request.headers.get("origin");
|
|
120
64
|
const referer = request.headers.get("referer");
|
|
121
65
|
const host = request.headers.get("host");
|
|
122
|
-
|
|
123
66
|
if (!host) return false;
|
|
124
|
-
|
|
125
|
-
// Build the allowed origin using the request's actual protocol only,
|
|
126
|
-
// preventing protocol downgrade attacks (e.g. HTTP origin to HTTPS endpoint)
|
|
127
|
-
const protocol = request.nextUrl.protocol; // "https:" or "http:"
|
|
67
|
+
const protocol = request.nextUrl.protocol;
|
|
128
68
|
const allowedOrigin = `${protocol}//${host}`;
|
|
129
|
-
|
|
130
|
-
// Check Origin header first (most reliable)
|
|
131
|
-
if (origin) {
|
|
132
|
-
return origin === allowedOrigin;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Fall back to Referer header
|
|
69
|
+
if (origin) return origin === allowedOrigin;
|
|
136
70
|
if (referer) {
|
|
137
71
|
try {
|
|
138
|
-
|
|
139
|
-
return refererOrigin === allowedOrigin;
|
|
72
|
+
return new URL(referer).origin === allowedOrigin;
|
|
140
73
|
} catch {
|
|
141
|
-
// Malformed referer URL — treat as non-matching origin
|
|
142
74
|
return false;
|
|
143
75
|
}
|
|
144
76
|
}
|
|
145
|
-
|
|
146
|
-
// No Origin or Referer on a mutation request is suspicious — block it.
|
|
147
|
-
// Legitimate browser form submissions and fetch() calls include Origin.
|
|
148
77
|
return false;
|
|
149
78
|
}
|
|
150
79
|
|
|
151
|
-
/**
|
|
152
|
-
* Fetch the authenticated user's role from Better Auth's get-session endpoint.
|
|
153
|
-
* Returns the role string (e.g. "platform_admin", "user") or null if the
|
|
154
|
-
* session is invalid or the request fails. Fails closed: any error → null.
|
|
155
|
-
*/
|
|
156
|
-
async function getSessionRole(request: NextRequest): Promise<string | null> {
|
|
157
|
-
const sessionCookie =
|
|
158
|
-
request.cookies.get("better-auth.session_token") ??
|
|
159
|
-
request.cookies.get("__Secure-better-auth.session_token");
|
|
160
|
-
|
|
161
|
-
if (!sessionCookie?.value.trim()) return null;
|
|
162
|
-
|
|
163
|
-
try {
|
|
164
|
-
const res = await fetch(`${PLATFORM_BASE_URL}/api/auth/get-session`, {
|
|
165
|
-
headers: {
|
|
166
|
-
cookie: `${sessionCookie.name}=${sessionCookie.value}`,
|
|
167
|
-
},
|
|
168
|
-
});
|
|
169
|
-
if (!res.ok) return null;
|
|
170
|
-
const data = await res.json();
|
|
171
|
-
return data?.user?.role ?? null;
|
|
172
|
-
} catch (e) {
|
|
173
|
-
log.warn("Failed to fetch user role for middleware routing", e);
|
|
174
|
-
return null;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
80
|
export default async function middleware(request: NextRequest) {
|
|
179
81
|
const { pathname } = request.nextUrl;
|
|
180
|
-
const host = request.headers.get("host") || "";
|
|
181
82
|
|
|
182
|
-
//
|
|
83
|
+
// Sidecar proxy: rewrite /_sidecar/* to the core API.
|
|
84
|
+
// The core's tenant-proxy middleware resolves the user's instance
|
|
85
|
+
// from their session, finds the container URL, and proxies upstream.
|
|
86
|
+
if (pathname.startsWith("/_sidecar")) {
|
|
87
|
+
const apiOrigin = process.env.INTERNAL_API_URL || "http://core:3001";
|
|
88
|
+
const target = new URL(`${apiOrigin}${pathname}${request.nextUrl.search}`);
|
|
89
|
+
return NextResponse.rewrite(target);
|
|
90
|
+
}
|
|
91
|
+
|
|
183
92
|
const nonce = crypto.randomUUID();
|
|
184
93
|
const cspHeaderValue = buildCsp(nonce, request.url, request.headers.get("host") ?? "");
|
|
185
94
|
|
|
186
|
-
/** Apply CSP and cache-busting headers to any response before returning it. */
|
|
187
95
|
function withCsp(response: NextResponse): NextResponse {
|
|
188
96
|
response.headers.set("Content-Security-Policy", cspHeaderValue);
|
|
189
|
-
// Nonce is passed to server components via request headers (not response headers).
|
|
190
|
-
// See nextWithNonce() below — it uses NextResponse.next({ request: { headers } }).
|
|
191
97
|
response.headers.set("Vary", "*");
|
|
192
98
|
return response;
|
|
193
99
|
}
|
|
194
100
|
|
|
195
|
-
/**
|
|
196
|
-
* Create a NextResponse.next() that forwards the CSP nonce to server components
|
|
197
|
-
* via request headers, without exposing it in the HTTP response.
|
|
198
|
-
*/
|
|
199
101
|
function nextWithNonce(): NextResponse {
|
|
200
102
|
const requestHeaders = new Headers(request.headers);
|
|
201
103
|
requestHeaders.set("x-nonce", nonce);
|
|
202
|
-
|
|
203
|
-
// Strip any client-supplied x-tenant-id before conditionally setting from the
|
|
204
|
-
// trusted HttpOnly cookie. Without this delete, a client that sends their own
|
|
205
|
-
// x-tenant-id header could spoof a tenant when no cookie is present.
|
|
206
104
|
requestHeaders.delete("x-tenant-id");
|
|
207
|
-
|
|
208
|
-
// Forward HttpOnly tenant cookie as request header for server components
|
|
209
105
|
const tenantCookie = request.cookies.get(TENANT_COOKIE_NAME);
|
|
210
106
|
if (tenantCookie?.value) {
|
|
211
107
|
requestHeaders.set("x-tenant-id", tenantCookie.value);
|
|
212
108
|
}
|
|
213
|
-
|
|
214
109
|
return NextResponse.next({ request: { headers: requestHeaders } });
|
|
215
110
|
}
|
|
216
111
|
|
|
217
|
-
// CSRF protection
|
|
112
|
+
// CSRF protection on API mutations
|
|
218
113
|
if (pathname.startsWith("/api") && MUTATION_METHODS.has(request.method)) {
|
|
219
|
-
// OAuth callback endpoints receive cross-origin POSTs from identity providers (POST only)
|
|
220
114
|
const isCsrfExempt =
|
|
221
|
-
CSRF_EXEMPT_AUTH_PATHS.some((p) => pathname === p || pathname.startsWith(`${p}/`)) &&
|
|
222
|
-
request.method === "POST";
|
|
115
|
+
CSRF_EXEMPT_AUTH_PATHS.some((p) => pathname === p || pathname.startsWith(`${p}/`)) && request.method === "POST";
|
|
223
116
|
if (!isCsrfExempt && !validateCsrfOrigin(request)) {
|
|
224
117
|
return NextResponse.json({ error: "CSRF validation failed" }, { status: 403 });
|
|
225
118
|
}
|
|
226
119
|
}
|
|
227
120
|
|
|
228
|
-
//
|
|
229
|
-
// On the app subdomain, redirect to HOME_PATH. On the base domain, redirect to app subdomain.
|
|
230
|
-
// NOTE: This check requires the Better Auth server to set the session cookie with
|
|
231
|
-
// domain=".<base-domain>" so it is visible on both the app and marketing subdomains.
|
|
232
|
-
// See: wopr-platform/src/auth/better-auth.ts advanced.cookies.session_token.attributes.domain
|
|
233
|
-
if (pathname === "/") {
|
|
234
|
-
const sessionToken =
|
|
235
|
-
request.cookies.get("better-auth.session_token") ??
|
|
236
|
-
request.cookies.get("__Secure-better-auth.session_token");
|
|
237
|
-
if (sessionToken?.value.trim()) {
|
|
238
|
-
const appDomain =
|
|
239
|
-
process.env.NEXT_PUBLIC_BRAND_APP_DOMAIN || process.env.NEXT_PUBLIC_APP_DOMAIN;
|
|
240
|
-
if (appDomain && !host.startsWith("app.")) {
|
|
241
|
-
// On marketing domain — redirect to the app subdomain
|
|
242
|
-
const appUrl = new URL(`https://${appDomain}`);
|
|
243
|
-
appUrl.pathname = HOME_PATH;
|
|
244
|
-
return withCsp(NextResponse.redirect(appUrl));
|
|
245
|
-
}
|
|
246
|
-
// On app subdomain (or no configured app domain) — redirect to home
|
|
247
|
-
return withCsp(NextResponse.redirect(new URL(HOME_PATH, request.url)));
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// --- Admin route authorization (server-side) ---
|
|
252
|
-
// Non-admins are redirected before any page JS loads.
|
|
253
|
-
// Unauthenticated users fall through to the session check below (→ /login).
|
|
254
|
-
if (pathname.startsWith("/admin")) {
|
|
255
|
-
const sessionCookie =
|
|
256
|
-
request.cookies.get("better-auth.session_token") ??
|
|
257
|
-
request.cookies.get("__Secure-better-auth.session_token");
|
|
258
|
-
if (sessionCookie?.value.trim()) {
|
|
259
|
-
const role = await getSessionRole(request);
|
|
260
|
-
if (role !== "platform_admin") {
|
|
261
|
-
return withCsp(NextResponse.redirect(new URL(HOME_PATH, request.url)));
|
|
262
|
-
}
|
|
263
|
-
// Admin confirmed — serve page with anti-cache headers so revocation
|
|
264
|
-
// is detected on the very next navigation (browser must revalidate).
|
|
265
|
-
const response = nextWithNonce();
|
|
266
|
-
response.headers.set("Cache-Control", "no-store, no-cache, must-revalidate");
|
|
267
|
-
response.headers.set("Pragma", "no-cache");
|
|
268
|
-
response.headers.set("Expires", "0");
|
|
269
|
-
return withCsp(response);
|
|
270
|
-
}
|
|
271
|
-
// No session cookie → fall through to the session check below which redirects to /login
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// Allow public paths
|
|
275
|
-
if (publicExactPaths.has(pathname) || publicPaths.some((p) => pathname.startsWith(p))) {
|
|
276
|
-
return withCsp(nextWithNonce());
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// Allow static files (but not API paths with dots, e.g. /api/config.json)
|
|
280
|
-
if (pathname.startsWith("/_next") || (pathname.includes(".") && !pathname.startsWith("/api"))) {
|
|
281
|
-
return withCsp(nextWithNonce());
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// Check for session cookie (Better Auth uses "better-auth.session_token" by default).
|
|
285
|
-
// NOTE: Bearer token auth (Authorization: Bearer <token>) is intentionally not supported
|
|
286
|
-
// here. This is a browser-facing UI application; all API consumers are the Next.js
|
|
287
|
-
// front-end itself (cookie-based). Automation/SDK/CLI clients should use the platform
|
|
288
|
-
// API directly (wopr-platform), which issues and validates Bearer tokens independently.
|
|
289
|
-
const sessionToken =
|
|
290
|
-
request.cookies.get("better-auth.session_token") ??
|
|
291
|
-
request.cookies.get("__Secure-better-auth.session_token");
|
|
292
|
-
|
|
293
|
-
if (!sessionToken || !sessionToken.value.trim()) {
|
|
294
|
-
const loginUrl = new URL("/login", request.url);
|
|
295
|
-
loginUrl.searchParams.set("callbackUrl", sanitizeRedirectUrl(pathname));
|
|
296
|
-
return withCsp(NextResponse.redirect(loginUrl));
|
|
297
|
-
}
|
|
298
|
-
|
|
121
|
+
// Everything gets CSP + nonce. Auth is handled by pages via useRequireAuth().
|
|
299
122
|
return withCsp(nextWithNonce());
|
|
300
123
|
}
|
|
301
124
|
|