@trackunit/react-modal 2.1.22 → 2.1.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.cjs.js +81 -4
- package/index.esm.js +81 -4
- package/package.json +3 -3
- package/src/modal/trustedHostOrigin.d.ts +24 -0
package/index.cjs.js
CHANGED
|
@@ -581,6 +581,69 @@ const ModalHeader = react.forwardRef(({ heading, subHeading, onClickClose, "data
|
|
|
581
581
|
}), "data-testid": dataTestId, id: id, ref: ref, style: style, children: [jsxRuntime.jsxs("div", { className: cvaHeadingContainer(), children: [jsxRuntime.jsxs("div", { className: cvaTitleContainer(), children: [jsxRuntime.jsx(reactComponents.Heading, { variant: "tertiary", children: heading }), accessories] }), Boolean(subHeading) ? (jsxRuntime.jsx(reactComponents.Text, { size: "small", subtle: true, children: subHeading })) : null, children] }), jsxRuntime.jsx("div", { className: cvaIconContainer(), children: jsxRuntime.jsx(reactComponents.IconButton, { className: "!h-min", "data-testid": dataTestId ? `${dataTestId}-close-button` : "modal-close-button", icon: jsxRuntime.jsx(reactComponents.Icon, { name: "XMark", size: "small" }), onClick: onClickClose, size: "small", title: t("modalHeader.close"), variant: "ghost-neutral" }) })] }));
|
|
582
582
|
});
|
|
583
583
|
|
|
584
|
+
/**
|
|
585
|
+
* Trust anchor for the modal registry's outbound (iframe → host) postMessage.
|
|
586
|
+
*
|
|
587
|
+
* `@trackunit/react-modal` is a standalone, reusable UI library that must not
|
|
588
|
+
* depend on `iris-app-runtime` (see `modalStackRegistry.ts` for why the modal
|
|
589
|
+
* stack bridge uses raw postMessage rather than penpal). So, exactly as the
|
|
590
|
+
* dependency-free `iris-app-loader` does, the trusted-host-origin resolver is
|
|
591
|
+
* duplicated here instead of imported.
|
|
592
|
+
*
|
|
593
|
+
* The `BRANDED_HOST_DOMAINS` list is the single security-relevant piece that
|
|
594
|
+
* must stay in sync across all copies (this file, the loader copy, the runtime
|
|
595
|
+
* core resolver, and `BrandedUrls` in
|
|
596
|
+
* `libs/iris-app-sdk/iris-app-api/src/types/irisAppCspInput.ts`); a
|
|
597
|
+
* `platform-test` (`branded-host-origins-in-sync.spec.ts`) fails the build on
|
|
598
|
+
* drift.
|
|
599
|
+
*/
|
|
600
|
+
const BRANDED_HOST_DOMAINS = [
|
|
601
|
+
"trackunit.com",
|
|
602
|
+
"wackerneuson.com",
|
|
603
|
+
"manitou.com",
|
|
604
|
+
"niftylinkmanager.com",
|
|
605
|
+
"skyjack.com",
|
|
606
|
+
"ahernaccess.com",
|
|
607
|
+
"magnith.com",
|
|
608
|
+
"terberg.com",
|
|
609
|
+
"mymecalac.com",
|
|
610
|
+
"delille.be",
|
|
611
|
+
];
|
|
612
|
+
const escapeForRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
613
|
+
const brandedHostPattern = (domain) => new RegExp(`^https:\\/\\/([a-z0-9-]+\\.)*${escapeForRegExp(domain)}$`);
|
|
614
|
+
/** Static whitelist of trusted host origins (branded domains + dev/localhost). */
|
|
615
|
+
const trustedHostOrigins = [
|
|
616
|
+
...BRANDED_HOST_DOMAINS.map(brandedHostPattern),
|
|
617
|
+
/^https:\/\/([a-z0-9-]+\.)*trackunit\.app$/,
|
|
618
|
+
/^http:\/\/localhost(:\d+)?$/,
|
|
619
|
+
];
|
|
620
|
+
/** Whether the given window origin belongs to a trusted Trackunit host. */
|
|
621
|
+
const isWhitelistedHostOrigin = (origin) => trustedHostOrigins.some(entry => (typeof entry === "string" ? entry === origin : entry.test(origin)));
|
|
622
|
+
// Capture-once cache: the iframe never switches domain, so the embedding host
|
|
623
|
+
// origin is resolved on first read and reused for the document's lifetime.
|
|
624
|
+
let cachedHostOrigin;
|
|
625
|
+
/**
|
|
626
|
+
* Resolves the embedding host's origin from `document.referrer`, returning it
|
|
627
|
+
* only when it is whitelisted; otherwise `undefined` (non-whitelisted, blank,
|
|
628
|
+
* or unreadable referrer). Memoized on first call.
|
|
629
|
+
*/
|
|
630
|
+
const getTrustedHostOrigin = () => {
|
|
631
|
+
if (cachedHostOrigin) {
|
|
632
|
+
return cachedHostOrigin.value;
|
|
633
|
+
}
|
|
634
|
+
let value;
|
|
635
|
+
try {
|
|
636
|
+
const referrer = document.referrer;
|
|
637
|
+
const origin = referrer ? new URL(referrer).origin : undefined;
|
|
638
|
+
value = origin && isWhitelistedHostOrigin(origin) ? origin : undefined;
|
|
639
|
+
}
|
|
640
|
+
catch {
|
|
641
|
+
value = undefined;
|
|
642
|
+
}
|
|
643
|
+
cachedHostOrigin = { value };
|
|
644
|
+
return cachedHostOrigin.value;
|
|
645
|
+
};
|
|
646
|
+
|
|
584
647
|
/**
|
|
585
648
|
* Modal Stack Registry - Cross-Bundle Modal Awareness
|
|
586
649
|
*
|
|
@@ -668,15 +731,29 @@ const MODAL_STACK_MESSAGE_TYPE = "IRIS_APP_HOST_MODAL_STACK_CHANGE";
|
|
|
668
731
|
* The host aggregates these per-iframe for accurate total tracking.
|
|
669
732
|
*/
|
|
670
733
|
const IFRAME_MODAL_STACK_MESSAGE_TYPE = "IRIS_APP_IFRAME_MODAL_STACK_CHANGE";
|
|
671
|
-
const isInIframe = typeof window !== "undefined" && window.parent !== window;
|
|
672
734
|
/**
|
|
673
735
|
* Broadcast this iframe's modal count to the parent (host).
|
|
674
|
-
*
|
|
736
|
+
*
|
|
737
|
+
* Only runs when in an iframe context. Called automatically on
|
|
738
|
+
* register/unregister.
|
|
739
|
+
*
|
|
740
|
+
* Security: the message is posted to the *resolved trusted host origin*, not
|
|
741
|
+
* `"*"`. When the embedding host can't be resolved to a whitelisted Trackunit
|
|
742
|
+
* origin (non-Trackunit / blank / unreadable referrer) we fail closed and drop
|
|
743
|
+
* the broadcast rather than leaking modal-stack data to an arbitrary embedder.
|
|
744
|
+
* This mirrors the loader's `postMessageToHost` chokepoint and uses the same
|
|
745
|
+
* (dependency-safe, duplicated) trusted-host-origin resolver.
|
|
675
746
|
*/
|
|
676
747
|
const broadcastToParent = (modalCount) => {
|
|
677
|
-
|
|
678
|
-
|
|
748
|
+
const isInIframe = typeof window !== "undefined" && window.parent !== window;
|
|
749
|
+
if (!isInIframe) {
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
const targetOrigin = getTrustedHostOrigin();
|
|
753
|
+
if (!targetOrigin) {
|
|
754
|
+
return;
|
|
679
755
|
}
|
|
756
|
+
window.parent.postMessage({ type: IFRAME_MODAL_STACK_MESSAGE_TYPE, data: { iframeModalCount: modalCount } }, targetOrigin);
|
|
680
757
|
};
|
|
681
758
|
/**
|
|
682
759
|
* Set up the global message listener for cross-bundle modal stack communication.
|
package/index.esm.js
CHANGED
|
@@ -579,6 +579,69 @@ const ModalHeader = forwardRef(({ heading, subHeading, onClickClose, "data-testi
|
|
|
579
579
|
}), "data-testid": dataTestId, id: id, ref: ref, style: style, children: [jsxs("div", { className: cvaHeadingContainer(), children: [jsxs("div", { className: cvaTitleContainer(), children: [jsx(Heading, { variant: "tertiary", children: heading }), accessories] }), Boolean(subHeading) ? (jsx(Text, { size: "small", subtle: true, children: subHeading })) : null, children] }), jsx("div", { className: cvaIconContainer(), children: jsx(IconButton, { className: "!h-min", "data-testid": dataTestId ? `${dataTestId}-close-button` : "modal-close-button", icon: jsx(Icon, { name: "XMark", size: "small" }), onClick: onClickClose, size: "small", title: t("modalHeader.close"), variant: "ghost-neutral" }) })] }));
|
|
580
580
|
});
|
|
581
581
|
|
|
582
|
+
/**
|
|
583
|
+
* Trust anchor for the modal registry's outbound (iframe → host) postMessage.
|
|
584
|
+
*
|
|
585
|
+
* `@trackunit/react-modal` is a standalone, reusable UI library that must not
|
|
586
|
+
* depend on `iris-app-runtime` (see `modalStackRegistry.ts` for why the modal
|
|
587
|
+
* stack bridge uses raw postMessage rather than penpal). So, exactly as the
|
|
588
|
+
* dependency-free `iris-app-loader` does, the trusted-host-origin resolver is
|
|
589
|
+
* duplicated here instead of imported.
|
|
590
|
+
*
|
|
591
|
+
* The `BRANDED_HOST_DOMAINS` list is the single security-relevant piece that
|
|
592
|
+
* must stay in sync across all copies (this file, the loader copy, the runtime
|
|
593
|
+
* core resolver, and `BrandedUrls` in
|
|
594
|
+
* `libs/iris-app-sdk/iris-app-api/src/types/irisAppCspInput.ts`); a
|
|
595
|
+
* `platform-test` (`branded-host-origins-in-sync.spec.ts`) fails the build on
|
|
596
|
+
* drift.
|
|
597
|
+
*/
|
|
598
|
+
const BRANDED_HOST_DOMAINS = [
|
|
599
|
+
"trackunit.com",
|
|
600
|
+
"wackerneuson.com",
|
|
601
|
+
"manitou.com",
|
|
602
|
+
"niftylinkmanager.com",
|
|
603
|
+
"skyjack.com",
|
|
604
|
+
"ahernaccess.com",
|
|
605
|
+
"magnith.com",
|
|
606
|
+
"terberg.com",
|
|
607
|
+
"mymecalac.com",
|
|
608
|
+
"delille.be",
|
|
609
|
+
];
|
|
610
|
+
const escapeForRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
611
|
+
const brandedHostPattern = (domain) => new RegExp(`^https:\\/\\/([a-z0-9-]+\\.)*${escapeForRegExp(domain)}$`);
|
|
612
|
+
/** Static whitelist of trusted host origins (branded domains + dev/localhost). */
|
|
613
|
+
const trustedHostOrigins = [
|
|
614
|
+
...BRANDED_HOST_DOMAINS.map(brandedHostPattern),
|
|
615
|
+
/^https:\/\/([a-z0-9-]+\.)*trackunit\.app$/,
|
|
616
|
+
/^http:\/\/localhost(:\d+)?$/,
|
|
617
|
+
];
|
|
618
|
+
/** Whether the given window origin belongs to a trusted Trackunit host. */
|
|
619
|
+
const isWhitelistedHostOrigin = (origin) => trustedHostOrigins.some(entry => (typeof entry === "string" ? entry === origin : entry.test(origin)));
|
|
620
|
+
// Capture-once cache: the iframe never switches domain, so the embedding host
|
|
621
|
+
// origin is resolved on first read and reused for the document's lifetime.
|
|
622
|
+
let cachedHostOrigin;
|
|
623
|
+
/**
|
|
624
|
+
* Resolves the embedding host's origin from `document.referrer`, returning it
|
|
625
|
+
* only when it is whitelisted; otherwise `undefined` (non-whitelisted, blank,
|
|
626
|
+
* or unreadable referrer). Memoized on first call.
|
|
627
|
+
*/
|
|
628
|
+
const getTrustedHostOrigin = () => {
|
|
629
|
+
if (cachedHostOrigin) {
|
|
630
|
+
return cachedHostOrigin.value;
|
|
631
|
+
}
|
|
632
|
+
let value;
|
|
633
|
+
try {
|
|
634
|
+
const referrer = document.referrer;
|
|
635
|
+
const origin = referrer ? new URL(referrer).origin : undefined;
|
|
636
|
+
value = origin && isWhitelistedHostOrigin(origin) ? origin : undefined;
|
|
637
|
+
}
|
|
638
|
+
catch {
|
|
639
|
+
value = undefined;
|
|
640
|
+
}
|
|
641
|
+
cachedHostOrigin = { value };
|
|
642
|
+
return cachedHostOrigin.value;
|
|
643
|
+
};
|
|
644
|
+
|
|
582
645
|
/**
|
|
583
646
|
* Modal Stack Registry - Cross-Bundle Modal Awareness
|
|
584
647
|
*
|
|
@@ -666,15 +729,29 @@ const MODAL_STACK_MESSAGE_TYPE = "IRIS_APP_HOST_MODAL_STACK_CHANGE";
|
|
|
666
729
|
* The host aggregates these per-iframe for accurate total tracking.
|
|
667
730
|
*/
|
|
668
731
|
const IFRAME_MODAL_STACK_MESSAGE_TYPE = "IRIS_APP_IFRAME_MODAL_STACK_CHANGE";
|
|
669
|
-
const isInIframe = typeof window !== "undefined" && window.parent !== window;
|
|
670
732
|
/**
|
|
671
733
|
* Broadcast this iframe's modal count to the parent (host).
|
|
672
|
-
*
|
|
734
|
+
*
|
|
735
|
+
* Only runs when in an iframe context. Called automatically on
|
|
736
|
+
* register/unregister.
|
|
737
|
+
*
|
|
738
|
+
* Security: the message is posted to the *resolved trusted host origin*, not
|
|
739
|
+
* `"*"`. When the embedding host can't be resolved to a whitelisted Trackunit
|
|
740
|
+
* origin (non-Trackunit / blank / unreadable referrer) we fail closed and drop
|
|
741
|
+
* the broadcast rather than leaking modal-stack data to an arbitrary embedder.
|
|
742
|
+
* This mirrors the loader's `postMessageToHost` chokepoint and uses the same
|
|
743
|
+
* (dependency-safe, duplicated) trusted-host-origin resolver.
|
|
673
744
|
*/
|
|
674
745
|
const broadcastToParent = (modalCount) => {
|
|
675
|
-
|
|
676
|
-
|
|
746
|
+
const isInIframe = typeof window !== "undefined" && window.parent !== window;
|
|
747
|
+
if (!isInIframe) {
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
const targetOrigin = getTrustedHostOrigin();
|
|
751
|
+
if (!targetOrigin) {
|
|
752
|
+
return;
|
|
677
753
|
}
|
|
754
|
+
window.parent.postMessage({ type: IFRAME_MODAL_STACK_MESSAGE_TYPE, data: { iframeModalCount: modalCount } }, targetOrigin);
|
|
678
755
|
};
|
|
679
756
|
/**
|
|
680
757
|
* Set up the global message listener for cross-bundle modal stack communication.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trackunit/react-modal",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.23",
|
|
4
4
|
"repository": "https://github.com/Trackunit/manager",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.txt",
|
|
6
6
|
"engines": {
|
|
@@ -8,12 +8,12 @@
|
|
|
8
8
|
},
|
|
9
9
|
"dependencies": {
|
|
10
10
|
"@floating-ui/react": "^0.26.25",
|
|
11
|
-
"@trackunit/react-components": "2.1.
|
|
11
|
+
"@trackunit/react-components": "2.1.21",
|
|
12
12
|
"@trackunit/css-class-variance-utilities": "1.13.28",
|
|
13
13
|
"@trackunit/shared-utils": "1.15.28",
|
|
14
14
|
"@floating-ui/react-dom": "2.1.2",
|
|
15
15
|
"@trackunit/react-core-contexts-api": "1.17.30",
|
|
16
|
-
"@trackunit/i18n-library-translation": "2.0.
|
|
16
|
+
"@trackunit/i18n-library-translation": "2.0.23"
|
|
17
17
|
},
|
|
18
18
|
"peerDependencies": {
|
|
19
19
|
"@tanstack/react-router": "^1.114.29",
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trust anchor for the modal registry's outbound (iframe → host) postMessage.
|
|
3
|
+
*
|
|
4
|
+
* `@trackunit/react-modal` is a standalone, reusable UI library that must not
|
|
5
|
+
* depend on `iris-app-runtime` (see `modalStackRegistry.ts` for why the modal
|
|
6
|
+
* stack bridge uses raw postMessage rather than penpal). So, exactly as the
|
|
7
|
+
* dependency-free `iris-app-loader` does, the trusted-host-origin resolver is
|
|
8
|
+
* duplicated here instead of imported.
|
|
9
|
+
*
|
|
10
|
+
* The `BRANDED_HOST_DOMAINS` list is the single security-relevant piece that
|
|
11
|
+
* must stay in sync across all copies (this file, the loader copy, the runtime
|
|
12
|
+
* core resolver, and `BrandedUrls` in
|
|
13
|
+
* `libs/iris-app-sdk/iris-app-api/src/types/irisAppCspInput.ts`); a
|
|
14
|
+
* `platform-test` (`branded-host-origins-in-sync.spec.ts`) fails the build on
|
|
15
|
+
* drift.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Resolves the embedding host's origin from `document.referrer`, returning it
|
|
19
|
+
* only when it is whitelisted; otherwise `undefined` (non-whitelisted, blank,
|
|
20
|
+
* or unreadable referrer). Memoized on first call.
|
|
21
|
+
*/
|
|
22
|
+
export declare const getTrustedHostOrigin: () => string | undefined;
|
|
23
|
+
/** Test-only: clears the capture-once cache so a test can vary `document.referrer`. */
|
|
24
|
+
export declare const resetTrustedHostOriginCacheForTests: () => void;
|