@gooddata/sdk-ui-pluggable-host 11.40.0-alpha.3

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 (126) hide show
  1. package/LICENSE +19 -0
  2. package/README.md +20 -0
  3. package/esm/assets/logo-white.svg +3 -0
  4. package/esm/components/FullScreenLoader.d.ts +1 -0
  5. package/esm/components/FullScreenLoader.js +8 -0
  6. package/esm/components/HostUiContainer.d.ts +16 -0
  7. package/esm/components/HostUiContainer.js +141 -0
  8. package/esm/components/HostUiContainer.scss +5 -0
  9. package/esm/components/Root.d.ts +16 -0
  10. package/esm/components/Root.js +64 -0
  11. package/esm/components/Root.scss +14 -0
  12. package/esm/components/lib/translations.d.ts +7 -0
  13. package/esm/components/lib/translations.js +64 -0
  14. package/esm/components/useRedirectNavigation.d.ts +7 -0
  15. package/esm/components/useRedirectNavigation.js +23 -0
  16. package/esm/components/useRedirectTarget.d.ts +19 -0
  17. package/esm/components/useRedirectTarget.js +62 -0
  18. package/esm/debug.d.ts +9 -0
  19. package/esm/debug.js +18 -0
  20. package/esm/index.d.ts +11 -0
  21. package/esm/index.js +10 -0
  22. package/esm/lib/chunkReloadGuard.d.ts +89 -0
  23. package/esm/lib/chunkReloadGuard.js +203 -0
  24. package/esm/lib/hostNotifications.d.ts +20 -0
  25. package/esm/lib/hostNotifications.js +50 -0
  26. package/esm/lib/isProduction.d.ts +12 -0
  27. package/esm/lib/isProduction.js +13 -0
  28. package/esm/loader/lastVisitedApp.d.ts +11 -0
  29. package/esm/loader/lastVisitedApp.js +43 -0
  30. package/esm/loader/localLoader.d.ts +16 -0
  31. package/esm/loader/localLoader.js +38 -0
  32. package/esm/loader/pluggableApplicationsLoader.d.ts +13 -0
  33. package/esm/loader/pluggableApplicationsLoader.js +55 -0
  34. package/esm/loader/redirectLogic.d.ts +30 -0
  35. package/esm/loader/redirectLogic.js +143 -0
  36. package/esm/loader/remoteLoader.d.ts +5 -0
  37. package/esm/loader/remoteLoader.js +117 -0
  38. package/esm/loader/remoteUrlSecurity.d.ts +1 -0
  39. package/esm/loader/remoteUrlSecurity.js +26 -0
  40. package/esm/loader/routing.d.ts +22 -0
  41. package/esm/loader/routing.js +87 -0
  42. package/esm/platformContext/backend.d.ts +44 -0
  43. package/esm/platformContext/backend.js +131 -0
  44. package/esm/platformContext/bootstrap.d.ts +15 -0
  45. package/esm/platformContext/bootstrap.js +122 -0
  46. package/esm/platformContext/loadPlatformContext.d.ts +18 -0
  47. package/esm/platformContext/loadPlatformContext.js +50 -0
  48. package/esm/platformContext/tigerNotAuthenticatedHandler.d.ts +3 -0
  49. package/esm/platformContext/tigerNotAuthenticatedHandler.js +16 -0
  50. package/esm/platformContext/types.d.ts +17 -0
  51. package/esm/platformContext/types.js +2 -0
  52. package/esm/platformContext/useLoadPlatformContext.d.ts +35 -0
  53. package/esm/platformContext/useLoadPlatformContext.js +131 -0
  54. package/esm/platformContext/useWorkspacePermissions.d.ts +26 -0
  55. package/esm/platformContext/useWorkspacePermissions.js +52 -0
  56. package/esm/platformContext/useWorkspaceSettings.d.ts +25 -0
  57. package/esm/platformContext/useWorkspaceSettings.js +46 -0
  58. package/esm/registry/pluggableApplicationsRegistry.d.ts +55 -0
  59. package/esm/registry/pluggableApplicationsRegistry.js +203 -0
  60. package/esm/sdk-ui-pluggable-host.d.ts +262 -0
  61. package/esm/styles/global.css +16 -0
  62. package/esm/translations/de-DE.json +34 -0
  63. package/esm/translations/en-AU.json +34 -0
  64. package/esm/translations/en-GB.json +34 -0
  65. package/esm/translations/en-US.json +130 -0
  66. package/esm/translations/es-419.json +34 -0
  67. package/esm/translations/es-ES.json +34 -0
  68. package/esm/translations/fi-FI.json +34 -0
  69. package/esm/translations/fr-CA.json +34 -0
  70. package/esm/translations/fr-FR.json +34 -0
  71. package/esm/translations/id-ID.json +34 -0
  72. package/esm/translations/it-IT.json +34 -0
  73. package/esm/translations/ja-JP.json +34 -0
  74. package/esm/translations/ko-KR.json +34 -0
  75. package/esm/translations/nl-NL.json +34 -0
  76. package/esm/translations/pl-PL.json +34 -0
  77. package/esm/translations/pt-BR.json +34 -0
  78. package/esm/translations/pt-PT.json +34 -0
  79. package/esm/translations/ru-RU.json +34 -0
  80. package/esm/translations/sl-SI.json +34 -0
  81. package/esm/translations/th-TH.json +34 -0
  82. package/esm/translations/tr-TR.json +34 -0
  83. package/esm/translations/uk-UA.json +34 -0
  84. package/esm/translations/vi-VN.json +34 -0
  85. package/esm/translations/zh-HK.json +34 -0
  86. package/esm/translations/zh-Hans.json +34 -0
  87. package/esm/translations/zh-Hant.json +34 -0
  88. package/esm/tsdoc-metadata.json +11 -0
  89. package/esm/types/lifecycle.d.ts +18 -0
  90. package/esm/types/lifecycle.js +2 -0
  91. package/esm/ui/DefaultHostUi.d.ts +12 -0
  92. package/esm/ui/DefaultHostUi.js +101 -0
  93. package/esm/ui/DefaultHostUi.scss +8 -0
  94. package/esm/ui/GenAIChat.d.ts +43 -0
  95. package/esm/ui/GenAIChat.js +102 -0
  96. package/esm/ui/HostChrome.d.ts +19 -0
  97. package/esm/ui/HostChrome.js +115 -0
  98. package/esm/ui/HostChrome.scss +24 -0
  99. package/esm/ui/HostIntlProvider.d.ts +9 -0
  100. package/esm/ui/HostIntlProvider.js +13 -0
  101. package/esm/ui/HostNotificationDispatcher.d.ts +12 -0
  102. package/esm/ui/HostNotificationDispatcher.js +42 -0
  103. package/esm/ui/PluggableApplicationRenderer.d.ts +10 -0
  104. package/esm/ui/PluggableApplicationRenderer.js +100 -0
  105. package/esm/ui/PluggableApplicationRenderer.scss +29 -0
  106. package/esm/ui/SemanticSearch.d.ts +23 -0
  107. package/esm/ui/SemanticSearch.js +46 -0
  108. package/esm/ui/WorkspacePicker.d.ts +9 -0
  109. package/esm/ui/WorkspacePicker.js +29 -0
  110. package/esm/ui/appMenuItems.d.ts +17 -0
  111. package/esm/ui/appMenuItems.js +81 -0
  112. package/esm/ui/chromeHelpers.d.ts +17 -0
  113. package/esm/ui/chromeHelpers.js +29 -0
  114. package/esm/ui/hostChromeBem.d.ts +1 -0
  115. package/esm/ui/hostChromeBem.js +3 -0
  116. package/esm/ui/resolveHostUiModule.d.ts +8 -0
  117. package/esm/ui/resolveHostUiModule.js +22 -0
  118. package/esm/ui/useHostChromeChat.d.ts +29 -0
  119. package/esm/ui/useHostChromeChat.js +38 -0
  120. package/esm/ui/useHostChromePricing.d.ts +52 -0
  121. package/esm/ui/useHostChromePricing.js +37 -0
  122. package/esm/ui/useHostChromeSearch.d.ts +20 -0
  123. package/esm/ui/useHostChromeSearch.js +18 -0
  124. package/esm/ui/useHostChromeWorkspaceFeatures.d.ts +19 -0
  125. package/esm/ui/useHostChromeWorkspaceFeatures.js +36 -0
  126. package/package.json +114 -0
@@ -0,0 +1,143 @@
1
+ // (C) 2026 GoodData Corporation
2
+ import { debugLog } from "../debug.js";
3
+ import { getLastVisitedApp, setLastVisitedApp } from "./lastVisitedApp.js";
4
+ import { getActiveInternalApplication, getApplicationHref } from "./routing.js";
5
+ /**
6
+ * Thrown when the current URL does not correspond to any accessible application.
7
+ */
8
+ export class AppNotFoundError extends Error {
9
+ constructor(message) {
10
+ super(message);
11
+ this.name = "AppNotFoundError";
12
+ }
13
+ }
14
+ /**
15
+ * Returns whether the given pathname is at the root of the specified scope.
16
+ *
17
+ * @example
18
+ * - `/` → true (always considered a scope root)
19
+ * - `/organization` → true; `/organization/ai-hub` → false
20
+ * - `/workspace/abc` → true; `/workspace/abc/dashboards` → false
21
+ * - `/workspace/` with no workspaceId → true (no workspace selected yet)
22
+ */
23
+ function isAtScopeRoot(pathname, scope, workspaceId) {
24
+ if (!pathname || pathname === "/") {
25
+ return true;
26
+ }
27
+ if (scope === "organization") {
28
+ return pathname === "/organization" || pathname === "/organization/";
29
+ }
30
+ if (scope === "workspace") {
31
+ if (!workspaceId) {
32
+ // No workspace ID in the URL means we haven't landed on a specific workspace yet — treat as root
33
+ return true;
34
+ }
35
+ const wsRoot = `/workspace/${workspaceId}`;
36
+ return pathname === wsRoot || pathname === wsRoot + "/";
37
+ }
38
+ // TypeScript exhaustive check — catches unhandled ApplicationScope additions at compile time
39
+ const _ = scope;
40
+ throw new Error(`[host-runtime/redirectLogic] Unhandled application scope: ${_}`);
41
+ }
42
+ /**
43
+ * Returns the last-visited app for the given scope if it is in the eligible list,
44
+ * otherwise falls back to the first app.
45
+ */
46
+ function preferLastVisitedApp(apps, scope) {
47
+ const lastVisitedId = getLastVisitedApp(scope);
48
+ if (lastVisitedId) {
49
+ const match = apps.find((app) => app.id === lastVisitedId);
50
+ if (match) {
51
+ return match;
52
+ }
53
+ }
54
+ return apps[0];
55
+ }
56
+ /**
57
+ * Handles navigation when the user is inside the organization scope.
58
+ * Either redirects to the preferred org app (last-visited or first, when at the org root)
59
+ * or validates that the current path maps to a known app.
60
+ * Individual modules are responsible for their own permission checks.
61
+ */
62
+ function resolveOrgScopeTarget(apps, ctx, pathname) {
63
+ if (isAtScopeRoot(pathname, "organization")) {
64
+ const target = preferLastVisitedApp(apps, "organization");
65
+ if (!target) {
66
+ debugLog("[host-runtime/redirect] org scope: at root but no org apps available — throwing not-found");
67
+ throw new AppNotFoundError("No organization-scoped applications are available.");
68
+ }
69
+ const href = getApplicationHref(target, ctx, pathname);
70
+ debugLog(`[host-runtime/redirect] org scope: at root, redirecting to preferred org app → ${href}`);
71
+ return href;
72
+ }
73
+ const active = getActiveInternalApplication(apps, ctx, pathname);
74
+ if (!active) {
75
+ debugLog(`[host-runtime/redirect] org scope: no app matched pathname → ${pathname} — throwing not-found`);
76
+ throw new AppNotFoundError(`No application found at path: ${pathname}`);
77
+ }
78
+ debugLog(`[host-runtime/redirect] org scope: active app matched → ${active.id}`);
79
+ setLastVisitedApp("organization", active.id);
80
+ return null;
81
+ }
82
+ /**
83
+ * Resolves where the shell app should navigate given the current URL and permission context.
84
+ *
85
+ * @returns
86
+ * - A URL string → caller must navigate to this URL (e.g. via React Router)
87
+ * - `null` → the current URL is valid; render the active app
88
+ *
89
+ * @throws {AppNotFoundError} The current path maps to no accessible application (show 404)
90
+ * @throws {Error} Unexpected failure (show generic error screen)
91
+ */
92
+ export async function resolveRedirectTarget({ apps, ctx, pathname, fetchFirstWorkspaceId, }) {
93
+ const scope = ctx.currentApplicationScope;
94
+ const workspaceId = ctx.currentWorkspaceId;
95
+ debugLog(`[host-runtime/redirect] resolveRedirectTarget: scope=${scope ?? "(none)"} workspaceId=${workspaceId ?? "(none)"} pathname=${pathname} apps=${apps.length}`);
96
+ if (scope === "organization") {
97
+ return resolveOrgScopeTarget(apps, ctx, pathname);
98
+ }
99
+ if (scope === "workspace" && !isAtScopeRoot(pathname, scope, workspaceId)) {
100
+ // User is at a specific path inside workspace scope — validate it maps to a permitted app
101
+ const active = getActiveInternalApplication(apps, ctx, pathname);
102
+ if (!active) {
103
+ debugLog(`[host-runtime/redirect] workspace scope: no app matched pathname → ${pathname} — throwing not-found`);
104
+ throw new AppNotFoundError(`No application found at path: ${pathname}`);
105
+ }
106
+ debugLog(`[host-runtime/redirect] workspace scope: active app matched → ${active.id}`);
107
+ setLastVisitedApp("workspace", active.id);
108
+ return null;
109
+ }
110
+ // At a workspace root (with or without workspace ID) or the top-level app root.
111
+ // Strategy: redirect in up to two hops via client-side navigation.
112
+ // 1. If no workspace ID → fetch the first workspace → redirect to /workspace/{id}
113
+ // 2. If at workspace root with ID → redirect to the first permitted app
114
+ if (!workspaceId) {
115
+ // When scope is undefined (user landed on "/"), prefer organization scope
116
+ // for users with org management permission.
117
+ if (!scope && ctx.organizationPermissions?.canManageOrganization) {
118
+ debugLog("[host-runtime/redirect] user has canManageOrganization — redirecting to /organization");
119
+ return "/organization";
120
+ }
121
+ // Hop 1: resolve a workspace ID and redirect to its root so that the next render cycle
122
+ // can load workspace permissions and filter apps accurately.
123
+ debugLog("[host-runtime/redirect] no workspace ID — fetching first workspace");
124
+ const resolvedWorkspaceId = await fetchFirstWorkspaceId();
125
+ if (!resolvedWorkspaceId) {
126
+ debugLog("[host-runtime/redirect] no workspace available for user — throwing not-found");
127
+ throw new AppNotFoundError("No workspace is available for this user.");
128
+ }
129
+ const wsRootHref = `/workspace/${resolvedWorkspaceId}`;
130
+ debugLog(`[host-runtime/redirect] redirecting to workspace root → ${wsRootHref}`);
131
+ return wsRootHref;
132
+ }
133
+ // Hop 2: workspace ID is known, workspace permissions are loaded, apps are fully filtered.
134
+ // Redirect to the preferred (last-visited or first) workspace app.
135
+ const targetApp = preferLastVisitedApp(apps, "workspace");
136
+ if (!targetApp) {
137
+ debugLog("[host-runtime/redirect] no permitted workspace apps — throwing not-found");
138
+ throw new AppNotFoundError("No workspace-scoped applications are available for this workspace.");
139
+ }
140
+ const href = getApplicationHref(targetApp, ctx, pathname);
141
+ debugLog(`[host-runtime/redirect] redirecting to preferred workspace app → ${href} (app: ${targetApp.id})`);
142
+ return href;
143
+ }
@@ -0,0 +1,5 @@
1
+ import { type IRemotePluggableApplicationModule } from "@gooddata/sdk-model";
2
+ import { type IPluggableApp, type IHostUiModule } from "@gooddata/sdk-pluggable-application-model";
3
+ export declare function loadRemotePluggableApplication(remote: IRemotePluggableApplicationModule): Promise<IPluggableApp>;
4
+ export declare function loadRemoteHostUiModule(remote: IRemotePluggableApplicationModule): Promise<IHostUiModule>;
5
+ export declare function preloadRemotePluggableApplication(remote: IRemotePluggableApplicationModule): Promise<IPluggableApp>;
@@ -0,0 +1,117 @@
1
+ // (C) 2026 GoodData Corporation
2
+ import { createInstance } from "@module-federation/runtime";
3
+ import { isAllowedRemoteHostname } from "./remoteUrlSecurity.js";
4
+ const remoteAppPromises = new Map();
5
+ const registeredRemoteScopeUrls = new Map();
6
+ let federation;
7
+ function getFederation() {
8
+ if (!federation) {
9
+ federation = createInstance({ name: "gdc_host", remotes: [] });
10
+ }
11
+ return federation;
12
+ }
13
+ function normalizeModuleName(moduleName) {
14
+ return moduleName.replace(/^\.\//, "");
15
+ }
16
+ function isJSEntry(url) {
17
+ return /\.m?js(?:$|\?)/.test(url);
18
+ }
19
+ function asPluggableApp(remote, loaded) {
20
+ const app = loaded.pluggableApp ?? loaded.default;
21
+ if (!app || typeof app.mount !== "function") {
22
+ throw new Error(`[host-runtime/remote-loader] Remote module "${remote.scope}/${remote.module}" does not export a valid pluggable app. (type: ${typeof loaded}, hasMount: ${typeof loaded?.mount}, keys: ${Object.keys(loaded ?? {})})`);
23
+ }
24
+ return app;
25
+ }
26
+ function getCacheKey(remote) {
27
+ return `${remote.scope}::${remote.url}::${remote.module}`;
28
+ }
29
+ function registerRemote(remote) {
30
+ const existingUrl = registeredRemoteScopeUrls.get(remote.scope);
31
+ if (!remote.url) {
32
+ throw new Error(existingUrl
33
+ ? `[host-runtime/remote-loader] Remote "${remote.scope}" has no URL configured. Refusing to load to avoid using previously registered URL "${existingUrl}".`
34
+ : `[host-runtime/remote-loader] Remote "${remote.scope}" has no URL configured.`);
35
+ }
36
+ // Security: reject remotes not hosted on the same hostname or an allowlisted hostname.
37
+ if (!isAllowedRemoteHostname(remote.url)) {
38
+ throw new Error(existingUrl
39
+ ? `[host-runtime/remote-loader] Remote "${remote.scope}" URL "${remote.url}" is not on an allowed hostname. Refusing to load to avoid using previously registered URL "${existingUrl}".`
40
+ : `[host-runtime/remote-loader] Remote "${remote.scope}" URL "${remote.url}" is not on an allowed hostname.`);
41
+ }
42
+ if (existingUrl === remote.url) {
43
+ return;
44
+ }
45
+ const force = existingUrl !== undefined && existingUrl !== remote.url;
46
+ const remoteRegistration = {
47
+ name: remote.scope,
48
+ entry: remote.url,
49
+ ...(isJSEntry(remote.url) ? { type: "module" } : {}),
50
+ };
51
+ try {
52
+ getFederation().registerRemotes([remoteRegistration], force ? { force: true } : undefined);
53
+ }
54
+ catch (error) {
55
+ const message = error instanceof Error ? error.message : String(error);
56
+ throw new Error(`[host-runtime/remote-loader] Failed to register remote "${remote.scope}" from "${remote.url}": ${message}`);
57
+ }
58
+ registeredRemoteScopeUrls.set(remote.scope, remote.url);
59
+ }
60
+ async function loadRemoteModule(remote) {
61
+ registerRemote(remote);
62
+ const moduleName = normalizeModuleName(remote.module);
63
+ const loaded = await getFederation().loadRemote(`${remote.scope}/${moduleName}`);
64
+ if (!loaded) {
65
+ throw new Error(`[host-runtime/remote-loader] Remote module "${remote.scope}/${moduleName}" resolved to null.`);
66
+ }
67
+ return loaded;
68
+ }
69
+ export function loadRemotePluggableApplication(remote) {
70
+ const cacheKey = getCacheKey(remote);
71
+ const cached = remoteAppPromises.get(cacheKey);
72
+ if (cached) {
73
+ return cached;
74
+ }
75
+ const loadingPromise = loadRemoteModule(remote)
76
+ .then((loaded) => asPluggableApp(remote, loaded))
77
+ .catch((error) => {
78
+ remoteAppPromises.delete(cacheKey);
79
+ throw error;
80
+ });
81
+ remoteAppPromises.set(cacheKey, loadingPromise);
82
+ return loadingPromise;
83
+ }
84
+ function asHostUiModule(remote, loaded) {
85
+ const mod = loaded.hostUiModule ?? loaded.default;
86
+ if (!mod || typeof mod.mount !== "function") {
87
+ throw new Error(`[host-runtime/remote-loader] Remote UI module "${remote.scope}/${remote.module}" does not export a valid IHostUiModule.`);
88
+ }
89
+ return mod;
90
+ }
91
+ export async function loadRemoteHostUiModule(remote) {
92
+ const loaded = await loadRemoteModule(remote);
93
+ return asHostUiModule(remote, loaded);
94
+ }
95
+ export function preloadRemotePluggableApplication(remote) {
96
+ registerRemote(remote);
97
+ // Runtime manifest preloading works for manifest URLs (for example mf-manifest.json).
98
+ // For JS entry URLs (remoteEntry.js), preloadRemote tries to parse JSON and fails,
99
+ // so we skip manifest preload and only warm the actual exposed module below.
100
+ if (!isJSEntry(remote.url)) {
101
+ void getFederation()
102
+ .preloadRemote([
103
+ {
104
+ nameOrAlias: remote.scope,
105
+ exposes: [normalizeModuleName(remote.module)],
106
+ },
107
+ ])
108
+ .catch((error) => {
109
+ console.error(`[host-runtime/remote-loader] Failed to preload remote entry for "${remote.scope}/${remote.module}".`, error);
110
+ });
111
+ }
112
+ // Warm the module promise itself so hover/focus reduces click-to-mount latency.
113
+ return loadRemotePluggableApplication(remote).catch((error) => {
114
+ console.error(`[host-runtime/remote-loader] Failed to preload remote module "${remote.scope}/${remote.module}".`, error);
115
+ throw error;
116
+ });
117
+ }
@@ -0,0 +1 @@
1
+ export declare function isAllowedRemoteHostname(url: string): boolean;
@@ -0,0 +1,26 @@
1
+ // (C) 2026 GoodData Corporation
2
+ /**
3
+ * Security guard: only allow remote modules hosted on the same hostname as the host app,
4
+ * or on an explicit allowlist of trusted hostnames (e.g. the demo-dashboard-plugins bucket).
5
+ *
6
+ * This is the sole safeguard preventing arbitrary third-party JavaScript from being loaded
7
+ * as a Module Federation remote. Remote modules execute with full application privileges,
8
+ * so loading from an untrusted origin would be equivalent to an XSS attack.
9
+ *
10
+ * Relative URLs (e.g. "/__CDN_URL_PLACEHOLDER__/...") always resolve to the current origin
11
+ * and are therefore allowed. Absolute URLs from a different hostname are blocked unless the
12
+ * hostname is in ALLOWED_REMOTE_HOSTNAMES.
13
+ */
14
+ const ALLOWED_REMOTE_HOSTNAMES = new Set([
15
+ // infra1: s3://gdc-panther-prod-demo-dashboard-plugins (apps from gdc-ui-pluggable-applications playground)
16
+ "demo-dashboard-plugins.gooddata.com",
17
+ ]);
18
+ export function isAllowedRemoteHostname(url) {
19
+ try {
20
+ const { hostname } = new URL(url, window.location.origin);
21
+ return hostname === window.location.hostname || ALLOWED_REMOTE_HOSTNAMES.has(hostname);
22
+ }
23
+ catch {
24
+ return false;
25
+ }
26
+ }
@@ -0,0 +1,22 @@
1
+ import { type ApplicationScope, type PluggableApplicationRegistryItem } from "@gooddata/sdk-model";
2
+ import { type IPlatformContext } from "@gooddata/sdk-pluggable-application-model";
3
+ /**
4
+ * Returns the application scope for the given URL path, or undefined if the path
5
+ * does not match a known scope.
6
+ */
7
+ export declare function getApplicationScopeFromPath(path: string): ApplicationScope | undefined;
8
+ /**
9
+ * Returns the workspace id for the given URL path, or undefined if the path
10
+ * does not contain a workspace id.
11
+ */
12
+ export declare function getWorkspaceIdFromPath(pathname: string | undefined): string | undefined;
13
+ /**
14
+ * Substitutes `{workspaceId}` placeholders in an external app URL with the
15
+ * currently resolved workspace id. URLs with no placeholder pass through
16
+ * unchanged, so apps that don't care about workspace context can keep using
17
+ * a plain literal URL (e.g. `https://docs.example.com`).
18
+ */
19
+ export declare function substituteExternalUrlPlaceholders(url: string, ctx: IPlatformContext, pathname?: string): string;
20
+ export declare function getApplicationHref(app: PluggableApplicationRegistryItem, ctx: IPlatformContext, pathname?: string): string;
21
+ export declare function isInternalAppRouteActive(app: PluggableApplicationRegistryItem, ctx: IPlatformContext, pathname: string): boolean;
22
+ export declare function getActiveInternalApplication(apps: PluggableApplicationRegistryItem[], ctx: IPlatformContext, pathname: string): PluggableApplicationRegistryItem | undefined;
@@ -0,0 +1,87 @@
1
+ // (C) 2026 GoodData Corporation
2
+ import { isExternalPluggableApplicationRegistryItem, isLocalPluggableApplicationRegistryItem, isRemotePluggableApplicationRegistryItem, } from "@gooddata/sdk-model";
3
+ const WORKSPACE_PATH_PATTERN = /^\/workspace\/(?<workspaceId>[^/]+)(?:\/|$)/;
4
+ /**
5
+ * Returns the application scope for the given URL path, or undefined if the path
6
+ * does not match a known scope.
7
+ */
8
+ export function getApplicationScopeFromPath(path) {
9
+ const isPathMatching = (definitionPath) => path === definitionPath || path.startsWith(definitionPath + "/");
10
+ if (isPathMatching("/organization")) {
11
+ return "organization";
12
+ }
13
+ if (isPathMatching("/workspace")) {
14
+ return "workspace";
15
+ }
16
+ return undefined;
17
+ }
18
+ /**
19
+ * Returns the workspace id for the given URL path, or undefined if the path
20
+ * does not contain a workspace id.
21
+ */
22
+ export function getWorkspaceIdFromPath(pathname) {
23
+ return WORKSPACE_PATH_PATTERN.exec(pathname ?? "")?.groups?.["workspaceId"];
24
+ }
25
+ function ensureLeadingSlash(path) {
26
+ return path.startsWith("/") ? path : `/${path}`;
27
+ }
28
+ function normalizePath(path) {
29
+ const prefixed = ensureLeadingSlash(path);
30
+ if (prefixed === "/") {
31
+ return prefixed;
32
+ }
33
+ return prefixed.replace(/\/+$/, "");
34
+ }
35
+ function resolveWorkspaceId(ctx, pathname) {
36
+ if (ctx.currentWorkspaceId) {
37
+ return ctx.currentWorkspaceId;
38
+ }
39
+ return getWorkspaceIdFromPath(pathname);
40
+ }
41
+ function composeInternalAppPath(app, routeBase, ctx, pathname) {
42
+ const scopeBase = app.applicationScope === "organization"
43
+ ? "/organization"
44
+ : app.applicationScope === "workspace"
45
+ ? `/workspace/${resolveWorkspaceId(ctx, pathname) ?? ""}`
46
+ : undefined;
47
+ if (!scopeBase) {
48
+ throw new Error(`[host-runtime/routing] Unsupported application scope "${app.applicationScope}" for app "${app.id}".`);
49
+ }
50
+ return normalizePath(`${normalizePath(scopeBase)}${ensureLeadingSlash(routeBase)}`);
51
+ }
52
+ /**
53
+ * Substitutes `{workspaceId}` placeholders in an external app URL with the
54
+ * currently resolved workspace id. URLs with no placeholder pass through
55
+ * unchanged, so apps that don't care about workspace context can keep using
56
+ * a plain literal URL (e.g. `https://docs.example.com`).
57
+ */
58
+ export function substituteExternalUrlPlaceholders(url, ctx, pathname) {
59
+ if (!url.includes("{workspaceId}")) {
60
+ return url;
61
+ }
62
+ const workspaceId = resolveWorkspaceId(ctx, pathname) ?? "";
63
+ return url.replaceAll("{workspaceId}", workspaceId);
64
+ }
65
+ export function getApplicationHref(app, ctx, pathname) {
66
+ if (isExternalPluggableApplicationRegistryItem(app)) {
67
+ return substituteExternalUrlPlaceholders(app.external.url, ctx, pathname);
68
+ }
69
+ if (isLocalPluggableApplicationRegistryItem(app)) {
70
+ return composeInternalAppPath(app, app.local.routeBase, ctx, pathname);
71
+ }
72
+ if (isRemotePluggableApplicationRegistryItem(app)) {
73
+ return composeInternalAppPath(app, app.remote.routeBase, ctx, pathname);
74
+ }
75
+ return "#";
76
+ }
77
+ export function isInternalAppRouteActive(app, ctx, pathname) {
78
+ if (isExternalPluggableApplicationRegistryItem(app)) {
79
+ return false;
80
+ }
81
+ const basePath = normalizePath(getApplicationHref(app, ctx, pathname));
82
+ const normalizedPathname = normalizePath(pathname);
83
+ return normalizedPathname === basePath || normalizedPathname.startsWith(`${basePath}/`);
84
+ }
85
+ export function getActiveInternalApplication(apps, ctx, pathname) {
86
+ return apps.find((app) => !isExternalPluggableApplicationRegistryItem(app) && isInternalAppRouteActive(app, ctx, pathname));
87
+ }
@@ -0,0 +1,44 @@
1
+ import { type IAnalyticalBackend, type NotAuthenticatedHandler } from "@gooddata/sdk-backend-spi";
2
+ import { type IAuthCredentials } from "@gooddata/sdk-pluggable-application-model";
3
+ export interface ICreateBackendOptions {
4
+ /**
5
+ * Package name reported to the backend for telemetry.
6
+ * Defaults to `"@gooddata/sdk-ui-pluggable-host"`.
7
+ */
8
+ packageName?: string;
9
+ /**
10
+ * Optional override for the package version reported to backend.
11
+ *
12
+ * @remarks
13
+ * By default, this is taken from `window.COMMITHASH` (same as AD), falling back to `"dev"`.
14
+ */
15
+ packageVersion?: string;
16
+ auth?: IAuthCredentials;
17
+ }
18
+ export interface IBackendFactory {
19
+ getBackend: () => IAnalyticalBackend;
20
+ getAuthCredentials: () => IAuthCredentials;
21
+ setNotAuthenticatedHandler: (handler: NotAuthenticatedHandler) => void;
22
+ setApiToken: (token: string) => void;
23
+ setJwt: (jwt: string, secondsBeforeTokenExpirationToCallReminder?: number) => void;
24
+ }
25
+ export declare function createBackendFactory(options?: ICreateBackendOptions): IBackendFactory;
26
+ /**
27
+ * Sets the package name reported to the backend for telemetry.
28
+ * Must be called before the first backend request (i.e., before Root renders).
29
+ *
30
+ * @alpha
31
+ */
32
+ export declare function setRuntimePackageName(name: string): void;
33
+ /**
34
+ * Recreates backend lifecycle instance with new auth configuration.
35
+ */
36
+ export declare function reinitializeBackend(auth?: IAuthCredentials): void;
37
+ /**
38
+ * @alpha
39
+ */
40
+ export declare function getBackend(): IAnalyticalBackend;
41
+ export declare function getAuthCredentials(): IAuthCredentials;
42
+ export declare function setNotAuthenticatedHandler(handler: NotAuthenticatedHandler): void;
43
+ export declare function setApiToken(token: string): void;
44
+ export declare function setJwt(jwt: string, secondsBeforeTokenExpirationToCallReminder?: number): void;
@@ -0,0 +1,131 @@
1
+ // (C) 2026 GoodData Corporation
2
+ import { RecommendedCachingConfiguration, withCaching } from "@gooddata/sdk-backend-base";
3
+ import { ContextDeferredAuthProvider, TigerJwtAuthProvider, TigerTokenAuthProvider, tigerFactory, } from "@gooddata/sdk-backend-tiger";
4
+ import { createNotAuthenticatedHandler } from "./tigerNotAuthenticatedHandler.js";
5
+ const decorateBackend = (backend) => {
6
+ return withCaching(backend, RecommendedCachingConfiguration);
7
+ };
8
+ function getDefaultAuthCredentials() {
9
+ const apiToken = typeof TIGER_API_TOKEN === "string" && TIGER_API_TOKEN.length ? TIGER_API_TOKEN : undefined;
10
+ if (apiToken) {
11
+ return { type: "apiToken", token: apiToken };
12
+ }
13
+ return { type: "contextDeferred" };
14
+ }
15
+ export function createBackendFactory(options = {}) {
16
+ let setApiTokenHandler = undefined;
17
+ let setJwtHandler = undefined;
18
+ const jwtIsAboutToExpire = (setJwt) => {
19
+ setJwtHandler = setJwt;
20
+ };
21
+ const packageVersion = options.packageVersion ??
22
+ (typeof window === "undefined" || !window.COMMITHASH ? "dev" : window.COMMITHASH);
23
+ let currentAuth = options.auth ?? getDefaultAuthCredentials();
24
+ let notAuthenticatedErrorHandler;
25
+ const baseBackend = tigerFactory(undefined, {
26
+ packageName: options.packageName ?? "@gooddata/sdk-ui-pluggable-host",
27
+ packageVersion,
28
+ });
29
+ const externalProviderId = currentAuth.type === "contextDeferred" ? currentAuth.externalProviderId : undefined;
30
+ const defaultNotAuthenticatedHandler = createNotAuthenticatedHandler(externalProviderId);
31
+ const backendNotAuthenticatedHandler = (context, error) => {
32
+ const handler = notAuthenticatedErrorHandler;
33
+ if (handler) {
34
+ handler(context, error);
35
+ return;
36
+ }
37
+ defaultNotAuthenticatedHandler(context, error);
38
+ };
39
+ const getBackendImplementation = (auth) => {
40
+ switch (auth.type) {
41
+ case "apiToken": {
42
+ const tigerTokenAuthProvider = new TigerTokenAuthProvider(auth.token, backendNotAuthenticatedHandler);
43
+ setApiTokenHandler = tigerTokenAuthProvider.updateApiToken;
44
+ return baseBackend.withAuthentication(tigerTokenAuthProvider);
45
+ }
46
+ case "jwt": {
47
+ const jwtAuthProvider = new TigerJwtAuthProvider(auth.token, backendNotAuthenticatedHandler, jwtIsAboutToExpire, auth.secondsBeforeTokenExpirationToCallReminder);
48
+ setJwtHandler = jwtAuthProvider.updateJwt;
49
+ return baseBackend.withAuthentication(jwtAuthProvider);
50
+ }
51
+ case "contextDeferred":
52
+ return baseBackend.withAuthentication(new ContextDeferredAuthProvider(backendNotAuthenticatedHandler));
53
+ }
54
+ };
55
+ let backend = decorateBackend(getBackendImplementation(currentAuth));
56
+ const setNotAuthenticatedHandler = (handler) => {
57
+ notAuthenticatedErrorHandler = handler;
58
+ };
59
+ const setApiToken = (token) => {
60
+ currentAuth = { type: "apiToken", token };
61
+ if (setApiTokenHandler) {
62
+ setApiTokenHandler(token);
63
+ return;
64
+ }
65
+ backend = decorateBackend(getBackendImplementation(currentAuth));
66
+ };
67
+ const setJwt = (jwt, secondsBeforeTokenExpirationToCallReminder) => {
68
+ currentAuth = { type: "jwt", token: jwt, secondsBeforeTokenExpirationToCallReminder };
69
+ if (setJwtHandler) {
70
+ setJwtHandler(jwt, secondsBeforeTokenExpirationToCallReminder);
71
+ return;
72
+ }
73
+ backend = decorateBackend(getBackendImplementation(currentAuth));
74
+ };
75
+ /**
76
+ * Returns a snapshot of the current auth credentials.
77
+ *
78
+ * Note: callers receive a point-in-time snapshot. If tokens change after
79
+ * the snapshot is taken, the caller must re-invoke `getAuthCredentials()`
80
+ * or the host must push an updated context via `updateContext`.
81
+ */
82
+ const getAuthCredentials = () => currentAuth;
83
+ return {
84
+ getBackend: () => backend,
85
+ getAuthCredentials,
86
+ setNotAuthenticatedHandler,
87
+ setApiToken,
88
+ setJwt,
89
+ };
90
+ }
91
+ let runtimePackageName = "@gooddata/sdk-ui-pluggable-host";
92
+ let backendFactory;
93
+ function getOrCreateFactory() {
94
+ if (!backendFactory) {
95
+ backendFactory = createBackendFactory({ packageName: runtimePackageName });
96
+ }
97
+ return backendFactory;
98
+ }
99
+ /**
100
+ * Sets the package name reported to the backend for telemetry.
101
+ * Must be called before the first backend request (i.e., before Root renders).
102
+ *
103
+ * @alpha
104
+ */
105
+ export function setRuntimePackageName(name) {
106
+ runtimePackageName = name;
107
+ }
108
+ /**
109
+ * Recreates backend lifecycle instance with new auth configuration.
110
+ */
111
+ export function reinitializeBackend(auth) {
112
+ backendFactory = createBackendFactory({ packageName: runtimePackageName, auth });
113
+ }
114
+ /**
115
+ * @alpha
116
+ */
117
+ export function getBackend() {
118
+ return getOrCreateFactory().getBackend();
119
+ }
120
+ export function getAuthCredentials() {
121
+ return getOrCreateFactory().getAuthCredentials();
122
+ }
123
+ export function setNotAuthenticatedHandler(handler) {
124
+ getOrCreateFactory().setNotAuthenticatedHandler(handler);
125
+ }
126
+ export function setApiToken(token) {
127
+ getOrCreateFactory().setApiToken(token);
128
+ }
129
+ export function setJwt(jwt, secondsBeforeTokenExpirationToCallReminder) {
130
+ getOrCreateFactory().setJwt(jwt, secondsBeforeTokenExpirationToCallReminder);
131
+ }
@@ -0,0 +1,15 @@
1
+ import { type IAnalyticalBackend } from "@gooddata/sdk-backend-spi";
2
+ import { type IEntitlementDescriptor, type ITheme, type IWhiteLabeling } from "@gooddata/sdk-model";
3
+ import { type IOrganization, type IOrganizationPermissions, type IPlatformContext, PantherTier } from "@gooddata/sdk-pluggable-application-model";
4
+ export interface IBootstrapResult {
5
+ user: IPlatformContext["user"];
6
+ userSettings: IPlatformContext["userSettings"];
7
+ organization: IOrganization | undefined;
8
+ organizationPermissions: IOrganizationPermissions;
9
+ entitlements: IEntitlementDescriptor[];
10
+ whiteLabeling: IWhiteLabeling;
11
+ pantherTier: PantherTier;
12
+ theme: ITheme;
13
+ }
14
+ export declare const PLATFORM_CONTEXT_VERSION: IPlatformContext["version"];
15
+ export declare function bootstrapApplication(backend: IAnalyticalBackend): Promise<IBootstrapResult>;