@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
package/next.config.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  import type { NextConfig } from "next";
2
2
 
3
- const isSecureOrigin =
4
- (process.env.NEXT_PUBLIC_API_URL ?? "").startsWith("https://");
3
+ const isSecureOrigin = (process.env.NEXT_PUBLIC_API_URL ?? "").startsWith("https://");
5
4
 
6
5
  const nextConfig: NextConfig = {
7
6
  output: "standalone",
package/package.json CHANGED
@@ -1,13 +1,12 @@
1
1
  {
2
2
  "name": "@wopr-network/platform-ui-core",
3
- "version": "1.27.8",
3
+ "version": "1.27.9",
4
4
  "description": "Brand-agnostic AI agent platform UI — deploy as any brand via env vars",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "https://github.com/wopr-network/platform-ui-core.git"
8
8
  },
9
9
  "license": "UNLICENSED",
10
- "packageManager": "pnpm@10.31.0",
11
10
  "exports": {
12
11
  ".": "./src/lib/index.ts",
13
12
  "./app/*": "./src/app/*",
@@ -32,22 +31,13 @@
32
31
  "vitest.config.ts",
33
32
  ".env.wopr"
34
33
  ],
35
- "scripts": {
36
- "dev": "next dev",
37
- "build": "next build",
38
- "start": "next start",
39
- "lint": "biome check src/",
40
- "format": "biome format --write src/",
41
- "check": "biome check src/ && tsc --noEmit",
42
- "test": "vitest run",
43
- "test:e2e": "playwright test"
44
- },
45
34
  "dependencies": {
46
35
  "@hookform/resolvers": "^5.2.2",
47
36
  "@noble/hashes": "^2.0.1",
37
+ "@radix-ui/react-portal": "^1.1.10",
48
38
  "@scure/bip32": "^2.0.1",
49
39
  "@scure/bip39": "^2.0.1",
50
- "@stripe/react-stripe-js": "^5.6.0",
40
+ "@stripe/react-stripe-js": "^6.0.0",
51
41
  "@stripe/stripe-js": "^8.7.0",
52
42
  "@tanstack/react-query": "^5.90.21",
53
43
  "@trpc/client": "^11.10.0",
@@ -56,7 +46,7 @@
56
46
  "class-variance-authority": "^0.7.1",
57
47
  "clsx": "^2.1.1",
58
48
  "framer-motion": "^12.34.0",
59
- "lucide-react": "^0.563.0",
49
+ "lucide-react": "^1.7.0",
60
50
  "next": "16.1.6",
61
51
  "next-themes": "^0.4.6",
62
52
  "qrcode.react": "^4.2.0",
@@ -84,9 +74,9 @@
84
74
  "@types/node": "^20",
85
75
  "@types/react": "^19",
86
76
  "@types/react-dom": "^19",
87
- "@vitejs/plugin-react": "^5.1.4",
77
+ "@vitejs/plugin-react": "^6.0.0",
88
78
  "@vitest/coverage-v8": "^4.0.18",
89
- "jsdom": "^28.0.0",
79
+ "jsdom": "^29.0.1",
90
80
  "shadcn": "^3.8.4",
91
81
  "tailwindcss": "^4",
92
82
  "tw-animate-css": "^1.4.0",
@@ -95,5 +85,15 @@
95
85
  },
96
86
  "release": {
97
87
  "extends": "@wopr-network/semantic-release-config"
88
+ },
89
+ "scripts": {
90
+ "dev": "next dev",
91
+ "build": "next build",
92
+ "start": "next start",
93
+ "lint": "biome check src/",
94
+ "format": "biome format --write src/",
95
+ "check": "biome check src/ && tsc --noEmit",
96
+ "test": "vitest run",
97
+ "test:e2e": "playwright test"
98
98
  }
99
- }
99
+ }
@@ -1,5 +1,4 @@
1
1
  import { render, screen } from "@testing-library/react";
2
- import userEvent from "@testing-library/user-event";
3
2
  import { beforeEach, describe, expect, it, vi } from "vitest";
4
3
 
5
4
  const mockSwitchTenant = vi.fn();
@@ -39,36 +38,38 @@ describe("AccountSwitcher", () => {
39
38
  expect(screen.getByText("Alice")).toBeInTheDocument();
40
39
  });
41
40
 
42
- it("shows PERSONAL badge for personal account", () => {
41
+ it("renders a fallback icon for tenants without images", () => {
43
42
  render(<AccountSwitcher />);
44
- expect(screen.getByText("PERSONAL")).toBeInTheDocument();
43
+ // The component renders a span with bg-sidebar-accent class as the fallback icon
44
+ const icon = document.querySelector(".bg-sidebar-accent");
45
+ expect(icon).not.toBeNull();
45
46
  });
46
47
 
47
- it("shows all tenants when clicked", async () => {
48
- const user = userEvent.setup();
49
- render(<AccountSwitcher />);
50
- await user.click(screen.getByRole("button"));
51
- expect(screen.getByText("My Team")).toBeInTheDocument();
52
- });
48
+ it("renders nothing when no tenants exist", () => {
49
+ mockUseTenant.mockReturnValue({
50
+ activeTenantId: "",
51
+ tenants: [],
52
+ isLoading: false,
53
+ switchTenant: mockSwitchTenant,
54
+ });
53
55
 
54
- it("calls switchTenant when an org is selected", async () => {
55
- const user = userEvent.setup();
56
- render(<AccountSwitcher />);
57
- await user.click(screen.getByRole("button"));
58
- await user.click(screen.getByText("My Team"));
59
- expect(mockSwitchTenant).toHaveBeenCalledWith("org-1");
56
+ const { container } = render(<AccountSwitcher />);
57
+ expect(container.firstChild).toBeNull();
60
58
  });
61
59
 
62
- it("renders nothing when only one tenant exists", () => {
60
+ it("falls back to first tenant when activeTenantId does not match", () => {
63
61
  mockUseTenant.mockReturnValue({
64
- activeTenantId: "user-1",
65
- tenants: [{ id: "user-1", name: "Alice", type: "personal" as const, image: null }],
62
+ activeTenantId: "nonexistent",
63
+ tenants: [
64
+ { id: "user-1", name: "Alice", type: "personal" as const, image: null },
65
+ { id: "org-1", name: "My Team", type: "org" as const, image: null },
66
+ ],
66
67
  isLoading: false,
67
68
  switchTenant: mockSwitchTenant,
68
69
  });
69
70
 
70
- const { container } = render(<AccountSwitcher />);
71
- expect(container.firstChild).toBeNull();
71
+ render(<AccountSwitcher />);
72
+ expect(screen.getByText("Alice")).toBeInTheDocument();
72
73
  });
73
74
 
74
75
  it("renders nothing while loading", () => {
@@ -120,9 +120,7 @@ describe("ActivityPage", () => {
120
120
  // Wait for debounce (300ms) + API call
121
121
  await vi.waitFor(
122
122
  () => {
123
- expect(mockFetchAuditLog).toHaveBeenLastCalledWith(
124
- expect.objectContaining({ search: "billing", offset: 0 }),
125
- );
123
+ expect(mockFetchAuditLog).toHaveBeenLastCalledWith(expect.objectContaining({ search: "billing", offset: 0 }));
126
124
  },
127
125
  { timeout: 1000 },
128
126
  );
@@ -160,9 +158,7 @@ describe("ActivityPage", () => {
160
158
  });
161
159
  await user.click(screen.getByRole("button", { name: "Next" }));
162
160
 
163
- expect(mockFetchAuditLog).toHaveBeenLastCalledWith(
164
- expect.objectContaining({ offset: 50, limit: 50 }),
165
- );
161
+ expect(mockFetchAuditLog).toHaveBeenLastCalledWith(expect.objectContaining({ offset: 50, limit: 50 }));
166
162
  });
167
163
 
168
164
  it("calls fetchAuditLog with correct params", async () => {
@@ -8,9 +8,7 @@ const mockUseStripe = vi.fn(() => ({ confirmSetup: mockConfirmSetup }));
8
8
  const mockUseElements = vi.fn(() => ({}));
9
9
 
10
10
  vi.mock("@stripe/react-stripe-js", () => ({
11
- Elements: ({ children }: { children: React.ReactNode }) => (
12
- <div data-testid="stripe-elements">{children}</div>
13
- ),
11
+ Elements: ({ children }: { children: React.ReactNode }) => <div data-testid="stripe-elements">{children}</div>,
14
12
  PaymentElement: () => <div data-testid="payment-element" />,
15
13
  useStripe: () => mockUseStripe(),
16
14
  useElements: () => mockUseElements(),
@@ -44,9 +42,7 @@ describe("AddPaymentMethodDialog", () => {
44
42
  });
45
43
 
46
44
  it("fetches setup intent and renders PaymentElement when opened", async () => {
47
- render(
48
- <AddPaymentMethodDialog open={true} onOpenChange={onOpenChange} onSuccess={onSuccess} />,
49
- );
45
+ render(<AddPaymentMethodDialog open={true} onOpenChange={onOpenChange} onSuccess={onSuccess} />);
50
46
 
51
47
  await waitFor(() => {
52
48
  expect(createSetupIntent).toHaveBeenCalledOnce();
@@ -56,13 +52,9 @@ describe("AddPaymentMethodDialog", () => {
56
52
  });
57
53
 
58
54
  it("shows error and retry button when setup intent fails", async () => {
59
- (createSetupIntent as ReturnType<typeof vi.fn>).mockRejectedValueOnce(
60
- new Error("Network error"),
61
- );
55
+ (createSetupIntent as ReturnType<typeof vi.fn>).mockRejectedValueOnce(new Error("Network error"));
62
56
 
63
- render(
64
- <AddPaymentMethodDialog open={true} onOpenChange={onOpenChange} onSuccess={onSuccess} />,
65
- );
57
+ render(<AddPaymentMethodDialog open={true} onOpenChange={onOpenChange} onSuccess={onSuccess} />);
66
58
 
67
59
  await waitFor(() => {
68
60
  expect(screen.getByText(/failed to initialize/i)).toBeInTheDocument();
@@ -74,9 +66,7 @@ describe("AddPaymentMethodDialog", () => {
74
66
  it("calls confirmSetup on form submit and triggers onSuccess", async () => {
75
67
  mockConfirmSetup.mockResolvedValueOnce({ setupIntent: { status: "succeeded" } });
76
68
 
77
- render(
78
- <AddPaymentMethodDialog open={true} onOpenChange={onOpenChange} onSuccess={onSuccess} />,
79
- );
69
+ render(<AddPaymentMethodDialog open={true} onOpenChange={onOpenChange} onSuccess={onSuccess} />);
80
70
 
81
71
  await waitFor(() => {
82
72
  expect(screen.getByTestId("payment-element")).toBeInTheDocument();
@@ -96,9 +86,7 @@ describe("AddPaymentMethodDialog", () => {
96
86
  error: { message: "Your card was declined." },
97
87
  });
98
88
 
99
- render(
100
- <AddPaymentMethodDialog open={true} onOpenChange={onOpenChange} onSuccess={onSuccess} />,
101
- );
89
+ render(<AddPaymentMethodDialog open={true} onOpenChange={onOpenChange} onSuccess={onSuccess} />);
102
90
 
103
91
  await waitFor(() => {
104
92
  expect(screen.getByTestId("payment-element")).toBeInTheDocument();
@@ -115,17 +103,13 @@ describe("AddPaymentMethodDialog", () => {
115
103
  });
116
104
 
117
105
  it("does not fetch setup intent when closed", () => {
118
- render(
119
- <AddPaymentMethodDialog open={false} onOpenChange={onOpenChange} onSuccess={onSuccess} />,
120
- );
106
+ render(<AddPaymentMethodDialog open={false} onOpenChange={onOpenChange} onSuccess={onSuccess} />);
121
107
 
122
108
  expect(createSetupIntent).not.toHaveBeenCalled();
123
109
  });
124
110
 
125
111
  it("closes dialog when cancel is clicked", async () => {
126
- render(
127
- <AddPaymentMethodDialog open={true} onOpenChange={onOpenChange} onSuccess={onSuccess} />,
128
- );
112
+ render(<AddPaymentMethodDialog open={true} onOpenChange={onOpenChange} onSuccess={onSuccess} />);
129
113
 
130
114
  await waitFor(() => {
131
115
  expect(screen.getByTestId("payment-element")).toBeInTheDocument();
@@ -142,14 +126,7 @@ describe("AddPaymentMethodDialog", () => {
142
126
  clientSecret: "seti_org_test_secret_456",
143
127
  });
144
128
 
145
- render(
146
- <AddPaymentMethodDialog
147
- open={true}
148
- onOpenChange={onOpenChange}
149
- onSuccess={onSuccess}
150
- orgId="org-123"
151
- />,
152
- );
129
+ render(<AddPaymentMethodDialog open={true} onOpenChange={onOpenChange} onSuccess={onSuccess} orgId="org-123" />);
153
130
 
154
131
  await waitFor(() => {
155
132
  expect(createOrgSetupIntent).toHaveBeenCalledWith("org-123");
@@ -48,12 +48,7 @@ import {
48
48
  getAffiliateVelocity,
49
49
  } from "@/lib/admin-affiliate-api";
50
50
 
51
- import {
52
- getCacheStats,
53
- getDailyCost,
54
- getPageCost,
55
- getSessionCost,
56
- } from "@/lib/admin-inference-api";
51
+ import { getCacheStats, getDailyCost, getPageCost, getSessionCost } from "@/lib/admin-inference-api";
57
52
 
58
53
  beforeEach(() => {
59
54
  vi.clearAllMocks();
@@ -112,9 +112,7 @@ describe("provisionGpuNode", () => {
112
112
 
113
113
  it("throws when provisioning fails", async () => {
114
114
  mockApiFetch.mockRejectedValue(new Error("Quota exceeded"));
115
- await expect(
116
- provisionGpuNode({ name: "x", region: "nyc3", size: "gpu-h100x1" }),
117
- ).rejects.toThrow("Quota exceeded");
115
+ await expect(provisionGpuNode({ name: "x", region: "nyc3", size: "gpu-h100x1" })).rejects.toThrow("Quota exceeded");
118
116
  });
119
117
  });
120
118
 
@@ -74,10 +74,7 @@ describe("getAllPlugins", () => {
74
74
 
75
75
  describe("getDiscoveryQueue", () => {
76
76
  it("returns only unreviewed plugins from tRPC", async () => {
77
- const plugins = [
78
- fakePlugin({ id: "a", reviewed: true }),
79
- fakePlugin({ id: "b", reviewed: false }),
80
- ];
77
+ const plugins = [fakePlugin({ id: "a", reviewed: true }), fakePlugin({ id: "b", reviewed: false })];
81
78
  mockListPlugins.query.mockResolvedValue(plugins);
82
79
 
83
80
  const result = await getDiscoveryQueue();
@@ -1,8 +1,7 @@
1
- import { beforeEach, describe, expect, it, vi } from "vitest";
1
+ import { describe, expect, it } from "vitest";
2
2
 
3
3
  // We test the middleware function directly by importing from the module.
4
- // The middleware was renamed from proxy.ts to middleware.ts by WOP-1128.
5
- import middleware from "../proxy";
4
+ import middleware, { validateCsrfOrigin } from "../proxy";
6
5
 
7
6
  // Minimal NextRequest-compatible mock
8
7
  function mockRequest(opts: {
@@ -16,9 +15,7 @@ function mockRequest(opts: {
16
15
  if (!headers.has("host")) {
17
16
  headers.set("host", url.host);
18
17
  }
19
- const cookieMap = new Map(
20
- Object.entries(opts.cookies ?? {}).map(([k, v]) => [k, { name: k, value: v }]),
21
- );
18
+ const cookieMap = new Map(Object.entries(opts.cookies ?? {}).map(([k, v]) => [k, { name: k, value: v }]));
22
19
  return {
23
20
  method: opts.method ?? "GET",
24
21
  url: opts.url,
@@ -32,126 +29,122 @@ function mockRequest(opts: {
32
29
  } as unknown as Parameters<typeof middleware>[0];
33
30
  }
34
31
 
35
- // Mock global fetch for Better Auth session calls
36
- const originalFetch = globalThis.fetch;
37
-
38
- beforeEach(() => {
39
- globalThis.fetch = originalFetch;
40
- });
41
-
42
- describe("Admin route middleware authorization", () => {
43
- it("redirects non-admin users from /admin/tenants to /marketplace", async () => {
44
- // Mock Better Auth get-session returning a regular user
45
- globalThis.fetch = vi.fn().mockResolvedValue({
46
- ok: true,
47
- json: async () => ({
48
- session: { id: "s1" },
49
- user: { id: "u1", role: "user" },
50
- }),
51
- });
52
-
32
+ describe("Middleware CSP, CSRF, nonce, tenant forwarding", () => {
33
+ it("sets Content-Security-Policy header on responses", async () => {
53
34
  const req = mockRequest({
54
- url: "https://localhost:3000/admin/tenants",
55
- cookies: { "better-auth.session_token": "valid-token" },
35
+ url: "https://localhost:3000/marketplace",
56
36
  });
57
37
 
58
38
  const res = await middleware(req);
59
- // Should redirect to /marketplace
60
- expect(res.status).toBe(307);
61
- expect(res.headers.get("location")).toContain("/marketplace");
39
+ expect(res.headers.get("content-security-policy")).not.toBeNull();
40
+ expect(res.headers.get("content-security-policy")).toContain("default-src 'self'");
62
41
  });
63
42
 
64
- it("allows platform_admin users to access /admin/tenants", async () => {
65
- globalThis.fetch = vi.fn().mockResolvedValue({
66
- ok: true,
67
- json: async () => ({
68
- session: { id: "s1" },
69
- user: { id: "u1", role: "platform_admin" },
70
- }),
43
+ it("sets Vary header on responses", async () => {
44
+ const req = mockRequest({
45
+ url: "https://localhost:3000/marketplace",
71
46
  });
72
47
 
48
+ const res = await middleware(req);
49
+ expect(res.headers.get("vary")).toBe("*");
50
+ });
51
+
52
+ it("passes through GET requests to any route without auth check", async () => {
73
53
  const req = mockRequest({
74
54
  url: "https://localhost:3000/admin/tenants",
75
- cookies: { "better-auth.session_token": "valid-token" },
76
55
  });
77
56
 
78
57
  const res = await middleware(req);
79
- // Should pass through (not a redirect)
58
+ // Middleware no longer checks auth — just sets CSP headers and passes through
80
59
  expect(res.status).not.toBe(307);
60
+ expect(res.headers.get("content-security-policy")).not.toBeNull();
81
61
  });
82
62
 
83
- it("redirects unauthenticated users from /admin to /login", async () => {
63
+ it("rejects CSRF-invalid POST to /api routes with 403", async () => {
84
64
  const req = mockRequest({
85
- url: "https://localhost:3000/admin/tenants",
86
- // No session cookie
65
+ method: "POST",
66
+ url: "https://localhost:3000/api/some-endpoint",
67
+ headers: {
68
+ host: "localhost:3000",
69
+ // No origin or referer — CSRF fails
70
+ },
87
71
  });
88
72
 
89
73
  const res = await middleware(req);
90
- // Existing auth check should redirect to /login
91
- expect(res.status).toBe(307);
92
- expect(res.headers.get("location")).toContain("/login");
74
+ expect(res.status).toBe(403);
93
75
  });
94
76
 
95
- it("redirects to /marketplace when session fetch fails", async () => {
96
- // Simulate network error or 500 from Better Auth
97
- globalThis.fetch = vi.fn().mockResolvedValue({
98
- ok: false,
99
- status: 500,
100
- json: async () => ({}),
101
- });
102
-
77
+ it("allows CSRF-valid POST to /api routes", async () => {
103
78
  const req = mockRequest({
104
- url: "https://localhost:3000/admin/tenants",
105
- cookies: { "better-auth.session_token": "valid-token" },
79
+ method: "POST",
80
+ url: "https://localhost:3000/api/some-endpoint",
81
+ headers: {
82
+ host: "localhost:3000",
83
+ origin: "https://localhost:3000",
84
+ },
106
85
  });
107
86
 
108
87
  const res = await middleware(req);
109
- // On failure, deny access (fail closed)
110
- expect(res.status).toBe(307);
111
- expect(res.headers.get("location")).toContain("/marketplace");
88
+ expect(res.status).not.toBe(403);
112
89
  });
113
90
 
114
- it("sets no-cache headers on admin page responses for admin users", async () => {
115
- globalThis.fetch = vi.fn().mockResolvedValue({
116
- ok: true,
117
- json: async () => ({
118
- session: { id: "s1" },
119
- user: { id: "u1", role: "platform_admin" },
120
- }),
121
- });
122
-
91
+ it("exempts /api/auth/callback POST from CSRF checks", async () => {
123
92
  const req = mockRequest({
124
- url: "https://localhost:3000/admin/tenants",
125
- cookies: { "better-auth.session_token": "valid-token" },
93
+ method: "POST",
94
+ url: "https://localhost:3000/api/auth/callback",
95
+ headers: {
96
+ host: "localhost:3000",
97
+ // No origin — would normally fail CSRF
98
+ },
126
99
  });
127
100
 
128
101
  const res = await middleware(req);
129
- expect(res.headers.get("cache-control")).toBe("no-store, no-cache, must-revalidate");
130
- expect(res.headers.get("pragma")).toBe("no-cache");
102
+ expect(res.status).not.toBe(403);
131
103
  });
132
104
 
133
- it("does not set restrictive cache headers on non-admin pages", async () => {
105
+ it("does not perform CSRF checks on GET requests to /api", async () => {
134
106
  const req = mockRequest({
135
- url: "https://localhost:3000/marketplace",
136
- cookies: { "better-auth.session_token": "valid-token" },
107
+ method: "GET",
108
+ url: "https://localhost:3000/api/some-endpoint",
137
109
  });
138
110
 
139
111
  const res = await middleware(req);
140
- expect(res.headers.get("cache-control")).toBeNull();
141
- expect(res.headers.get("pragma")).toBeNull();
112
+ expect(res.status).not.toBe(403);
142
113
  });
114
+ });
143
115
 
144
- it("does not call get-session for non-admin routes", async () => {
145
- const fetchSpy = vi.fn();
146
- globalThis.fetch = fetchSpy;
116
+ describe("validateCsrfOrigin", () => {
117
+ it("returns true when origin matches host", () => {
118
+ const req = mockRequest({
119
+ url: "https://localhost:3000/api/test",
120
+ headers: {
121
+ host: "localhost:3000",
122
+ origin: "https://localhost:3000",
123
+ },
124
+ });
125
+ expect(validateCsrfOrigin(req)).toBe(true);
126
+ });
147
127
 
128
+ it("returns false when origin does not match host", () => {
148
129
  const req = mockRequest({
149
- url: "https://localhost:3000/marketplace",
150
- cookies: { "better-auth.session_token": "valid-token" },
130
+ url: "https://localhost:3000/api/test",
131
+ headers: {
132
+ host: "localhost:3000",
133
+ origin: "https://evil.com",
134
+ },
151
135
  });
136
+ expect(validateCsrfOrigin(req)).toBe(false);
137
+ });
152
138
 
153
- await middleware(req);
154
- // Should NOT have called Better Auth — not an admin route
155
- expect(fetchSpy).not.toHaveBeenCalled();
139
+ it("returns false when no host header", () => {
140
+ const req = mockRequest({
141
+ url: "https://localhost:3000/api/test",
142
+ headers: {
143
+ origin: "https://localhost:3000",
144
+ },
145
+ });
146
+ // Override host to empty
147
+ req.headers.delete("host");
148
+ expect(validateCsrfOrigin(req)).toBe(false);
156
149
  });
157
150
  });
@@ -164,8 +164,8 @@ describe("Affiliate Dashboard error state", () => {
164
164
  });
165
165
  });
166
166
 
167
- describe("Billing layout with Referrals nav", () => {
168
- it("renders Refer & Earn navigation link", async () => {
167
+ describe("Billing layout with Referrals content", () => {
168
+ it("renders child content inside billing layout", async () => {
169
169
  const { default: BillingLayout } = await import("../app/(dashboard)/billing/layout");
170
170
  render(
171
171
  <BillingLayout>
@@ -173,6 +173,6 @@ describe("Billing layout with Referrals nav", () => {
173
173
  </BillingLayout>,
174
174
  );
175
175
 
176
- expect(screen.getByText("Refer & Earn")).toBeInTheDocument();
176
+ expect(screen.getByText("child content")).toBeInTheDocument();
177
177
  });
178
178
  });
@@ -5,6 +5,17 @@ vi.mock("@/lib/api-config", () => ({
5
5
  PLATFORM_BASE_URL: "https://api.test",
6
6
  }));
7
7
 
8
+ /**
9
+ * Helper: wait for all pending microtasks/promises to flush.
10
+ * handleUnauthorized fires an async session check internally;
11
+ * we need to let that settle before asserting on mockLocation.href.
12
+ */
13
+ function flushPromises() {
14
+ return new Promise<void>((resolve) => {
15
+ setTimeout(resolve, 0);
16
+ });
17
+ }
18
+
8
19
  describe("401 redirect handling", () => {
9
20
  const mockLocation = { href: "", pathname: "/dashboard", search: "" };
10
21
  const mockFetch = vi.fn();
@@ -23,9 +34,15 @@ describe("401 redirect handling", () => {
23
34
  vi.unstubAllGlobals();
24
35
  });
25
36
 
26
- it("handleUnauthorized redirects to /login with reason and callbackUrl", async () => {
37
+ it("handleUnauthorized throws UnauthorizedError and redirects when session is expired", async () => {
38
+ // Mock the internal session check to return no session (expired)
39
+ mockFetch.mockResolvedValueOnce({
40
+ json: () => Promise.resolve({ session: null }),
41
+ });
27
42
  const { handleUnauthorized } = await import("@/lib/fetch-utils");
28
43
  expect(() => handleUnauthorized()).toThrow("Session expired");
44
+ // Wait for the async session check to complete and trigger redirect
45
+ await flushPromises();
29
46
  expect(mockLocation.href).toBe("/login?reason=expired&callbackUrl=%2Fdashboard");
30
47
  });
31
48
 
@@ -37,27 +54,41 @@ describe("401 redirect handling", () => {
37
54
  });
38
55
 
39
56
  it("apiFetch redirects on 401", async () => {
57
+ // First call: the API request itself returns 401
40
58
  mockFetch.mockResolvedValueOnce({ ok: false, status: 401, statusText: "Unauthorized" });
59
+ // Second call: the internal session check returns expired
60
+ mockFetch.mockResolvedValueOnce({
61
+ json: () => Promise.resolve({ session: null }),
62
+ });
41
63
  const { getProfile } = await import("@/lib/api");
42
64
  await expect(getProfile()).rejects.toThrow("Session expired");
65
+ await flushPromises();
43
66
  expect(mockLocation.href).toContain("/login?reason=expired");
44
67
  });
45
68
 
46
- it("fleetFetch redirects on 401", async () => {
69
+ it("fleetFetch (updateInstanceConfig) redirects on 401", async () => {
47
70
  mockFetch.mockResolvedValueOnce({ ok: false, status: 401, statusText: "Unauthorized" });
48
- const { listInstances } = await import("@/lib/api");
49
- await expect(listInstances()).rejects.toThrow("Session expired");
71
+ mockFetch.mockResolvedValueOnce({
72
+ json: () => Promise.resolve({ session: null }),
73
+ });
74
+ const { updateInstanceConfig } = await import("@/lib/api");
75
+ await expect(updateInstanceConfig("bot-1", {})).rejects.toThrow("Session expired");
76
+ await flushPromises();
50
77
  expect(mockLocation.href).toContain("/login?reason=expired");
51
78
  });
52
79
 
53
- it("trpcFetch redirects on 401", async () => {
80
+ it("apiFetch (listProviderKeys) redirects on 401", async () => {
54
81
  mockFetch.mockResolvedValueOnce({ ok: false, status: 401, statusText: "Unauthorized" });
55
- const { getCurrentPlan } = await import("@/lib/api");
56
- await expect(getCurrentPlan()).rejects.toThrow("Session expired");
82
+ mockFetch.mockResolvedValueOnce({
83
+ json: () => Promise.resolve({ session: null }),
84
+ });
85
+ const { listProviderKeys } = await import("@/lib/api");
86
+ await expect(listProviderKeys()).rejects.toThrow("Session expired");
87
+ await flushPromises();
57
88
  expect(mockLocation.href).toContain("/login?reason=expired");
58
89
  });
59
90
 
60
- it("non-401 errors still throw generic message from apiFetch", async () => {
91
+ it("non-401 errors still throw without redirecting", async () => {
61
92
  mockFetch.mockResolvedValueOnce({
62
93
  ok: false,
63
94
  status: 500,
@@ -65,14 +96,20 @@ describe("401 redirect handling", () => {
65
96
  json: () => Promise.resolve({}),
66
97
  });
67
98
  const { getProfile } = await import("@/lib/api");
68
- await expect(getProfile()).rejects.toThrow("API error: 500 Internal Server Error");
99
+ // getProfile uses tRPC, so the error message comes from tRPC internals
100
+ await expect(getProfile()).rejects.toThrow();
101
+ // The key assertion: non-401 errors must NOT redirect to /login
69
102
  expect(mockLocation.href).toBe("");
70
103
  });
71
104
 
72
105
  it("bot-settings-data apiFetch redirects on 401", async () => {
73
106
  mockFetch.mockResolvedValueOnce({ ok: false, status: 401, statusText: "Unauthorized" });
107
+ mockFetch.mockResolvedValueOnce({
108
+ json: () => Promise.resolve({ session: null }),
109
+ });
74
110
  const { getBotSettings } = await import("@/lib/bot-settings-data");
75
111
  await expect(getBotSettings("bot-1")).rejects.toThrow("Session expired");
112
+ await flushPromises();
76
113
  expect(mockLocation.href).toContain("/login?reason=expired");
77
114
  });
78
115
  });