@vertesia/ui 1.5.0-dev.20260713.151751Z → 1.5.0-dev.20260717.131047Z

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/lib/env/index.d.ts +19 -0
  2. package/lib/env/index.d.ts.map +1 -1
  3. package/lib/env/index.js +6 -0
  4. package/lib/env/index.js.map +1 -1
  5. package/lib/features/agent/chat/ArtifactsTab.d.ts.map +1 -1
  6. package/lib/features/agent/chat/ArtifactsTab.js +65 -11
  7. package/lib/features/agent/chat/ArtifactsTab.js.map +1 -1
  8. package/lib/features/agent/chat/ModernAgentConversation.js +51 -48
  9. package/lib/features/agent/chat/ModernAgentConversation.js.map +1 -1
  10. package/lib/features/agent/chat/hooks/useArtifacts.d.ts +8 -1
  11. package/lib/features/agent/chat/hooks/useArtifacts.d.ts.map +1 -1
  12. package/lib/features/agent/chat/hooks/useArtifacts.js +28 -10
  13. package/lib/features/agent/chat/hooks/useArtifacts.js.map +1 -1
  14. package/lib/features/document-viewer/UniversalDocumentViewer.d.ts +73 -0
  15. package/lib/features/document-viewer/UniversalDocumentViewer.d.ts.map +1 -0
  16. package/lib/features/document-viewer/UniversalDocumentViewer.js +415 -0
  17. package/lib/features/document-viewer/UniversalDocumentViewer.js.map +1 -0
  18. package/lib/features/document-viewer/index.d.ts +2 -0
  19. package/lib/features/document-viewer/index.d.ts.map +1 -0
  20. package/lib/features/document-viewer/index.js +2 -0
  21. package/lib/features/document-viewer/index.js.map +1 -0
  22. package/lib/features/index.d.ts +1 -0
  23. package/lib/features/index.d.ts.map +1 -1
  24. package/lib/features/index.js +1 -0
  25. package/lib/features/index.js.map +1 -1
  26. package/lib/features/permissions/UserPermissionsProvider.d.ts +2 -3
  27. package/lib/features/permissions/UserPermissionsProvider.d.ts.map +1 -1
  28. package/lib/features/permissions/UserPermissionsProvider.js +31 -38
  29. package/lib/features/permissions/UserPermissionsProvider.js.map +1 -1
  30. package/lib/features/store/objects/components/ContentOverview.d.ts.map +1 -1
  31. package/lib/features/store/objects/components/ContentOverview.js +73 -13
  32. package/lib/features/store/objects/components/ContentOverview.js.map +1 -1
  33. package/lib/i18n/locales/ar.json +2 -0
  34. package/lib/i18n/locales/de.json +2 -0
  35. package/lib/i18n/locales/en.json +2 -0
  36. package/lib/i18n/locales/es.json +2 -0
  37. package/lib/i18n/locales/fr.json +2 -0
  38. package/lib/i18n/locales/it.json +2 -0
  39. package/lib/i18n/locales/ja.json +2 -0
  40. package/lib/i18n/locales/ko.json +2 -0
  41. package/lib/i18n/locales/pt.json +2 -0
  42. package/lib/i18n/locales/ru.json +2 -0
  43. package/lib/i18n/locales/tr.json +2 -0
  44. package/lib/i18n/locales/zh-TW.json +2 -0
  45. package/lib/i18n/locales/zh.json +2 -0
  46. package/lib/router/HistoryNavigator.d.ts.map +1 -1
  47. package/lib/router/HistoryNavigator.js +4 -1
  48. package/lib/router/HistoryNavigator.js.map +1 -1
  49. package/lib/router/Nav.d.ts.map +1 -1
  50. package/lib/router/Nav.js +7 -1
  51. package/lib/router/Nav.js.map +1 -1
  52. package/lib/router/Router.d.ts.map +1 -1
  53. package/lib/router/Router.js +19 -4
  54. package/lib/router/Router.js.map +1 -1
  55. package/lib/router/path.d.ts +15 -0
  56. package/lib/router/path.d.ts.map +1 -1
  57. package/lib/router/path.js +43 -0
  58. package/lib/router/path.js.map +1 -1
  59. package/lib/session/UserSession.d.ts +6 -0
  60. package/lib/session/UserSession.d.ts.map +1 -1
  61. package/lib/session/UserSession.js +31 -9
  62. package/lib/session/UserSession.js.map +1 -1
  63. package/lib/session/UserSessionProvider.d.ts.map +1 -1
  64. package/lib/session/UserSessionProvider.js +111 -49
  65. package/lib/session/UserSessionProvider.js.map +1 -1
  66. package/lib/session/auth/composable.d.ts.map +1 -1
  67. package/lib/session/auth/composable.js +52 -3
  68. package/lib/session/auth/composable.js.map +1 -1
  69. package/lib/session/auth/domainRouting.d.ts +25 -0
  70. package/lib/session/auth/domainRouting.d.ts.map +1 -1
  71. package/lib/session/auth/domainRouting.js +36 -0
  72. package/lib/session/auth/domainRouting.js.map +1 -1
  73. package/lib/shell/login/SignInRestrictedEnvStep.d.ts +1 -1
  74. package/lib/shell/login/SignInRestrictedEnvStep.js +1 -1
  75. package/lib/shell/login/SigninScreen.js +9 -1
  76. package/lib/shell/login/SigninScreen.js.map +1 -1
  77. package/lib/shell/login/productionUrl.d.ts +0 -14
  78. package/lib/shell/login/productionUrl.d.ts.map +1 -1
  79. package/lib/shell/login/productionUrl.js +14 -5
  80. package/lib/shell/login/productionUrl.js.map +1 -1
  81. package/lib/vertesia-ui-core.js +3 -3
  82. package/lib/vertesia-ui-core.js.map +1 -1
  83. package/lib/vertesia-ui-env.js +1 -1
  84. package/lib/vertesia-ui-env.js.map +1 -1
  85. package/lib/vertesia-ui-features.js +28 -28
  86. package/lib/vertesia-ui-features.js.map +1 -1
  87. package/lib/vertesia-ui-i18n.js +13 -13
  88. package/lib/vertesia-ui-router.js +1 -1
  89. package/lib/vertesia-ui-router.js.map +1 -1
  90. package/lib/vertesia-ui-session.js +1 -1
  91. package/lib/vertesia-ui-session.js.map +1 -1
  92. package/lib/vertesia-ui-shell.js +1 -1
  93. package/lib/vertesia-ui-shell.js.map +1 -1
  94. package/lib/vertesia-ui-widgets.js +2 -2
  95. package/lib/vertesia-ui-widgets.js.map +1 -1
  96. package/llms.txt +70 -70
  97. package/package.json +8 -7
  98. package/src/env/index.ts +24 -0
  99. package/src/features/agent/chat/ArtifactsTab.tsx +155 -21
  100. package/src/features/agent/chat/ModernAgentConversation.tsx +53 -49
  101. package/src/features/agent/chat/hooks/useArtifacts.ts +44 -11
  102. package/src/features/document-viewer/UniversalDocumentViewer.test.tsx +123 -0
  103. package/src/features/document-viewer/UniversalDocumentViewer.tsx +746 -0
  104. package/src/features/document-viewer/index.ts +1 -0
  105. package/src/features/index.ts +1 -0
  106. package/src/features/permissions/UserPermissionsProvider.tsx +36 -39
  107. package/src/features/store/objects/components/ContentOverview.tsx +99 -26
  108. package/src/i18n/locales/ar.json +2 -0
  109. package/src/i18n/locales/de.json +2 -0
  110. package/src/i18n/locales/en.json +2 -0
  111. package/src/i18n/locales/es.json +2 -0
  112. package/src/i18n/locales/fr.json +2 -0
  113. package/src/i18n/locales/it.json +2 -0
  114. package/src/i18n/locales/ja.json +2 -0
  115. package/src/i18n/locales/ko.json +2 -0
  116. package/src/i18n/locales/pt.json +2 -0
  117. package/src/i18n/locales/ru.json +2 -0
  118. package/src/i18n/locales/tr.json +2 -0
  119. package/src/i18n/locales/zh-TW.json +2 -0
  120. package/src/i18n/locales/zh.json +2 -0
  121. package/src/router/HistoryNavigator.ts +4 -1
  122. package/src/router/Nav.tsx +7 -1
  123. package/src/router/Router.tsx +18 -4
  124. package/src/router/path.test.ts +87 -0
  125. package/src/router/path.ts +38 -0
  126. package/src/session/UserSession.ts +32 -9
  127. package/src/session/UserSessionProvider.tsx +138 -62
  128. package/src/session/auth/composable.test.ts +111 -0
  129. package/src/session/auth/composable.ts +58 -3
  130. package/src/session/auth/domainRouting.test.ts +28 -1
  131. package/src/session/auth/domainRouting.ts +38 -0
  132. package/src/shell/login/SignInRestrictedEnvStep.tsx +1 -1
  133. package/src/shell/login/SigninScreen.tsx +9 -1
  134. package/src/shell/login/productionUrl.test.ts +8 -5
  135. package/src/shell/login/productionUrl.ts +15 -5
@@ -17,6 +17,44 @@ interface ComposableTokenResponse {
17
17
  message?: string;
18
18
  }
19
19
 
20
+ function normalizeIssuer(value: string | undefined): string | undefined {
21
+ return value?.replace(/\/+$/, '');
22
+ }
23
+
24
+ function decodeToken(token: string): AuthTokenPayload {
25
+ return jwtDecode(token) as AuthTokenPayload;
26
+ }
27
+
28
+ function isVertesiaIssuedToken(token: string | undefined): token is string {
29
+ if (!token) return false;
30
+ try {
31
+ const decoded = decodeToken(token) as AuthTokenPayload & { iss?: string };
32
+ return normalizeIssuer(decoded.iss) === normalizeIssuer(Env.endpoints.sts);
33
+ } catch {
34
+ return false;
35
+ }
36
+ }
37
+
38
+ function canUseVertesiaTokenDirectly(token: string, accountId?: string, projectId?: string): boolean {
39
+ const decoded = decodeToken(token);
40
+ const hasAuthorizationClaims = Boolean(
41
+ decoded.permissions?.length ||
42
+ decoded.account_roles?.length ||
43
+ decoded.project_roles?.length ||
44
+ decoded.apps?.length,
45
+ );
46
+ if (!hasAuthorizationClaims) {
47
+ return false;
48
+ }
49
+ if (accountId && decoded.account?.id !== accountId) {
50
+ return false;
51
+ }
52
+ if (projectId && decoded.project?.id !== projectId) {
53
+ return false;
54
+ }
55
+ return true;
56
+ }
57
+
20
58
  export async function fetchComposableToken(
21
59
  getIdToken: () => Promise<string | null | undefined>,
22
60
  accountId?: string,
@@ -319,23 +357,40 @@ export async function getComposableToken(
319
357
  const selectedAccount = accountId ?? localStorage.getItem(LastSelectedAccountId_KEY) ?? undefined;
320
358
  const selectedProject =
321
359
  projectId ?? localStorage.getItem(`${LastSelectedProjectId_KEY}-${selectedAccount}`) ?? undefined;
360
+ const devAuthToken = Env.isLocalDev ? Env.devAuthToken : undefined;
361
+ const suppliedToken = devAuthToken ?? initToken ?? AUTH_TOKEN_RAW;
322
362
 
323
363
  //token is still valid for more than 5 minutes
324
364
  if (!forceRefresh && AUTH_TOKEN_RAW && AUTH_TOKEN && AUTH_TOKEN.exp > Date.now() / 1000 + 300) {
325
365
  return { rawToken: AUTH_TOKEN_RAW, token: AUTH_TOKEN, error: false };
326
366
  }
327
367
 
368
+ if (
369
+ !forceRefresh &&
370
+ isVertesiaIssuedToken(suppliedToken) &&
371
+ canUseVertesiaTokenDirectly(suppliedToken, selectedAccount, selectedProject)
372
+ ) {
373
+ AUTH_TOKEN_RAW = suppliedToken;
374
+ AUTH_TOKEN = decodeToken(AUTH_TOKEN_RAW);
375
+ if (!AUTH_TOKEN.exp) {
376
+ throw new Error('Invalid composable token');
377
+ }
378
+ return { rawToken: AUTH_TOKEN_RAW, token: AUTH_TOKEN, error: false };
379
+ }
380
+
328
381
  //token is close to expire, refresh it
329
- if (!useInternalAuth && getFirebaseAuth().currentUser) {
382
+ if (!devAuthToken && !useInternalAuth && getFirebaseAuth().currentUser) {
330
383
  //we have a firebase user, get the token from there
331
384
  AUTH_TOKEN_RAW = await fetchComposableTokenFromFirebaseToken(selectedAccount, selectedProject);
332
- } else if (initToken || AUTH_TOKEN_RAW) {
385
+ } else if (!devAuthToken && (initToken || AUTH_TOKEN_RAW)) {
333
386
  // we have a token already and no firebase user, refresh it
334
387
  AUTH_TOKEN_RAW = await fetchComposableToken(
335
388
  () => Promise.resolve(initToken ?? AUTH_TOKEN_RAW),
336
389
  selectedAccount,
337
390
  selectedProject,
338
391
  );
392
+ } else if (devAuthToken) {
393
+ AUTH_TOKEN_RAW = devAuthToken;
339
394
  }
340
395
 
341
396
  if (!AUTH_TOKEN_RAW) {
@@ -348,7 +403,7 @@ export async function getComposableToken(
348
403
  throw new Error('Cannot acquire a composable token');
349
404
  }
350
405
 
351
- AUTH_TOKEN = jwtDecode(AUTH_TOKEN_RAW) as AuthTokenPayload;
406
+ AUTH_TOKEN = decodeToken(AUTH_TOKEN_RAW);
352
407
 
353
408
  if (!AUTH_TOKEN?.exp || !AUTH_TOKEN_RAW) {
354
409
  console.error('Invalid composable token', AUTH_TOKEN);
@@ -1,9 +1,15 @@
1
1
  import { afterEach, describe, expect, it } from 'vitest';
2
- import { shouldRedirectToCentralAuth, shouldUseFirebaseAuth } from './domainRouting';
2
+ import { mountRootUrl, shouldRedirectToCentralAuth, shouldUseFirebaseAuth } from './domainRouting';
3
+
4
+ // Stub document.baseURI to simulate the served `<base href>` (deep gateway mount) or its absence.
5
+ function setBaseURI(baseURI: string) {
6
+ (globalThis as { document?: unknown }).document = { baseURI };
7
+ }
3
8
 
4
9
  describe('domainRouting', () => {
5
10
  afterEach(() => {
6
11
  delete (globalThis as { window?: unknown }).window;
12
+ delete (globalThis as { document?: unknown }).document;
7
13
  });
8
14
 
9
15
  it("uses Firebase auth when AUTH_MODE is 'firebase'", () => {
@@ -23,4 +29,25 @@ describe('domainRouting', () => {
23
29
  expect(shouldUseFirebaseAuth()).toBe(false);
24
30
  expect(shouldRedirectToCentralAuth()).toBe(true);
25
31
  });
32
+
33
+ describe('mountRootUrl', () => {
34
+ it('returns the deep gateway mount root (dropping any deep route / query / hash)', () => {
35
+ const mount = 'https://gw.example.com/tenants/05948c_5ed5f4/apps/furniture-catalog/versions/v1/app/';
36
+ setBaseURI(mount);
37
+ expect(mountRootUrl().toString()).toBe(mount);
38
+ });
39
+
40
+ it('strips an existing query and hash so callers control the reload target', () => {
41
+ setBaseURI('https://gw.example.com/tenants/t/apps/a/versions/v/app/');
42
+ const url = mountRootUrl();
43
+ url.searchParams.set('a', 'acct1');
44
+ url.searchParams.set('p', 'proj1');
45
+ expect(url.toString()).toBe('https://gw.example.com/tenants/t/apps/a/versions/v/app/?a=acct1&p=proj1');
46
+ });
47
+
48
+ it('is the origin root for the Studio UI (no <base> element)', () => {
49
+ setBaseURI('https://studio.vertesia.io/');
50
+ expect(mountRootUrl().toString()).toBe('https://studio.vertesia.io/');
51
+ });
52
+ });
26
53
  });
@@ -11,3 +11,41 @@ export function shouldUseFirebaseAuth(_hostname?: string) {
11
11
  export function shouldRedirectToCentralAuth(_hostname?: string) {
12
12
  return !shouldUseFirebaseAuth();
13
13
  }
14
+
15
+ /**
16
+ * The page URL to return to after a central-auth round-trip (`redirect_uri`).
17
+ *
18
+ * Apps served under a deep gateway mount carry a `<base href="/tenants/<t>/apps/<app>/.../app/">`
19
+ * that the app-gateway injects at serve time. The in-app router rewrites the address bar relative
20
+ * to the origin rather than that mount, so by the time the auth flow runs `window.location` can
21
+ * read as the bare origin `/`. Building `redirect_uri` from that drops the app path, and the
22
+ * post-login token bounces back to a URL that serves no app.
23
+ *
24
+ * Prefer the live location when it still sits under the mount (preserves the in-app deep route);
25
+ * otherwise fall back to `document.baseURI` — the mount, which the router cannot clobber. With no
26
+ * `<base>` element (the Studio UI), `document.baseURI` is the document URL and its `pathname` is a
27
+ * prefix of the live location, so the live URL is used unchanged — no behavior change there.
28
+ */
29
+ export function authReturnUrl(): URL {
30
+ const base = new URL(document.baseURI);
31
+ const current = new URL(window.location.href);
32
+ const target = current.pathname.startsWith(base.pathname) ? current : base;
33
+ target.hash = '';
34
+ return target;
35
+ }
36
+
37
+ /**
38
+ * The app's mount root URL — the served `<base href>` (or the origin root for the Studio UI).
39
+ *
40
+ * Used for full-reload navigations that intentionally reset to the app root (logout, account /
41
+ * project switch). Building these from a bare `/` drops a gateway-mounted app off its mount and
42
+ * lands on a URL that serves no app; resolving against `document.baseURI` keeps the reload inside
43
+ * the mount. For the Studio UI (no `<base>` element) `document.baseURI` is the origin root, so the
44
+ * behavior is unchanged.
45
+ */
46
+ export function mountRootUrl(): URL {
47
+ const url = new URL(document.baseURI);
48
+ url.hash = '';
49
+ url.search = '';
50
+ return url;
51
+ }
@@ -15,7 +15,7 @@ interface SignInRestrictedEnvStepProps {
15
15
  const AUTO_REDIRECT_SECONDS = 15;
16
16
 
17
17
  /**
18
- * Shown when the STS rejects sign-in because the current environment (preview/preprod) is
18
+ * Shown when the STS rejects sign-in because the current environment (any non-production tier) is
19
19
  * restricted to early-access users. Offers a redirect to the production web application — in the
20
20
  * user's own region — and a way to sign in with a different account. If the user takes no action, it
21
21
  * redirects automatically after {@link AUTO_REDIRECT_SECONDS}. See
@@ -89,7 +89,15 @@ function SigninScreenImpl({
89
89
 
90
90
  useEffect(() => {
91
91
  if (!preservePath) {
92
- history.replaceState({}, '', '/');
92
+ // Reset to the app's mount root, not the bare origin. A gateway-mounted app carries a
93
+ // served `<base href>` deep mount; collapsing to '/' drops the app off that mount (the
94
+ // bare origin serves no app) and the address bar can no longer be reloaded. This effect
95
+ // also flashes briefly for already-authenticated users during the loading transition, so
96
+ // a bare '/' here loses the URL even on a normal login. Deriving the root from
97
+ // `document.baseURI` keeps it inside the mount; for the origin-served Studio UI the
98
+ // pathname is '/', so the behavior is unchanged.
99
+ const mountRoot = new URL(document.baseURI).pathname || '/';
100
+ history.replaceState({}, '', mountRoot);
93
101
  }
94
102
  }, [preservePath]);
95
103
 
@@ -2,17 +2,20 @@ import { describe, expect, it } from 'vitest';
2
2
  import { getProductionAppUrl } from './productionUrl.js';
3
3
 
4
4
  describe('getProductionAppUrl', () => {
5
- it('sends a non-US region to its own regional production site', () => {
5
+ it('sends every production region to its regional site (one cloud.{region} pattern)', () => {
6
+ // us1 is not special-cased — it uses the same regional pattern as every other region.
7
+ expect(getProductionAppUrl('us1')).toBe('https://cloud.us1.vertesia.io/');
8
+ expect(getProductionAppUrl('us2')).toBe('https://cloud.us2.vertesia.io/');
6
9
  expect(getProductionAppUrl('eu1')).toBe('https://cloud.eu1.vertesia.io/');
7
10
  expect(getProductionAppUrl('jp1')).toBe('https://cloud.jp1.vertesia.io/');
8
11
  });
9
12
 
10
- it('sends US regions to the canonical non-regional production site', () => {
11
- expect(getProductionAppUrl('us1')).toBe('https://cloud.vertesia.io/');
12
- expect(getProductionAppUrl('us')).toBe('https://cloud.vertesia.io/');
13
+ it('sends a dev region to the production region it mirrors, not the dev host', () => {
14
+ // dev1 mirrors us1: a rejected user must land on production, never back on cloud.dev1.
15
+ expect(getProductionAppUrl('dev1')).toBe('https://cloud.us1.vertesia.io/');
13
16
  });
14
17
 
15
- it('falls back to the canonical production site when the region is unknown', () => {
18
+ it('falls back to the canonical non-regional site when the region is unknown', () => {
16
19
  expect(getProductionAppUrl(undefined)).toBe('https://cloud.vertesia.io/');
17
20
  expect(getProductionAppUrl('')).toBe('https://cloud.vertesia.io/');
18
21
  });
@@ -4,15 +4,25 @@
4
4
  * they do have access — in their own region, never another one (an EU user must not land on the US
5
5
  * site, and vice versa).
6
6
  *
7
- * The mapping mirrors the server's `detectUiUrl` production branch (packages/server-common):
8
- * - US (`us` / `us1`) or unknown region → `https://cloud.vertesia.io/` (canonical, non-regional)
9
- * - any other region (e.g. `eu1`, `jp1`) → `https://cloud.{region}.vertesia.io/`
7
+ * Every production region follows one URL pattern `https://cloud.{region}.vertesia.io/` (e.g.
8
+ * `us1`, `us2`, `eu1`, `jp1`) with no region special-cased. Only the legacy non-regional
9
+ * deployment has no region and falls back to the apex `https://cloud.vertesia.io/`.
10
+ *
11
+ * Dev/ephemeral regions have no production site of their own — they mirror a production region — so
12
+ * they are mapped to it first. Sending a rejected user to the dev host (e.g. `cloud.dev1.vertesia.io`)
13
+ * would just bounce them back to the same restricted environment. `dev1` (us-central1) mirrors the
14
+ * `us1` production region.
10
15
  *
11
16
  * Region is preferred over parsing the current hostname because it is well-defined for every serving
12
17
  * pattern (including the direct-routing `*.ui.{region}.vertesia.io` hosts, which carry no
13
18
  * `preview.`/`preprod.` prefix to strip). Callers pass `Env.region` from `@vertesia/ui/env`.
14
19
  */
20
+ const DEV_REGION_TO_PRODUCTION_REGION: Record<string, string> = {
21
+ // dev1 (us-central1) is a dev mirror of the us1 production region.
22
+ dev1: 'us1',
23
+ };
24
+
15
25
  export function getProductionAppUrl(region?: string): string {
16
- const isNonUsRegion = region && region !== 'us' && region !== 'us1';
17
- return isNonUsRegion ? `https://cloud.${region}.vertesia.io/` : 'https://cloud.vertesia.io/';
26
+ const prodRegion = (region && DEV_REGION_TO_PRODUCTION_REGION[region]) || region;
27
+ return prodRegion ? `https://cloud.${prodRegion}.vertesia.io/` : 'https://cloud.vertesia.io/';
18
28
  }