@vertesia/ui 1.5.0-dev.20260910.052912Z → 1.6.0-dev.20260912.110338Z
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/README.md +125 -0
- package/lib/boot/branding.d.ts +58 -0
- package/lib/boot/branding.d.ts.map +1 -0
- package/lib/boot/branding.js +62 -0
- package/lib/boot/branding.js.map +1 -0
- package/lib/boot/index.d.ts +27 -0
- package/lib/boot/index.d.ts.map +1 -0
- package/lib/boot/index.js +325 -0
- package/lib/boot/index.js.map +1 -0
- package/lib/boot/vite.d.ts +63 -0
- package/lib/boot/vite.d.ts.map +1 -0
- package/lib/boot/vite.js +108 -0
- package/lib/boot/vite.js.map +1 -0
- package/lib/core/components/Link.js +1 -1
- package/lib/core/components/Link.js.map +1 -1
- package/lib/core/components/Spinner.js +1 -1
- package/lib/core/components/Spinner.js.map +1 -1
- package/lib/core/components/TagsInput.js +7 -7
- package/lib/core/components/TagsInput.js.map +1 -1
- package/lib/core/components/shadcn/badge.js +1 -1
- package/lib/core/components/shadcn/badge.js.map +1 -1
- package/lib/core/components/shadcn/button.d.ts.map +1 -1
- package/lib/core/components/shadcn/button.js +3 -4
- package/lib/core/components/shadcn/button.js.map +1 -1
- package/lib/core/components/shadcn/filters/comboBox/DateCombobox.js +2 -2
- package/lib/core/components/shadcn/filters/comboBox/DateCombobox.js.map +1 -1
- package/lib/core/components/shadcn/filters/comboBox/SelectCombobox.js +1 -1
- package/lib/core/components/shadcn/filters/comboBox/SelectCombobox.js.map +1 -1
- package/lib/core/components/shadcn/filters/comboBox/StringListCombobox.js +1 -1
- package/lib/core/components/shadcn/filters/comboBox/StringListCombobox.js.map +1 -1
- package/lib/core/components/shadcn/filters/comboBox/TextCombobox.js +1 -1
- package/lib/core/components/shadcn/filters/comboBox/TextCombobox.js.map +1 -1
- package/lib/core/components/shadcn/filters/filter/dateFilter.js +1 -1
- package/lib/core/components/shadcn/filters/filter/dateFilter.js.map +1 -1
- package/lib/core/components/shadcn/filters/filter-styles.d.ts +1 -1
- package/lib/core/components/shadcn/filters/filter-styles.d.ts.map +1 -1
- package/lib/core/components/shadcn/filters/filter-styles.js +4 -4
- package/lib/core/components/shadcn/filters/filters.js +1 -1
- package/lib/core/components/shadcn/filters/filters.js.map +1 -1
- package/lib/core/components/shadcn/label.js +1 -1
- package/lib/core/components/shadcn/label.js.map +1 -1
- package/lib/core/components/shadcn/overflowTabs.js +2 -2
- package/lib/core/components/shadcn/overflowTabs.js.map +1 -1
- package/lib/core/components/shadcn/tabs.js +1 -1
- package/lib/core/components/shadcn/tabs.js.map +1 -1
- package/lib/env/index.d.ts +28 -1
- package/lib/env/index.d.ts.map +1 -1
- package/lib/env/index.js +51 -3
- package/lib/env/index.js.map +1 -1
- package/lib/features/activity-doc/ActivityDoc.js +1 -1
- package/lib/features/activity-doc/ActivityDoc.js.map +1 -1
- package/lib/features/agent/PayloadBuilder.d.ts +4 -0
- package/lib/features/agent/PayloadBuilder.d.ts.map +1 -1
- package/lib/features/agent/PayloadBuilder.js.map +1 -1
- package/lib/features/agent/chat/AgentRunFeedback.d.ts +59 -0
- package/lib/features/agent/chat/AgentRunFeedback.d.ts.map +1 -0
- package/lib/features/agent/chat/AgentRunFeedback.js +286 -0
- package/lib/features/agent/chat/AgentRunFeedback.js.map +1 -0
- package/lib/features/agent/chat/AskUserWidget.d.ts +1 -0
- package/lib/features/agent/chat/AskUserWidget.d.ts.map +1 -1
- package/lib/features/agent/chat/AskUserWidget.js +15 -4
- package/lib/features/agent/chat/AskUserWidget.js.map +1 -1
- package/lib/features/agent/chat/ModernAgentConversation.d.ts.map +1 -1
- package/lib/features/agent/chat/ModernAgentConversation.js +3 -2
- package/lib/features/agent/chat/ModernAgentConversation.js.map +1 -1
- package/lib/features/agent/chat/ModernAgentOutput/AllMessagesMixed.d.ts +3 -1
- package/lib/features/agent/chat/ModernAgentOutput/AllMessagesMixed.d.ts.map +1 -1
- package/lib/features/agent/chat/ModernAgentOutput/AllMessagesMixed.js +16 -13
- package/lib/features/agent/chat/ModernAgentOutput/AllMessagesMixed.js.map +1 -1
- package/lib/features/agent/chat/ModernAgentOutput/Header.d.ts.map +1 -1
- package/lib/features/agent/chat/ModernAgentOutput/Header.js.map +1 -1
- package/lib/features/agent/chat/ModernAgentOutput/MessageItem.d.ts +3 -1
- package/lib/features/agent/chat/ModernAgentOutput/MessageItem.d.ts.map +1 -1
- package/lib/features/agent/chat/ModernAgentOutput/MessageItem.js +30 -8
- package/lib/features/agent/chat/ModernAgentOutput/MessageItem.js.map +1 -1
- package/lib/features/agent/chat/ModernAgentOutput/SummaryConversation.d.ts.map +1 -1
- package/lib/features/agent/chat/ModernAgentOutput/SummaryConversation.js +2 -0
- package/lib/features/agent/chat/ModernAgentOutput/SummaryConversation.js.map +1 -1
- package/lib/features/agent/chat/index.d.ts +1 -0
- package/lib/features/agent/chat/index.d.ts.map +1 -1
- package/lib/features/agent/chat/index.js +1 -0
- package/lib/features/agent/chat/index.js.map +1 -1
- package/lib/features/facets/AgentRunnerFacetsNav.d.ts +8 -0
- package/lib/features/facets/AgentRunnerFacetsNav.d.ts.map +1 -1
- package/lib/features/facets/AgentRunnerFacetsNav.js +12 -0
- package/lib/features/facets/AgentRunnerFacetsNav.js.map +1 -1
- package/lib/features/layout/GenericPageNavHeader.js +1 -1
- package/lib/features/layout/GenericPageNavHeader.js.map +1 -1
- package/lib/features/magic-pdf/AnnotatedImageSlider.js +1 -1
- package/lib/features/magic-pdf/AnnotatedImageSlider.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/permissions/UserPermissionsProvider.d.ts +15 -1
- package/lib/features/permissions/UserPermissionsProvider.d.ts.map +1 -1
- package/lib/features/permissions/UserPermissionsProvider.js +20 -12
- package/lib/features/permissions/UserPermissionsProvider.js.map +1 -1
- package/lib/features/store/objects/DocumentPreviewPanel.js +5 -5
- package/lib/features/store/objects/DocumentPreviewPanel.js.map +1 -1
- package/lib/features/store/objects/components/DocumentInput.js +1 -1
- package/lib/features/store/objects/components/DocumentInput.js.map +1 -1
- package/lib/features/store/objects/selection/SelectionActions.js +1 -1
- package/lib/features/store/objects/selection/SelectionActions.js.map +1 -1
- package/lib/features/store/objects/upload/DocumentUploadModal.js +1 -1
- package/lib/features/store/objects/upload/DocumentUploadModal.js.map +1 -1
- package/lib/features/store/types/TypeRegistry.d.ts +130 -0
- package/lib/features/store/types/TypeRegistry.d.ts.map +1 -1
- package/lib/features/store/types/search/ObjectTypeSearchContext.d.ts +130 -0
- package/lib/features/store/types/search/ObjectTypeSearchContext.d.ts.map +1 -1
- package/lib/features/user/UserInfo.js +1 -1
- package/lib/features/user/UserInfo.js.map +1 -1
- package/lib/i18n/index.d.ts +5 -4
- package/lib/i18n/index.d.ts.map +1 -1
- package/lib/i18n/index.js +16 -2
- package/lib/i18n/index.js.map +1 -1
- package/lib/i18n/locales/ar.json +30 -0
- package/lib/i18n/locales/de.json +30 -0
- package/lib/i18n/locales/en.json +30 -0
- package/lib/i18n/locales/es.json +30 -0
- package/lib/i18n/locales/fr.json +30 -0
- package/lib/i18n/locales/it.json +30 -0
- package/lib/i18n/locales/ja.json +30 -0
- package/lib/i18n/locales/ko.json +30 -0
- package/lib/i18n/locales/pt.json +30 -0
- package/lib/i18n/locales/ru.json +30 -0
- package/lib/i18n/locales/tr.json +30 -0
- package/lib/i18n/locales/zh-TW.json +30 -0
- package/lib/i18n/locales/zh.json +30 -0
- package/lib/layout/ErrorPage.js +1 -1
- package/lib/layout/ErrorPage.js.map +1 -1
- package/lib/layout/Navbar.d.ts.map +1 -1
- package/lib/layout/Navbar.js +3 -2
- package/lib/layout/Navbar.js.map +1 -1
- package/lib/router/RouteComponent.d.ts.map +1 -1
- package/lib/router/RouteComponent.js +15 -12
- package/lib/router/RouteComponent.js.map +1 -1
- package/lib/session/UserSession.d.ts.map +1 -1
- package/lib/session/UserSession.js +6 -3
- package/lib/session/UserSession.js.map +1 -1
- package/lib/session/UserSessionProvider.d.ts +2 -1
- package/lib/session/UserSessionProvider.d.ts.map +1 -1
- package/lib/session/UserSessionProvider.js +10 -1
- package/lib/session/UserSessionProvider.js.map +1 -1
- package/lib/session/auth/composable.d.ts.map +1 -1
- package/lib/session/auth/composable.js +88 -3
- package/lib/session/auth/composable.js.map +1 -1
- package/lib/session/auth/domainRouting.d.ts +22 -12
- package/lib/session/auth/domainRouting.d.ts.map +1 -1
- package/lib/session/auth/domainRouting.js +35 -21
- package/lib/session/auth/domainRouting.js.map +1 -1
- package/lib/session/auth/useAuthState.d.ts +9 -4
- package/lib/session/auth/useAuthState.d.ts.map +1 -1
- package/lib/session/auth/useAuthState.js +9 -4
- package/lib/session/auth/useAuthState.js.map +1 -1
- package/lib/session/index.d.ts.map +1 -1
- package/lib/session/index.js +3 -1
- package/lib/session/index.js.map +1 -1
- package/lib/shell/AuthScreenPreview.d.ts +12 -0
- package/lib/shell/AuthScreenPreview.d.ts.map +1 -0
- package/lib/shell/AuthScreenPreview.js +140 -0
- package/lib/shell/AuthScreenPreview.js.map +1 -0
- package/lib/shell/BrandedAuthScreens.d.ts +19 -0
- package/lib/shell/BrandedAuthScreens.d.ts.map +1 -0
- package/lib/shell/BrandedAuthScreens.js +64 -0
- package/lib/shell/BrandedAuthScreens.js.map +1 -0
- package/lib/shell/SplashScreen.d.ts +13 -2
- package/lib/shell/SplashScreen.d.ts.map +1 -1
- package/lib/shell/SplashScreen.js +21 -20
- package/lib/shell/SplashScreen.js.map +1 -1
- package/lib/shell/VertesiaShell.d.ts +15 -4
- package/lib/shell/VertesiaShell.d.ts.map +1 -1
- package/lib/shell/VertesiaShell.js +8 -3
- package/lib/shell/VertesiaShell.js.map +1 -1
- package/lib/shell/apps/StandaloneApp.js +5 -1
- package/lib/shell/apps/StandaloneApp.js.map +1 -1
- package/lib/shell/index.d.ts +6 -0
- package/lib/shell/index.d.ts.map +1 -1
- package/lib/shell/index.js +5 -0
- package/lib/shell/index.js.map +1 -1
- package/lib/shell/login/SignInFlow.d.ts +68 -0
- package/lib/shell/login/SignInFlow.d.ts.map +1 -0
- package/lib/shell/login/SignInFlow.js +108 -0
- package/lib/shell/login/SignInFlow.js.map +1 -0
- package/lib/shell/login/SignInPageShell.d.ts +13 -0
- package/lib/shell/login/SignInPageShell.d.ts.map +1 -0
- package/lib/shell/login/SignInPageShell.js +14 -0
- package/lib/shell/login/SignInPageShell.js.map +1 -0
- package/lib/shell/login/SignInPrimitives.d.ts +3 -3
- package/lib/shell/login/SignInPrimitives.d.ts.map +1 -1
- package/lib/shell/login/SignInPrimitives.js +13 -11
- package/lib/shell/login/SignInPrimitives.js.map +1 -1
- package/lib/shell/login/SignInRestrictedEnvStep.d.ts +5 -0
- package/lib/shell/login/SignInRestrictedEnvStep.d.ts.map +1 -1
- package/lib/shell/login/SignInRestrictedEnvStep.js +8 -4
- package/lib/shell/login/SignInRestrictedEnvStep.js.map +1 -1
- package/lib/shell/login/SigninScreen.d.ts +38 -3
- package/lib/shell/login/SigninScreen.d.ts.map +1 -1
- package/lib/shell/login/SigninScreen.js +33 -86
- package/lib/shell/login/SigninScreen.js.map +1 -1
- package/lib/shell/login/SignupForm.d.ts +3 -1
- package/lib/shell/login/SignupForm.d.ts.map +1 -1
- package/lib/shell/login/SignupForm.js +7 -3
- package/lib/shell/login/SignupForm.js.map +1 -1
- package/lib/shell/login/signInUtils.d.ts +11 -0
- package/lib/shell/login/signInUtils.d.ts.map +1 -1
- package/lib/shell/login/signInUtils.js +29 -5
- package/lib/shell/login/signInUtils.js.map +1 -1
- package/lib/vertesia-ui-boot.js +237 -0
- package/lib/vertesia-ui-boot.js.map +1 -0
- package/lib/vertesia-ui-core.js +46 -46
- 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 +222 -208
- package/lib/vertesia-ui-features.js.map +1 -1
- package/lib/vertesia-ui-i18n.B3NFclwl.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.B3NFclwl.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.BWSrNIxc.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.BWSrNIxc.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.BWvWeB1p.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.BWvWeB1p.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.BZCXvTC1.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.BZCXvTC1.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.BbJMvFZW.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.BbJMvFZW.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.CE5ldwpt.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.CE5ldwpt.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.CMC0IShY.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.CMC0IShY.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.CkcRqBKU.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.CkcRqBKU.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.D-Mq1sw_.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.D-Mq1sw_.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.DkK68-A-.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.DkK68-A-.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.XE_dcCg6.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.XE_dcCg6.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.js +2 -2
- package/lib/vertesia-ui-i18n.js.map +1 -1
- package/lib/vertesia-ui-i18n.qgNqMPvK.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.qgNqMPvK.chunk.js.map +1 -0
- package/lib/vertesia-ui-layout.js +13 -13
- package/lib/vertesia-ui-layout.js.map +1 -1
- package/lib/vertesia-ui-rich-text.js +68 -68
- package/lib/vertesia-ui-rich-text.js.map +1 -1
- package/lib/vertesia-ui-router.js +1 -1
- package/lib/vertesia-ui-router.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 +58 -33
- package/lib/vertesia-ui-shell.js.map +1 -1
- package/lib/vertesia-ui-widgets.js +94 -94
- package/lib/vertesia-ui-widgets.js.map +1 -1
- package/lib/widgets/json-view/JSONCode.js +1 -1
- package/lib/widgets/json-view/JSONCode.js.map +1 -1
- package/lib/widgets/upload/DropZone.d.ts.map +1 -1
- package/lib/widgets/upload/DropZone.js +1 -1
- package/lib/widgets/upload/DropZone.js.map +1 -1
- package/llms.txt +6 -2
- package/package.json +44 -35
- package/src/__tests__/a11y.test.tsx +25 -0
- package/src/__tests__/theme.test.ts +91 -0
- package/src/boot/branding.test.ts +72 -0
- package/src/boot/branding.ts +108 -0
- package/src/boot/index.test.ts +216 -0
- package/src/boot/index.ts +356 -0
- package/src/boot/vite.test.ts +93 -0
- package/src/boot/vite.ts +118 -0
- package/src/core/components/Link.tsx +1 -1
- package/src/core/components/Spinner.tsx +1 -1
- package/src/core/components/TagsInput.tsx +7 -7
- package/src/core/components/shadcn/badge.tsx +1 -1
- package/src/core/components/shadcn/button.tsx +3 -5
- package/src/core/components/shadcn/filters/comboBox/DateCombobox.tsx +2 -2
- package/src/core/components/shadcn/filters/comboBox/SelectCombobox.tsx +1 -1
- package/src/core/components/shadcn/filters/comboBox/StringListCombobox.tsx +1 -1
- package/src/core/components/shadcn/filters/comboBox/TextCombobox.tsx +1 -1
- package/src/core/components/shadcn/filters/filter/dateFilter.tsx +1 -1
- package/src/core/components/shadcn/filters/filter-styles.ts +4 -4
- package/src/core/components/shadcn/filters/filters.tsx +1 -1
- package/src/core/components/shadcn/label.tsx +1 -1
- package/src/core/components/shadcn/overflowTabs.tsx +2 -2
- package/src/core/components/shadcn/tabs.tsx +1 -1
- package/src/css/utilities.css +7 -0
- package/src/env/index.test.ts +100 -0
- package/src/env/index.ts +66 -3
- package/src/features/activity-doc/ActivityDoc.tsx +1 -1
- package/src/features/agent/PayloadBuilder.tsx +4 -0
- package/src/features/agent/chat/AgentRunFeedback.test.tsx +330 -0
- package/src/features/agent/chat/AgentRunFeedback.tsx +452 -0
- package/src/features/agent/chat/AskUserWidget.test.tsx +24 -2
- package/src/features/agent/chat/AskUserWidget.tsx +19 -4
- package/src/features/agent/chat/ModernAgentConversation.tsx +46 -42
- package/src/features/agent/chat/ModernAgentOutput/AllMessagesMixed.test.tsx +207 -0
- package/src/features/agent/chat/ModernAgentOutput/AllMessagesMixed.tsx +50 -31
- package/src/features/agent/chat/ModernAgentOutput/Header.tsx +4 -0
- package/src/features/agent/chat/ModernAgentOutput/MessageItem.tsx +42 -8
- package/src/features/agent/chat/ModernAgentOutput/SummaryConversation.ts +2 -0
- package/src/features/agent/chat/ModernAgentOutput/utils.test.ts +14 -0
- package/src/features/agent/chat/hooks/useFileProcessing.test.tsx +0 -5
- package/src/features/agent/chat/index.ts +6 -0
- package/src/features/facets/AgentRunnerFacetsNav.tsx +26 -0
- package/src/features/layout/GenericPageNavHeader.test.tsx +36 -0
- package/src/features/layout/GenericPageNavHeader.tsx +1 -1
- package/src/features/magic-pdf/AnnotatedImageSlider.tsx +1 -1
- package/src/features/magic-pdf/GroundedExtractionView.tsx +3 -3
- package/src/features/permissions/UserPermissionsProvider.test.tsx +50 -1
- package/src/features/permissions/UserPermissionsProvider.tsx +82 -44
- package/src/features/store/objects/DocumentPreviewPanel.tsx +10 -10
- package/src/features/store/objects/components/DocumentInput.tsx +1 -1
- package/src/features/store/objects/selection/SelectionActions.tsx +4 -1
- package/src/features/store/objects/upload/DocumentUploadModal.tsx +1 -1
- package/src/features/user/UserInfo.tsx +1 -1
- package/src/i18n/index.tsx +25 -2
- package/src/i18n/locales/ar.json +30 -0
- package/src/i18n/locales/de.json +30 -0
- package/src/i18n/locales/en.json +30 -0
- package/src/i18n/locales/es.json +30 -0
- package/src/i18n/locales/fr.json +30 -0
- package/src/i18n/locales/it.json +30 -0
- package/src/i18n/locales/ja.json +30 -0
- package/src/i18n/locales/ko.json +30 -0
- package/src/i18n/locales/pt.json +30 -0
- package/src/i18n/locales/ru.json +30 -0
- package/src/i18n/locales/tr.json +30 -0
- package/src/i18n/locales/zh-TW.json +30 -0
- package/src/i18n/locales/zh.json +30 -0
- package/src/layout/ErrorPage.tsx +1 -1
- package/src/layout/Navbar.test.tsx +58 -0
- package/src/layout/Navbar.tsx +5 -2
- package/src/router/RouteComponent.test.tsx +65 -1
- package/src/router/RouteComponent.tsx +18 -15
- package/src/session/UserSession.test.ts +65 -0
- package/src/session/UserSession.ts +6 -4
- package/src/session/UserSessionProvider.strict-mode.test.tsx +44 -0
- package/src/session/UserSessionProvider.test.ts +62 -1
- package/src/session/UserSessionProvider.tsx +9 -1
- package/src/session/auth/composable.renewal.test.ts +170 -0
- package/src/session/auth/composable.test.ts +23 -1
- package/src/session/auth/composable.ts +101 -2
- package/src/session/auth/domainRouting.test.ts +35 -65
- package/src/session/auth/domainRouting.ts +51 -34
- package/src/session/auth/useAuthState.ts +11 -4
- package/src/session/index.ts +3 -1
- package/src/shell/AuthScreenPreview.test.tsx +44 -0
- package/src/shell/AuthScreenPreview.tsx +228 -0
- package/src/shell/BrandedAuthScreens.test.tsx +181 -0
- package/src/shell/BrandedAuthScreens.tsx +117 -0
- package/src/shell/SplashScreen.test.tsx +37 -0
- package/src/shell/SplashScreen.tsx +45 -40
- package/src/shell/VertesiaShell.tsx +49 -22
- package/src/shell/apps/StandaloneApp.tsx +5 -1
- package/src/shell/index.tsx +6 -0
- package/src/shell/login/SignInFlow.tsx +213 -0
- package/src/shell/login/SignInPageShell.tsx +62 -0
- package/src/shell/login/SignInPrimitives.tsx +14 -11
- package/src/shell/login/SignInRestrictedEnvStep.tsx +19 -7
- package/src/shell/login/SigninScreen.test.tsx +104 -0
- package/src/shell/login/SigninScreen.tsx +115 -145
- package/src/shell/login/SignupForm.tsx +8 -3
- package/src/shell/login/signInUtils.ts +30 -5
- package/src/widgets/json-view/JSONCode.test.tsx +2 -2
- package/src/widgets/json-view/JSONCode.tsx +1 -1
- package/src/widgets/markdown/markdown.css +21 -21
- package/src/widgets/upload/DropZone.tsx +2 -4
- package/lib/vertesia-ui-i18n.BfN7iOHb.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.BfN7iOHb.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.BjH2zEZw.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.BjH2zEZw.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.BjSwAZXl.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.BjSwAZXl.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.C7PUm5Au.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.C7PUm5Au.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.CF_1mUS5.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.CF_1mUS5.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.ChfUl3_-.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.ChfUl3_-.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.D-Sk-5d1.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.D-Sk-5d1.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.D4rnu1_S.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.D4rnu1_S.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.DDf0-laK.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.DDf0-laK.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.DN79y06J.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.DN79y06J.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.DWLIciYQ.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.DWLIciYQ.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.DvF_YaAJ.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.DvF_YaAJ.chunk.js.map +0 -1
- package/src/session/auth/authState.test.ts +0 -35
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { act, cleanup, render, screen, waitFor } from '@testing-library/react';
|
|
2
|
-
import {
|
|
2
|
+
import { ErrorBoundary } from 'react-error-boundary';
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
4
|
import { prefetchLazyRoutes, RouteComponent } from './RouteComponent';
|
|
4
5
|
import { type LazyRouteModule, useNavigate } from './Router';
|
|
5
6
|
import { RouterProvider } from './RouterProvider';
|
|
@@ -38,6 +39,44 @@ describe('RouteComponent lazy routes', () => {
|
|
|
38
39
|
window.history.pushState({}, '', '/');
|
|
39
40
|
});
|
|
40
41
|
|
|
42
|
+
it.each(['missing module', 'rejected import', 'synchronous failure'] as const)(
|
|
43
|
+
'reports a %s through the error boundary instead of remaining on the spinner',
|
|
44
|
+
async (failure) => {
|
|
45
|
+
const error = new Error('Module could not be loaded');
|
|
46
|
+
const onError = vi.fn();
|
|
47
|
+
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
|
48
|
+
const routes = [
|
|
49
|
+
{
|
|
50
|
+
path: '/a',
|
|
51
|
+
LazyComponent: () => {
|
|
52
|
+
if (failure === 'synchronous failure') throw error;
|
|
53
|
+
return failure === 'missing module'
|
|
54
|
+
? Promise.resolve(undefined as unknown as LazyRouteModule)
|
|
55
|
+
: Promise.reject(error);
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
];
|
|
59
|
+
try {
|
|
60
|
+
render(
|
|
61
|
+
<ErrorBoundary fallback={<div>Route unavailable</div>} onError={onError}>
|
|
62
|
+
<RouterProvider routes={routes}>
|
|
63
|
+
<RouteComponent spinner={<div>loading</div>} />
|
|
64
|
+
</RouterProvider>
|
|
65
|
+
</ErrorBoundary>,
|
|
66
|
+
);
|
|
67
|
+
await screen.findByText('Route unavailable');
|
|
68
|
+
expect(screen.queryByText('loading')).toBeNull();
|
|
69
|
+
expect(onError.mock.calls[0][0]).toEqual(
|
|
70
|
+
failure === 'missing module'
|
|
71
|
+
? new Error('Lazy module for /a does not have a default export')
|
|
72
|
+
: error,
|
|
73
|
+
);
|
|
74
|
+
} finally {
|
|
75
|
+
consoleError.mockRestore();
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
);
|
|
79
|
+
|
|
41
80
|
it('stops showing the previous lazy route while the next one loads', async () => {
|
|
42
81
|
const pageA = deferredModule('Page A');
|
|
43
82
|
const pageB = deferredModule('Page B');
|
|
@@ -87,6 +126,31 @@ describe('RouteComponent lazy routes', () => {
|
|
|
87
126
|
expect(screen.getByText('Page B')).toBeDefined();
|
|
88
127
|
});
|
|
89
128
|
|
|
129
|
+
it('ignores a rejected import after navigating away', async () => {
|
|
130
|
+
let reject!: (error: Error) => void;
|
|
131
|
+
const promise = new Promise<LazyRouteModule>((_resolve, rejectPromise) => {
|
|
132
|
+
reject = rejectPromise;
|
|
133
|
+
});
|
|
134
|
+
const onError = vi.fn();
|
|
135
|
+
render(
|
|
136
|
+
<ErrorBoundary fallback={<div>Route unavailable</div>} onError={onError}>
|
|
137
|
+
<RouterProvider
|
|
138
|
+
routes={[
|
|
139
|
+
{ path: '/a', LazyComponent: () => promise },
|
|
140
|
+
{ path: '/b', Component: () => <div>Page B</div> },
|
|
141
|
+
]}
|
|
142
|
+
>
|
|
143
|
+
<CaptureNavigate />
|
|
144
|
+
<RouteComponent />
|
|
145
|
+
</RouterProvider>
|
|
146
|
+
</ErrorBoundary>,
|
|
147
|
+
);
|
|
148
|
+
await act(async () => navigateFn('/b'));
|
|
149
|
+
await act(async () => reject(new Error('Previous route failed')));
|
|
150
|
+
expect(screen.getByText('Page B')).toBeDefined();
|
|
151
|
+
expect(onError).not.toHaveBeenCalled();
|
|
152
|
+
});
|
|
153
|
+
|
|
90
154
|
it('renders a previously resolved lazy route synchronously on revisit', async () => {
|
|
91
155
|
const pageA = deferredModule('Page A');
|
|
92
156
|
const pageB = deferredModule('Page B');
|
|
@@ -13,14 +13,13 @@ import {
|
|
|
13
13
|
// between two lazy routes blanks the page for at least a frame, even when the module is loaded.
|
|
14
14
|
const resolvedComponents = new Map<LazyComponentRoute['LazyComponent'], LazyRouteModule['default']>();
|
|
15
15
|
|
|
16
|
-
function resolveLazyRoute(route: LazyComponentRoute): Promise<LazyRouteModule['default']> {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
});
|
|
16
|
+
async function resolveLazyRoute(route: LazyComponentRoute): Promise<LazyRouteModule['default']> {
|
|
17
|
+
const module = await route.LazyComponent();
|
|
18
|
+
if (!module?.default) {
|
|
19
|
+
throw new Error(`Lazy module for ${route.path} does not have a default export`);
|
|
20
|
+
}
|
|
21
|
+
resolvedComponents.set(route.LazyComponent, module.default);
|
|
22
|
+
return module.default;
|
|
24
23
|
}
|
|
25
24
|
|
|
26
25
|
/**
|
|
@@ -64,21 +63,25 @@ function LazyRouteComponent({ route, spinner }: LazyRouteComponentProps) {
|
|
|
64
63
|
const [Component, setComponent] = useState<LazyRouteModule['default'] | null>(
|
|
65
64
|
() => resolvedComponents.get(route.LazyComponent) ?? null,
|
|
66
65
|
);
|
|
66
|
+
const [error, setError] = useState<unknown>();
|
|
67
67
|
useEffect(() => {
|
|
68
68
|
// A slow import must not land after this instance moved on (unmount, or a route object
|
|
69
69
|
// swap without a path change): the stale module would overwrite the current one.
|
|
70
70
|
let stale = false;
|
|
71
|
-
void resolveLazyRoute(route).then(
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
71
|
+
void resolveLazyRoute(route).then(
|
|
72
|
+
(component) => {
|
|
73
|
+
if (!stale) setComponent(() => component);
|
|
74
|
+
},
|
|
75
|
+
(error: unknown) => {
|
|
76
|
+
// Render failures through the surrounding error boundary, including import failures.
|
|
77
|
+
if (!stale) setError(() => error);
|
|
78
|
+
},
|
|
79
|
+
);
|
|
78
80
|
return () => {
|
|
79
81
|
stale = true;
|
|
80
82
|
};
|
|
81
83
|
}, [route]);
|
|
82
84
|
|
|
85
|
+
if (error !== undefined) throw error;
|
|
83
86
|
return Component ? <Component /> : spinner || null;
|
|
84
87
|
}
|
|
@@ -1,17 +1,64 @@
|
|
|
1
1
|
import type { VertesiaClient } from '@vertesia/client';
|
|
2
|
+
import { type AuthTokenPayload, PrincipalType } from '@vertesia/common';
|
|
2
3
|
import { describe, expect, it, vi } from 'vitest';
|
|
3
4
|
import { UserSession } from './UserSession';
|
|
4
5
|
|
|
6
|
+
const projectToken: AuthTokenPayload = {
|
|
7
|
+
sub: 'test-user',
|
|
8
|
+
name: 'Test User',
|
|
9
|
+
type: PrincipalType.User,
|
|
10
|
+
account: { id: 'test-account', name: 'Test Account' },
|
|
11
|
+
account_roles: [],
|
|
12
|
+
accounts: [],
|
|
13
|
+
project: { id: 'test-project', name: 'Test Project', account: 'test-account' },
|
|
14
|
+
apps: [],
|
|
15
|
+
iss: 'https://auth.example.com',
|
|
16
|
+
aud: 'test-client',
|
|
17
|
+
exp: 4102444800,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function encodeToken(payload: AuthTokenPayload): string {
|
|
21
|
+
return `${btoa(JSON.stringify({ alg: 'none' }))}.${btoa(JSON.stringify(payload))}.`;
|
|
22
|
+
}
|
|
23
|
+
|
|
5
24
|
function createSession(onboardingProgress: () => Promise<Record<string, boolean>>) {
|
|
6
25
|
const client = {
|
|
7
26
|
account: { onboardingProgress },
|
|
27
|
+
withAuthCallback: vi.fn(),
|
|
8
28
|
} as unknown as VertesiaClient;
|
|
9
29
|
const setSession = vi.fn<(session: UserSession) => void>();
|
|
10
30
|
const session = new UserSession(client, setSession);
|
|
31
|
+
session.authToken = { ...projectToken };
|
|
11
32
|
return { session, setSession };
|
|
12
33
|
}
|
|
13
34
|
|
|
14
35
|
describe('UserSession.fetchOnboardingStatus', () => {
|
|
36
|
+
it('skips onboarding before authentication without publishing a status', async () => {
|
|
37
|
+
const onboardingProgress = vi.fn(async () => ({ project_created: true }));
|
|
38
|
+
const { session, setSession } = createSession(onboardingProgress);
|
|
39
|
+
session.authToken = undefined;
|
|
40
|
+
|
|
41
|
+
await expect(session.fetchOnboardingStatus()).resolves.toBe(false);
|
|
42
|
+
expect(onboardingProgress).not.toHaveBeenCalled();
|
|
43
|
+
expect(session.onboardingComplete).toBeUndefined();
|
|
44
|
+
expect(setSession).not.toHaveBeenCalled();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('defers onboarding during account-only login and loads it after project selection', async () => {
|
|
48
|
+
const onboardingProgress = vi.fn(async () => ({ project_created: true }));
|
|
49
|
+
const { session, setSession } = createSession(onboardingProgress);
|
|
50
|
+
|
|
51
|
+
await session.login(encodeToken({ ...projectToken, project: undefined }));
|
|
52
|
+
expect(onboardingProgress).not.toHaveBeenCalled();
|
|
53
|
+
expect(session.onboardingComplete).toBeUndefined();
|
|
54
|
+
expect(setSession).not.toHaveBeenCalled();
|
|
55
|
+
|
|
56
|
+
await session.login(encodeToken(projectToken));
|
|
57
|
+
expect(onboardingProgress).toHaveBeenCalledOnce();
|
|
58
|
+
expect(session.onboardingComplete).toBe(true);
|
|
59
|
+
expect(setSession).toHaveBeenCalledOnce();
|
|
60
|
+
});
|
|
61
|
+
|
|
15
62
|
it('does not replace the session when an incomplete status is unchanged', async () => {
|
|
16
63
|
const { session, setSession } = createSession(async () => ({ project_created: false }));
|
|
17
64
|
session.onboardingComplete = false;
|
|
@@ -37,3 +84,21 @@ describe('UserSession.fetchOnboardingStatus', () => {
|
|
|
37
84
|
expect(setSession.mock.calls[0]?.[0]?.onboardingComplete).toBe(false);
|
|
38
85
|
});
|
|
39
86
|
});
|
|
87
|
+
|
|
88
|
+
describe('UserSession.signOut', () => {
|
|
89
|
+
it('keeps the receiver when used as a callback, including cloned sessions', () => {
|
|
90
|
+
const { session } = createSession(async () => ({}));
|
|
91
|
+
const clone = session.clone();
|
|
92
|
+
const logout = vi.spyOn(session, 'logout').mockImplementation(() => undefined);
|
|
93
|
+
const cloneLogout = vi.spyOn(clone, 'logout').mockImplementation(() => undefined);
|
|
94
|
+
const { signOut } = session;
|
|
95
|
+
const { signOut: signOutClone } = clone;
|
|
96
|
+
|
|
97
|
+
signOut();
|
|
98
|
+
expect(logout).toHaveBeenCalledOnce();
|
|
99
|
+
expect(cloneLogout).not.toHaveBeenCalled();
|
|
100
|
+
signOutClone();
|
|
101
|
+
expect(cloneLogout).toHaveBeenCalledOnce();
|
|
102
|
+
expect(logout).toHaveBeenCalledOnce();
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -5,15 +5,13 @@ import { jwtDecode } from 'jwt-decode';
|
|
|
5
5
|
import { createContext, useContext } from 'react';
|
|
6
6
|
|
|
7
7
|
import { getComposableToken } from './auth/composable';
|
|
8
|
-
import { authReturnUrl, mountRootUrl, shouldRedirectToCentralAuth } from './auth/domainRouting';
|
|
8
|
+
import { authReturnUrl, centralAuthUrl, mountRootUrl, shouldRedirectToCentralAuth } from './auth/domainRouting';
|
|
9
9
|
import { getFirebaseAuth } from './auth/firebase';
|
|
10
10
|
|
|
11
11
|
import { LastSelectedAccountId_KEY, LastSelectedProjectId_KEY } from './constants';
|
|
12
12
|
|
|
13
13
|
export { LastSelectedAccountId_KEY, LastSelectedProjectId_KEY };
|
|
14
14
|
|
|
15
|
-
const CENTRAL_AUTH_REDIRECT = 'https://internal-auth.vertesia.app/';
|
|
16
|
-
|
|
17
15
|
export interface UserSessionLoginOptions {
|
|
18
16
|
loadOnboardingStatus?: boolean;
|
|
19
17
|
}
|
|
@@ -44,6 +42,7 @@ class UserSession {
|
|
|
44
42
|
}
|
|
45
43
|
|
|
46
44
|
this.logout = this.logout.bind(this);
|
|
45
|
+
this.signOut = this.signOut.bind(this);
|
|
47
46
|
}
|
|
48
47
|
|
|
49
48
|
get store() {
|
|
@@ -151,7 +150,7 @@ class UserSession {
|
|
|
151
150
|
this.setSession = undefined;
|
|
152
151
|
this.client.withAuthCallback(undefined);
|
|
153
152
|
|
|
154
|
-
const logoutUrl = new URL(
|
|
153
|
+
const logoutUrl = new URL(centralAuthUrl());
|
|
155
154
|
const currentUrl = authReturnUrl();
|
|
156
155
|
logoutUrl.pathname = '/logout';
|
|
157
156
|
logoutUrl.searchParams.set('redirect_uri', currentUrl.toString());
|
|
@@ -251,6 +250,9 @@ class UserSession {
|
|
|
251
250
|
}
|
|
252
251
|
|
|
253
252
|
async fetchOnboardingStatus(): Promise<boolean> {
|
|
253
|
+
if (!this.project?.id) {
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
254
256
|
if (this.onboardingComplete) {
|
|
255
257
|
console.log('Onboarding already completed');
|
|
256
258
|
return false;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { cleanup, render, waitFor } from '@testing-library/react';
|
|
2
|
+
import { Env } from '@vertesia/ui/env';
|
|
3
|
+
import { StrictMode } from 'react';
|
|
4
|
+
import { afterEach, expect, it, vi } from 'vitest';
|
|
5
|
+
import { redirectToCentralAuth } from './auth/domainRouting';
|
|
6
|
+
import { UserSessionProvider } from './UserSessionProvider';
|
|
7
|
+
|
|
8
|
+
vi.mock('./auth/useAuthState', () => ({ useAuthState: () => ({}) }));
|
|
9
|
+
vi.mock('./auth/domainRouting', async (importOriginal) => ({
|
|
10
|
+
...(await importOriginal<typeof import('./auth/domainRouting')>()),
|
|
11
|
+
shouldRedirectToCentralAuth: () => true,
|
|
12
|
+
redirectToCentralAuth: vi.fn(),
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
cleanup();
|
|
17
|
+
vi.clearAllMocks();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('continues authentication after StrictMode cancels the first host-token attempt', async () => {
|
|
21
|
+
const authTokenProvider = vi.fn(async () => undefined);
|
|
22
|
+
Env.init({
|
|
23
|
+
name: 'test',
|
|
24
|
+
version: '1',
|
|
25
|
+
type: 'development',
|
|
26
|
+
isLocalDev: true,
|
|
27
|
+
isDocker: false,
|
|
28
|
+
endpoints: {
|
|
29
|
+
studio: 'https://api.example.com',
|
|
30
|
+
zeno: 'https://api.example.com',
|
|
31
|
+
sts: 'https://sts.example.com',
|
|
32
|
+
},
|
|
33
|
+
authTokenProvider,
|
|
34
|
+
});
|
|
35
|
+
render(
|
|
36
|
+
<StrictMode>
|
|
37
|
+
<UserSessionProvider>
|
|
38
|
+
<span>App</span>
|
|
39
|
+
</UserSessionProvider>
|
|
40
|
+
</StrictMode>,
|
|
41
|
+
);
|
|
42
|
+
await waitFor(() => expect(redirectToCentralAuth).toHaveBeenCalledTimes(1));
|
|
43
|
+
expect(authTokenProvider).toHaveBeenCalledTimes(2);
|
|
44
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import { RequestedScopeUnavailableError } from './auth/composable';
|
|
3
|
-
import { sanitizeRejectedScopeUrl } from './UserSessionProvider';
|
|
3
|
+
import { buildCentralAuthRedirectUrl, sanitizeRejectedScopeUrl } from './UserSessionProvider';
|
|
4
4
|
|
|
5
5
|
describe('sanitizeRejectedScopeUrl', () => {
|
|
6
6
|
it.each([
|
|
@@ -67,3 +67,64 @@ describe('sanitizeRejectedScopeUrl', () => {
|
|
|
67
67
|
expect(error.requestedScope).toBe('project');
|
|
68
68
|
});
|
|
69
69
|
});
|
|
70
|
+
|
|
71
|
+
describe('buildCentralAuthRedirectUrl', () => {
|
|
72
|
+
const RETURN_URL = new URL('https://app.example.test/studio/dashboard?tab=runs');
|
|
73
|
+
const STS = 'https://sts.example.test';
|
|
74
|
+
|
|
75
|
+
it('carries the sts, redirect_uri and state parameters', () => {
|
|
76
|
+
const url = buildCentralAuthRedirectUrl('https://auth.example.test/', STS, RETURN_URL, 'state-123');
|
|
77
|
+
|
|
78
|
+
expect(url.origin).toBe('https://auth.example.test');
|
|
79
|
+
expect(url.pathname).toBe('/');
|
|
80
|
+
expect(url.searchParams.get('sts')).toBe(STS);
|
|
81
|
+
expect(url.searchParams.get('redirect_uri')).toBe(RETURN_URL.toString());
|
|
82
|
+
expect(url.searchParams.get('state')).toBe('state-123');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('carries the selected account and project through a gateway-mounted app redirect', () => {
|
|
86
|
+
const returnUrl = new URL('https://apps.example.test/tenants/t/apps/a/versions/v/app/?view=grid');
|
|
87
|
+
const url = buildCentralAuthRedirectUrl('https://auth.example.test/', STS, returnUrl, 'state-123', {
|
|
88
|
+
accountId: 'account-1',
|
|
89
|
+
projectId: 'project-1',
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
expect(url.searchParams.get('redirect_uri')).toBe(
|
|
93
|
+
'https://apps.example.test/tenants/t/apps/a/versions/v/app/?view=grid&p=project-1&a=account-1',
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// The endpoint is configurable, so it can arrive with a query of its own. Concatenating
|
|
98
|
+
// `?sts=...` onto it folds the parameter into the existing value and Central Auth sees no sts.
|
|
99
|
+
it('preserves a query already present on the configured endpoint', () => {
|
|
100
|
+
const url = buildCentralAuthRedirectUrl('https://auth.example.test/?tenant=acme', STS, RETURN_URL, 'state-123');
|
|
101
|
+
|
|
102
|
+
expect(url.searchParams.get('tenant')).toBe('acme');
|
|
103
|
+
expect(url.searchParams.get('sts')).toBe(STS);
|
|
104
|
+
expect(url.searchParams.get('redirect_uri')).toBe(RETURN_URL.toString());
|
|
105
|
+
expect(url.searchParams.get('state')).toBe('state-123');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Same hazard from the other side: appended after a fragment, the parameters land inside it.
|
|
109
|
+
it('keeps the parameters out of a fragment on the configured endpoint', () => {
|
|
110
|
+
const url = buildCentralAuthRedirectUrl('https://auth.example.test/#section', STS, RETURN_URL, 'state-123');
|
|
111
|
+
|
|
112
|
+
expect(url.hash).toBe('#section');
|
|
113
|
+
expect(url.searchParams.get('sts')).toBe(STS);
|
|
114
|
+
expect(url.searchParams.get('state')).toBe('state-123');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('preserves a path prefix on the configured endpoint', () => {
|
|
118
|
+
const url = buildCentralAuthRedirectUrl('https://auth.example.test/signin', STS, RETURN_URL, 'state-123');
|
|
119
|
+
|
|
120
|
+
expect(url.pathname).toBe('/signin');
|
|
121
|
+
expect(url.searchParams.get('sts')).toBe(STS);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('percent-encodes the parameter values', () => {
|
|
125
|
+
const url = buildCentralAuthRedirectUrl('https://auth.example.test/', STS, RETURN_URL, 'a b&c=d');
|
|
126
|
+
|
|
127
|
+
expect(url.search).toContain('sts=https%3A%2F%2Fsts.example.test');
|
|
128
|
+
expect(url.searchParams.get('state')).toBe('a b&c=d');
|
|
129
|
+
});
|
|
130
|
+
});
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
resolveAuthSelection,
|
|
13
13
|
UserNotFoundError,
|
|
14
14
|
} from './auth/composable';
|
|
15
|
-
import { redirectToCentralAuth, shouldRedirectToCentralAuth } from './auth/domainRouting';
|
|
15
|
+
import { buildCentralAuthRedirectUrl, redirectToCentralAuth, shouldRedirectToCentralAuth } from './auth/domainRouting';
|
|
16
16
|
import { getFirebaseAuth } from './auth/firebase';
|
|
17
17
|
import { useAuthState } from './auth/useAuthState';
|
|
18
18
|
import { UserSession, UserSessionContext } from './UserSession';
|
|
@@ -23,6 +23,10 @@ function clearAuthHash() {
|
|
|
23
23
|
window.history.replaceState(window.history.state, '', `${url.pathname}${url.search}`);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
// Re-exported from its new home in ./auth/domainRouting, which the token layer can import without
|
|
27
|
+
// a cycle -- it renews an expired Central Auth session through the same URL.
|
|
28
|
+
export { buildCentralAuthRedirectUrl };
|
|
29
|
+
|
|
26
30
|
export function sanitizeRejectedScopeUrl(
|
|
27
31
|
currentUrl: URL,
|
|
28
32
|
error: RequestedScopeUnavailableError,
|
|
@@ -268,6 +272,7 @@ export function UserSessionProvider({ children, loadOnboardingStatus = true }: U
|
|
|
268
272
|
session.setSession = setSession;
|
|
269
273
|
void Env.authTokenProvider()
|
|
270
274
|
.then(async (injectedToken) => {
|
|
275
|
+
if (cancelled) return;
|
|
271
276
|
if (!injectedToken) {
|
|
272
277
|
startFirebaseOrCentralAuth();
|
|
273
278
|
return;
|
|
@@ -283,6 +288,7 @@ export function UserSessionProvider({ children, loadOnboardingStatus = true }: U
|
|
|
283
288
|
if (!cancelled) setSession(session.clone());
|
|
284
289
|
})
|
|
285
290
|
.catch((err: unknown) => {
|
|
291
|
+
if (cancelled) return;
|
|
286
292
|
if (surfaceAuthError(err)) return;
|
|
287
293
|
console.warn('Auth: failed to initialize injected auth token', err);
|
|
288
294
|
Env.logger.warn('Failed to initialize injected auth token', {
|
|
@@ -298,6 +304,7 @@ export function UserSessionProvider({ children, loadOnboardingStatus = true }: U
|
|
|
298
304
|
});
|
|
299
305
|
return () => {
|
|
300
306
|
cancelled = true;
|
|
307
|
+
hasInitiatedAuthRef.current = false;
|
|
301
308
|
unsubscribe?.();
|
|
302
309
|
};
|
|
303
310
|
}
|
|
@@ -305,6 +312,7 @@ export function UserSessionProvider({ children, loadOnboardingStatus = true }: U
|
|
|
305
312
|
startFirebaseOrCentralAuth();
|
|
306
313
|
return () => {
|
|
307
314
|
cancelled = true;
|
|
315
|
+
hasInitiatedAuthRef.current = false;
|
|
308
316
|
unsubscribe?.();
|
|
309
317
|
};
|
|
310
318
|
};
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
//
|
|
3
|
+
// Renewal of a Central Auth session: the case where the browser holds an STS JWT, nothing else, and
|
|
4
|
+
// that JWT has expired. There is no Firebase user to mint a replacement from and no refresh token,
|
|
5
|
+
// so the token layer either bounces through the broker or the page dead-ends until someone reloads
|
|
6
|
+
// it by hand.
|
|
7
|
+
import { NO_ACCESSIBLE_ACCOUNT_ERROR_CODE } from '@vertesia/common';
|
|
8
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
9
|
+
|
|
10
|
+
const logger = { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() };
|
|
11
|
+
|
|
12
|
+
vi.mock('@vertesia/ui/env', () => ({
|
|
13
|
+
Env: {
|
|
14
|
+
endpoints: { sts: 'https://sts.test', studio: 'https://studio.test', auth: 'https://auth.test/' },
|
|
15
|
+
logger,
|
|
16
|
+
},
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
// No Firebase user: that is what makes this a Central Auth session rather than a Firebase one.
|
|
20
|
+
vi.mock('./firebase', () => ({
|
|
21
|
+
getFirebaseAuth: () => ({ currentUser: null }),
|
|
22
|
+
getFirebaseAuthToken: vi.fn(),
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
function unsignedJwt(payload: Record<string, unknown>): string {
|
|
26
|
+
const encode = (value: string) => btoa(value).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
|
|
27
|
+
return `${encode(JSON.stringify({ alg: 'none', typ: 'JWT' }))}.${encode(JSON.stringify(payload))}.signature`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const ACCOUNT_ID = 'account-1';
|
|
31
|
+
const PROJECT_ID = 'project-1';
|
|
32
|
+
|
|
33
|
+
/** A token STS would accept back for re-issuance: its own issuer, and real authorization claims. */
|
|
34
|
+
function stsToken(secondsFromNow: number): string {
|
|
35
|
+
return unsignedJwt({
|
|
36
|
+
iss: 'https://sts.test',
|
|
37
|
+
exp: Math.floor(Date.now() / 1000) + secondsFromNow,
|
|
38
|
+
account: { id: ACCOUNT_ID },
|
|
39
|
+
project: { id: PROJECT_ID },
|
|
40
|
+
permissions: ['content:read'],
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function okResponse(token: string): Response {
|
|
45
|
+
return new Response(JSON.stringify({ token }), { status: 200 });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let replace: ReturnType<typeof vi.fn>;
|
|
49
|
+
|
|
50
|
+
beforeEach(() => {
|
|
51
|
+
vi.restoreAllMocks();
|
|
52
|
+
vi.resetModules();
|
|
53
|
+
localStorage.clear();
|
|
54
|
+
sessionStorage.clear();
|
|
55
|
+
logger.info.mockClear();
|
|
56
|
+
logger.warn.mockClear();
|
|
57
|
+
|
|
58
|
+
// `location.replace` is the redirect under test; jsdom's own implementation would only warn.
|
|
59
|
+
replace = vi.fn();
|
|
60
|
+
vi.stubGlobal('location', {
|
|
61
|
+
href: 'https://cloud.test/studio/environments',
|
|
62
|
+
replace,
|
|
63
|
+
});
|
|
64
|
+
// `shouldRedirectToCentralAuth()` is "not Firebase mode", which is the default here.
|
|
65
|
+
window.AUTH_MODE = undefined;
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('forced refresh of a Central Auth session', () => {
|
|
69
|
+
it('re-mints through STS instead of handing back the credential it already holds', async () => {
|
|
70
|
+
// The bug this pins: the refresh path adopted a still-valid Vertesia token as its own
|
|
71
|
+
// result, so `refreshAuthToken()` -- whose whole purpose is to re-read claims STS
|
|
72
|
+
// recomputes on every issuance, such as `apps` after an ACE change -- silently returned the
|
|
73
|
+
// very token whose claims were suspect. A Central Auth session has no other credential, so
|
|
74
|
+
// the no-op was total there.
|
|
75
|
+
const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue(okResponse(stsToken(7200)));
|
|
76
|
+
|
|
77
|
+
const { getComposableToken } = await import('./composable');
|
|
78
|
+
// Adopted without a round-trip, which is the right call when nothing forced a refresh.
|
|
79
|
+
const first = await getComposableToken(ACCOUNT_ID, PROJECT_ID, stsToken(3600));
|
|
80
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
81
|
+
|
|
82
|
+
const refreshed = await getComposableToken(undefined, undefined, undefined, true);
|
|
83
|
+
|
|
84
|
+
// Before the fix this was zero: the same shortcut swallowed the forced refresh too.
|
|
85
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
86
|
+
expect(refreshed.token.exp).toBeGreaterThan(first.token.exp);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('an expired Central Auth session', () => {
|
|
91
|
+
/**
|
|
92
|
+
* Establish a session whose token is already inside the five-minute renewal window, then answer
|
|
93
|
+
* the renewal attempt with `status`. This is the shape of the reported incident: the tab sat
|
|
94
|
+
* idle, the token aged out, and the next click tried to renew from a credential STS refuses.
|
|
95
|
+
*/
|
|
96
|
+
async function sessionThenRenewalFailure(status: number, body?: unknown) {
|
|
97
|
+
const nearlyExpired = stsToken(60);
|
|
98
|
+
vi.spyOn(globalThis, 'fetch')
|
|
99
|
+
.mockResolvedValueOnce(okResponse(nearlyExpired))
|
|
100
|
+
.mockResolvedValueOnce(new Response(body === undefined ? null : JSON.stringify(body), { status }));
|
|
101
|
+
|
|
102
|
+
const { getComposableToken } = await import('./composable');
|
|
103
|
+
await getComposableToken(ACCOUNT_ID, PROJECT_ID, nearlyExpired);
|
|
104
|
+
return getComposableToken;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
it('bounces through the broker rather than dead-ending', async () => {
|
|
108
|
+
const getComposableToken = await sessionThenRenewalFailure(401);
|
|
109
|
+
|
|
110
|
+
await expect(getComposableToken(ACCOUNT_ID, PROJECT_ID)).rejects.toThrow();
|
|
111
|
+
|
|
112
|
+
expect(replace).toHaveBeenCalledTimes(1);
|
|
113
|
+
const url = new URL(replace.mock.calls[0][0] as string);
|
|
114
|
+
expect(url.origin).toBe('https://auth.test');
|
|
115
|
+
expect(url.searchParams.get('sts')).toBe('https://sts.test');
|
|
116
|
+
expect(url.searchParams.get('redirect_uri')).toBe('https://cloud.test/studio/environments');
|
|
117
|
+
// The broker echoes `state` back; the return leg verifies it against this record.
|
|
118
|
+
expect(url.searchParams.get('state')).toBe(sessionStorage.getItem('auth_state'));
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('redirects once per expiry, not once per failing call', async () => {
|
|
122
|
+
// Every in-flight request fails at the same moment, and the first failure discards the
|
|
123
|
+
// credential -- so the calls behind it reach the "no credential at all" branch. Without the
|
|
124
|
+
// cooldown each one would start its own navigation.
|
|
125
|
+
const getComposableToken = await sessionThenRenewalFailure(401);
|
|
126
|
+
|
|
127
|
+
await expect(getComposableToken(ACCOUNT_ID, PROJECT_ID)).rejects.toThrow();
|
|
128
|
+
await expect(getComposableToken(ACCOUNT_ID, PROJECT_ID)).rejects.toThrow(/Cannot acquire/);
|
|
129
|
+
|
|
130
|
+
expect(replace).toHaveBeenCalledTimes(1);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('does not redirect again for a broker round-trip that just failed', async () => {
|
|
134
|
+
// The loop guard, from the other side of the navigation: module state is gone, so the
|
|
135
|
+
// cooldown has to be read back from sessionStorage. A broker that returns a credential STS
|
|
136
|
+
// also refuses would otherwise bounce the browser between the two forever.
|
|
137
|
+
sessionStorage.setItem('vt.centralAuthRenewalAt', String(Date.now() - 1_000));
|
|
138
|
+
const getComposableToken = await sessionThenRenewalFailure(401);
|
|
139
|
+
|
|
140
|
+
await expect(getComposableToken(ACCOUNT_ID, PROJECT_ID)).rejects.toThrow();
|
|
141
|
+
|
|
142
|
+
expect(replace).not.toHaveBeenCalled();
|
|
143
|
+
expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining('already attempted'), expect.anything());
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('leaves an account-level refusal alone', async () => {
|
|
147
|
+
// The broker would hand back the same identity and STS would refuse it again. This is a
|
|
148
|
+
// question for the recovery screen, not for another round-trip.
|
|
149
|
+
const getComposableToken = await sessionThenRenewalFailure(403, {
|
|
150
|
+
errorCode: NO_ACCESSIBLE_ACCOUNT_ERROR_CODE,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
await expect(getComposableToken(ACCOUNT_ID, PROJECT_ID)).rejects.toThrow();
|
|
154
|
+
|
|
155
|
+
expect(replace).not.toHaveBeenCalled();
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe('a browser with no session yet', () => {
|
|
160
|
+
it('does not redirect from the token layer', async () => {
|
|
161
|
+
// First load is UserSessionProvider's to route: it holds the account/project selection this
|
|
162
|
+
// layer cannot see, and it already redirects. Renewing from here would race it.
|
|
163
|
+
vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response(null, { status: 401 }));
|
|
164
|
+
|
|
165
|
+
const { getComposableToken } = await import('./composable');
|
|
166
|
+
await expect(getComposableToken(ACCOUNT_ID, PROJECT_ID, stsToken(-60))).rejects.toThrow();
|
|
167
|
+
|
|
168
|
+
expect(replace).not.toHaveBeenCalled();
|
|
169
|
+
});
|
|
170
|
+
});
|
|
@@ -5,7 +5,10 @@ function makeJwt(payload: Record<string, unknown>) {
|
|
|
5
5
|
return `${encode({ alg: 'ES256', typ: 'JWT' })}.${encode(payload)}.signature`;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
async function importComposableAuth(
|
|
8
|
+
async function importComposableAuth(
|
|
9
|
+
authTokenProvider?: () => Promise<string | undefined>,
|
|
10
|
+
defaultAuthSelection?: { accountId?: string; projectId?: string },
|
|
11
|
+
) {
|
|
9
12
|
vi.resetModules();
|
|
10
13
|
const [{ Env }, composableAuth] = await Promise.all([import('@vertesia/ui/env'), import('./composable')]);
|
|
11
14
|
Env.init({
|
|
@@ -20,6 +23,7 @@ async function importComposableAuth(authTokenProvider?: () => Promise<string | u
|
|
|
20
23
|
sts: 'https://sts.dev1.vertesia.io',
|
|
21
24
|
},
|
|
22
25
|
authTokenProvider,
|
|
26
|
+
defaultAuthSelection,
|
|
23
27
|
});
|
|
24
28
|
return composableAuth;
|
|
25
29
|
}
|
|
@@ -374,3 +378,21 @@ describe('resolveAuthSelection', () => {
|
|
|
374
378
|
});
|
|
375
379
|
});
|
|
376
380
|
});
|
|
381
|
+
|
|
382
|
+
describe('configured workspace selection', () => {
|
|
383
|
+
afterEach(() => localStorage.clear());
|
|
384
|
+
it.each([
|
|
385
|
+
[{ accountId: 'env-a', projectId: 'env-p' }, '', { accountId: 'env-a', projectId: 'env-p' }],
|
|
386
|
+
[{ projectId: 'env-p' }, '', { accountId: undefined, projectId: 'env-p' }],
|
|
387
|
+
[{ accountId: 'env-a' }, '', { accountId: 'env-a', projectId: 'env-a-cached-p' }],
|
|
388
|
+
[{ accountId: 'env-a', projectId: 'env-p' }, '?p=url-p', { accountId: undefined, projectId: 'url-p' }],
|
|
389
|
+
[{ accountId: 'env-a', projectId: 'env-p' }, '?a=url-a', { accountId: 'url-a', projectId: undefined }],
|
|
390
|
+
[{ accountId: 'env-a', projectId: 'env-p' }, '?a=url-a&p=url-p', { accountId: 'url-a', projectId: 'url-p' }],
|
|
391
|
+
] as const)('selects %j with URL %s', async (selection, query, expected) => {
|
|
392
|
+
localStorage.setItem('composableai.lastSelectedAccountId', 'old-a');
|
|
393
|
+
localStorage.setItem('composableai.lastSelectedProjectId-old-a', 'old-p');
|
|
394
|
+
localStorage.setItem('composableai.lastSelectedProjectId-env-a', 'env-a-cached-p');
|
|
395
|
+
const { resolveAuthSelection } = await importComposableAuth(undefined, selection);
|
|
396
|
+
expect(resolveAuthSelection(new URL(`https://app.example.test/${query}`))).toEqual(expected);
|
|
397
|
+
});
|
|
398
|
+
});
|