@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.
- package/lib/boot/branding.d.ts +13 -0
- package/lib/boot/branding.d.ts.map +1 -1
- package/lib/boot/branding.js +7 -0
- package/lib/boot/branding.js.map +1 -1
- package/lib/boot/index.d.ts +1 -1
- package/lib/boot/index.d.ts.map +1 -1
- package/lib/boot/index.js +5 -23
- package/lib/boot/index.js.map +1 -1
- package/lib/boot/loading.d.ts +4 -0
- package/lib/boot/loading.d.ts.map +1 -0
- package/lib/boot/loading.js +28 -0
- package/lib/boot/loading.js.map +1 -0
- package/lib/core/components/SelectList.d.ts.map +1 -1
- package/lib/core/components/SelectList.js +3 -3
- package/lib/core/components/SelectList.js.map +1 -1
- package/lib/core/components/SidePanel.d.ts.map +1 -1
- package/lib/core/components/SidePanel.js +2 -1
- package/lib/core/components/SidePanel.js.map +1 -1
- package/lib/core/components/shadcn/theme/ThemeProvider.d.ts.map +1 -1
- package/lib/core/components/shadcn/theme/ThemeProvider.js +21 -5
- package/lib/core/components/shadcn/theme/ThemeProvider.js.map +1 -1
- package/lib/env/index.d.ts +11 -0
- package/lib/env/index.d.ts.map +1 -1
- package/lib/env/index.js +16 -3
- package/lib/env/index.js.map +1 -1
- package/lib/features/agent/chat/ModernAgentConversation.js +4 -1
- package/lib/features/agent/chat/ModernAgentConversation.js.map +1 -1
- package/lib/features/agent/chat/ModernAgentOutput/MessageInput.d.ts +5 -1
- package/lib/features/agent/chat/ModernAgentOutput/MessageInput.d.ts.map +1 -1
- package/lib/features/agent/chat/ModernAgentOutput/MessageInput.js +9 -2
- package/lib/features/agent/chat/ModernAgentOutput/MessageInput.js.map +1 -1
- package/lib/features/magic-pdf/GroundedExtractionView.js +3 -3
- package/lib/features/magic-pdf/GroundedExtractionView.js.map +1 -1
- package/lib/features/media-viewer/AudioPanel.d.ts +10 -2
- package/lib/features/media-viewer/AudioPanel.d.ts.map +1 -1
- package/lib/features/media-viewer/AudioPanel.js +78 -13
- package/lib/features/media-viewer/AudioPanel.js.map +1 -1
- package/lib/features/oauth/McpConnectionsButton.d.ts +2 -2
- package/lib/features/oauth/McpConnectionsButton.d.ts.map +1 -1
- package/lib/features/oauth/McpConnectionsButton.js +9 -7
- package/lib/features/oauth/McpConnectionsButton.js.map +1 -1
- package/lib/features/oauth/McpConnectionsDialog.d.ts.map +1 -1
- package/lib/features/oauth/McpConnectionsDialog.js +7 -3
- package/lib/features/oauth/McpConnectionsDialog.js.map +1 -1
- package/lib/features/oauth/useMcpConnections.d.ts +4 -2
- package/lib/features/oauth/useMcpConnections.d.ts.map +1 -1
- package/lib/features/oauth/useMcpConnections.js +15 -9
- package/lib/features/oauth/useMcpConnections.js.map +1 -1
- package/lib/features/store/objects/selection/ObjectsActionContext.d.ts.map +1 -1
- package/lib/features/store/objects/selection/ObjectsActionContext.js +2 -0
- package/lib/features/store/objects/selection/ObjectsActionContext.js.map +1 -1
- package/lib/features/store/objects/selection/SelectionActions.d.ts +4 -2
- package/lib/features/store/objects/selection/SelectionActions.d.ts.map +1 -1
- package/lib/features/store/objects/selection/SelectionActions.js +6 -3
- package/lib/features/store/objects/selection/SelectionActions.js.map +1 -1
- package/lib/features/store/objects/selection/actions/StartWorkflowAction.d.ts +4 -0
- package/lib/features/store/objects/selection/actions/StartWorkflowAction.d.ts.map +1 -0
- package/lib/features/store/objects/selection/actions/StartWorkflowAction.js +81 -0
- package/lib/features/store/objects/selection/actions/StartWorkflowAction.js.map +1 -0
- package/lib/features/store/objects/selection/actions/index.d.ts +1 -0
- package/lib/features/store/objects/selection/actions/index.d.ts.map +1 -1
- package/lib/features/store/objects/selection/actions/index.js +1 -0
- package/lib/features/store/objects/selection/actions/index.js.map +1 -1
- package/lib/features/store/types/TypeRegistry.d.ts +345 -155
- package/lib/features/store/types/TypeRegistry.d.ts.map +1 -1
- package/lib/features/store/types/search/ObjectTypeSearchContext.d.ts +345 -155
- package/lib/features/store/types/search/ObjectTypeSearchContext.d.ts.map +1 -1
- package/lib/i18n/locales/ar.json +10 -2
- package/lib/i18n/locales/de.json +6 -2
- package/lib/i18n/locales/en.json +6 -2
- package/lib/i18n/locales/es.json +6 -2
- package/lib/i18n/locales/fr.json +7 -2
- package/lib/i18n/locales/it.json +6 -2
- package/lib/i18n/locales/ja.json +6 -2
- package/lib/i18n/locales/ko.json +6 -2
- package/lib/i18n/locales/pt.json +6 -2
- package/lib/i18n/locales/ru.json +8 -2
- package/lib/i18n/locales/tr.json +6 -2
- package/lib/i18n/locales/zh-TW.json +6 -2
- package/lib/i18n/locales/zh.json +6 -2
- package/lib/session/UserSession.d.ts +3 -0
- package/lib/session/UserSession.d.ts.map +1 -1
- package/lib/session/UserSession.js +18 -8
- package/lib/session/UserSession.js.map +1 -1
- package/lib/session/UserSessionProvider.d.ts.map +1 -1
- package/lib/session/UserSessionProvider.js +34 -24
- package/lib/session/UserSessionProvider.js.map +1 -1
- package/lib/session/auth/authRoundTrip.d.ts +8 -0
- package/lib/session/auth/authRoundTrip.d.ts.map +1 -0
- package/lib/session/auth/authRoundTrip.js +22 -0
- package/lib/session/auth/authRoundTrip.js.map +1 -0
- package/lib/session/auth/composable.d.ts.map +1 -1
- package/lib/session/auth/composable.js +9 -7
- package/lib/session/auth/composable.js.map +1 -1
- package/lib/session/auth/domainRouting.d.ts +13 -6
- package/lib/session/auth/domainRouting.d.ts.map +1 -1
- package/lib/session/auth/domainRouting.js +2 -21
- package/lib/session/auth/domainRouting.js.map +1 -1
- package/lib/session/auth/oauth.d.ts +6 -1
- package/lib/session/auth/oauth.d.ts.map +1 -1
- package/lib/session/auth/oauth.js +145 -8
- package/lib/session/auth/oauth.js.map +1 -1
- package/lib/session/auth/tokenIssuer.d.ts +3 -0
- package/lib/session/auth/tokenIssuer.d.ts.map +1 -0
- package/lib/session/auth/tokenIssuer.js +27 -0
- package/lib/session/auth/tokenIssuer.js.map +1 -0
- package/lib/session/auth/vertesiaHosts.d.ts +47 -0
- package/lib/session/auth/vertesiaHosts.d.ts.map +1 -0
- package/lib/session/auth/vertesiaHosts.js +79 -0
- package/lib/session/auth/vertesiaHosts.js.map +1 -0
- package/lib/session/index.d.ts +1 -0
- package/lib/session/index.d.ts.map +1 -1
- package/lib/session/index.js +1 -0
- package/lib/session/index.js.map +1 -1
- package/lib/shell/AuthScreenPreview.d.ts +1 -1
- package/lib/shell/AuthScreenPreview.d.ts.map +1 -1
- package/lib/shell/AuthScreenPreview.js +2 -5
- package/lib/shell/AuthScreenPreview.js.map +1 -1
- package/lib/shell/BrandedAuthScreens.d.ts +1 -4
- package/lib/shell/BrandedAuthScreens.d.ts.map +1 -1
- package/lib/shell/BrandedAuthScreens.js +5 -26
- package/lib/shell/BrandedAuthScreens.js.map +1 -1
- package/lib/shell/BrandedLoadingIndicator.d.ts +11 -0
- package/lib/shell/BrandedLoadingIndicator.d.ts.map +1 -0
- package/lib/shell/BrandedLoadingIndicator.js +27 -0
- package/lib/shell/BrandedLoadingIndicator.js.map +1 -0
- package/lib/shell/SplashScreen.d.ts.map +1 -1
- package/lib/shell/SplashScreen.js +2 -1
- package/lib/shell/SplashScreen.js.map +1 -1
- package/lib/shell/apps/AppInstallationProvider.d.ts +16 -4
- package/lib/shell/apps/AppInstallationProvider.d.ts.map +1 -1
- package/lib/shell/apps/iframe-auth.d.ts +2 -0
- package/lib/shell/apps/iframe-auth.d.ts.map +1 -1
- package/lib/shell/apps/iframe-auth.js +6 -2
- package/lib/shell/apps/iframe-auth.js.map +1 -1
- package/lib/shell/index.d.ts +1 -0
- package/lib/shell/index.d.ts.map +1 -1
- package/lib/shell/index.js +1 -0
- package/lib/shell/index.js.map +1 -1
- package/lib/shell/login/SignInAuthPending.js +2 -2
- package/lib/shell/login/SignInAuthPending.js.map +1 -1
- package/lib/shell/login/SignInEmailStep.d.ts.map +1 -1
- package/lib/shell/login/SignInEmailStep.js +2 -2
- package/lib/shell/login/SignInEmailStep.js.map +1 -1
- package/lib/shell/login/TerminalLogin.d.ts.map +1 -1
- package/lib/shell/login/TerminalLogin.js +28 -21
- package/lib/shell/login/TerminalLogin.js.map +1 -1
- package/lib/vertesia-ui-boot.js +33 -29
- package/lib/vertesia-ui-boot.js.map +1 -1
- package/lib/vertesia-ui-core.js +34 -34
- package/lib/vertesia-ui-core.js.map +1 -1
- package/lib/vertesia-ui-env.js +1 -1
- package/lib/vertesia-ui-env.js.map +1 -1
- package/lib/vertesia-ui-features.js +163 -163
- package/lib/vertesia-ui-features.js.map +1 -1
- package/lib/vertesia-ui-i18n.Bl8KeZUK.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.Bl8KeZUK.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.BmT06XFI.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.BmT06XFI.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.Ch-Jjj0L.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.Ch-Jjj0L.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.CjhloW9s.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.CjhloW9s.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.CkfHdQzY.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.CkfHdQzY.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.DFuKfTe1.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.DFuKfTe1.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.DHqNVekZ.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.DHqNVekZ.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.DOaKbmaj.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.DOaKbmaj.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.Dge3Gl-h.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.Dge3Gl-h.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.GLt-10Lx.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.GLt-10Lx.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.T-XtAGbw.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.T-XtAGbw.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.VOJyTpMM.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.VOJyTpMM.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.js +3 -3
- package/lib/vertesia-ui-i18n.js.map +1 -1
- package/lib/vertesia-ui-layout.js +15 -15
- package/lib/vertesia-ui-layout.js.map +1 -1
- package/lib/vertesia-ui-rich-text.js +44 -44
- package/lib/vertesia-ui-rich-text.js.map +1 -1
- package/lib/vertesia-ui-session.js +1 -1
- package/lib/vertesia-ui-session.js.map +1 -1
- package/lib/vertesia-ui-shell.js +72 -48
- package/lib/vertesia-ui-shell.js.map +1 -1
- package/lib/vertesia-ui-widgets.js +51 -51
- package/lib/vertesia-ui-widgets.js.map +1 -1
- package/package.json +19 -19
- package/src/boot/branding.ts +8 -0
- package/src/boot/index.ts +6 -23
- package/src/boot/loading.ts +27 -0
- package/src/core/components/SelectList.tsx +5 -2
- package/src/core/components/SidePanel.tsx +65 -60
- package/src/core/components/shadcn/theme/ThemeProvider.test.tsx +101 -0
- package/src/core/components/shadcn/theme/ThemeProvider.tsx +21 -9
- package/src/env/index.test.ts +21 -0
- package/src/env/index.ts +33 -2
- package/src/features/agent/chat/ModernAgentConversation.test.tsx +81 -0
- package/src/features/agent/chat/ModernAgentConversation.tsx +5 -0
- package/src/features/agent/chat/ModernAgentOutput/MessageInput.tsx +17 -1
- package/src/features/magic-pdf/GroundedExtractionView.tsx +3 -3
- package/src/features/media-viewer/AudioPanel.test.ts +84 -0
- package/src/features/media-viewer/AudioPanel.tsx +97 -14
- package/src/features/oauth/McpConnectionsButton.test.tsx +56 -0
- package/src/features/oauth/McpConnectionsButton.tsx +9 -6
- package/src/features/oauth/McpConnectionsDialog.tsx +14 -5
- package/src/features/oauth/useMcpConnections.test.ts +10 -0
- package/src/features/oauth/useMcpConnections.ts +18 -10
- package/src/features/store/objects/selection/ObjectsActionContext.tsx +2 -0
- package/src/features/store/objects/selection/SelectionActions.tsx +13 -6
- package/src/features/store/objects/selection/actions/StartWorkflowAction.tsx +162 -0
- package/src/features/store/objects/selection/actions/index.ts +1 -0
- package/src/i18n/locales/ar.json +10 -2
- package/src/i18n/locales/de.json +6 -2
- package/src/i18n/locales/en.json +6 -2
- package/src/i18n/locales/es.json +6 -2
- package/src/i18n/locales/fr.json +7 -2
- package/src/i18n/locales/it.json +6 -2
- package/src/i18n/locales/ja.json +6 -2
- package/src/i18n/locales/ko.json +6 -2
- package/src/i18n/locales/pt.json +6 -2
- package/src/i18n/locales/ru.json +8 -2
- package/src/i18n/locales/tr.json +6 -2
- package/src/i18n/locales/zh-TW.json +6 -2
- package/src/i18n/locales/zh.json +6 -2
- package/src/session/UserSession.test.ts +48 -3
- package/src/session/UserSession.ts +20 -8
- package/src/session/UserSessionProvider.handoff.test.tsx +103 -0
- package/src/session/UserSessionProvider.iframe.test.tsx +178 -0
- package/src/session/UserSessionProvider.state-mismatch.test.tsx +43 -0
- package/src/session/UserSessionProvider.tsx +35 -23
- package/src/session/auth/auth-flow.md +18 -5
- package/src/session/auth/authRoundTrip.ts +24 -0
- package/src/session/auth/composable.test.ts +23 -1
- package/src/session/auth/composable.ts +10 -8
- package/src/session/auth/domainRouting.ts +4 -25
- package/src/session/auth/oauth.test.ts +121 -4
- package/src/session/auth/oauth.ts +172 -11
- package/src/session/auth/tokenIssuer.test.ts +25 -0
- package/src/session/auth/tokenIssuer.ts +25 -0
- package/src/session/auth/vertesiaHosts.test.ts +26 -0
- package/src/session/auth/vertesiaHosts.ts +85 -0
- package/src/session/index.ts +1 -0
- package/src/shell/AuthScreenPreview.tsx +2 -6
- package/src/shell/BrandedAuthScreens.test.tsx +26 -0
- package/src/shell/BrandedAuthScreens.tsx +4 -40
- package/src/shell/BrandedLoadingIndicator.tsx +42 -0
- package/src/shell/SplashScreen.tsx +4 -4
- package/src/shell/apps/iframe-auth.test.ts +8 -11
- package/src/shell/apps/iframe-auth.ts +10 -1
- package/src/shell/index.tsx +1 -0
- package/src/shell/login/SignInAuthPending.tsx +2 -2
- package/src/shell/login/SignInEmailStep.tsx +4 -2
- package/src/shell/login/TerminalLogin.test.tsx +111 -0
- package/src/shell/login/TerminalLogin.tsx +61 -37
- package/lib/features/store/objects/selection/actions/StartWorkflowButton.d.ts +0 -7
- package/lib/features/store/objects/selection/actions/StartWorkflowButton.d.ts.map +0 -1
- package/lib/features/store/objects/selection/actions/StartWorkflowButton.js +0 -51
- package/lib/features/store/objects/selection/actions/StartWorkflowButton.js.map +0 -1
- package/lib/vertesia-ui-i18n.B3NFclwl.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.B3NFclwl.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.BWSrNIxc.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.BWSrNIxc.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.BWvWeB1p.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.BWvWeB1p.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.BZCXvTC1.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.BZCXvTC1.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.BbJMvFZW.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.BbJMvFZW.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.CE5ldwpt.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.CE5ldwpt.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.CMC0IShY.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.CMC0IShY.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.CkcRqBKU.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.CkcRqBKU.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.D-Mq1sw_.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.D-Mq1sw_.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.DkK68-A-.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.DkK68-A-.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.XE_dcCg6.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.XE_dcCg6.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.qgNqMPvK.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.qgNqMPvK.chunk.js.map +0 -1
- 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 =
|
|
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
|
|
195
|
-
│
|
|
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
|
|
488
|
-
│
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
*
|