@trackunit/iris-app-runtime-core 1.17.31-alpha-84ab66282fc.0 → 1.17.32-alpha-0424feab298.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.
- package/index.cjs.js +105 -1
- package/index.esm.js +105 -1
- package/package.json +2 -2
- package/src/trustedHostOrigin.d.ts +56 -0
package/index.cjs.js
CHANGED
|
@@ -3,6 +3,107 @@
|
|
|
3
3
|
var irisAppRuntimeCoreApi = require('@trackunit/iris-app-runtime-core-api');
|
|
4
4
|
var penpal = require('penpal');
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Trust anchor for the child (Iris app) side of the penpal bridge.
|
|
8
|
+
*
|
|
9
|
+
* An Iris app runs inside an iframe whose parent is the host (manager) page.
|
|
10
|
+
* Before this module the child connected with `allowedOrigins: ["*"]`, so any
|
|
11
|
+
* page could embed an app and drive it through the bridge. We pin the
|
|
12
|
+
* handshake to a *static whitelist* of Trackunit-controlled host domains — the
|
|
13
|
+
* whitelist, not the attacker-influenceable `document.referrer`, is the trust
|
|
14
|
+
* anchor.
|
|
15
|
+
*
|
|
16
|
+
* `document.referrer` is only used to *decide whether* we are confident enough
|
|
17
|
+
* to enforce the whitelist: the iframe sets `<meta name="referrer"
|
|
18
|
+
* content="strict-origin">`, so the referrer carries the embedding host's
|
|
19
|
+
* origin. When that origin resolves to a whitelisted Trackunit host we pin
|
|
20
|
+
* `allowedOrigins` to the whitelist; in every other case (non-whitelisted,
|
|
21
|
+
* blank, or unreadable referrer) we fail closed with `[]` so no parent can
|
|
22
|
+
* complete the handshake. There is deliberately no `["*"]` fallback — an
|
|
23
|
+
* embedder could blank or spoof its own referrer, so a wildcard fallback would
|
|
24
|
+
* re-open the exact unpinned path this module exists to close.
|
|
25
|
+
*
|
|
26
|
+
* The branded-domain list mirrors `BrandedUrls` in
|
|
27
|
+
* `libs/iris-app-sdk/iris-app-api/src/types/irisAppCspInput.ts`. It is
|
|
28
|
+
* duplicated here (as origin-matching RegExps rather than CSP `https://*.x`
|
|
29
|
+
* patterns) because `iris-app-runtime-core` does not depend on `iris-app-api`
|
|
30
|
+
* and adding that dependency for a static list isn't warranted. Keep the two
|
|
31
|
+
* lists in sync.
|
|
32
|
+
*/
|
|
33
|
+
const BRANDED_HOST_DOMAINS = [
|
|
34
|
+
"trackunit.com",
|
|
35
|
+
"wackerneuson.com",
|
|
36
|
+
"manitou.com",
|
|
37
|
+
"niftylinkmanager.com",
|
|
38
|
+
"skyjack.com",
|
|
39
|
+
"ahernaccess.com",
|
|
40
|
+
"magnith.com",
|
|
41
|
+
"terberg.com",
|
|
42
|
+
"mymecalac.com",
|
|
43
|
+
"delille.be",
|
|
44
|
+
];
|
|
45
|
+
const escapeForRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
46
|
+
/**
|
|
47
|
+
* Matches `https://<domain>` and `https://<any-subdomain>.<domain>` (the
|
|
48
|
+
* leading subdomain segments only contain DNS-safe characters, so a look-alike
|
|
49
|
+
* like `https://trackunit.com.evil.com` does not match).
|
|
50
|
+
*
|
|
51
|
+
* Note: unlike the CSP `https://*.<domain>` form this mirrors, the `(…)*` makes
|
|
52
|
+
* the subdomain optional, so the apex (`https://trackunit.com`) also matches.
|
|
53
|
+
* That is intentional — the apex is Trackunit-controlled — so don't "fix" it to
|
|
54
|
+
* require a subdomain to match the CSP shape.
|
|
55
|
+
*/
|
|
56
|
+
const brandedHostPattern = (domain) => new RegExp(`^https:\\/\\/([a-z0-9-]+\\.)*${escapeForRegExp(domain)}$`);
|
|
57
|
+
/**
|
|
58
|
+
* The static whitelist of trusted host origins, suitable for direct use as
|
|
59
|
+
* penpal `WindowMessenger.allowedOrigins` (which accepts `Array<string |
|
|
60
|
+
* RegExp>` and tests each incoming message origin against the list).
|
|
61
|
+
*/
|
|
62
|
+
const trustedHostOrigins = [
|
|
63
|
+
...BRANDED_HOST_DOMAINS.map(brandedHostPattern),
|
|
64
|
+
// Dev / feature-branch host (e.g. `dev.iris.trackunit.app`).
|
|
65
|
+
/^https:\/\/([a-z0-9-]+\.)*trackunit\.app$/,
|
|
66
|
+
// Local development host.
|
|
67
|
+
/^http:\/\/localhost(:\d+)?$/,
|
|
68
|
+
];
|
|
69
|
+
/**
|
|
70
|
+
* Whether the given window origin belongs to a trusted Trackunit host.
|
|
71
|
+
*/
|
|
72
|
+
const isWhitelistedHostOrigin = (origin) => trustedHostOrigins.some(entry => (typeof entry === "string" ? entry === origin : entry.test(origin)));
|
|
73
|
+
// Capture-once cache. The iframe never switches domain, so the embedding host
|
|
74
|
+
// origin is resolved on first read and reused for the lifetime of the document
|
|
75
|
+
// (it is intentionally not recomputed after a `location.replace`).
|
|
76
|
+
let cachedHostOrigin;
|
|
77
|
+
/**
|
|
78
|
+
* Resolves the embedding host's origin from `document.referrer`, returning it
|
|
79
|
+
* only when it is whitelisted; otherwise `undefined` (non-whitelisted, blank,
|
|
80
|
+
* or unreadable referrer). The value is captured on the first call and
|
|
81
|
+
* memoized for the lifetime of the document.
|
|
82
|
+
*/
|
|
83
|
+
const getTrustedHostOrigin = () => {
|
|
84
|
+
if (cachedHostOrigin) {
|
|
85
|
+
return cachedHostOrigin.value;
|
|
86
|
+
}
|
|
87
|
+
let value;
|
|
88
|
+
try {
|
|
89
|
+
const referrer = document.referrer;
|
|
90
|
+
const origin = referrer ? new URL(referrer).origin : undefined;
|
|
91
|
+
value = origin && isWhitelistedHostOrigin(origin) ? origin : undefined;
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
value = undefined;
|
|
95
|
+
}
|
|
96
|
+
cachedHostOrigin = { value };
|
|
97
|
+
return cachedHostOrigin.value;
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* The `allowedOrigins` to use for the child→host penpal handshake: the trusted
|
|
101
|
+
* whitelist when the embedding host origin resolves to a whitelisted domain,
|
|
102
|
+
* otherwise `[]` — fail closed. There is no `["*"]` fallback, so a parent that
|
|
103
|
+
* isn't a whitelisted Trackunit host can never complete the handshake.
|
|
104
|
+
*/
|
|
105
|
+
const getHostConnectorAllowedOrigins = () => getTrustedHostOrigin() ? trustedHostOrigins : [];
|
|
106
|
+
|
|
6
107
|
const doNothingByDefault = () => {
|
|
7
108
|
// do nothing by default
|
|
8
109
|
};
|
|
@@ -95,7 +196,10 @@ const setupHostConnector = (subscribedMethods, channel) => {
|
|
|
95
196
|
...subscribedMethods,
|
|
96
197
|
};
|
|
97
198
|
const connection = penpal.connect({
|
|
98
|
-
|
|
199
|
+
// Pin the handshake to trusted Trackunit host origins. Degrades to ["*"]
|
|
200
|
+
// when the embedding host origin can't be resolved (older SDKs / unknown
|
|
201
|
+
// referrer) — see `trustedHostOrigin.ts`.
|
|
202
|
+
messenger: new penpal.WindowMessenger({ remoteWindow: window.parent, allowedOrigins: getHostConnectorAllowedOrigins() }),
|
|
99
203
|
methods,
|
|
100
204
|
channel,
|
|
101
205
|
timeout: 10000,
|
package/index.esm.js
CHANGED
|
@@ -1,6 +1,107 @@
|
|
|
1
1
|
export * from '@trackunit/iris-app-runtime-core-api';
|
|
2
2
|
import { connect, WindowMessenger } from 'penpal';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Trust anchor for the child (Iris app) side of the penpal bridge.
|
|
6
|
+
*
|
|
7
|
+
* An Iris app runs inside an iframe whose parent is the host (manager) page.
|
|
8
|
+
* Before this module the child connected with `allowedOrigins: ["*"]`, so any
|
|
9
|
+
* page could embed an app and drive it through the bridge. We pin the
|
|
10
|
+
* handshake to a *static whitelist* of Trackunit-controlled host domains — the
|
|
11
|
+
* whitelist, not the attacker-influenceable `document.referrer`, is the trust
|
|
12
|
+
* anchor.
|
|
13
|
+
*
|
|
14
|
+
* `document.referrer` is only used to *decide whether* we are confident enough
|
|
15
|
+
* to enforce the whitelist: the iframe sets `<meta name="referrer"
|
|
16
|
+
* content="strict-origin">`, so the referrer carries the embedding host's
|
|
17
|
+
* origin. When that origin resolves to a whitelisted Trackunit host we pin
|
|
18
|
+
* `allowedOrigins` to the whitelist; in every other case (non-whitelisted,
|
|
19
|
+
* blank, or unreadable referrer) we fail closed with `[]` so no parent can
|
|
20
|
+
* complete the handshake. There is deliberately no `["*"]` fallback — an
|
|
21
|
+
* embedder could blank or spoof its own referrer, so a wildcard fallback would
|
|
22
|
+
* re-open the exact unpinned path this module exists to close.
|
|
23
|
+
*
|
|
24
|
+
* The branded-domain list mirrors `BrandedUrls` in
|
|
25
|
+
* `libs/iris-app-sdk/iris-app-api/src/types/irisAppCspInput.ts`. It is
|
|
26
|
+
* duplicated here (as origin-matching RegExps rather than CSP `https://*.x`
|
|
27
|
+
* patterns) because `iris-app-runtime-core` does not depend on `iris-app-api`
|
|
28
|
+
* and adding that dependency for a static list isn't warranted. Keep the two
|
|
29
|
+
* lists in sync.
|
|
30
|
+
*/
|
|
31
|
+
const BRANDED_HOST_DOMAINS = [
|
|
32
|
+
"trackunit.com",
|
|
33
|
+
"wackerneuson.com",
|
|
34
|
+
"manitou.com",
|
|
35
|
+
"niftylinkmanager.com",
|
|
36
|
+
"skyjack.com",
|
|
37
|
+
"ahernaccess.com",
|
|
38
|
+
"magnith.com",
|
|
39
|
+
"terberg.com",
|
|
40
|
+
"mymecalac.com",
|
|
41
|
+
"delille.be",
|
|
42
|
+
];
|
|
43
|
+
const escapeForRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
44
|
+
/**
|
|
45
|
+
* Matches `https://<domain>` and `https://<any-subdomain>.<domain>` (the
|
|
46
|
+
* leading subdomain segments only contain DNS-safe characters, so a look-alike
|
|
47
|
+
* like `https://trackunit.com.evil.com` does not match).
|
|
48
|
+
*
|
|
49
|
+
* Note: unlike the CSP `https://*.<domain>` form this mirrors, the `(…)*` makes
|
|
50
|
+
* the subdomain optional, so the apex (`https://trackunit.com`) also matches.
|
|
51
|
+
* That is intentional — the apex is Trackunit-controlled — so don't "fix" it to
|
|
52
|
+
* require a subdomain to match the CSP shape.
|
|
53
|
+
*/
|
|
54
|
+
const brandedHostPattern = (domain) => new RegExp(`^https:\\/\\/([a-z0-9-]+\\.)*${escapeForRegExp(domain)}$`);
|
|
55
|
+
/**
|
|
56
|
+
* The static whitelist of trusted host origins, suitable for direct use as
|
|
57
|
+
* penpal `WindowMessenger.allowedOrigins` (which accepts `Array<string |
|
|
58
|
+
* RegExp>` and tests each incoming message origin against the list).
|
|
59
|
+
*/
|
|
60
|
+
const trustedHostOrigins = [
|
|
61
|
+
...BRANDED_HOST_DOMAINS.map(brandedHostPattern),
|
|
62
|
+
// Dev / feature-branch host (e.g. `dev.iris.trackunit.app`).
|
|
63
|
+
/^https:\/\/([a-z0-9-]+\.)*trackunit\.app$/,
|
|
64
|
+
// Local development host.
|
|
65
|
+
/^http:\/\/localhost(:\d+)?$/,
|
|
66
|
+
];
|
|
67
|
+
/**
|
|
68
|
+
* Whether the given window origin belongs to a trusted Trackunit host.
|
|
69
|
+
*/
|
|
70
|
+
const isWhitelistedHostOrigin = (origin) => trustedHostOrigins.some(entry => (typeof entry === "string" ? entry === origin : entry.test(origin)));
|
|
71
|
+
// Capture-once cache. The iframe never switches domain, so the embedding host
|
|
72
|
+
// origin is resolved on first read and reused for the lifetime of the document
|
|
73
|
+
// (it is intentionally not recomputed after a `location.replace`).
|
|
74
|
+
let cachedHostOrigin;
|
|
75
|
+
/**
|
|
76
|
+
* Resolves the embedding host's origin from `document.referrer`, returning it
|
|
77
|
+
* only when it is whitelisted; otherwise `undefined` (non-whitelisted, blank,
|
|
78
|
+
* or unreadable referrer). The value is captured on the first call and
|
|
79
|
+
* memoized for the lifetime of the document.
|
|
80
|
+
*/
|
|
81
|
+
const getTrustedHostOrigin = () => {
|
|
82
|
+
if (cachedHostOrigin) {
|
|
83
|
+
return cachedHostOrigin.value;
|
|
84
|
+
}
|
|
85
|
+
let value;
|
|
86
|
+
try {
|
|
87
|
+
const referrer = document.referrer;
|
|
88
|
+
const origin = referrer ? new URL(referrer).origin : undefined;
|
|
89
|
+
value = origin && isWhitelistedHostOrigin(origin) ? origin : undefined;
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
value = undefined;
|
|
93
|
+
}
|
|
94
|
+
cachedHostOrigin = { value };
|
|
95
|
+
return cachedHostOrigin.value;
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* The `allowedOrigins` to use for the child→host penpal handshake: the trusted
|
|
99
|
+
* whitelist when the embedding host origin resolves to a whitelisted domain,
|
|
100
|
+
* otherwise `[]` — fail closed. There is no `["*"]` fallback, so a parent that
|
|
101
|
+
* isn't a whitelisted Trackunit host can never complete the handshake.
|
|
102
|
+
*/
|
|
103
|
+
const getHostConnectorAllowedOrigins = () => getTrustedHostOrigin() ? trustedHostOrigins : [];
|
|
104
|
+
|
|
4
105
|
const doNothingByDefault = () => {
|
|
5
106
|
// do nothing by default
|
|
6
107
|
};
|
|
@@ -93,7 +194,10 @@ const setupHostConnector = (subscribedMethods, channel) => {
|
|
|
93
194
|
...subscribedMethods,
|
|
94
195
|
};
|
|
95
196
|
const connection = connect({
|
|
96
|
-
|
|
197
|
+
// Pin the handshake to trusted Trackunit host origins. Degrades to ["*"]
|
|
198
|
+
// when the embedding host origin can't be resolved (older SDKs / unknown
|
|
199
|
+
// referrer) — see `trustedHostOrigin.ts`.
|
|
200
|
+
messenger: new WindowMessenger({ remoteWindow: window.parent, allowedOrigins: getHostConnectorAllowedOrigins() }),
|
|
97
201
|
methods,
|
|
98
202
|
channel,
|
|
99
203
|
timeout: 10000,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trackunit/iris-app-runtime-core",
|
|
3
|
-
"version": "1.17.
|
|
3
|
+
"version": "1.17.32-alpha-0424feab298.0",
|
|
4
4
|
"repository": "https://github.com/Trackunit/manager",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.txt",
|
|
6
6
|
"engines": {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"dependencies": {
|
|
10
10
|
"penpal": "^7.0.6",
|
|
11
|
-
"@trackunit/iris-app-runtime-core-api": "1.16.31-alpha-
|
|
11
|
+
"@trackunit/iris-app-runtime-core-api": "1.16.31-alpha-0424feab298.0"
|
|
12
12
|
},
|
|
13
13
|
"module": "./index.esm.js",
|
|
14
14
|
"main": "./index.cjs.js",
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trust anchor for the child (Iris app) side of the penpal bridge.
|
|
3
|
+
*
|
|
4
|
+
* An Iris app runs inside an iframe whose parent is the host (manager) page.
|
|
5
|
+
* Before this module the child connected with `allowedOrigins: ["*"]`, so any
|
|
6
|
+
* page could embed an app and drive it through the bridge. We pin the
|
|
7
|
+
* handshake to a *static whitelist* of Trackunit-controlled host domains — the
|
|
8
|
+
* whitelist, not the attacker-influenceable `document.referrer`, is the trust
|
|
9
|
+
* anchor.
|
|
10
|
+
*
|
|
11
|
+
* `document.referrer` is only used to *decide whether* we are confident enough
|
|
12
|
+
* to enforce the whitelist: the iframe sets `<meta name="referrer"
|
|
13
|
+
* content="strict-origin">`, so the referrer carries the embedding host's
|
|
14
|
+
* origin. When that origin resolves to a whitelisted Trackunit host we pin
|
|
15
|
+
* `allowedOrigins` to the whitelist; in every other case (non-whitelisted,
|
|
16
|
+
* blank, or unreadable referrer) we fail closed with `[]` so no parent can
|
|
17
|
+
* complete the handshake. There is deliberately no `["*"]` fallback — an
|
|
18
|
+
* embedder could blank or spoof its own referrer, so a wildcard fallback would
|
|
19
|
+
* re-open the exact unpinned path this module exists to close.
|
|
20
|
+
*
|
|
21
|
+
* The branded-domain list mirrors `BrandedUrls` in
|
|
22
|
+
* `libs/iris-app-sdk/iris-app-api/src/types/irisAppCspInput.ts`. It is
|
|
23
|
+
* duplicated here (as origin-matching RegExps rather than CSP `https://*.x`
|
|
24
|
+
* patterns) because `iris-app-runtime-core` does not depend on `iris-app-api`
|
|
25
|
+
* and adding that dependency for a static list isn't warranted. Keep the two
|
|
26
|
+
* lists in sync.
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* The static whitelist of trusted host origins, suitable for direct use as
|
|
30
|
+
* penpal `WindowMessenger.allowedOrigins` (which accepts `Array<string |
|
|
31
|
+
* RegExp>` and tests each incoming message origin against the list).
|
|
32
|
+
*/
|
|
33
|
+
export declare const trustedHostOrigins: Array<string | RegExp>;
|
|
34
|
+
/**
|
|
35
|
+
* Whether the given window origin belongs to a trusted Trackunit host.
|
|
36
|
+
*/
|
|
37
|
+
export declare const isWhitelistedHostOrigin: (origin: string) => boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Resolves the embedding host's origin from `document.referrer`, returning it
|
|
40
|
+
* only when it is whitelisted; otherwise `undefined` (non-whitelisted, blank,
|
|
41
|
+
* or unreadable referrer). The value is captured on the first call and
|
|
42
|
+
* memoized for the lifetime of the document.
|
|
43
|
+
*/
|
|
44
|
+
export declare const getTrustedHostOrigin: () => string | undefined;
|
|
45
|
+
/**
|
|
46
|
+
* The `allowedOrigins` to use for the child→host penpal handshake: the trusted
|
|
47
|
+
* whitelist when the embedding host origin resolves to a whitelisted domain,
|
|
48
|
+
* otherwise `[]` — fail closed. There is no `["*"]` fallback, so a parent that
|
|
49
|
+
* isn't a whitelisted Trackunit host can never complete the handshake.
|
|
50
|
+
*/
|
|
51
|
+
export declare const getHostConnectorAllowedOrigins: () => Array<string | RegExp>;
|
|
52
|
+
/**
|
|
53
|
+
* Test-only: clears the capture-once cache so each test can exercise a
|
|
54
|
+
* different `document.referrer`. Not exported from the package index.
|
|
55
|
+
*/
|
|
56
|
+
export declare const resetTrustedHostOriginCacheForTests: () => void;
|