@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.
Files changed (353) hide show
  1. package/next.config.ts +1 -2
  2. package/package.json +17 -17
  3. package/src/__tests__/account-switcher.test.tsx +21 -20
  4. package/src/__tests__/activity-page.test.tsx +2 -6
  5. package/src/__tests__/add-payment-method-dialog.test.tsx +9 -32
  6. package/src/__tests__/admin-api.test.ts +1 -6
  7. package/src/__tests__/admin-gpu-api.test.ts +1 -3
  8. package/src/__tests__/admin-marketplace-api.test.ts +1 -4
  9. package/src/__tests__/admin-middleware.test.ts +76 -83
  10. package/src/__tests__/affiliate-dashboard.test.tsx +3 -3
  11. package/src/__tests__/api-401-redirect.test.ts +46 -9
  12. package/src/__tests__/api-client.test.ts +3 -5
  13. package/src/__tests__/api-config.test.ts +22 -42
  14. package/src/__tests__/api-fleet-resources.test.ts +1 -2
  15. package/src/__tests__/api-fleet-trpc.test.ts +2 -8
  16. package/src/__tests__/api-null-guards.test.ts +3 -1
  17. package/src/__tests__/audit-log-table-pagination.test.tsx +2 -6
  18. package/src/__tests__/auth-password-reset.test.tsx +7 -21
  19. package/src/__tests__/auth-redirect.test.tsx +8 -2
  20. package/src/__tests__/auth.test.tsx +25 -23
  21. package/src/__tests__/auto-topup-card.test.tsx +4 -12
  22. package/src/__tests__/backups-tab.test.tsx +3 -4
  23. package/src/__tests__/billing-layout-nav-hidden.test.tsx +5 -37
  24. package/src/__tests__/billing-payment-org-invoices.test.tsx +2 -18
  25. package/src/__tests__/billing.test.tsx +8 -39
  26. package/src/__tests__/bot-settings/resources-tab.test.tsx +1 -3
  27. package/src/__tests__/bot-settings/storage-tab.test.tsx +1 -3
  28. package/src/__tests__/bot-settings/vps-upgrade-card.test.tsx +1 -3
  29. package/src/__tests__/bot-settings-restart.test.tsx +1 -3
  30. package/src/__tests__/bot-settings.test.tsx +2 -6
  31. package/src/__tests__/brand.test.ts +6 -26
  32. package/src/__tests__/buy-credits-panel.test.tsx +1 -3
  33. package/src/__tests__/buy-crypto-credits-panel.test.tsx +101 -119
  34. package/src/__tests__/capability-conflicts.test.ts +2 -8
  35. package/src/__tests__/capability-resolver.test.tsx +2 -12
  36. package/src/__tests__/channel-wizard.test.tsx +4 -17
  37. package/src/__tests__/chat/chat-panel.test.tsx +1 -4
  38. package/src/__tests__/chat-store.test.ts +5 -15
  39. package/src/__tests__/command-center.test.tsx +10 -12
  40. package/src/__tests__/compliance-retention-edit.test.tsx +3 -6
  41. package/src/__tests__/confirmation-tracker.test.tsx +3 -18
  42. package/src/__tests__/coupon-input.test.tsx +1 -3
  43. package/src/__tests__/create-instance.test.tsx +1 -3
  44. package/src/__tests__/credit-balance.test.tsx +4 -12
  45. package/src/__tests__/credits.test.tsx +32 -85
  46. package/src/__tests__/email-verification-banner.test.tsx +2 -6
  47. package/src/__tests__/error-boundaries.test.tsx +0 -1
  48. package/src/__tests__/fetch-pricing.test.ts +2 -1
  49. package/src/__tests__/field-oauth.test.tsx +2 -6
  50. package/src/__tests__/fixtures/mock-manifests-data.js +1 -3
  51. package/src/__tests__/fixtures/mock-manifests.ts +2 -4
  52. package/src/__tests__/fleet-health-timestamp.test.tsx +1 -8
  53. package/src/__tests__/fleet-health-update.test.tsx +1 -8
  54. package/src/__tests__/gpu-dashboard.test.tsx +2 -6
  55. package/src/__tests__/instance-detail.test.tsx +3 -9
  56. package/src/__tests__/instance-list.test.tsx +1 -5
  57. package/src/__tests__/layout-snapshots.test.tsx +64 -11
  58. package/src/__tests__/marketplace-admin.test.tsx +2 -6
  59. package/src/__tests__/marketplace.test.tsx +11 -35
  60. package/src/__tests__/merge-api-rates.test.ts +1 -6
  61. package/src/__tests__/middleware.test.ts +32 -219
  62. package/src/__tests__/next-config-headers.test.ts +1 -3
  63. package/src/__tests__/notifications.test.tsx +4 -11
  64. package/src/__tests__/oauth-buttons.test.tsx +36 -59
  65. package/src/__tests__/oauth-error-mapping.test.tsx +2 -6
  66. package/src/__tests__/observability.test.tsx +23 -36
  67. package/src/__tests__/onboarding-page.test.tsx +4 -6
  68. package/src/__tests__/org-billing-api.test.tsx +1 -6
  69. package/src/__tests__/plugin-install-flow.test.tsx +28 -58
  70. package/src/__tests__/plugin-registry.test.tsx +3 -11
  71. package/src/__tests__/plugin-tool-sync.test.ts +1 -3
  72. package/src/__tests__/plugins-catalog-error.test.tsx +2 -6
  73. package/src/__tests__/plugins-toggle-race.test.tsx +3 -5
  74. package/src/__tests__/portfolio-chart.test.tsx +2 -6
  75. package/src/__tests__/promotion-form.test.tsx +2 -6
  76. package/src/__tests__/promotions-list.test.tsx +1 -3
  77. package/src/__tests__/provider-key-api.test.ts +2 -1
  78. package/src/__tests__/resend-verification-button.test.tsx +8 -24
  79. package/src/__tests__/secrets-audit-pagination.test.tsx +1 -3
  80. package/src/__tests__/settings.test.tsx +11 -21
  81. package/src/__tests__/setup-checklist.test.tsx +3 -9
  82. package/src/__tests__/setup.ts +25 -6
  83. package/src/__tests__/snapshot-api.test.ts +2 -1
  84. package/src/__tests__/step-superpowers.test.tsx +1 -3
  85. package/src/__tests__/tenant-context.test.tsx +1 -6
  86. package/src/__tests__/tenant-keys-api.test.ts +3 -4
  87. package/src/__tests__/tenant-table-pagination.test.tsx +2 -6
  88. package/src/__tests__/terminal-log-cleanup.test.tsx +0 -1
  89. package/src/__tests__/transaction-history.test.tsx +190 -238
  90. package/src/__tests__/trpc-types.test.ts +2 -6
  91. package/src/__tests__/use-chat.test.ts +1 -3
  92. package/src/__tests__/use-plugin-setup-chat-stale-closure.test.ts +1 -4
  93. package/src/__tests__/use-sidecar-bridge.test.tsx +105 -0
  94. package/src/__tests__/use-webmcp.test.ts +1 -3
  95. package/src/__tests__/validate-elevenlabs-key.test.ts +2 -1
  96. package/src/__tests__/verify-page.test.tsx +4 -13
  97. package/src/__tests__/verify-redirect.test.tsx +2 -6
  98. package/src/app/(auth)/error.tsx +1 -7
  99. package/src/app/(auth)/forgot-password/page.tsx +4 -18
  100. package/src/app/(auth)/login/page.tsx +5 -22
  101. package/src/app/(auth)/reset-password/page.tsx +2 -12
  102. package/src/app/(auth)/signup/page.tsx +10 -44
  103. package/src/app/(auth)/verify/page.tsx +47 -0
  104. package/src/app/(dashboard)/billing/credits/page.tsx +14 -67
  105. package/src/app/(dashboard)/billing/error.tsx +2 -10
  106. package/src/app/(dashboard)/billing/layout.tsx +12 -62
  107. package/src/app/(dashboard)/billing/payment/page.tsx +17 -68
  108. package/src/app/(dashboard)/billing/plans/page.tsx +3 -9
  109. package/src/app/(dashboard)/billing/usage/hosted/page.tsx +8 -25
  110. package/src/app/(dashboard)/billing/usage/page.tsx +63 -103
  111. package/src/app/(dashboard)/changesets/[id]/changeset-detail-client.tsx +9 -27
  112. package/src/app/(dashboard)/changesets/[id]/error.tsx +2 -6
  113. package/src/app/(dashboard)/changesets/error.tsx +1 -7
  114. package/src/app/(dashboard)/chat/page.tsx +2 -6
  115. package/src/app/(dashboard)/dashboard/network/page.tsx +5 -19
  116. package/src/app/(dashboard)/error.tsx +1 -7
  117. package/src/app/(dashboard)/layout.tsx +15 -36
  118. package/src/app/(dashboard)/marketplace/[plugin]/page.tsx +14 -51
  119. package/src/app/(dashboard)/marketplace/error.tsx +1 -7
  120. package/src/app/(dashboard)/marketplace/page.tsx +6 -27
  121. package/src/app/(dashboard)/not-found.tsx +2 -5
  122. package/src/app/(dashboard)/onboarding/page.tsx +5 -22
  123. package/src/app/(dashboard)/settings/account/page.tsx +1 -6
  124. package/src/app/(dashboard)/settings/activity/page.tsx +8 -34
  125. package/src/app/(dashboard)/settings/api-keys/page.tsx +15 -60
  126. package/src/app/(dashboard)/settings/brain/page.tsx +9 -31
  127. package/src/app/(dashboard)/settings/error.tsx +2 -10
  128. package/src/app/(dashboard)/settings/notifications/page.tsx +2 -6
  129. package/src/app/(dashboard)/settings/org/page.tsx +13 -56
  130. package/src/app/(dashboard)/settings/page.tsx +1 -0
  131. package/src/app/(dashboard)/settings/profile/page.tsx +126 -73
  132. package/src/app/(dashboard)/settings/providers/page.tsx +21 -78
  133. package/src/app/(dashboard)/settings/secrets/page.tsx +13 -58
  134. package/src/app/(dashboard)/settings/security/page.tsx +31 -111
  135. package/src/app/admin/email-templates/email-templates-client.tsx +15 -58
  136. package/src/app/admin/error.tsx +1 -7
  137. package/src/app/admin/fleet-updates/error.tsx +1 -7
  138. package/src/app/admin/fleet-updates/fleet-updates-client.tsx +10 -50
  139. package/src/app/admin/layout.tsx +4 -0
  140. package/src/app/admin/payment-methods/page.tsx +9 -38
  141. package/src/app/admin/products/error.tsx +2 -7
  142. package/src/app/admin/products/page.tsx +1 -4
  143. package/src/app/admin/promotions/[id]/page.tsx +9 -38
  144. package/src/app/admin/promotions/page.tsx +9 -36
  145. package/src/app/admin/rate-overrides/page.tsx +9 -45
  146. package/src/app/auth/callback/[provider]/page.tsx +1 -8
  147. package/src/app/auth/verify/page.tsx +9 -36
  148. package/src/app/channels/error.tsx +2 -10
  149. package/src/app/channels/layout.tsx +9 -0
  150. package/src/app/channels/page.tsx +8 -20
  151. package/src/app/channels/setup/[plugin]/page.tsx +3 -5
  152. package/src/app/error.tsx +1 -7
  153. package/src/app/fleet/error.tsx +1 -7
  154. package/src/app/fleet/layout.tsx +5 -0
  155. package/src/app/fleet/settings/page.tsx +1 -3
  156. package/src/app/global-error.tsx +2 -10
  157. package/src/app/globals.css +1 -4
  158. package/src/app/instances/[id]/instance-detail-client.tsx +51 -125
  159. package/src/app/instances/error.tsx +2 -10
  160. package/src/app/instances/instance-list-client.tsx +20 -69
  161. package/src/app/instances/layout.tsx +9 -0
  162. package/src/app/instances/new/create-instance-client.tsx +10 -31
  163. package/src/app/layout.tsx +2 -10
  164. package/src/app/not-found.tsx +1 -3
  165. package/src/app/page.tsx +1 -2
  166. package/src/app/plugins/error.tsx +2 -10
  167. package/src/app/plugins/layout.tsx +5 -0
  168. package/src/app/plugins/page.tsx +16 -48
  169. package/src/app/pricing/error.tsx +1 -7
  170. package/src/app/privacy/page.tsx +93 -150
  171. package/src/app/status/error.tsx +1 -7
  172. package/src/app/terms/page.tsx +89 -144
  173. package/src/components/account-switcher.tsx +25 -52
  174. package/src/components/admin/accounting-dashboard.tsx +1 -3
  175. package/src/components/admin/admin-guard.tsx +1 -3
  176. package/src/components/admin/admin-nav.tsx +1 -3
  177. package/src/components/admin/affiliate-dashboard.tsx +25 -94
  178. package/src/components/admin/audit-log-table.tsx +13 -49
  179. package/src/components/admin/billing-health-dashboard.tsx +7 -25
  180. package/src/components/admin/bulk-actions-bar.test.tsx +1 -7
  181. package/src/components/admin/bulk-actions-bar.tsx +1 -3
  182. package/src/components/admin/bulk-export-dialog.test.tsx +1 -7
  183. package/src/components/admin/bulk-export-dialog.tsx +6 -32
  184. package/src/components/admin/bulk-grant-dialog.test.tsx +2 -6
  185. package/src/components/admin/bulk-grant-dialog.tsx +4 -15
  186. package/src/components/admin/bulk-preview-dialog.tsx +3 -12
  187. package/src/components/admin/bulk-reactivate-dialog.tsx +1 -7
  188. package/src/components/admin/bulk-select-all-banner.tsx +1 -6
  189. package/src/components/admin/bulk-suspend-dialog.tsx +5 -12
  190. package/src/components/admin/bulk-undo-toast.tsx +1 -2
  191. package/src/components/admin/compliance-dashboard.tsx +31 -101
  192. package/src/components/admin/gpu-dashboard.tsx +21 -70
  193. package/src/components/admin/grant-credits-dialog.tsx +4 -17
  194. package/src/components/admin/incident-dashboard.tsx +10 -25
  195. package/src/components/admin/inference-dashboard.tsx +14 -54
  196. package/src/components/admin/marketplace-admin.tsx +18 -60
  197. package/src/components/admin/migrations-dashboard.tsx +9 -42
  198. package/src/components/admin/onboarding-dashboard.tsx +14 -64
  199. package/src/components/admin/pool-config-dashboard.tsx +4 -10
  200. package/src/components/admin/products/fleet-form.tsx +2 -11
  201. package/src/components/admin/products/nav-editor.tsx +3 -10
  202. package/src/components/admin/promotions/promotion-form.tsx +9 -42
  203. package/src/components/admin/roles-dashboard.tsx +7 -34
  204. package/src/components/admin/suspend-dialog.tsx +4 -11
  205. package/src/components/admin/tenant-notes-panel.tsx +1 -3
  206. package/src/components/admin/tenant-row-actions.tsx +4 -20
  207. package/src/components/admin/tenant-table.tsx +12 -49
  208. package/src/components/auth/auth-redirect.tsx +11 -3
  209. package/src/components/auth/email-verification-result-banner.tsx +1 -3
  210. package/src/components/auth/resend-verification-button.tsx +2 -10
  211. package/src/components/auth/wopr-wordmark.tsx +1 -3
  212. package/src/components/billing/add-payment-method-dialog.tsx +1 -2
  213. package/src/components/billing/affiliate-dashboard.tsx +4 -16
  214. package/src/components/billing/amount-selector.tsx +1 -3
  215. package/src/components/billing/auto-topup-card.tsx +2 -11
  216. package/src/components/billing/buy-credits-panel.tsx +4 -14
  217. package/src/components/billing/byok-callout.tsx +6 -8
  218. package/src/components/billing/confirmation-tracker.tsx +4 -14
  219. package/src/components/billing/credit-balance-badge.tsx +22 -0
  220. package/src/components/billing/credit-balance.tsx +3 -9
  221. package/src/components/billing/crypto-checkout.tsx +5 -24
  222. package/src/components/billing/degraded-state-banner.tsx +1 -3
  223. package/src/components/billing/deposit-view.tsx +301 -41
  224. package/src/components/billing/dividend-banner.tsx +1 -3
  225. package/src/components/billing/dividend-eligibility.tsx +3 -12
  226. package/src/components/billing/dividend-pool-stats.tsx +6 -20
  227. package/src/components/billing/first-dividend-dialog.tsx +2 -2
  228. package/src/components/billing/org-billing-page.tsx +8 -31
  229. package/src/components/billing/payment-method-picker.tsx +2 -10
  230. package/src/components/billing/suspension-banner.tsx +2 -7
  231. package/src/components/billing/transaction-history.tsx +10 -58
  232. package/src/components/billing/unified-checkout.tsx +547 -0
  233. package/src/components/bot-settings/backups-tab.tsx +9 -33
  234. package/src/components/bot-settings/bot-settings-client.tsx +32 -134
  235. package/src/components/bot-settings/resources-tab.tsx +2 -9
  236. package/src/components/bot-settings/storage-tab.tsx +19 -48
  237. package/src/components/bot-settings/vps-info-panel.tsx +3 -11
  238. package/src/components/bot-settings/vps-upgrade-card.tsx +3 -4
  239. package/src/components/brand-hydrator.tsx +13 -0
  240. package/src/components/channel-wizard/field-interactive.tsx +1 -3
  241. package/src/components/channel-wizard/field-qr.tsx +10 -39
  242. package/src/components/channel-wizard/step-renderer.tsx +5 -28
  243. package/src/components/channel-wizard/wizard.tsx +6 -31
  244. package/src/components/chat/chat-message.tsx +1 -4
  245. package/src/components/chat/chat-panel.tsx +4 -18
  246. package/src/components/chat/chat-widget.tsx +3 -14
  247. package/src/components/dashboard/command-center.tsx +15 -61
  248. package/src/components/fleet/update-settings-card.tsx +7 -23
  249. package/src/components/instance-update-banner.tsx +130 -0
  250. package/src/components/instances/friends-tab.test.tsx +2 -9
  251. package/src/components/instances/friends-tab.tsx +18 -74
  252. package/src/components/instances/update-available-badge.tsx +2 -11
  253. package/src/components/landing/hero.tsx +3 -9
  254. package/src/components/landing/landing-page.tsx +1 -3
  255. package/src/components/landing/portfolio-chart.tsx +4 -9
  256. package/src/components/landing/story-sections.tsx +1 -3
  257. package/src/components/landing/terminal-sequence.tsx +4 -17
  258. package/src/components/marketplace/empty-state.tsx +2 -6
  259. package/src/components/marketplace/first-visit-hero.tsx +1 -3
  260. package/src/components/marketplace/install-wizard.tsx +20 -77
  261. package/src/components/marketplace/marketplace-tabs.tsx +1 -4
  262. package/src/components/marketplace/plugin-card.tsx +2 -9
  263. package/src/components/marketplace/superpower-content.tsx +1 -3
  264. package/src/components/marketplace/terminal-search.tsx +2 -8
  265. package/src/components/oauth-buttons.tsx +29 -14
  266. package/src/components/observability/fleet-health.tsx +5 -18
  267. package/src/components/observability/health-overview.tsx +7 -20
  268. package/src/components/observability/logs-viewer.tsx +8 -32
  269. package/src/components/observability/metrics-dashboard.tsx +2 -15
  270. package/src/components/onboarding/fallback-setup.tsx +6 -25
  271. package/src/components/onboarding/setup-checklist.tsx +18 -51
  272. package/src/components/onboarding/step-superpowers.tsx +1 -4
  273. package/src/components/plugin-setup/setup-chat-panel.tsx +6 -22
  274. package/src/components/pricing/dividend-calculator.tsx +6 -12
  275. package/src/components/pricing/dividend-stats.tsx +5 -17
  276. package/src/components/pricing/pricing-page.tsx +17 -36
  277. package/src/components/settings/create-org-wizard.tsx +2 -5
  278. package/src/components/sidebar.tsx +7 -42
  279. package/src/components/sidecar-frame.tsx +78 -0
  280. package/src/components/status/status-page.tsx +6 -28
  281. package/src/components/ui/alert-dialog.tsx +8 -25
  282. package/src/components/ui/badge.tsx +2 -8
  283. package/src/components/ui/banner.tsx +1 -6
  284. package/src/components/ui/card.tsx +5 -24
  285. package/src/components/ui/checkbox.tsx +1 -5
  286. package/src/components/ui/collapsible.tsx +3 -8
  287. package/src/components/ui/dialog.tsx +4 -10
  288. package/src/components/ui/dropdown-menu.tsx +9 -18
  289. package/src/components/ui/form.tsx +2 -16
  290. package/src/components/ui/popover.tsx +3 -23
  291. package/src/components/ui/progress.tsx +1 -5
  292. package/src/components/ui/radio-group.tsx +3 -15
  293. package/src/components/ui/select.tsx +4 -17
  294. package/src/components/ui/sheet.tsx +5 -19
  295. package/src/components/ui/skeleton.tsx +1 -7
  296. package/src/components/ui/table.tsx +5 -22
  297. package/src/components/ui/tabs.tsx +3 -13
  298. package/src/components/ui/tooltip.tsx +1 -1
  299. package/src/components/unified-sidebar.tsx +493 -0
  300. package/src/hooks/__tests__/use-fleet-sse.test.ts +1 -4
  301. package/src/hooks/__tests__/use-save-queue.test.ts +2 -8
  302. package/src/hooks/use-credit-balance.ts +27 -0
  303. package/src/hooks/use-my-org-role.ts +1 -3
  304. package/src/hooks/use-plugin-registry.ts +8 -14
  305. package/src/hooks/use-plugin-setup-chat.ts +2 -5
  306. package/src/hooks/use-sidecar-bridge.tsx +148 -0
  307. package/src/hooks/use-webmcp.ts +1 -4
  308. package/src/lib/__tests__/admin-api.test.ts +1 -3
  309. package/src/lib/__tests__/api-bot-crud.test.ts +8 -18
  310. package/src/lib/__tests__/api-fetch.test.ts +4 -16
  311. package/src/lib/__tests__/org-billing-api.test.ts +1 -3
  312. package/src/lib/__tests__/pricing-data.test.ts +0 -8
  313. package/src/lib/__tests__/settings-api.test.ts +1 -3
  314. package/src/lib/admin-affiliate-api.ts +2 -7
  315. package/src/lib/admin-api.ts +6 -26
  316. package/src/lib/admin-incident-api.ts +11 -19
  317. package/src/lib/admin-marketplace-api.ts +1 -5
  318. package/src/lib/api-config.test.ts +5 -50
  319. package/src/lib/api.ts +143 -122
  320. package/src/lib/auth-client.ts +1 -2
  321. package/src/lib/bot-settings-data.ts +11 -36
  322. package/src/lib/brand-config.ts +56 -115
  323. package/src/lib/brand.ts +2 -15
  324. package/src/lib/chat/use-chat.ts +2 -7
  325. package/src/lib/cost-comparison-data.test.ts +1 -3
  326. package/src/lib/cost-comparison-data.ts +1 -4
  327. package/src/lib/errors.ts +1 -4
  328. package/src/lib/fetch-utils.test.ts +26 -9
  329. package/src/lib/fetch-utils.ts +40 -11
  330. package/src/lib/logger.ts +2 -0
  331. package/src/lib/marketplace-data.ts +3 -11
  332. package/src/lib/oauth-errors.ts +2 -4
  333. package/src/lib/onboarding-data.ts +3 -11
  334. package/src/lib/org-api.ts +2 -10
  335. package/src/lib/org-billing-api.ts +5 -19
  336. package/src/lib/plugin/tool-definitions.ts +1 -2
  337. package/src/lib/require-auth.ts +57 -0
  338. package/src/lib/settings-api.ts +1 -4
  339. package/src/lib/sidecar-routes.ts +43 -0
  340. package/src/lib/trpc-server.ts +49 -0
  341. package/src/lib/trpc-types.ts +4 -6
  342. package/src/lib/trpc.tsx +12 -4
  343. package/src/lib/validate-redirect-url.ts +1 -4
  344. package/src/lib/webmcp/marketplace-onboarding-tools.ts +6 -16
  345. package/src/lib/webmcp/register.ts +1 -4
  346. package/src/lib/webmcp/tools.ts +2 -9
  347. package/src/proxy.ts +35 -212
  348. package/src/types/missing-deps.d.ts +2 -8
  349. package/tsconfig.json +1 -8
  350. package/biome.json +0 -52
  351. package/src/__tests__/__snapshots__/layout-snapshots.test.tsx.snap +0 -741
  352. package/src/__tests__/billing-byok-callout.test.tsx +0 -76
  353. 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
@@ -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
+ }
@@ -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
- return tenantId ? { "x-tenant-id": tenantId } : {};
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
- return tenantId ? { "x-tenant-id": tenantId } : {};
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
  /**
@@ -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
- const log = logger("middleware");
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
- const refererOrigin = new URL(referer).origin;
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
- // Generate a per-request nonce for CSP
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: validate Origin/Referer on state-changing API requests.
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
- // Redirect authenticated users from "/" to the app subdomain if on the marketing domain.
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