@gooddata/sdk-ui-pluggable-host 11.41.0-alpha.5 → 11.42.0-alpha.0

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 (36) hide show
  1. package/esm/loader/appSecurityValidation.d.ts +57 -0
  2. package/esm/loader/appSecurityValidation.d.ts.map +1 -0
  3. package/esm/loader/appSecurityValidation.js +102 -0
  4. package/esm/translations/de-DE.json +4 -1
  5. package/esm/translations/en-AU.json +4 -1
  6. package/esm/translations/en-GB.json +4 -1
  7. package/esm/translations/en-US.json +52 -0
  8. package/esm/translations/es-419.json +4 -1
  9. package/esm/translations/es-ES.json +4 -1
  10. package/esm/translations/fi-FI.json +4 -1
  11. package/esm/translations/fr-CA.json +4 -1
  12. package/esm/translations/fr-FR.json +4 -1
  13. package/esm/translations/id-ID.json +4 -1
  14. package/esm/translations/it-IT.json +4 -1
  15. package/esm/translations/ja-JP.json +4 -1
  16. package/esm/translations/ko-KR.json +4 -1
  17. package/esm/translations/nl-NL.json +4 -1
  18. package/esm/translations/pl-PL.json +4 -1
  19. package/esm/translations/pt-BR.json +4 -1
  20. package/esm/translations/pt-PT.json +4 -1
  21. package/esm/translations/ru-RU.json +4 -1
  22. package/esm/translations/sl-SI.json +4 -1
  23. package/esm/translations/th-TH.json +4 -1
  24. package/esm/translations/tr-TR.json +4 -1
  25. package/esm/translations/uk-UA.json +4 -1
  26. package/esm/translations/vi-VN.json +4 -1
  27. package/esm/translations/zh-HK.json +4 -1
  28. package/esm/translations/zh-Hans.json +4 -1
  29. package/esm/translations/zh-Hant.json +4 -1
  30. package/esm/ui/HostChrome.d.ts +2 -2
  31. package/esm/ui/HostChrome.d.ts.map +1 -1
  32. package/esm/ui/HostChrome.js +8 -11
  33. package/esm/ui/PluggableApplicationRenderer.d.ts.map +1 -1
  34. package/esm/ui/PluggableApplicationRenderer.js +68 -19
  35. package/esm/ui/PluggableApplicationRenderer.scss +31 -1
  36. package/package.json +17 -17
@@ -0,0 +1,57 @@
1
+ import { type PluggableApplicationRegistryItem } from "@gooddata/sdk-model";
2
+ import { type IPluggableApp, type IPlatformContext } from "@gooddata/sdk-pluggable-application-model";
3
+ /**
4
+ * Maximum age of a secured-remote build, in milliseconds.
5
+ *
6
+ * @remarks
7
+ * Demo apps shipped from the playground are explicitly time-boxed; the host
8
+ * refuses to mount builds older than this window. App owners must rebuild
9
+ * and redeploy at least this often, even if nothing else changed.
10
+ */
11
+ export declare const BUILD_FRESHNESS_WINDOW_MS: number;
12
+ /**
13
+ * Why a secured-remote app was rejected by {@link validateAppSecurity}.
14
+ */
15
+ export type AppSecurityFailure = {
16
+ kind: "organization-not-allowed";
17
+ } | {
18
+ kind: "build-expired";
19
+ ageMs: number;
20
+ } | {
21
+ kind: "metadata-missing";
22
+ };
23
+ /**
24
+ * Returns true when a remote URL points at a hostname that requires
25
+ * build-time-baked security metadata on the loaded app.
26
+ */
27
+ export declare function isSecuredRemoteUrl(url: string | undefined): boolean;
28
+ /**
29
+ * Validates the build-time-baked security metadata of a loaded pluggable app
30
+ * against the current platform context.
31
+ *
32
+ * @remarks
33
+ * Returns `undefined` when the app is not subject to these checks (i.e. it
34
+ * was not loaded from a secured remote hostname). Otherwise returns a
35
+ * structured failure describing the reason; the caller is responsible for
36
+ * surfacing it via the renderer's error UI.
37
+ *
38
+ * The check fails closed: a secured-remote app missing either
39
+ * `allowedOrganizations` or `buildTimestamp` — or carrying a non-finite or
40
+ * future-dated `buildTimestamp` (which would otherwise bypass the
41
+ * freshness window via a negative age) — is rejected with
42
+ * `metadata-missing`. An empty allowlist is treated like any other
43
+ * allowlist — it simply matches no organization.
44
+ */
45
+ export declare function validateAppSecurity(app: PluggableApplicationRegistryItem, loadedApp: IPluggableApp, ctx: IPlatformContext, now?: number): AppSecurityFailure | undefined;
46
+ /**
47
+ * Returns the timestamp until which a secured-remote ("demo bucket") app stays
48
+ * within its build freshness window, or `undefined` when the app is not a
49
+ * secured-remote app carrying a usable build timestamp.
50
+ *
51
+ * @remarks
52
+ * Intended to be called only for apps that have already passed
53
+ * {@link validateAppSecurity}. The host surfaces the returned date in a small
54
+ * "valid till" badge so demo users can see when the build will expire.
55
+ */
56
+ export declare function getSecuredRemoteAppValidUntil(app: PluggableApplicationRegistryItem, loadedApp: IPluggableApp): number | undefined;
57
+ //# sourceMappingURL=appSecurityValidation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"appSecurityValidation.d.ts","sourceRoot":"","sources":["../../src/loader/appSecurityValidation.ts"],"names":[],"mappings":"AAEA,OAAO,EACH,KAAK,gCAAgC,EAExC,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,KAAK,aAAa,EAAE,KAAK,gBAAgB,EAAE,MAAM,2CAA2C,CAAC;AAgBtG;;;;;;;GAOG;AACH,eAAO,MAAM,yBAAyB,QAA2B,CAAC;AAElE;;GAEG;AACH,MAAM,MAAM,kBAAkB,GACxB;IAAE,IAAI,EAAE,0BAA0B,CAAA;CAAE,GACpC;IAAE,IAAI,EAAE,eAAe,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACxC;IAAE,IAAI,EAAE,kBAAkB,CAAA;CAAE,CAAC;AAEnC;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAUnE;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,mBAAmB,CAC/B,GAAG,EAAE,gCAAgC,EACrC,SAAS,EAAE,aAAa,EACxB,GAAG,EAAE,gBAAgB,EACrB,GAAG,GAAE,MAAmB,GACzB,kBAAkB,GAAG,SAAS,CA4BhC;AAED;;;;;;;;;GASG;AACH,wBAAgB,6BAA6B,CACzC,GAAG,EAAE,gCAAgC,EACrC,SAAS,EAAE,aAAa,GACzB,MAAM,GAAG,SAAS,CAWpB"}
@@ -0,0 +1,102 @@
1
+ // (C) 2026 GoodData Corporation
2
+ import { isRemotePluggableApplicationRegistryItem, } from "@gooddata/sdk-model";
3
+ /**
4
+ * Hostnames that trigger build-time-baked security checks on loaded apps.
5
+ *
6
+ * @remarks
7
+ * These hostnames host artifacts written by external pipelines (e.g. the
8
+ * gdc-ui-pluggable-applications playground). The build artifacts are expected
9
+ * to embed `allowedOrganizations` and `buildTimestamp` on the exported
10
+ * pluggable app; the host enforces both at load time. Apps loaded from any
11
+ * other origin (same-hostname, local bundle) are not subject to these checks
12
+ * — their integrity is already constrained by the hostname allowlist in
13
+ * `remoteUrlSecurity.ts`.
14
+ */
15
+ const SECURED_REMOTE_HOSTNAMES = new Set(["demo-dashboard-plugins.gooddata.com"]);
16
+ /**
17
+ * Maximum age of a secured-remote build, in milliseconds.
18
+ *
19
+ * @remarks
20
+ * Demo apps shipped from the playground are explicitly time-boxed; the host
21
+ * refuses to mount builds older than this window. App owners must rebuild
22
+ * and redeploy at least this often, even if nothing else changed.
23
+ */
24
+ export const BUILD_FRESHNESS_WINDOW_MS = 30 * 24 * 60 * 60 * 1000;
25
+ /**
26
+ * Returns true when a remote URL points at a hostname that requires
27
+ * build-time-baked security metadata on the loaded app.
28
+ */
29
+ export function isSecuredRemoteUrl(url) {
30
+ if (!url) {
31
+ return false;
32
+ }
33
+ try {
34
+ const { hostname } = new URL(url, window.location.origin);
35
+ return SECURED_REMOTE_HOSTNAMES.has(hostname);
36
+ }
37
+ catch {
38
+ return false;
39
+ }
40
+ }
41
+ /**
42
+ * Validates the build-time-baked security metadata of a loaded pluggable app
43
+ * against the current platform context.
44
+ *
45
+ * @remarks
46
+ * Returns `undefined` when the app is not subject to these checks (i.e. it
47
+ * was not loaded from a secured remote hostname). Otherwise returns a
48
+ * structured failure describing the reason; the caller is responsible for
49
+ * surfacing it via the renderer's error UI.
50
+ *
51
+ * The check fails closed: a secured-remote app missing either
52
+ * `allowedOrganizations` or `buildTimestamp` — or carrying a non-finite or
53
+ * future-dated `buildTimestamp` (which would otherwise bypass the
54
+ * freshness window via a negative age) — is rejected with
55
+ * `metadata-missing`. An empty allowlist is treated like any other
56
+ * allowlist — it simply matches no organization.
57
+ */
58
+ export function validateAppSecurity(app, loadedApp, ctx, now = Date.now()) {
59
+ if (!isRemotePluggableApplicationRegistryItem(app)) {
60
+ return undefined;
61
+ }
62
+ if (!isSecuredRemoteUrl(app.remote.url)) {
63
+ return undefined;
64
+ }
65
+ if (!Array.isArray(loadedApp.allowedOrganizations) ||
66
+ typeof loadedApp.buildTimestamp !== "number" ||
67
+ !Number.isFinite(loadedApp.buildTimestamp) ||
68
+ loadedApp.buildTimestamp > now) {
69
+ return { kind: "metadata-missing" };
70
+ }
71
+ const orgId = ctx.organization?.id;
72
+ if (!orgId || !loadedApp.allowedOrganizations.includes(orgId)) {
73
+ return { kind: "organization-not-allowed" };
74
+ }
75
+ const ageMs = now - loadedApp.buildTimestamp;
76
+ if (ageMs > BUILD_FRESHNESS_WINDOW_MS) {
77
+ return { kind: "build-expired", ageMs };
78
+ }
79
+ return undefined;
80
+ }
81
+ /**
82
+ * Returns the timestamp until which a secured-remote ("demo bucket") app stays
83
+ * within its build freshness window, or `undefined` when the app is not a
84
+ * secured-remote app carrying a usable build timestamp.
85
+ *
86
+ * @remarks
87
+ * Intended to be called only for apps that have already passed
88
+ * {@link validateAppSecurity}. The host surfaces the returned date in a small
89
+ * "valid till" badge so demo users can see when the build will expire.
90
+ */
91
+ export function getSecuredRemoteAppValidUntil(app, loadedApp) {
92
+ if (!isRemotePluggableApplicationRegistryItem(app)) {
93
+ return undefined;
94
+ }
95
+ if (!isSecuredRemoteUrl(app.remote.url)) {
96
+ return undefined;
97
+ }
98
+ if (typeof loadedApp.buildTimestamp !== "number" || !Number.isFinite(loadedApp.buildTimestamp)) {
99
+ return undefined;
100
+ }
101
+ return loadedApp.buildTimestamp + BUILD_FRESHNESS_WINDOW_MS;
102
+ }
@@ -30,5 +30,8 @@
30
30
  "messages.genAi.visualisation.link.copied": "Der Visualisierungslink wurde in Ihre Zwischenablage kopiert.",
31
31
  "messages.showMore": "Mehr anzeigen",
32
32
  "messages.showLess": "Weniger anzeigen",
33
- "gen-ai.ask-assistant.search": "Erstellen Sie eine neue Visualisierung basierend auf: {question}"
33
+ "gen-ai.ask-assistant.search": "Erstellen Sie eine neue Visualisierung basierend auf: {question}",
34
+ "gs.host.error.appNotAllowedForOrganization": "This application is not available for your organization.",
35
+ "gs.host.error.appBuildExpired": "This application is out of date and must be rebuilt by its author before it can run again.",
36
+ "gs.host.error.appSecurityMetadataMissing": "This application is missing required security metadata and cannot be loaded."
34
37
  }
@@ -30,5 +30,8 @@
30
30
  "messages.genAi.visualisation.link.copied": "The visualisation link has been copied to your clipboard.",
31
31
  "messages.showMore": "Show more",
32
32
  "messages.showLess": "Show less",
33
- "gen-ai.ask-assistant.search": "Create new visualisation based on: {question}"
33
+ "gen-ai.ask-assistant.search": "Create new visualisation based on: {question}",
34
+ "gs.host.error.appNotAllowedForOrganization": "This application is not available for your organization.",
35
+ "gs.host.error.appBuildExpired": "This application is out of date and must be rebuilt by its author before it can run again.",
36
+ "gs.host.error.appSecurityMetadataMissing": "This application is missing required security metadata and cannot be loaded."
34
37
  }
@@ -30,5 +30,8 @@
30
30
  "messages.genAi.visualisation.link.copied": "The visualisation link has been copied to your clipboard.",
31
31
  "messages.showMore": "Show more",
32
32
  "messages.showLess": "Show less",
33
- "gen-ai.ask-assistant.search": "Build new visualisation based on: {question}"
33
+ "gen-ai.ask-assistant.search": "Build new visualisation based on: {question}",
34
+ "gs.host.error.appNotAllowedForOrganization": "This application is not available for your organization.",
35
+ "gs.host.error.appBuildExpired": "This application is out of date and must be rebuilt by its author before it can run again.",
36
+ "gs.host.error.appSecurityMetadataMissing": "This application is missing required security metadata and cannot be loaded."
34
37
  }
@@ -75,6 +75,22 @@
75
75
  "text": "Something went wrong",
76
76
  "crowdinContext": "Heading displayed when an unexpected error occurs during application redirect resolution."
77
77
  },
78
+ "gs.host.error.appNotAllowedForOrganization": {
79
+ "text": "This application is not available for your organization.",
80
+ "crowdinContext": "Error shown when the host refuses to mount a pluggable application because the current organization is not in the application's build-time-baked allowlist of permitted organizations."
81
+ },
82
+ "gs.host.error.appBuildExpired": {
83
+ "text": "This application is out of date and must be rebuilt by its author before it can run again.",
84
+ "crowdinContext": "Error shown when the host refuses to mount a pluggable application because its build is older than the freshness window (currently 30 days)."
85
+ },
86
+ "gs.host.error.appSecurityMetadataMissing": {
87
+ "text": "This application is missing required security metadata and cannot be loaded.",
88
+ "crowdinContext": "Error shown when the host refuses to mount a pluggable application loaded from a secured remote origin that does not declare its allowed organizations and build timestamp."
89
+ },
90
+ "gs.host.demoApp.validUntil": {
91
+ "text": "Demo application valid till {date}",
92
+ "crowdinContext": "Text in a small floating badge shown at the bottom-right of the page for a validated demo application loaded from the demo bucket. {date} is the localized date until which the demo build remains valid."
93
+ },
78
94
  "gs.host.notification.newDeployment.message": {
79
95
  "text": "A new version of GoodData is available.",
80
96
  "crowdinContext": "Toast message shown when the host detects that a newer build of the application has been deployed while the user's tab was open. Followed by a reload link."
@@ -99,6 +115,42 @@
99
115
  "text": "Managing users and user groups",
100
116
  "crowdinContext": "Link to user management documentation in help menu"
101
117
  },
118
+ "gs.header.helpMenu.connecting": {
119
+ "text": "Connecting data",
120
+ "crowdinContext": "LDM Modeler help menu link to data connection guide"
121
+ },
122
+ "gs.header.helpMenu.starting": {
123
+ "text": "Starting with LDM Modeler",
124
+ "crowdinContext": "LDM Modeler help menu link to getting started guide"
125
+ },
126
+ "gs.header.helpMenu.modeling": {
127
+ "text": "Modeling data in depth",
128
+ "crowdinContext": "LDM Modeler help menu link to data modeling guide"
129
+ },
130
+ "gs.header.helpMenu.understanding": {
131
+ "text": "Understanding the Logical Data Model",
132
+ "crowdinContext": "LDM Modeler help menu link to LDM concept guide"
133
+ },
134
+ "gs.header.helpMenu.learning": {
135
+ "text": "Learning the principles of data modeling",
136
+ "crowdinContext": "LDM Modeler help menu link to data modeling principles"
137
+ },
138
+ "gs.header.helpMenu.firstSteps": {
139
+ "text": "First steps in Analytical Designer",
140
+ "crowdinContext": "Analytical Designer help menu link to getting started guide"
141
+ },
142
+ "gs.header.helpMenu.visualizing": {
143
+ "text": "Visualizing your data",
144
+ "crowdinContext": "Analytical Designer help menu link to data visualization guide"
145
+ },
146
+ "gs.header.helpMenu.sorting": {
147
+ "text": "Sorting and filtering your results",
148
+ "crowdinContext": "Analytical Designer help menu link to sorting and filtering guide"
149
+ },
150
+ "gs.header.helpMenu.embedding": {
151
+ "text": "Embedding Analytical Designer into your app",
152
+ "crowdinContext": "Analytical Designer help menu link to embedding integration guide"
153
+ },
102
154
  "messages.genAi.visualisation.saved.success": {
103
155
  "text": "Great! We saved your visualization.",
104
156
  "crowdinContext": "Success message when generative AI visualization is saved"
@@ -30,5 +30,8 @@
30
30
  "messages.genAi.visualisation.link.copied": "El enlace de visualización se ha copiado en el portapapeles.",
31
31
  "messages.showMore": "Mostrar más",
32
32
  "messages.showLess": "Mostrar menos",
33
- "gen-ai.ask-assistant.search": "Crear una nueva visualización basada en: {question}"
33
+ "gen-ai.ask-assistant.search": "Crear una nueva visualización basada en: {question}",
34
+ "gs.host.error.appNotAllowedForOrganization": "This application is not available for your organization.",
35
+ "gs.host.error.appBuildExpired": "This application is out of date and must be rebuilt by its author before it can run again.",
36
+ "gs.host.error.appSecurityMetadataMissing": "This application is missing required security metadata and cannot be loaded."
34
37
  }
@@ -30,5 +30,8 @@
30
30
  "messages.genAi.visualisation.link.copied": "El enlace de visualización se ha copiado en el portapapeles.",
31
31
  "messages.showMore": "Mostrar más",
32
32
  "messages.showLess": "Mostrar menos",
33
- "gen-ai.ask-assistant.search": "Construir una nueva visualización basada en{question}: "
33
+ "gen-ai.ask-assistant.search": "Construir una nueva visualización basada en{question}: ",
34
+ "gs.host.error.appNotAllowedForOrganization": "This application is not available for your organization.",
35
+ "gs.host.error.appBuildExpired": "This application is out of date and must be rebuilt by its author before it can run again.",
36
+ "gs.host.error.appSecurityMetadataMissing": "This application is missing required security metadata and cannot be loaded."
34
37
  }
@@ -30,5 +30,8 @@
30
30
  "messages.genAi.visualisation.link.copied": "Visualisointilinkki on kopioitu leikepöydällesi.",
31
31
  "messages.showMore": "Näytä lisää",
32
32
  "messages.showLess": "Näytä vähemmän",
33
- "gen-ai.ask-assistant.search": "Luo uusi visualisointi, joka perustuu{question}: "
33
+ "gen-ai.ask-assistant.search": "Luo uusi visualisointi, joka perustuu{question}: ",
34
+ "gs.host.error.appNotAllowedForOrganization": "This application is not available for your organization.",
35
+ "gs.host.error.appBuildExpired": "This application is out of date and must be rebuilt by its author before it can run again.",
36
+ "gs.host.error.appSecurityMetadataMissing": "This application is missing required security metadata and cannot be loaded."
34
37
  }
@@ -30,5 +30,8 @@
30
30
  "messages.genAi.visualisation.link.copied": "Le lien de visualisation a été copié dans votre presse-papiers.",
31
31
  "messages.showMore": "Montrer plus",
32
32
  "messages.showLess": "Montrer moins",
33
- "gen-ai.ask-assistant.search": "Construire une nouvelle visualisation basée sur: {question}"
33
+ "gen-ai.ask-assistant.search": "Construire une nouvelle visualisation basée sur: {question}",
34
+ "gs.host.error.appNotAllowedForOrganization": "This application is not available for your organization.",
35
+ "gs.host.error.appBuildExpired": "This application is out of date and must be rebuilt by its author before it can run again.",
36
+ "gs.host.error.appSecurityMetadataMissing": "This application is missing required security metadata and cannot be loaded."
34
37
  }
@@ -30,5 +30,8 @@
30
30
  "messages.genAi.visualisation.link.copied": "Le lien de la visualisation a été copié dans votre presse-papiers.",
31
31
  "messages.showMore": "Montrer plus",
32
32
  "messages.showLess": "Montrer moins",
33
- "gen-ai.ask-assistant.search": "Construire une nouvelle visualisation bas\u0000e9e sur\u0000a0: {question}"
33
+ "gen-ai.ask-assistant.search": "Construire une nouvelle visualisation bas\u0000e9e sur\u0000a0: {question}",
34
+ "gs.host.error.appNotAllowedForOrganization": "This application is not available for your organization.",
35
+ "gs.host.error.appBuildExpired": "This application is out of date and must be rebuilt by its author before it can run again.",
36
+ "gs.host.error.appSecurityMetadataMissing": "This application is missing required security metadata and cannot be loaded."
34
37
  }
@@ -30,5 +30,8 @@
30
30
  "messages.genAi.visualisation.link.copied": "Tautan visualisasi telah disalin ke clipboard Anda.",
31
31
  "messages.showMore": "Tampilkan lebih banyak",
32
32
  "messages.showLess": "Tampilkan lebih sedikit",
33
- "gen-ai.ask-assistant.search": "Bangun visualisasi baru berdasarkan: {question}"
33
+ "gen-ai.ask-assistant.search": "Bangun visualisasi baru berdasarkan: {question}",
34
+ "gs.host.error.appNotAllowedForOrganization": "This application is not available for your organization.",
35
+ "gs.host.error.appBuildExpired": "This application is out of date and must be rebuilt by its author before it can run again.",
36
+ "gs.host.error.appSecurityMetadataMissing": "This application is missing required security metadata and cannot be loaded."
34
37
  }
@@ -30,5 +30,8 @@
30
30
  "messages.genAi.visualisation.link.copied": "Il link di visualizzazione è stato copiato negli appunti.",
31
31
  "messages.showMore": "Mostra di più",
32
32
  "messages.showLess": "Mostra di meno",
33
- "gen-ai.ask-assistant.search": "Costruire nuove visualizzazioni basate su: {question}"
33
+ "gen-ai.ask-assistant.search": "Costruire nuove visualizzazioni basate su: {question}",
34
+ "gs.host.error.appNotAllowedForOrganization": "This application is not available for your organization.",
35
+ "gs.host.error.appBuildExpired": "This application is out of date and must be rebuilt by its author before it can run again.",
36
+ "gs.host.error.appSecurityMetadataMissing": "This application is missing required security metadata and cannot be loaded."
34
37
  }
@@ -30,5 +30,8 @@
30
30
  "messages.genAi.visualisation.link.copied": "視覚化リンクがクリップボードにコピーされました。",
31
31
  "messages.showMore": "もっと表示",
32
32
  "messages.showLess": "表示を減らす",
33
- "gen-ai.ask-assistant.search": "新しいビジュアライゼーションを構築する:{question}"
33
+ "gen-ai.ask-assistant.search": "新しいビジュアライゼーションを構築する:{question}",
34
+ "gs.host.error.appNotAllowedForOrganization": "This application is not available for your organization.",
35
+ "gs.host.error.appBuildExpired": "This application is out of date and must be rebuilt by its author before it can run again.",
36
+ "gs.host.error.appSecurityMetadataMissing": "This application is missing required security metadata and cannot be loaded."
34
37
  }
@@ -30,5 +30,8 @@
30
30
  "messages.genAi.visualisation.link.copied": "시각화 링크가 클립보드에 복사되었습니다.",
31
31
  "messages.showMore": "더 보기",
32
32
  "messages.showLess": "간단히 보기",
33
- "gen-ai.ask-assistant.search": "새 시각화 생성 기준: {question}"
33
+ "gen-ai.ask-assistant.search": "새 시각화 생성 기준: {question}",
34
+ "gs.host.error.appNotAllowedForOrganization": "This application is not available for your organization.",
35
+ "gs.host.error.appBuildExpired": "This application is out of date and must be rebuilt by its author before it can run again.",
36
+ "gs.host.error.appSecurityMetadataMissing": "This application is missing required security metadata and cannot be loaded."
34
37
  }
@@ -30,5 +30,8 @@
30
30
  "messages.genAi.visualisation.link.copied": "De visualisatielink is gekopieerd naar je klembord.",
31
31
  "messages.showMore": "Meer tonen",
32
32
  "messages.showLess": "Minder tonen",
33
- "gen-ai.ask-assistant.search": "Bouw nieuwe visualisatie gebaseerd op: {question}"
33
+ "gen-ai.ask-assistant.search": "Bouw nieuwe visualisatie gebaseerd op: {question}",
34
+ "gs.host.error.appNotAllowedForOrganization": "This application is not available for your organization.",
35
+ "gs.host.error.appBuildExpired": "This application is out of date and must be rebuilt by its author before it can run again.",
36
+ "gs.host.error.appSecurityMetadataMissing": "This application is missing required security metadata and cannot be loaded."
34
37
  }
@@ -30,5 +30,8 @@
30
30
  "messages.genAi.visualisation.link.copied": "Link do Wizualizacji został skopiowany do schowka.",
31
31
  "messages.showMore": "Pokaż więcej",
32
32
  "messages.showLess": "Pokaż mniej",
33
- "gen-ai.ask-assistant.search": "Utwórz nową wizualizację na podstawie: {question}"
33
+ "gen-ai.ask-assistant.search": "Utwórz nową wizualizację na podstawie: {question}",
34
+ "gs.host.error.appNotAllowedForOrganization": "This application is not available for your organization.",
35
+ "gs.host.error.appBuildExpired": "This application is out of date and must be rebuilt by its author before it can run again.",
36
+ "gs.host.error.appSecurityMetadataMissing": "This application is missing required security metadata and cannot be loaded."
34
37
  }
@@ -30,5 +30,8 @@
30
30
  "messages.genAi.visualisation.link.copied": "O link de visualização foi copiado para a área de transferência.",
31
31
  "messages.showMore": "Exibir mais",
32
32
  "messages.showLess": "Exibir menos",
33
- "gen-ai.ask-assistant.search": "Crie uma nova visualização com base em: {question}"
33
+ "gen-ai.ask-assistant.search": "Crie uma nova visualização com base em: {question}",
34
+ "gs.host.error.appNotAllowedForOrganization": "This application is not available for your organization.",
35
+ "gs.host.error.appBuildExpired": "This application is out of date and must be rebuilt by its author before it can run again.",
36
+ "gs.host.error.appSecurityMetadataMissing": "This application is missing required security metadata and cannot be loaded."
34
37
  }
@@ -30,5 +30,8 @@
30
30
  "messages.genAi.visualisation.link.copied": "O link de visualização foi copiado para a área de transferência.",
31
31
  "messages.showMore": "Mostrar mais",
32
32
  "messages.showLess": "Mostrar menos",
33
- "gen-ai.ask-assistant.search": "Crie uma nova visualização com base em: {question}"
33
+ "gen-ai.ask-assistant.search": "Crie uma nova visualização com base em: {question}",
34
+ "gs.host.error.appNotAllowedForOrganization": "This application is not available for your organization.",
35
+ "gs.host.error.appBuildExpired": "This application is out of date and must be rebuilt by its author before it can run again.",
36
+ "gs.host.error.appSecurityMetadataMissing": "This application is missing required security metadata and cannot be loaded."
34
37
  }
@@ -30,5 +30,8 @@
30
30
  "messages.genAi.visualisation.link.copied": "Ссылка на визуализацию скопирована в буфер обмена.",
31
31
  "messages.showMore": "Раскрыть",
32
32
  "messages.showLess": "Скрыть",
33
- "gen-ai.ask-assistant.search": "Создайте новую визуализацию на основе: {question}"
33
+ "gen-ai.ask-assistant.search": "Создайте новую визуализацию на основе: {question}",
34
+ "gs.host.error.appNotAllowedForOrganization": "This application is not available for your organization.",
35
+ "gs.host.error.appBuildExpired": "This application is out of date and must be rebuilt by its author before it can run again.",
36
+ "gs.host.error.appSecurityMetadataMissing": "This application is missing required security metadata and cannot be loaded."
34
37
  }
@@ -30,5 +30,8 @@
30
30
  "messages.genAi.visualisation.link.copied": "Povezava vizualizacije je bila kopirana v vašo odlagališče.",
31
31
  "messages.showMore": "Prikaži več",
32
32
  "messages.showLess": "Prikaži manj",
33
- "gen-ai.ask-assistant.search": "Ustvarite novo vizualizacijo na osnovi: {question}"
33
+ "gen-ai.ask-assistant.search": "Ustvarite novo vizualizacijo na osnovi: {question}",
34
+ "gs.host.error.appNotAllowedForOrganization": "This application is not available for your organization.",
35
+ "gs.host.error.appBuildExpired": "This application is out of date and must be rebuilt by its author before it can run again.",
36
+ "gs.host.error.appSecurityMetadataMissing": "This application is missing required security metadata and cannot be loaded."
34
37
  }
@@ -30,5 +30,8 @@
30
30
  "messages.genAi.visualisation.link.copied": "คัดลอกลิงก์การแสดงภาพข้อมูลไปยังคลิปบอร์ดของคุณแล้ว",
31
31
  "messages.showMore": "แสดงเพิ่มเติม",
32
32
  "messages.showLess": "แสดงน้อยลง",
33
- "gen-ai.ask-assistant.search": "สร้างการแสดงภาพข้อมูลใหม่โดยอิงตาม: {question}"
33
+ "gen-ai.ask-assistant.search": "สร้างการแสดงภาพข้อมูลใหม่โดยอิงตาม: {question}",
34
+ "gs.host.error.appNotAllowedForOrganization": "This application is not available for your organization.",
35
+ "gs.host.error.appBuildExpired": "This application is out of date and must be rebuilt by its author before it can run again.",
36
+ "gs.host.error.appSecurityMetadataMissing": "This application is missing required security metadata and cannot be loaded."
34
37
  }
@@ -30,5 +30,8 @@
30
30
  "messages.genAi.visualisation.link.copied": "Görselleştirme bağlantısı panonuza kopyalandı.",
31
31
  "messages.showMore": "Daha fazla göster",
32
32
  "messages.showLess": "Daha az göster",
33
- "gen-ai.ask-assistant.search": "Yeni görselleştirme oluştur: {question}"
33
+ "gen-ai.ask-assistant.search": "Yeni görselleştirme oluştur: {question}",
34
+ "gs.host.error.appNotAllowedForOrganization": "This application is not available for your organization.",
35
+ "gs.host.error.appBuildExpired": "This application is out of date and must be rebuilt by its author before it can run again.",
36
+ "gs.host.error.appSecurityMetadataMissing": "This application is missing required security metadata and cannot be loaded."
34
37
  }
@@ -30,5 +30,8 @@
30
30
  "messages.genAi.visualisation.link.copied": "Посилання на візуалізацію скопійовано до буфера обміну.",
31
31
  "messages.showMore": "Показати більше",
32
32
  "messages.showLess": "Показати менше",
33
- "gen-ai.ask-assistant.search": "Створити нову візуалізацію на основі: {question}"
33
+ "gen-ai.ask-assistant.search": "Створити нову візуалізацію на основі: {question}",
34
+ "gs.host.error.appNotAllowedForOrganization": "This application is not available for your organization.",
35
+ "gs.host.error.appBuildExpired": "This application is out of date and must be rebuilt by its author before it can run again.",
36
+ "gs.host.error.appSecurityMetadataMissing": "This application is missing required security metadata and cannot be loaded."
34
37
  }
@@ -30,5 +30,8 @@
30
30
  "messages.genAi.visualisation.link.copied": "Liên kết trực quan hóa đã được sao chép vào bảng nhớ tạm của bạn.",
31
31
  "messages.showMore": "Hiện thêm",
32
32
  "messages.showLess": "Hiện ít",
33
- "gen-ai.ask-assistant.search": "Tạo trực quan hóa mới dựa trên: {question}"
33
+ "gen-ai.ask-assistant.search": "Tạo trực quan hóa mới dựa trên: {question}",
34
+ "gs.host.error.appNotAllowedForOrganization": "This application is not available for your organization.",
35
+ "gs.host.error.appBuildExpired": "This application is out of date and must be rebuilt by its author before it can run again.",
36
+ "gs.host.error.appSecurityMetadataMissing": "This application is missing required security metadata and cannot be loaded."
34
37
  }
@@ -30,5 +30,8 @@
30
30
  "messages.genAi.visualisation.link.copied": "可視化連結已複製到剪貼板。",
31
31
  "messages.showMore": "顯示更多",
32
32
  "messages.showLess": "顯示簡要信息",
33
- "gen-ai.ask-assistant.search": "根據以下條件構建新嘅可視化: {question}"
33
+ "gen-ai.ask-assistant.search": "根據以下條件構建新嘅可視化: {question}",
34
+ "gs.host.error.appNotAllowedForOrganization": "This application is not available for your organization.",
35
+ "gs.host.error.appBuildExpired": "This application is out of date and must be rebuilt by its author before it can run again.",
36
+ "gs.host.error.appSecurityMetadataMissing": "This application is missing required security metadata and cannot be loaded."
34
37
  }
@@ -30,5 +30,8 @@
30
30
  "messages.genAi.visualisation.link.copied": "可视化链接已复制到您的剪贴板。",
31
31
  "messages.showMore": "显示更多",
32
32
  "messages.showLess": "显示更少",
33
- "gen-ai.ask-assistant.search": "根据以下内容构建新的可视化:{question}"
33
+ "gen-ai.ask-assistant.search": "根据以下内容构建新的可视化:{question}",
34
+ "gs.host.error.appNotAllowedForOrganization": "This application is not available for your organization.",
35
+ "gs.host.error.appBuildExpired": "This application is out of date and must be rebuilt by its author before it can run again.",
36
+ "gs.host.error.appSecurityMetadataMissing": "This application is missing required security metadata and cannot be loaded."
34
37
  }
@@ -30,5 +30,8 @@
30
30
  "messages.genAi.visualisation.link.copied": "視覺化連結已複製到您的剪貼簿。",
31
31
  "messages.showMore": "展示更多",
32
32
  "messages.showLess": "顯示較少",
33
- "gen-ai.ask-assistant.search": "根據以下內容建立新的視覺化:{question}"
33
+ "gen-ai.ask-assistant.search": "根據以下內容建立新的視覺化:{question}",
34
+ "gs.host.error.appNotAllowedForOrganization": "This application is not available for your organization.",
35
+ "gs.host.error.appBuildExpired": "This application is out of date and must be rebuilt by its author before it can run again.",
36
+ "gs.host.error.appSecurityMetadataMissing": "This application is missing required security metadata and cannot be loaded."
34
37
  }
@@ -15,8 +15,8 @@ export interface IHostChromeProps {
15
15
  headerOptions?: IAppHeaderOptions;
16
16
  notification?: IHostUiNotification | null;
17
17
  /**
18
- * Page-title segment set by the active pluggable application via its `onDocumentTitleChange`
19
- * callback. When omitted, the active application's manifest title is used instead.
18
+ * Page-title segment set by the active pluggable application via a document-title-changed
19
+ * event. When omitted, the active application's manifest title is used instead.
20
20
  */
21
21
  appPageTitle?: string;
22
22
  children?: ReactNode;
@@ -1 +1 @@
1
- {"version":3,"file":"HostChrome.d.ts","sourceRoot":"","sources":["../../src/ui/HostChrome.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAuC,KAAK,SAAS,EAAwB,MAAM,OAAO,CAAC;AAElG,OAAO,EAEH,KAAK,gCAAgC,EACxC,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACH,KAAK,iBAAiB,EACtB,KAAK,mBAAmB,EACxB,KAAK,gBAAgB,EACxB,MAAM,2CAA2C,CAAC;AA+BnD,OAAO,mBAAmB,CAAC;AAK3B,OAAO,0CAA0C,CAAC;AAClD,OAAO,6CAA6C,CAAC;AACrD,OAAO,sDAAsD,CAAC;AAC9D,OAAO,0DAA0D,CAAC;AAIlE,MAAM,WAAW,gBAAgB;IAC7B,GAAG,EAAE,gBAAgB,CAAC;IACtB,oBAAoB,EAAE,gCAAgC,EAAE,CAAC;IACzD,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,SAAS,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,aAAa,CAAC,EAAE,iBAAiB,CAAC;IAClC,YAAY,CAAC,EAAE,mBAAmB,GAAG,IAAI,CAAC;IAC1C;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,SAAS,CAAC;CACxB;AAED,wBAAgB,UAAU,CAAC,EACvB,GAAG,EACH,oBAAoB,EACpB,QAAQ,EACR,UAAU,EACV,SAAS,EAAE,UAAU,EACrB,aAAa,EACb,YAAmB,EACnB,YAAY,EACZ,QAAQ,EACX,EAAE,gBAAgB,2CAsNlB"}
1
+ {"version":3,"file":"HostChrome.d.ts","sourceRoot":"","sources":["../../src/ui/HostChrome.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAuC,KAAK,SAAS,EAAwB,MAAM,OAAO,CAAC;AAElG,OAAO,EAGH,KAAK,gCAAgC,EACxC,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACH,KAAK,iBAAiB,EACtB,KAAK,mBAAmB,EACxB,KAAK,gBAAgB,EACxB,MAAM,2CAA2C,CAAC;AAgCnD,OAAO,mBAAmB,CAAC;AAK3B,OAAO,0CAA0C,CAAC;AAClD,OAAO,6CAA6C,CAAC;AACrD,OAAO,sDAAsD,CAAC;AAC9D,OAAO,0DAA0D,CAAC;AAIlE,MAAM,WAAW,gBAAgB;IAC7B,GAAG,EAAE,gBAAgB,CAAC;IACtB,oBAAoB,EAAE,gCAAgC,EAAE,CAAC;IACzD,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,SAAS,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,aAAa,CAAC,EAAE,iBAAiB,CAAC;IAClC,YAAY,CAAC,EAAE,mBAAmB,GAAG,IAAI,CAAC;IAC1C;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,SAAS,CAAC;CACxB;AAED,wBAAgB,UAAU,CAAC,EACvB,GAAG,EACH,oBAAoB,EACpB,QAAQ,EACR,UAAU,EACV,SAAS,EAAE,UAAU,EACrB,aAAa,EACb,YAAmB,EACnB,YAAY,EACZ,QAAQ,EACX,EAAE,gBAAgB,2CAwNlB"}
@@ -4,7 +4,7 @@ import { useCallback, useMemo } from "react";
4
4
  import { isExternalPluggableApplicationRegistryItem, } from "@gooddata/sdk-model";
5
5
  import { BackendProvider, resolveLocale } from "@gooddata/sdk-ui";
6
6
  import { AppHeaderNotifications } from "@gooddata/sdk-ui-application-header";
7
- import { AppHeader, DocumentHeader, ToastsCenterContextProvider, generateHeaderStaticHelpMenuItems, } from "@gooddata/sdk-ui-kit";
7
+ import { AppHeader, DocumentHeader, ToastsCenterContextProvider, generateHeaderAccountMenuItems, generateHeaderStaticHelpMenuItems, } from "@gooddata/sdk-ui-kit";
8
8
  import { defaultHeaderTheme } from "@gooddata/sdk-ui-theme-provider";
9
9
  import defaultLogoUrl from "../assets/logo-white.svg";
10
10
  import { getAppLifecycleCallbacks, preloadPluggableApplication, } from "../loader/pluggableApplicationsLoader.js";
@@ -50,20 +50,17 @@ export function HostChrome({ ctx, resolvedApplications, pathname, onNavigate, on
50
50
  // menu. Apps may still supply their own help items via headerOptions.
51
51
  const helpMenuItems = useMemo(() => headerOptions?.helpMenuItems ??
52
52
  (ctx.whiteLabeling?.enabled ? [] : generateHeaderStaticHelpMenuItems()), [headerOptions, ctx.whiteLabeling?.enabled]);
53
- const accountMenuItems = useMemo(() => [
54
- {
55
- key: LOGOUT_MENU_ITEM_KEY,
56
- onClick: () => {
57
- void getBackend().deauthenticate();
58
- },
59
- },
60
- ], []);
53
+ const accountMenuItems = useMemo(() => generateHeaderAccountMenuItems(ctx.workspacePermissions ?? {}, features.workspaceId, ctx.settings), [ctx.workspacePermissions, features.workspaceId, ctx.settings]);
61
54
  const handleMenuItemClick = useCallback((item, e) => {
62
55
  e?.preventDefault();
63
56
  if (item.onClick) {
64
57
  item.onClick(e ?? null);
65
58
  return;
66
59
  }
60
+ if (item.key === LOGOUT_MENU_ITEM_KEY) {
61
+ void getBackend().deauthenticate();
62
+ return;
63
+ }
67
64
  if (item.href) {
68
65
  if (item.target === "_blank") {
69
66
  window.open(item.href, "_blank", "noopener,noreferrer");
@@ -114,8 +111,8 @@ export function HostChrome({ ctx, resolvedApplications, pathname, onNavigate, on
114
111
  // URL is cleared, instead of leaving the stale icon in place.
115
112
  const faviconUrl = ctx.whiteLabeling?.faviconUrl || "/favicon.ico";
116
113
  // The host owns the browser tab title as "{page} - {brand}". The page segment defaults to the
117
- // active application's manifest title, but an embedded app can override it dynamically via its
118
- // mount `onDocumentTitleChange` callback (surfaced here as `appPageTitle`). When white-labeling
114
+ // active application's manifest title, but an embedded app can override it dynamically by emitting
115
+ // a document-title-changed event (surfaced here as `appPageTitle`). When white-labeling
119
116
  // is enabled the brand is the organization name — preferring the user profile's name, then the
120
117
  // resolved organization descriptor title (the profile field is optional) — and is omitted (so
121
118
  // DocumentHeader drops the " - " separator) only when neither is set. The GoodData product name
@@ -1 +1 @@
1
- {"version":3,"file":"PluggableApplicationRenderer.d.ts","sourceRoot":"","sources":["../../src/ui/PluggableApplicationRenderer.tsx"],"names":[],"mappings":"AAMA,OAAO,EAAE,KAAK,gCAAgC,EAAE,MAAM,qBAAqB,CAAC;AAC5E,OAAO,EACH,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EAIxB,MAAM,2CAA2C,CAAC;AASnD,OAAO,qCAAqC,CAAC;AAM7C,MAAM,WAAW,kCAAkC;IAC/C,GAAG,EAAE,gCAAgC,CAAC;IACtC,GAAG,EAAE,gBAAgB,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,iBAAiB,KAAK,IAAI,CAAC;IACpE,qBAAqB,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,SAAS,KAAK,IAAI,CAAC;CAClF;AAED,wBAAgB,4BAA4B,CAAC,EACzC,GAAG,EACH,GAAG,EACH,QAAQ,EACR,cAAc,EACd,qBAAqB,EACxB,EAAE,kCAAkC,2CAqIpC"}
1
+ {"version":3,"file":"PluggableApplicationRenderer.d.ts","sourceRoot":"","sources":["../../src/ui/PluggableApplicationRenderer.tsx"],"names":[],"mappings":"AAMA,OAAO,EAAE,KAAK,gCAAgC,EAAE,MAAM,qBAAqB,CAAC;AAC5E,OAAO,EACH,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EAMxB,MAAM,2CAA2C,CAAC;AAcnD,OAAO,qCAAqC,CAAC;AAkB7C,MAAM,WAAW,kCAAkC;IAC/C,GAAG,EAAE,gCAAgC,CAAC;IACtC,GAAG,EAAE,gBAAgB,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,iBAAiB,KAAK,IAAI,CAAC;IACpE,qBAAqB,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,SAAS,KAAK,IAAI,CAAC;CAClF;AAED,wBAAgB,4BAA4B,CAAC,EACzC,GAAG,EACH,GAAG,EACH,QAAQ,EACR,cAAc,EACd,qBAAqB,EACxB,EAAE,kCAAkC,2CAsMpC"}
@@ -1,22 +1,36 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  // (C) 2026 GoodData Corporation
3
3
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4
- import { FormattedMessage } from "react-intl";
5
- import { isReloadPlatformContextRequestedEvent, } from "@gooddata/sdk-pluggable-application-model";
4
+ import { FormattedMessage, defineMessage, useIntl } from "react-intl";
5
+ import { isDocumentTitleChangedEvent, isReloadPlatformContextRequestedEvent, } from "@gooddata/sdk-pluggable-application-model";
6
6
  import { LoadingComponent, useAutoupdateRef } from "@gooddata/sdk-ui";
7
7
  import { bemFactory } from "@gooddata/sdk-ui-kit";
8
8
  import { now } from "../debug.js";
9
+ import { getSecuredRemoteAppValidUntil, validateAppSecurity, } from "../loader/appSecurityValidation.js";
9
10
  import { getAppLifecycleCallbacks, loadPluggableApplication } from "../loader/pluggableApplicationsLoader.js";
10
11
  import { getApplicationHref } from "../loader/routing.js";
11
12
  import { BackendPlatformContextProvider } from "../platformContext/useLoadPlatformContext.js";
12
13
  import "./PluggableApplicationRenderer.scss";
13
14
  const { b, e } = bemFactory("gd-pluggable-application-renderer");
15
+ // Use `defineMessage` for each id so the intl extractor and validator can statically
16
+ // match them against the locale JSON files. A plain `{ id }` literal passed to
17
+ // `intl.formatMessage` is opaque to the tooling.
18
+ const SECURITY_FAILURE_MESSAGES = {
19
+ "organization-not-allowed": defineMessage({ id: "gs.host.error.appNotAllowedForOrganization" }),
20
+ "build-expired": defineMessage({ id: "gs.host.error.appBuildExpired" }),
21
+ "metadata-missing": defineMessage({ id: "gs.host.error.appSecurityMetadataMissing" }),
22
+ };
14
23
  export function PluggableApplicationRenderer({ app, ctx, pathname, onHeaderChange, onDocumentTitleChange, }) {
24
+ const intl = useIntl();
25
+ const intlRef = useAutoupdateRef(intl);
15
26
  const ctxRef = useAutoupdateRef(ctx);
16
27
  const onHeaderChangeRef = useAutoupdateRef(onHeaderChange);
17
28
  const onDocumentTitleChangeRef = useAutoupdateRef(onDocumentTitleChange);
18
29
  const containerRef = useRef(null);
19
30
  const mountHandleRef = useRef(undefined);
31
+ // The app/module pair currently mounted. Held so the context-change effect can
32
+ // re-run the security check against the live organization without reloading.
33
+ const mountedAppRef = useRef(undefined);
20
34
  const [viewState, setViewState] = useState({ state: "loading" });
21
35
  const baseHref = getApplicationHref(app, ctx, pathname);
22
36
  const appBasePath = ctx.embeddingMode === "iframe" ? `/embedded${baseHref}` : baseHref;
@@ -25,17 +39,23 @@ export function PluggableApplicationRenderer({ app, ctx, pathname, onHeaderChang
25
39
  // onEvent is intentionally stable (empty deps) — pluggable apps capture it at mount time
26
40
  // and do not update it when the host re-renders. Do not make this callback depend on
27
41
  // any value that can change after mount, or the mounted app will silently call a stale closure.
42
+ // Values that change after mount are read through refs (e.g. onDocumentTitleChangeRef).
28
43
  const onEvent = useCallback((event) => {
29
44
  if (isReloadPlatformContextRequestedEvent(event)) {
30
45
  void BackendPlatformContextProvider.load();
46
+ return;
31
47
  }
32
- }, []);
48
+ if (isDocumentTitleChangedEvent(event)) {
49
+ onDocumentTitleChangeRef.current?.(app.id, event.payload.pageTitle);
50
+ }
51
+ }, [app.id, onDocumentTitleChangeRef]);
33
52
  useEffect(() => {
34
53
  let cancelled = false;
35
54
  const mountId = `${app.id}:${Date.now()}`;
36
55
  const totalStart = now();
37
56
  const prevHandle = mountHandleRef.current;
38
57
  mountHandleRef.current = undefined;
58
+ mountedAppRef.current = undefined;
39
59
  setViewState({ state: "loading" });
40
60
  // Defer unmount of the previous app to avoid synchronously unmounting
41
61
  // a React root while React is already rendering (race in strict mode).
@@ -49,6 +69,17 @@ export function PluggableApplicationRenderer({ app, ctx, pathname, onHeaderChang
49
69
  if (cancelled) {
50
70
  return;
51
71
  }
72
+ const securityFailure = validateAppSecurity(app, loadedApp, ctxRef.current);
73
+ if (securityFailure) {
74
+ const message = intlRef.current.formatMessage(SECURITY_FAILURE_MESSAGES[securityFailure.kind]);
75
+ console.error(`[host-runtime/renderer] Refusing to mount app "${app.id}": ${securityFailure.kind}.`, securityFailure);
76
+ lifecycle?.onLoadFailed?.(app.id, securityFailure.kind);
77
+ setViewState({ state: "error", message });
78
+ return;
79
+ }
80
+ // Report load success only after the security check passes, so a rejected
81
+ // app never produces both an onLoadCompleted and an onLoadFailed for the
82
+ // same attempt.
52
83
  lifecycle?.onLoadCompleted?.(app.id, now() - loadStart);
53
84
  const container = containerRef.current;
54
85
  if (!container) {
@@ -63,11 +94,11 @@ export function PluggableApplicationRenderer({ app, ctx, pathname, onHeaderChang
63
94
  onEvent,
64
95
  onTelemetryEvent,
65
96
  onHeaderChange: (header) => onHeaderChangeRef.current?.(app.id, header),
66
- onDocumentTitleChange: (pageTitle) => onDocumentTitleChangeRef.current?.(app.id, pageTitle),
67
97
  });
98
+ mountedAppRef.current = { app, loadedApp };
68
99
  lifecycle?.onMountCompleted?.(app.id, now() - mountStart);
69
100
  lifecycle?.onRendered?.(app.id, now() - totalStart);
70
- setViewState({ state: "ready" });
101
+ setViewState({ state: "ready", validUntil: getSecuredRemoteAppValidUntil(app, loadedApp) });
71
102
  })
72
103
  .catch((mountError) => {
73
104
  if (cancelled) {
@@ -86,27 +117,45 @@ export function PluggableApplicationRenderer({ app, ctx, pathname, onHeaderChang
86
117
  const handle = mountHandleRef.current;
87
118
  if (handle) {
88
119
  mountHandleRef.current = undefined;
120
+ mountedAppRef.current = undefined;
89
121
  queueMicrotask(() => {
90
122
  handle.unmount();
91
123
  lifecycle?.onUnmounted?.(app.id);
92
124
  });
93
125
  }
94
126
  };
95
- }, [
96
- app,
97
- appBasePath,
98
- ctxRef,
99
- onHeaderChangeRef,
100
- onDocumentTitleChangeRef,
101
- onTelemetryEvent,
102
- lifecycle,
103
- onEvent,
104
- ]);
127
+ }, [app, appBasePath, ctxRef, intlRef, onHeaderChangeRef, onTelemetryEvent, lifecycle, onEvent]);
105
128
  useEffect(() => {
106
- mountHandleRef.current?.updateContext?.(ctx);
107
- }, [ctx]);
129
+ const mounted = mountedAppRef.current;
130
+ const handle = mountHandleRef.current;
131
+ if (!mounted || !handle) {
132
+ return;
133
+ }
134
+ // Re-run the security check against the new context. The app's base path does not
135
+ // encode the organization id, so an organization change does not re-run the mount
136
+ // effect — without this an app could stay mounted under an organization that is no
137
+ // longer in its allowlist (or with a now-expired build).
138
+ const securityFailure = validateAppSecurity(mounted.app, mounted.loadedApp, ctx);
139
+ if (securityFailure) {
140
+ const message = intlRef.current.formatMessage(SECURITY_FAILURE_MESSAGES[securityFailure.kind]);
141
+ console.error(`[host-runtime/renderer] Unmounting app "${mounted.app.id}" after context change: ${securityFailure.kind}.`, securityFailure);
142
+ mountHandleRef.current = undefined;
143
+ mountedAppRef.current = undefined;
144
+ handle.unmount();
145
+ lifecycle?.onUnmounted?.(mounted.app.id);
146
+ lifecycle?.onLoadFailed?.(mounted.app.id, securityFailure.kind);
147
+ setViewState({ state: "error", message });
148
+ return;
149
+ }
150
+ handle.updateContext?.(ctx);
151
+ }, [ctx, intlRef, lifecycle]);
108
152
  return (_jsxs("section", { className: b(), children: [viewState.state === "loading" ? (_jsx("div", { className: e("loading"), children: _jsx(LoadingComponent, { height: 40 }) })) : null, viewState.state === "error" ? (_jsxs("div", { className: e("error"), children: [
109
153
  _jsx("h2", { children: _jsx(FormattedMessage, { id: "gs.host.error.applicationFailedToLoad" }) }), _jsx("p", { children: viewState.message })
110
- ] })) : null, _jsx("div", { ref: containerRef, className: e("container", { visible: viewState.state === "ready" }) })
111
- ] }));
154
+ ] })) : null, _jsx("div", { ref: containerRef, className: e("container", { visible: viewState.state === "ready" }) }), viewState.state === "ready" && viewState.validUntil !== undefined ? (_jsx("div", { className: e("demoBadge"), role: "status", children: _jsx(FormattedMessage, { id: "gs.host.demoApp.validUntil", values: {
155
+ date: intl.formatDate(viewState.validUntil, {
156
+ year: "numeric",
157
+ month: "long",
158
+ day: "numeric",
159
+ }),
160
+ } }) })) : null] }));
112
161
  }
@@ -12,10 +12,20 @@
12
12
 
13
13
  &__error {
14
14
  min-height: 240px;
15
+ height: 100%;
15
16
  display: flex;
16
17
  flex-direction: column;
17
- justify-content: center;
18
+ align-items: center;
19
+ text-align: center;
18
20
  gap: 8px;
21
+
22
+ // Push the error block to roughly one third from the top. The spacer's
23
+ // flex-basis is a percentage of this column's height (resolves against
24
+ // the 100% height above), so it stays proportional as the area resizes.
25
+ &::before {
26
+ content: "";
27
+ flex: 0 0 33%;
28
+ }
19
29
  }
20
30
 
21
31
  &__container {
@@ -26,4 +36,24 @@
26
36
  display: block;
27
37
  height: 100%;
28
38
  }
39
+
40
+ // Small semi-transparent floating badge pinned to the bottom-right of the
41
+ // page, shown only for validated demo-bucket apps. Non-interactive so it
42
+ // never intercepts clicks meant for the app beneath it.
43
+ &__demoBadge {
44
+ position: fixed;
45
+ right: 16px;
46
+ bottom: 16px;
47
+ z-index: 100;
48
+ padding: 6px 12px;
49
+ border-radius: 6px;
50
+ font-size: 12px;
51
+ line-height: 1.4;
52
+ color: #fff;
53
+ background: rgba(20, 35, 49, 0.16);
54
+ backdrop-filter: blur(4px);
55
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
56
+ pointer-events: none;
57
+ user-select: none;
58
+ }
29
59
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gooddata/sdk-ui-pluggable-host",
3
- "version": "11.41.0-alpha.5",
3
+ "version": "11.42.0-alpha.0",
4
4
  "description": "GoodData SDK runtime for hosting pluggable applications — registry, loader, routing, platform context, default UI chrome",
5
5
  "license": "MIT",
6
6
  "author": "GoodData Corporation",
@@ -29,20 +29,20 @@
29
29
  "dependencies": {
30
30
  "@module-federation/runtime": "2.3.1",
31
31
  "lodash-es": "^4.17.23",
32
- "@gooddata/sdk-backend-base": "11.41.0-alpha.5",
33
- "@gooddata/sdk-backend-spi": "11.41.0-alpha.5",
34
- "@gooddata/sdk-backend-tiger": "11.41.0-alpha.5",
35
- "@gooddata/sdk-model": "11.41.0-alpha.5",
36
- "@gooddata/sdk-embedding": "11.41.0-alpha.5",
37
- "@gooddata/sdk-pluggable-application-model": "11.41.0-alpha.5",
38
- "@gooddata/sdk-ui": "11.41.0-alpha.5",
39
- "@gooddata/sdk-ui-ext": "11.41.0-alpha.5",
40
- "@gooddata/sdk-ui-application-header": "11.41.0-alpha.5",
41
- "@gooddata/sdk-ui-gen-ai": "11.41.0-alpha.5",
42
- "@gooddata/sdk-ui-kit": "11.41.0-alpha.5",
43
- "@gooddata/sdk-ui-semantic-search": "11.41.0-alpha.5",
44
- "@gooddata/sdk-ui-theme-provider": "11.41.0-alpha.5",
45
- "@gooddata/util": "11.41.0-alpha.5"
32
+ "@gooddata/sdk-backend-base": "11.42.0-alpha.0",
33
+ "@gooddata/sdk-backend-tiger": "11.42.0-alpha.0",
34
+ "@gooddata/sdk-embedding": "11.42.0-alpha.0",
35
+ "@gooddata/sdk-backend-spi": "11.42.0-alpha.0",
36
+ "@gooddata/sdk-model": "11.42.0-alpha.0",
37
+ "@gooddata/sdk-pluggable-application-model": "11.42.0-alpha.0",
38
+ "@gooddata/sdk-ui": "11.42.0-alpha.0",
39
+ "@gooddata/sdk-ui-application-header": "11.42.0-alpha.0",
40
+ "@gooddata/sdk-ui-ext": "11.42.0-alpha.0",
41
+ "@gooddata/sdk-ui-gen-ai": "11.42.0-alpha.0",
42
+ "@gooddata/sdk-ui-kit": "11.42.0-alpha.0",
43
+ "@gooddata/sdk-ui-semantic-search": "11.42.0-alpha.0",
44
+ "@gooddata/sdk-ui-theme-provider": "11.42.0-alpha.0",
45
+ "@gooddata/util": "11.42.0-alpha.0"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@microsoft/api-documenter": "^7.17.0",
@@ -85,8 +85,8 @@
85
85
  "vite": "8.0.16",
86
86
  "vitest": "4.1.8",
87
87
  "vitest-dom": "0.1.1",
88
- "@gooddata/eslint-config": "11.41.0-alpha.5",
89
- "@gooddata/oxlint-config": "11.41.0-alpha.5"
88
+ "@gooddata/eslint-config": "11.42.0-alpha.0",
89
+ "@gooddata/oxlint-config": "11.42.0-alpha.0"
90
90
  },
91
91
  "peerDependencies": {
92
92
  "react": ">=18.3.1",