@vertesia/ui 1.6.0-dev.20260918.090710Z → 1.6.0-dev.20260923.085325Z

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 (288) hide show
  1. package/lib/boot/branding.d.ts +13 -0
  2. package/lib/boot/branding.d.ts.map +1 -1
  3. package/lib/boot/branding.js +7 -0
  4. package/lib/boot/branding.js.map +1 -1
  5. package/lib/boot/index.d.ts +1 -1
  6. package/lib/boot/index.d.ts.map +1 -1
  7. package/lib/boot/index.js +5 -23
  8. package/lib/boot/index.js.map +1 -1
  9. package/lib/boot/loading.d.ts +4 -0
  10. package/lib/boot/loading.d.ts.map +1 -0
  11. package/lib/boot/loading.js +28 -0
  12. package/lib/boot/loading.js.map +1 -0
  13. package/lib/core/components/SelectList.d.ts.map +1 -1
  14. package/lib/core/components/SelectList.js +3 -3
  15. package/lib/core/components/SelectList.js.map +1 -1
  16. package/lib/core/components/SidePanel.d.ts.map +1 -1
  17. package/lib/core/components/SidePanel.js +2 -1
  18. package/lib/core/components/SidePanel.js.map +1 -1
  19. package/lib/core/components/shadcn/theme/ThemeProvider.d.ts.map +1 -1
  20. package/lib/core/components/shadcn/theme/ThemeProvider.js +21 -5
  21. package/lib/core/components/shadcn/theme/ThemeProvider.js.map +1 -1
  22. package/lib/env/index.d.ts +11 -0
  23. package/lib/env/index.d.ts.map +1 -1
  24. package/lib/env/index.js +16 -3
  25. package/lib/env/index.js.map +1 -1
  26. package/lib/features/agent/chat/ModernAgentConversation.js +4 -1
  27. package/lib/features/agent/chat/ModernAgentConversation.js.map +1 -1
  28. package/lib/features/agent/chat/ModernAgentOutput/MessageInput.d.ts +5 -1
  29. package/lib/features/agent/chat/ModernAgentOutput/MessageInput.d.ts.map +1 -1
  30. package/lib/features/agent/chat/ModernAgentOutput/MessageInput.js +9 -2
  31. package/lib/features/agent/chat/ModernAgentOutput/MessageInput.js.map +1 -1
  32. package/lib/features/magic-pdf/GroundedExtractionView.js +3 -3
  33. package/lib/features/magic-pdf/GroundedExtractionView.js.map +1 -1
  34. package/lib/features/media-viewer/AudioPanel.d.ts +10 -2
  35. package/lib/features/media-viewer/AudioPanel.d.ts.map +1 -1
  36. package/lib/features/media-viewer/AudioPanel.js +78 -13
  37. package/lib/features/media-viewer/AudioPanel.js.map +1 -1
  38. package/lib/features/oauth/McpConnectionsButton.d.ts +2 -2
  39. package/lib/features/oauth/McpConnectionsButton.d.ts.map +1 -1
  40. package/lib/features/oauth/McpConnectionsButton.js +9 -7
  41. package/lib/features/oauth/McpConnectionsButton.js.map +1 -1
  42. package/lib/features/oauth/McpConnectionsDialog.d.ts.map +1 -1
  43. package/lib/features/oauth/McpConnectionsDialog.js +7 -3
  44. package/lib/features/oauth/McpConnectionsDialog.js.map +1 -1
  45. package/lib/features/oauth/useMcpConnections.d.ts +4 -2
  46. package/lib/features/oauth/useMcpConnections.d.ts.map +1 -1
  47. package/lib/features/oauth/useMcpConnections.js +15 -9
  48. package/lib/features/oauth/useMcpConnections.js.map +1 -1
  49. package/lib/features/store/objects/selection/ObjectsActionContext.d.ts.map +1 -1
  50. package/lib/features/store/objects/selection/ObjectsActionContext.js +2 -0
  51. package/lib/features/store/objects/selection/ObjectsActionContext.js.map +1 -1
  52. package/lib/features/store/objects/selection/SelectionActions.d.ts +4 -2
  53. package/lib/features/store/objects/selection/SelectionActions.d.ts.map +1 -1
  54. package/lib/features/store/objects/selection/SelectionActions.js +6 -3
  55. package/lib/features/store/objects/selection/SelectionActions.js.map +1 -1
  56. package/lib/features/store/objects/selection/actions/StartWorkflowAction.d.ts +4 -0
  57. package/lib/features/store/objects/selection/actions/StartWorkflowAction.d.ts.map +1 -0
  58. package/lib/features/store/objects/selection/actions/StartWorkflowAction.js +81 -0
  59. package/lib/features/store/objects/selection/actions/StartWorkflowAction.js.map +1 -0
  60. package/lib/features/store/objects/selection/actions/index.d.ts +1 -0
  61. package/lib/features/store/objects/selection/actions/index.d.ts.map +1 -1
  62. package/lib/features/store/objects/selection/actions/index.js +1 -0
  63. package/lib/features/store/objects/selection/actions/index.js.map +1 -1
  64. package/lib/features/store/types/TypeRegistry.d.ts +345 -155
  65. package/lib/features/store/types/TypeRegistry.d.ts.map +1 -1
  66. package/lib/features/store/types/search/ObjectTypeSearchContext.d.ts +345 -155
  67. package/lib/features/store/types/search/ObjectTypeSearchContext.d.ts.map +1 -1
  68. package/lib/i18n/locales/ar.json +10 -2
  69. package/lib/i18n/locales/de.json +6 -2
  70. package/lib/i18n/locales/en.json +6 -2
  71. package/lib/i18n/locales/es.json +6 -2
  72. package/lib/i18n/locales/fr.json +7 -2
  73. package/lib/i18n/locales/it.json +6 -2
  74. package/lib/i18n/locales/ja.json +6 -2
  75. package/lib/i18n/locales/ko.json +6 -2
  76. package/lib/i18n/locales/pt.json +6 -2
  77. package/lib/i18n/locales/ru.json +8 -2
  78. package/lib/i18n/locales/tr.json +6 -2
  79. package/lib/i18n/locales/zh-TW.json +6 -2
  80. package/lib/i18n/locales/zh.json +6 -2
  81. package/lib/session/UserSession.d.ts +3 -0
  82. package/lib/session/UserSession.d.ts.map +1 -1
  83. package/lib/session/UserSession.js +18 -8
  84. package/lib/session/UserSession.js.map +1 -1
  85. package/lib/session/UserSessionProvider.d.ts.map +1 -1
  86. package/lib/session/UserSessionProvider.js +34 -24
  87. package/lib/session/UserSessionProvider.js.map +1 -1
  88. package/lib/session/auth/authRoundTrip.d.ts +8 -0
  89. package/lib/session/auth/authRoundTrip.d.ts.map +1 -0
  90. package/lib/session/auth/authRoundTrip.js +22 -0
  91. package/lib/session/auth/authRoundTrip.js.map +1 -0
  92. package/lib/session/auth/composable.d.ts.map +1 -1
  93. package/lib/session/auth/composable.js +9 -7
  94. package/lib/session/auth/composable.js.map +1 -1
  95. package/lib/session/auth/domainRouting.d.ts +13 -6
  96. package/lib/session/auth/domainRouting.d.ts.map +1 -1
  97. package/lib/session/auth/domainRouting.js +2 -21
  98. package/lib/session/auth/domainRouting.js.map +1 -1
  99. package/lib/session/auth/oauth.d.ts +6 -1
  100. package/lib/session/auth/oauth.d.ts.map +1 -1
  101. package/lib/session/auth/oauth.js +145 -8
  102. package/lib/session/auth/oauth.js.map +1 -1
  103. package/lib/session/auth/tokenIssuer.d.ts +3 -0
  104. package/lib/session/auth/tokenIssuer.d.ts.map +1 -0
  105. package/lib/session/auth/tokenIssuer.js +27 -0
  106. package/lib/session/auth/tokenIssuer.js.map +1 -0
  107. package/lib/session/auth/vertesiaHosts.d.ts +47 -0
  108. package/lib/session/auth/vertesiaHosts.d.ts.map +1 -0
  109. package/lib/session/auth/vertesiaHosts.js +79 -0
  110. package/lib/session/auth/vertesiaHosts.js.map +1 -0
  111. package/lib/session/index.d.ts +1 -0
  112. package/lib/session/index.d.ts.map +1 -1
  113. package/lib/session/index.js +1 -0
  114. package/lib/session/index.js.map +1 -1
  115. package/lib/shell/AuthScreenPreview.d.ts +1 -1
  116. package/lib/shell/AuthScreenPreview.d.ts.map +1 -1
  117. package/lib/shell/AuthScreenPreview.js +2 -5
  118. package/lib/shell/AuthScreenPreview.js.map +1 -1
  119. package/lib/shell/BrandedAuthScreens.d.ts +1 -4
  120. package/lib/shell/BrandedAuthScreens.d.ts.map +1 -1
  121. package/lib/shell/BrandedAuthScreens.js +5 -26
  122. package/lib/shell/BrandedAuthScreens.js.map +1 -1
  123. package/lib/shell/BrandedLoadingIndicator.d.ts +11 -0
  124. package/lib/shell/BrandedLoadingIndicator.d.ts.map +1 -0
  125. package/lib/shell/BrandedLoadingIndicator.js +27 -0
  126. package/lib/shell/BrandedLoadingIndicator.js.map +1 -0
  127. package/lib/shell/SplashScreen.d.ts.map +1 -1
  128. package/lib/shell/SplashScreen.js +2 -1
  129. package/lib/shell/SplashScreen.js.map +1 -1
  130. package/lib/shell/apps/AppInstallationProvider.d.ts +16 -4
  131. package/lib/shell/apps/AppInstallationProvider.d.ts.map +1 -1
  132. package/lib/shell/apps/iframe-auth.d.ts +2 -0
  133. package/lib/shell/apps/iframe-auth.d.ts.map +1 -1
  134. package/lib/shell/apps/iframe-auth.js +6 -2
  135. package/lib/shell/apps/iframe-auth.js.map +1 -1
  136. package/lib/shell/index.d.ts +1 -0
  137. package/lib/shell/index.d.ts.map +1 -1
  138. package/lib/shell/index.js +1 -0
  139. package/lib/shell/index.js.map +1 -1
  140. package/lib/shell/login/SignInAuthPending.js +2 -2
  141. package/lib/shell/login/SignInAuthPending.js.map +1 -1
  142. package/lib/shell/login/SignInEmailStep.d.ts.map +1 -1
  143. package/lib/shell/login/SignInEmailStep.js +2 -2
  144. package/lib/shell/login/SignInEmailStep.js.map +1 -1
  145. package/lib/shell/login/TerminalLogin.d.ts.map +1 -1
  146. package/lib/shell/login/TerminalLogin.js +28 -21
  147. package/lib/shell/login/TerminalLogin.js.map +1 -1
  148. package/lib/vertesia-ui-boot.js +33 -29
  149. package/lib/vertesia-ui-boot.js.map +1 -1
  150. package/lib/vertesia-ui-core.js +34 -34
  151. package/lib/vertesia-ui-core.js.map +1 -1
  152. package/lib/vertesia-ui-env.js +1 -1
  153. package/lib/vertesia-ui-env.js.map +1 -1
  154. package/lib/vertesia-ui-features.js +163 -163
  155. package/lib/vertesia-ui-features.js.map +1 -1
  156. package/lib/vertesia-ui-i18n.Bl8KeZUK.chunk.js +5 -0
  157. package/lib/vertesia-ui-i18n.Bl8KeZUK.chunk.js.map +1 -0
  158. package/lib/vertesia-ui-i18n.BmT06XFI.chunk.js +5 -0
  159. package/lib/vertesia-ui-i18n.BmT06XFI.chunk.js.map +1 -0
  160. package/lib/vertesia-ui-i18n.Ch-Jjj0L.chunk.js +5 -0
  161. package/lib/vertesia-ui-i18n.Ch-Jjj0L.chunk.js.map +1 -0
  162. package/lib/vertesia-ui-i18n.CjhloW9s.chunk.js +5 -0
  163. package/lib/vertesia-ui-i18n.CjhloW9s.chunk.js.map +1 -0
  164. package/lib/vertesia-ui-i18n.CkfHdQzY.chunk.js +5 -0
  165. package/lib/vertesia-ui-i18n.CkfHdQzY.chunk.js.map +1 -0
  166. package/lib/vertesia-ui-i18n.DFuKfTe1.chunk.js +5 -0
  167. package/lib/vertesia-ui-i18n.DFuKfTe1.chunk.js.map +1 -0
  168. package/lib/vertesia-ui-i18n.DHqNVekZ.chunk.js +5 -0
  169. package/lib/vertesia-ui-i18n.DHqNVekZ.chunk.js.map +1 -0
  170. package/lib/vertesia-ui-i18n.DOaKbmaj.chunk.js +5 -0
  171. package/lib/vertesia-ui-i18n.DOaKbmaj.chunk.js.map +1 -0
  172. package/lib/vertesia-ui-i18n.Dge3Gl-h.chunk.js +5 -0
  173. package/lib/vertesia-ui-i18n.Dge3Gl-h.chunk.js.map +1 -0
  174. package/lib/vertesia-ui-i18n.GLt-10Lx.chunk.js +5 -0
  175. package/lib/vertesia-ui-i18n.GLt-10Lx.chunk.js.map +1 -0
  176. package/lib/vertesia-ui-i18n.T-XtAGbw.chunk.js +5 -0
  177. package/lib/vertesia-ui-i18n.T-XtAGbw.chunk.js.map +1 -0
  178. package/lib/vertesia-ui-i18n.VOJyTpMM.chunk.js +5 -0
  179. package/lib/vertesia-ui-i18n.VOJyTpMM.chunk.js.map +1 -0
  180. package/lib/vertesia-ui-i18n.js +3 -3
  181. package/lib/vertesia-ui-i18n.js.map +1 -1
  182. package/lib/vertesia-ui-layout.js +15 -15
  183. package/lib/vertesia-ui-layout.js.map +1 -1
  184. package/lib/vertesia-ui-rich-text.js +44 -44
  185. package/lib/vertesia-ui-rich-text.js.map +1 -1
  186. package/lib/vertesia-ui-session.js +1 -1
  187. package/lib/vertesia-ui-session.js.map +1 -1
  188. package/lib/vertesia-ui-shell.js +72 -48
  189. package/lib/vertesia-ui-shell.js.map +1 -1
  190. package/lib/vertesia-ui-widgets.js +51 -51
  191. package/lib/vertesia-ui-widgets.js.map +1 -1
  192. package/package.json +19 -19
  193. package/src/boot/branding.ts +8 -0
  194. package/src/boot/index.ts +6 -23
  195. package/src/boot/loading.ts +27 -0
  196. package/src/core/components/SelectList.tsx +5 -2
  197. package/src/core/components/SidePanel.tsx +65 -60
  198. package/src/core/components/shadcn/theme/ThemeProvider.test.tsx +101 -0
  199. package/src/core/components/shadcn/theme/ThemeProvider.tsx +21 -9
  200. package/src/env/index.test.ts +21 -0
  201. package/src/env/index.ts +33 -2
  202. package/src/features/agent/chat/ModernAgentConversation.test.tsx +81 -0
  203. package/src/features/agent/chat/ModernAgentConversation.tsx +5 -0
  204. package/src/features/agent/chat/ModernAgentOutput/MessageInput.tsx +17 -1
  205. package/src/features/magic-pdf/GroundedExtractionView.tsx +3 -3
  206. package/src/features/media-viewer/AudioPanel.test.ts +84 -0
  207. package/src/features/media-viewer/AudioPanel.tsx +97 -14
  208. package/src/features/oauth/McpConnectionsButton.test.tsx +56 -0
  209. package/src/features/oauth/McpConnectionsButton.tsx +9 -6
  210. package/src/features/oauth/McpConnectionsDialog.tsx +14 -5
  211. package/src/features/oauth/useMcpConnections.test.ts +10 -0
  212. package/src/features/oauth/useMcpConnections.ts +18 -10
  213. package/src/features/store/objects/selection/ObjectsActionContext.tsx +2 -0
  214. package/src/features/store/objects/selection/SelectionActions.tsx +13 -6
  215. package/src/features/store/objects/selection/actions/StartWorkflowAction.tsx +162 -0
  216. package/src/features/store/objects/selection/actions/index.ts +1 -0
  217. package/src/i18n/locales/ar.json +10 -2
  218. package/src/i18n/locales/de.json +6 -2
  219. package/src/i18n/locales/en.json +6 -2
  220. package/src/i18n/locales/es.json +6 -2
  221. package/src/i18n/locales/fr.json +7 -2
  222. package/src/i18n/locales/it.json +6 -2
  223. package/src/i18n/locales/ja.json +6 -2
  224. package/src/i18n/locales/ko.json +6 -2
  225. package/src/i18n/locales/pt.json +6 -2
  226. package/src/i18n/locales/ru.json +8 -2
  227. package/src/i18n/locales/tr.json +6 -2
  228. package/src/i18n/locales/zh-TW.json +6 -2
  229. package/src/i18n/locales/zh.json +6 -2
  230. package/src/session/UserSession.test.ts +48 -3
  231. package/src/session/UserSession.ts +20 -8
  232. package/src/session/UserSessionProvider.handoff.test.tsx +103 -0
  233. package/src/session/UserSessionProvider.iframe.test.tsx +178 -0
  234. package/src/session/UserSessionProvider.state-mismatch.test.tsx +43 -0
  235. package/src/session/UserSessionProvider.tsx +35 -23
  236. package/src/session/auth/auth-flow.md +18 -5
  237. package/src/session/auth/authRoundTrip.ts +24 -0
  238. package/src/session/auth/composable.test.ts +23 -1
  239. package/src/session/auth/composable.ts +10 -8
  240. package/src/session/auth/domainRouting.ts +4 -25
  241. package/src/session/auth/oauth.test.ts +121 -4
  242. package/src/session/auth/oauth.ts +172 -11
  243. package/src/session/auth/tokenIssuer.test.ts +25 -0
  244. package/src/session/auth/tokenIssuer.ts +25 -0
  245. package/src/session/auth/vertesiaHosts.test.ts +26 -0
  246. package/src/session/auth/vertesiaHosts.ts +85 -0
  247. package/src/session/index.ts +1 -0
  248. package/src/shell/AuthScreenPreview.tsx +2 -6
  249. package/src/shell/BrandedAuthScreens.test.tsx +26 -0
  250. package/src/shell/BrandedAuthScreens.tsx +4 -40
  251. package/src/shell/BrandedLoadingIndicator.tsx +42 -0
  252. package/src/shell/SplashScreen.tsx +4 -4
  253. package/src/shell/apps/iframe-auth.test.ts +8 -11
  254. package/src/shell/apps/iframe-auth.ts +10 -1
  255. package/src/shell/index.tsx +1 -0
  256. package/src/shell/login/SignInAuthPending.tsx +2 -2
  257. package/src/shell/login/SignInEmailStep.tsx +4 -2
  258. package/src/shell/login/TerminalLogin.test.tsx +111 -0
  259. package/src/shell/login/TerminalLogin.tsx +61 -37
  260. package/lib/features/store/objects/selection/actions/StartWorkflowButton.d.ts +0 -7
  261. package/lib/features/store/objects/selection/actions/StartWorkflowButton.d.ts.map +0 -1
  262. package/lib/features/store/objects/selection/actions/StartWorkflowButton.js +0 -51
  263. package/lib/features/store/objects/selection/actions/StartWorkflowButton.js.map +0 -1
  264. package/lib/vertesia-ui-i18n.B3NFclwl.chunk.js +0 -5
  265. package/lib/vertesia-ui-i18n.B3NFclwl.chunk.js.map +0 -1
  266. package/lib/vertesia-ui-i18n.BWSrNIxc.chunk.js +0 -5
  267. package/lib/vertesia-ui-i18n.BWSrNIxc.chunk.js.map +0 -1
  268. package/lib/vertesia-ui-i18n.BWvWeB1p.chunk.js +0 -5
  269. package/lib/vertesia-ui-i18n.BWvWeB1p.chunk.js.map +0 -1
  270. package/lib/vertesia-ui-i18n.BZCXvTC1.chunk.js +0 -5
  271. package/lib/vertesia-ui-i18n.BZCXvTC1.chunk.js.map +0 -1
  272. package/lib/vertesia-ui-i18n.BbJMvFZW.chunk.js +0 -5
  273. package/lib/vertesia-ui-i18n.BbJMvFZW.chunk.js.map +0 -1
  274. package/lib/vertesia-ui-i18n.CE5ldwpt.chunk.js +0 -5
  275. package/lib/vertesia-ui-i18n.CE5ldwpt.chunk.js.map +0 -1
  276. package/lib/vertesia-ui-i18n.CMC0IShY.chunk.js +0 -5
  277. package/lib/vertesia-ui-i18n.CMC0IShY.chunk.js.map +0 -1
  278. package/lib/vertesia-ui-i18n.CkcRqBKU.chunk.js +0 -5
  279. package/lib/vertesia-ui-i18n.CkcRqBKU.chunk.js.map +0 -1
  280. package/lib/vertesia-ui-i18n.D-Mq1sw_.chunk.js +0 -5
  281. package/lib/vertesia-ui-i18n.D-Mq1sw_.chunk.js.map +0 -1
  282. package/lib/vertesia-ui-i18n.DkK68-A-.chunk.js +0 -5
  283. package/lib/vertesia-ui-i18n.DkK68-A-.chunk.js.map +0 -1
  284. package/lib/vertesia-ui-i18n.XE_dcCg6.chunk.js +0 -5
  285. package/lib/vertesia-ui-i18n.XE_dcCg6.chunk.js.map +0 -1
  286. package/lib/vertesia-ui-i18n.qgNqMPvK.chunk.js +0 -5
  287. package/lib/vertesia-ui-i18n.qgNqMPvK.chunk.js.map +0 -1
  288. package/src/features/store/objects/selection/actions/StartWorkflowButton.tsx +0 -104
@@ -0,0 +1,178 @@
1
+ import { cleanup, render, waitFor } from '@testing-library/react';
2
+ import { type AuthTokenPayload, PrincipalType } from '@vertesia/common';
3
+ import { Env } from '@vertesia/ui/env';
4
+ import { onAuthStateChanged } from 'firebase/auth';
5
+ import { afterEach, expect, it, vi } from 'vitest';
6
+ import {
7
+ IFRAME_APP_HOST_ORIGIN_PARAM,
8
+ IFRAME_AUTH_RESPONSE,
9
+ requestIframeHostAuthToken,
10
+ } from '../shell/apps/iframe-auth';
11
+ import { getComposableToken } from './auth/composable';
12
+ import { redirectToCentralAuth, shouldRedirectToCentralAuth } from './auth/domainRouting';
13
+ import { getFirebaseAuth } from './auth/firebase';
14
+ import { useUserSession } from './UserSession';
15
+ import { UserSessionProvider } from './UserSessionProvider';
16
+
17
+ vi.mock('./auth/composable', async (original) => ({
18
+ ...(await original<typeof import('./auth/composable')>()),
19
+ getComposableToken: vi.fn(),
20
+ }));
21
+ vi.mock('./auth/domainRouting', async (original) => ({
22
+ ...(await original<typeof import('./auth/domainRouting')>()),
23
+ redirectToCentralAuth: vi.fn(),
24
+ shouldRedirectToCentralAuth: vi.fn(() => true),
25
+ }));
26
+ vi.mock('./auth/firebase', () => ({ getFirebaseAuth: vi.fn() }));
27
+ vi.mock('firebase/auth', async (original) => ({
28
+ ...(await original<typeof import('firebase/auth')>()),
29
+ onAuthStateChanged: vi.fn(() => vi.fn()),
30
+ }));
31
+
32
+ const originalParent = Object.getOwnPropertyDescriptor(window, 'parent');
33
+ const hostOrigin = 'https://cloud.vertesia.io';
34
+
35
+ afterEach(() => {
36
+ cleanup();
37
+ vi.clearAllMocks();
38
+ if (originalParent) Object.defineProperty(window, 'parent', originalParent);
39
+ else Reflect.deleteProperty(window, 'parent');
40
+ window.history.replaceState(null, '', '/');
41
+ sessionStorage.clear();
42
+ localStorage.clear();
43
+ });
44
+
45
+ function init(options: { allowLegacyIframeAuth?: boolean; isLocalDev?: boolean; provider?: boolean } = {}) {
46
+ Env.init({
47
+ name: 'test',
48
+ version: '1',
49
+ type: 'production',
50
+ isDocker: false,
51
+ isLocalDev: options.isLocalDev ?? false,
52
+ allowLegacyIframeAuth: options.allowLegacyIframeAuth,
53
+ endpoints: {
54
+ studio: 'https://api.dev1.vertesia.io',
55
+ zeno: 'https://api.dev1.vertesia.io',
56
+ sts: 'https://sts.dev1.vertesia.io',
57
+ },
58
+ authTokenProvider: options.provider === false ? undefined : () => requestIframeHostAuthToken(20),
59
+ });
60
+ window.history.replaceState(null, '', `/?${IFRAME_APP_HOST_ORIGIN_PARAM}=${encodeURIComponent(hostOrigin)}`);
61
+ }
62
+
63
+ function embed(response?: { token?: string; expiresAt?: number }) {
64
+ const parentWindow = {
65
+ postMessage: vi.fn((request: { requestId: string }) => {
66
+ if (!response) return;
67
+ queueMicrotask(() =>
68
+ window.dispatchEvent(
69
+ new MessageEvent('message', {
70
+ source: parentWindow as unknown as Window,
71
+ origin: hostOrigin,
72
+ data: { type: IFRAME_AUTH_RESPONSE, requestId: request.requestId, ...response },
73
+ }),
74
+ ),
75
+ );
76
+ }),
77
+ };
78
+ Object.defineProperty(window, 'parent', { configurable: true, value: parentWindow });
79
+ return parentWindow;
80
+ }
81
+
82
+ function Probe() {
83
+ const session = useUserSession();
84
+ return <span>{session.isLoading ? 'loading' : (session.authError?.message ?? session.user?.name ?? 'ready')}</span>;
85
+ }
86
+
87
+ const hostFailures = [
88
+ { name: 'denied', response: {} },
89
+ { name: 'expired', response: { token: 'expired', expiresAt: 0 } },
90
+ { name: 'NaN expiry', response: { token: 'invalid', expiresAt: Number.NaN } },
91
+ { name: 'infinite expiry', response: { token: 'invalid', expiresAt: Number.POSITIVE_INFINITY } },
92
+ { name: 'timeout', response: undefined },
93
+ ];
94
+
95
+ it.each(hostFailures)('does not fall back after a $name host response', async ({ response }) => {
96
+ init();
97
+ const parentWindow = embed(response);
98
+ const view = render(
99
+ <UserSessionProvider>
100
+ <Probe />
101
+ </UserSessionProvider>,
102
+ );
103
+ await view.findByText('Embedded authentication requires a valid token from the host.');
104
+ expect(parentWindow.postMessage).toHaveBeenCalled();
105
+ expect(getComposableToken).not.toHaveBeenCalled();
106
+ expect(redirectToCentralAuth).not.toHaveBeenCalled();
107
+ expect(getFirebaseAuth).not.toHaveBeenCalled();
108
+ expect(onAuthStateChanged).not.toHaveBeenCalled();
109
+ });
110
+
111
+ it.each([undefined, false])(
112
+ 'fails closed locally without a provider (allowLegacyIframeAuth=%s)',
113
+ async (allowLegacyIframeAuth) => {
114
+ init({ isLocalDev: true, allowLegacyIframeAuth, provider: false });
115
+ embed();
116
+ const view = render(
117
+ <UserSessionProvider>
118
+ <Probe />
119
+ </UserSessionProvider>,
120
+ );
121
+ await view.findByText('Embedded authentication requires a valid token from the host.');
122
+ expect(redirectToCentralAuth).not.toHaveBeenCalled();
123
+ expect(getFirebaseAuth).not.toHaveBeenCalled();
124
+ },
125
+ );
126
+
127
+ it.each([false, true])('preserves explicit legacy fallback (central=%s)', async (central) => {
128
+ init({ allowLegacyIframeAuth: true });
129
+ embed({});
130
+ vi.mocked(shouldRedirectToCentralAuth).mockReturnValue(central);
131
+ render(
132
+ <UserSessionProvider>
133
+ <Probe />
134
+ </UserSessionProvider>,
135
+ );
136
+ await waitFor(() => expect(central ? redirectToCentralAuth : onAuthStateChanged).toHaveBeenCalled());
137
+ });
138
+
139
+ it('preserves standalone sign-in when no host is present', async () => {
140
+ init();
141
+ vi.mocked(shouldRedirectToCentralAuth).mockReturnValue(true);
142
+ render(
143
+ <UserSessionProvider>
144
+ <Probe />
145
+ </UserSessionProvider>,
146
+ );
147
+ await waitFor(() => expect(redirectToCentralAuth).toHaveBeenCalled());
148
+ });
149
+
150
+ it('uses a valid scoped host token without starting another auth flow', async () => {
151
+ init();
152
+ const claims: AuthTokenPayload = {
153
+ sub: 'user',
154
+ user_id: 'user',
155
+ name: 'Embedded user',
156
+ type: PrincipalType.OAuthAccess,
157
+ iss: 'https://sts.dev1.vertesia.io',
158
+ aud: 'https://sts.dev1.vertesia.io',
159
+ apps: [],
160
+ accounts: [],
161
+ account_roles: [],
162
+ account: { id: 'a', name: 'Account' },
163
+ project: { id: 'p', name: 'Project', account: 'a' },
164
+ exp: Math.floor(Date.now() / 1000) + 3600,
165
+ };
166
+ const token = `e30.${btoa(JSON.stringify(claims))}.signature`;
167
+ embed({ token, expiresAt: Date.now() + 60_000 });
168
+ vi.mocked(getComposableToken).mockResolvedValue({ rawToken: token, token: claims, error: false });
169
+ const view = render(
170
+ <UserSessionProvider loadOnboardingStatus={false}>
171
+ <Probe />
172
+ </UserSessionProvider>,
173
+ );
174
+ await view.findByText('Embedded user');
175
+ expect(getComposableToken).toHaveBeenCalledWith(undefined, undefined, token, false, true);
176
+ expect(redirectToCentralAuth).not.toHaveBeenCalled();
177
+ expect(onAuthStateChanged).not.toHaveBeenCalled();
178
+ });
@@ -0,0 +1,43 @@
1
+ import { cleanup, render, waitFor } from '@testing-library/react';
2
+ import { Env } from '@vertesia/ui/env';
3
+ import { afterEach, expect, it, vi } from 'vitest';
4
+ import { UserSessionProvider } from './UserSessionProvider';
5
+
6
+ const mocks = vi.hoisted(() => ({ clear: vi.fn(), redirect: vi.fn(), exchange: vi.fn() }));
7
+ vi.mock('./auth/useAuthState', () => ({
8
+ useAuthState: () => ({ verifyState: () => 'mismatch', clearState: mocks.clear }),
9
+ }));
10
+ vi.mock('./auth/domainRouting', async (original) => ({
11
+ ...(await original<typeof import('./auth/domainRouting')>()),
12
+ shouldRedirectToCentralAuth: () => true,
13
+ redirectToCentralAuth: mocks.redirect,
14
+ }));
15
+ vi.mock('./auth/composable', async (original) => ({
16
+ ...(await original<typeof import('./auth/composable')>()),
17
+ getComposableToken: mocks.exchange,
18
+ }));
19
+ afterEach(() => {
20
+ cleanup();
21
+ vi.clearAllMocks();
22
+ window.history.replaceState(null, '', '/');
23
+ });
24
+ it('clears a mismatched fragment and returns without exchanging credentials', async () => {
25
+ window.history.replaceState(null, '', '/#token=untrusted&state=wrong');
26
+ Env.init({
27
+ name: 'test',
28
+ version: '1',
29
+ type: 'production',
30
+ isLocalDev: false,
31
+ isDocker: false,
32
+ endpoints: { studio: 'https://api.example', zeno: 'https://api.example', sts: 'https://sts.example' },
33
+ });
34
+ render(
35
+ <UserSessionProvider>
36
+ <span>App</span>
37
+ </UserSessionProvider>,
38
+ );
39
+ await waitFor(() => expect(mocks.redirect).toHaveBeenCalledOnce());
40
+ expect(mocks.clear).toHaveBeenCalledOnce();
41
+ expect(window.location.hash).toBe('');
42
+ expect(mocks.exchange).not.toHaveBeenCalled();
43
+ });
@@ -65,7 +65,7 @@ interface UserSessionProviderProps {
65
65
  children: ReactNode | ReactNode[];
66
66
  loadOnboardingStatus?: boolean;
67
67
  }
68
- export function UserSessionProvider({ children, loadOnboardingStatus = true }: UserSessionProviderProps) {
68
+ export function UserSessionProvider({ children, loadOnboardingStatus = false }: UserSessionProviderProps) {
69
69
  const hashParams = new URLSearchParams(location.hash.substring(1));
70
70
  const token = hashParams.get('token');
71
71
  const state = hashParams.get('state');
@@ -132,28 +132,6 @@ export function UserSessionProvider({ children, loadOnboardingStatus = true }: U
132
132
  },
133
133
  });
134
134
 
135
- if (usesGatewaySession() || usesAppOAuth()) {
136
- let cancelled = false;
137
- session.setSession = setSession;
138
- const initialize = usesGatewaySession()
139
- ? session.loginGatewaySession()
140
- : getAppOAuthToken().then((token) => session.login(token, { loadOnboardingStatus: false }));
141
- void initialize
142
- .then(() => {
143
- if (!cancelled) setSession(session.clone());
144
- })
145
- .catch((error: unknown) => {
146
- if (cancelled) return;
147
- session.isLoading = false;
148
- session.authError = error instanceof Error ? error : new Error(String(error));
149
- setSession(session.clone());
150
- });
151
- return () => {
152
- cancelled = true;
153
- hasInitiatedAuthRef.current = false;
154
- };
155
- }
156
-
157
135
  if (Env.isLocalDev && Env.devAuthToken) {
158
136
  session.setSession = setSession;
159
137
  getComposableToken(selectedAccount, selectedProject, Env.devAuthToken)
@@ -177,6 +155,30 @@ export function UserSessionProvider({ children, loadOnboardingStatus = true }: U
177
155
  return;
178
156
  }
179
157
 
158
+ if (usesGatewaySession() || usesAppOAuth()) {
159
+ let cancelled = false;
160
+ session.setSession = setSession;
161
+ const initialize = usesGatewaySession()
162
+ ? session.loginGatewaySession()
163
+ : getAppOAuthToken().then((token) =>
164
+ session.login(token, { loadOnboardingStatus, authMethod: 'oauth' }),
165
+ );
166
+ void initialize
167
+ .then(() => {
168
+ if (!cancelled) setSession(session.clone());
169
+ })
170
+ .catch((error: unknown) => {
171
+ if (cancelled || surfaceAuthError(error)) return;
172
+ session.isLoading = false;
173
+ session.authError = error instanceof Error ? error : new Error(String(error));
174
+ setSession(session.clone());
175
+ });
176
+ return () => {
177
+ cancelled = true;
178
+ hasInitiatedAuthRef.current = false;
179
+ };
180
+ }
181
+
180
182
  if (token && state) {
181
183
  session.setSession = setSession;
182
184
  const validationError = verifyState(state);
@@ -187,7 +189,10 @@ export function UserSessionProvider({ children, loadOnboardingStatus = true }: U
187
189
  state: state,
188
190
  },
189
191
  });
192
+ clearState();
193
+ clearAuthHash();
190
194
  redirectToCentralAuth({ accountId: selectedAccount, projectId: selectedProject });
195
+ return;
191
196
  } else {
192
197
  clearState();
193
198
  }
@@ -217,6 +222,13 @@ export function UserSessionProvider({ children, loadOnboardingStatus = true }: U
217
222
 
218
223
  const startFirebaseOrCentralAuth = () => {
219
224
  if (cancelled) return;
225
+ // A missing/expired host token must not upgrade an embedded app to a full user session.
226
+ if (window.parent !== window && !Env.allowLegacyIframeAuth) {
227
+ session.isLoading = false;
228
+ session.authError = new Error('Embedded authentication requires a valid token from the host.');
229
+ setSession(session.clone());
230
+ return;
231
+ }
220
232
 
221
233
  // If the current host is not in the Firebase allowlist, central auth owns sign-in.
222
234
  if (!session.isLoggedIn()) {
@@ -191,8 +191,8 @@ The diagrams below use the default hostname; redirects use the selected broker.
191
191
  ┌──────────────────────┐ │ - Microsoft │ ┌──────────────────────┐
192
192
  │ Build redirect URL: │ └──────────┬───────────┘ │ Redirect to SSO │
193
193
  │ │ │ │ provider (Okta, │
194
- │ auth.vertesia.io │ ▼ │ Azure AD, etc.) │
195
- │ .vertesia.app │ ┌──────────────────────┐ └──────────┬───────────┘
194
+ │ auth.vertesia.io │ ▼ │ Azure AD, etc.) │
195
+ │ │ ┌──────────────────────┐ └──────────┬───────────┘
196
196
  │ ?sts=... │ │ Firebase OAuth │ │
197
197
  │ &redirect_uri=... │ │ redirect & return │ ▼
198
198
  │ &state=... │ └──────────┬───────────┘ ┌──────────────────────┐
@@ -484,8 +484,8 @@ The diagrams below use the default hostname; redirects use the selected broker.
484
484
  │ Redirect to Central │ │ getFirebaseAuth() │
485
485
  │ Auth for logout: │ │ .signOut() │
486
486
  │ │ │ │
487
- │ auth.vertesia.io │ │ (Triggers │
488
- │ .vertesia.app │ │ onAuthStateChanged │
487
+ │ auth.vertesia.io │ │ (Triggers │
488
+ │ │ │ onAuthStateChanged │
489
489
  │ /logout │ │ with anonymous user)│
490
490
  │ │ └──────────┬───────────┘
491
491
  │ (Central auth │ │
@@ -1103,4 +1103,17 @@ flowchart TB
1103
1103
  LO5 --> LO7
1104
1104
  LO6 --> LO7
1105
1105
  LO7 --> LO8 --> LO9
1106
- end
1106
+ end
1107
+ ## OAuth credential lifetime
1108
+
1109
+ The short-lived access token and the ten-minute PKCE transaction are stored in tab-scoped
1110
+ sessionStorage. The state and verifier must survive the authorization redirect; the transaction is
1111
+ consumed once on return. Refresh credentials are held only in module memory, never browser storage.
1112
+ Reloading retains a valid access token; after it expires, a fresh broker round-trip is required.
1113
+ Logout revokes the in-memory refresh credential and clears both stored records. This does not make
1114
+ arbitrary scripts running on the application origin trusted: they share that origin's privileges.
1115
+
1116
+ Embedded applications reacquire scoped credentials through the parent message protocol. A response
1117
+ without a token is a denial, and an already-expired `expiresAt` is rejected. JWT expiry governs the
1118
+ session cache; app-scoped tokens use a 60-second renewal window and are never exchanged for a broader
1119
+ session token just because their lifetime is shorter than a normal session's.
@@ -0,0 +1,24 @@
1
+ const AUTH_ROUND_TRIP_COOKIE = 'vtsauth';
2
+ const AUTH_ROUND_TRIP_TTL_SECONDS = 300;
3
+
4
+ function authRoundTripCookie(value: string, maxAgeSeconds: number): string {
5
+ // Secure only over https: setting it on http://localhost would make the browser drop the
6
+ // cookie, and local development would silently lose the return-leg preloads.
7
+ const secure = window.location.protocol === 'https:' ? '; Secure' : '';
8
+ return `${AUTH_ROUND_TRIP_COOKIE}=${value}; Path=/; Max-Age=${maxAgeSeconds}; SameSite=Lax${secure}`;
9
+ }
10
+
11
+ /** Record that this browser is on its way to the broker. Called for you by {@link redirectToCentralAuth}. */
12
+ export function markCentralAuthRoundTripStarted(): void {
13
+ // biome-ignore lint/suspicious/noDocumentCookie: CookieStore is async; this must land before location.replace()
14
+ document.cookie = authRoundTripCookie('1', AUTH_ROUND_TRIP_TTL_SECONDS);
15
+ }
16
+
17
+ /**
18
+ * Drop the marker. An app calls this once it is actually rendering, so that its *next* cold load is
19
+ * recognized as an outgoing leg again and does not preload an application it is about to discard.
20
+ */
21
+ export function clearCentralAuthRoundTripMarker(): void {
22
+ // biome-ignore lint/suspicious/noDocumentCookie: pairs with the write above; see that comment.
23
+ document.cookie = authRoundTripCookie('', 0);
24
+ }
@@ -38,7 +38,7 @@ describe('getComposableToken', () => {
38
38
  vi.clearAllMocks();
39
39
  });
40
40
 
41
- it('uses an authorization-bearing STS-issued Vertesia token directly instead of exchanging it', async () => {
41
+ it.each([false, true])('uses an authorization-bearing STS token directly (branch=%s)', async (branch) => {
42
42
  const token = makeJwt({
43
43
  iss: 'https://sts.dev1.vertesia.io',
44
44
  exp: Math.floor(Date.now() / 1000) + 3600,
@@ -51,6 +51,10 @@ describe('getComposableToken', () => {
51
51
  vi.stubGlobal('fetch', fetchMock);
52
52
 
53
53
  const { getComposableToken } = await importComposableAuth();
54
+ if (branch) {
55
+ const { Env } = await import('@vertesia/ui/env');
56
+ Env.endpoints.sts = 'https://token-server-dev-example.api.dev1.vertesia.io';
57
+ }
54
58
  const result = await getComposableToken('account-id', 'project-id', token, false, true);
55
59
 
56
60
  expect(result.rawToken).toBe(token);
@@ -85,6 +89,24 @@ describe('getComposableToken', () => {
85
89
  expect(fetchMock).not.toHaveBeenCalled();
86
90
  });
87
91
 
92
+ it('uses a short-lived scoped iframe token directly, including a forced host refresh', async () => {
93
+ const token = makeJwt({
94
+ iss: 'https://sts.dev1.vertesia.io',
95
+ exp: Math.floor(Date.now() / 1000) + 120,
96
+ client_id: 'vertesia-app:analytics',
97
+ account: { id: 'account-id' },
98
+ project: { id: 'project-id' },
99
+ apps: ['analytics'],
100
+ });
101
+ const provider = vi.fn(async () => token);
102
+ const fetcher = vi.fn();
103
+ vi.stubGlobal('fetch', fetcher);
104
+ const { getComposableToken } = await importComposableAuth(provider);
105
+ expect((await getComposableToken('account-id', 'project-id', undefined, true, true)).rawToken).toBe(token);
106
+ expect(provider).toHaveBeenCalledOnce();
107
+ expect(fetcher).not.toHaveBeenCalled();
108
+ });
109
+
88
110
  it('falls back to the cached credential when the injected provider is unavailable', async () => {
89
111
  const expiringToken = makeJwt({
90
112
  iss: 'https://sts.dev1.vertesia.io',
@@ -18,6 +18,7 @@ import {
18
18
  shouldRedirectToCentralAuth,
19
19
  } from './domainRouting';
20
20
  import { getFirebaseAuth, getFirebaseAuthToken } from './firebase';
21
+ import { isStsTokenIssuer } from './tokenIssuer';
21
22
 
22
23
  let AUTH_TOKEN_RAW: string | undefined;
23
24
  let AUTH_TOKEN: AuthTokenPayload | undefined;
@@ -150,10 +151,6 @@ export function resolveAuthSelection(currentUrl: URL): { accountId?: string; pro
150
151
  return { accountId, projectId };
151
152
  }
152
153
 
153
- function normalizeIssuer(value: string | undefined): string | undefined {
154
- return value?.replace(/\/+$/, '');
155
- }
156
-
157
154
  function decodeToken(token: string): AuthTokenPayload {
158
155
  return jwtDecode(token) as AuthTokenPayload;
159
156
  }
@@ -162,7 +159,7 @@ function isVertesiaIssuedToken(token: string | undefined): token is string {
162
159
  if (!token) return false;
163
160
  try {
164
161
  const decoded = decodeToken(token) as AuthTokenPayload & { iss?: string };
165
- return normalizeIssuer(decoded.iss) === normalizeIssuer(Env.endpoints.sts);
162
+ return isStsTokenIssuer(decoded.iss, Env.endpoints.sts);
166
163
  } catch {
167
164
  return false;
168
165
  }
@@ -170,7 +167,11 @@ function isVertesiaIssuedToken(token: string | undefined): token is string {
170
167
 
171
168
  function canUseVertesiaTokenDirectly(token: string, accountId?: string, projectId?: string): boolean {
172
169
  const decoded = decodeToken(token);
173
- if (!decoded.exp || decoded.exp <= Date.now() / 1000 + 300) {
170
+ const appSession =
171
+ 'client_id' in decoded &&
172
+ typeof decoded.client_id === 'string' &&
173
+ decoded.client_id.startsWith('vertesia-app:');
174
+ if (!decoded.exp || decoded.exp <= Date.now() / 1000 + (appSession ? 60 : 300)) {
174
175
  return false;
175
176
  }
176
177
  const hasAuthorizationClaims = Boolean(
@@ -537,7 +538,8 @@ export async function getComposableToken(
537
538
  AUTH_TOKEN_RAW = await fetchComposableTokenFromFirebaseToken(selectedAccount, selectedProject);
538
539
  } else if (!devAuthToken) {
539
540
  // Embedded apps can reacquire a fresh credential from their host after their cached token expires.
540
- const refreshCredential = (await Env.authTokenProvider?.()) ?? initToken ?? AUTH_TOKEN_RAW;
541
+ const providerCredential = await Env.authTokenProvider?.();
542
+ const refreshCredential = providerCredential ?? initToken ?? AUTH_TOKEN_RAW;
541
543
  // `forceRefresh` has to defeat this shortcut, not just the cache above it. A caller
542
544
  // asking for a forced refresh wants claims recomputed -- `refreshAuthToken()` exists so
543
545
  // a stale `apps` claim can be re-read after an ACE change, and STS recomputes it on
@@ -545,7 +547,7 @@ export async function getComposableToken(
545
547
  // returns the very token whose claims were suspect, silently making the call a no-op
546
548
  // for exactly the sessions that have no other credential to fall back on.
547
549
  if (
548
- !forceRefresh &&
550
+ (!forceRefresh || !!providerCredential) &&
549
551
  refreshCredential &&
550
552
  isVertesiaIssuedToken(refreshCredential) &&
551
553
  canUseVertesiaTokenDirectly(refreshCredential, selectedAccount, selectedProject)
@@ -1,3 +1,7 @@
1
+ import { markCentralAuthRoundTripStarted } from './authRoundTrip';
2
+
3
+ export { clearCentralAuthRoundTripMarker, markCentralAuthRoundTripStarted } from './authRoundTrip';
4
+
1
5
  import { Env } from '@vertesia/ui/env';
2
6
  import { generateAuthState } from './authState';
3
7
  import { gatewayLoginUrl, usesGatewaySession } from './gateway';
@@ -114,31 +118,6 @@ export function buildCentralAuthRedirectUrl(
114
118
  * The TTL bounds the cost of the case where the app never mounts (the visitor abandons the login
115
119
  * screen): after it lapses, cold loads are back to withholding the application preloads.
116
120
  */
117
- const AUTH_ROUND_TRIP_COOKIE = 'vtsauth';
118
- const AUTH_ROUND_TRIP_TTL_SECONDS = 300;
119
-
120
- function authRoundTripCookie(value: string, maxAgeSeconds: number): string {
121
- // Secure only over https: setting it on http://localhost would make the browser drop the
122
- // cookie, and local development would silently lose the return-leg preloads.
123
- const secure = window.location.protocol === 'https:' ? '; Secure' : '';
124
- return `${AUTH_ROUND_TRIP_COOKIE}=${value}; Path=/; Max-Age=${maxAgeSeconds}; SameSite=Lax${secure}`;
125
- }
126
-
127
- /** Record that this browser is on its way to the broker. Called for you by {@link redirectToCentralAuth}. */
128
- export function markCentralAuthRoundTripStarted(): void {
129
- // biome-ignore lint/suspicious/noDocumentCookie: CookieStore is async; this must land before location.replace()
130
- document.cookie = authRoundTripCookie('1', AUTH_ROUND_TRIP_TTL_SECONDS);
131
- }
132
-
133
- /**
134
- * Drop the marker. An app calls this once it is actually rendering, so that its *next* cold load is
135
- * recognized as an outgoing leg again and does not preload an application it is about to discard.
136
- */
137
- export function clearCentralAuthRoundTripMarker(): void {
138
- // biome-ignore lint/suspicious/noDocumentCookie: pairs with the write above; see that comment.
139
- document.cookie = authRoundTripCookie('', 0);
140
- }
141
-
142
121
  /**
143
122
  * Start a Central Auth round-trip for the current page.
144
123
  *