@nocobase/flow-engine 2.1.31 → 2.2.0-alpha.10
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/lib/JSRunner.d.ts +1 -0
- package/lib/JSRunner.js +110 -20
- package/lib/components/FlowContextSelector.js +55 -12
- package/lib/components/FormItem.js +11 -7
- package/lib/components/MobilePopup.style.js +16 -5
- package/lib/components/variables/VariableHybridInput.d.ts +9 -0
- package/lib/components/variables/VariableHybridInput.js +146 -17
- package/lib/components/variables/VariableInput.js +19 -7
- package/lib/components/variables/VariableTag.js +48 -36
- package/lib/components/variables/types.d.ts +21 -0
- package/lib/flowContext.js +11 -1
- package/lib/flowI18n.js +3 -3
- package/lib/runjs-context/helpers.js +12 -5
- package/lib/types.d.ts +3 -1
- package/lib/types.js +1 -0
- package/lib/utils/index.d.ts +0 -1
- package/lib/utils/index.js +0 -11
- package/lib/utils/resolveRunJSObjectValues.js +3 -2
- package/lib/utils/runjsModuleLoader.js +0 -30
- package/package.json +4 -4
- package/src/JSRunner.ts +112 -25
- package/src/__tests__/JSRunner.test.ts +4 -5
- package/src/__tests__/flowI18n.test.ts +11 -0
- package/src/components/FlowContextSelector.tsx +66 -11
- package/src/components/FormItem.tsx +12 -7
- package/src/components/MobilePopup.style.ts +22 -6
- package/src/components/__tests__/FormItem.test.tsx +17 -2
- package/src/components/__tests__/MobilePopup.style.test.tsx +103 -0
- package/src/components/variables/VariableHybridInput.tsx +185 -14
- package/src/components/variables/VariableInput.tsx +32 -7
- package/src/components/variables/VariableTag.tsx +51 -37
- package/src/components/variables/__tests__/FlowContextSelector.test.tsx +60 -3
- package/src/components/variables/__tests__/VariableHybridInput.test.tsx +212 -0
- package/src/components/variables/__tests__/VariableInput.test.tsx +202 -6
- package/src/components/variables/__tests__/VariableTag.test.tsx +80 -0
- package/src/components/variables/types.ts +21 -0
- package/src/flowContext.ts +11 -1
- package/src/flowI18n.ts +8 -3
- package/src/runjs-context/helpers.ts +12 -6
- package/src/types.ts +2 -0
- package/src/utils/index.ts +0 -9
- package/src/utils/resolveRunJSObjectValues.ts +5 -2
- package/src/utils/runjsModuleLoader.ts +0 -32
- package/lib/utils/safeGlobals.d.ts +0 -28
- package/lib/utils/safeGlobals.js +0 -367
- package/src/utils/__tests__/runjsRequireAsyncAutoWhitelist.test.ts +0 -38
- package/src/utils/__tests__/safeGlobals.test.ts +0 -106
- package/src/utils/safeGlobals.ts +0 -406
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* This file is part of the NocoBase (R) project.
|
|
3
|
-
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
-
* Authors: NocoBase Team.
|
|
5
|
-
*
|
|
6
|
-
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
-
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { beforeEach, describe, expect, it } from 'vitest';
|
|
11
|
-
import {
|
|
12
|
-
__resetRunJSSafeGlobalsRegistryForTests,
|
|
13
|
-
createSafeDocument,
|
|
14
|
-
createSafeNavigator,
|
|
15
|
-
createSafeWindow,
|
|
16
|
-
registerRunJSSafeDocumentGlobals,
|
|
17
|
-
registerRunJSSafeWindowGlobals,
|
|
18
|
-
} from '../safeGlobals';
|
|
19
|
-
|
|
20
|
-
beforeEach(() => {
|
|
21
|
-
__resetRunJSSafeGlobalsRegistryForTests();
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
describe('safeGlobals', () => {
|
|
25
|
-
it('createSafeWindow exposes only allowed globals and extras', () => {
|
|
26
|
-
const win: any = createSafeWindow({ foo: 123 });
|
|
27
|
-
expect(typeof win.setTimeout).toBe('function');
|
|
28
|
-
expect(win.console).toBeDefined();
|
|
29
|
-
expect(win.foo).toBe(123);
|
|
30
|
-
expect(new win.FormData()).toBeInstanceOf(window.FormData);
|
|
31
|
-
if (typeof window.Blob !== 'undefined') {
|
|
32
|
-
expect(typeof win.Blob).toBe('function');
|
|
33
|
-
expect(new win.Blob(['x'])).toBeInstanceOf(window.Blob);
|
|
34
|
-
}
|
|
35
|
-
if (typeof window.URL !== 'undefined') {
|
|
36
|
-
expect(win.URL).toBe(window.URL);
|
|
37
|
-
expect(typeof win.URL.createObjectURL).toBe('function');
|
|
38
|
-
}
|
|
39
|
-
// access to location proxy is allowed, but sensitive props throw
|
|
40
|
-
expect(() => win.location.href).toThrow(/not allowed/);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it('createSafeWindow allows writing new props and reading them back', () => {
|
|
44
|
-
const win: any = createSafeWindow();
|
|
45
|
-
win.someLib = { ok: true };
|
|
46
|
-
expect(win.someLib).toEqual({ ok: true });
|
|
47
|
-
expect('someLib' in win).toBe(true);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('createSafeWindow can access dynamically registered globals from real window', () => {
|
|
51
|
-
const key = '__nb_safe_window_global__';
|
|
52
|
-
(window as any)[key] = { v: 1 };
|
|
53
|
-
registerRunJSSafeWindowGlobals([key]);
|
|
54
|
-
|
|
55
|
-
const win: any = createSafeWindow();
|
|
56
|
-
expect(win[key]).toEqual({ v: 1 });
|
|
57
|
-
|
|
58
|
-
delete (window as any)[key];
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it('createSafeDocument exposes whitelisted methods and extras', () => {
|
|
62
|
-
const doc: any = createSafeDocument({ bar: true });
|
|
63
|
-
expect(typeof doc.createElement).toBe('function');
|
|
64
|
-
expect(doc.bar).toBe(true);
|
|
65
|
-
expect(() => doc.cookie).toThrow(/not allowed/);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it('createSafeDocument allows writing new props and reading them back', () => {
|
|
69
|
-
const doc: any = createSafeDocument();
|
|
70
|
-
doc.someLib = { ok: true };
|
|
71
|
-
expect(doc.someLib).toEqual({ ok: true });
|
|
72
|
-
expect('someLib' in doc).toBe(true);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it('createSafeDocument can access dynamically registered globals from real document', () => {
|
|
76
|
-
const key = '__nb_safe_document_global__';
|
|
77
|
-
(document as any)[key] = 123;
|
|
78
|
-
registerRunJSSafeDocumentGlobals([key]);
|
|
79
|
-
|
|
80
|
-
const doc: any = createSafeDocument();
|
|
81
|
-
expect(doc[key]).toBe(123);
|
|
82
|
-
|
|
83
|
-
delete (document as any)[key];
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it('createSafeNavigator exposes limited props and guards others', () => {
|
|
87
|
-
const nav: any = createSafeNavigator();
|
|
88
|
-
// clipboard object should always exist
|
|
89
|
-
expect(typeof nav.clipboard).toBe('object');
|
|
90
|
-
// writeText may or may not exist depending on environment
|
|
91
|
-
if (typeof navigator !== 'undefined' && (navigator as any).clipboard?.writeText) {
|
|
92
|
-
expect(typeof nav.clipboard.writeText).toBe('function');
|
|
93
|
-
} else {
|
|
94
|
-
expect(typeof nav.clipboard.writeText === 'undefined' || typeof nav.clipboard.writeText === 'function').toBe(
|
|
95
|
-
true,
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
// readable properties
|
|
99
|
-
expect(() => void nav.onLine).not.toThrow();
|
|
100
|
-
expect(() => void nav.language).not.toThrow();
|
|
101
|
-
expect(() => void nav.languages).not.toThrow();
|
|
102
|
-
// blocked properties
|
|
103
|
-
expect(() => (nav as any).geolocation).toThrow(/not allowed/);
|
|
104
|
-
expect(() => (nav as any).userAgent).toThrow(/not allowed/);
|
|
105
|
-
});
|
|
106
|
-
});
|
package/src/utils/safeGlobals.ts
DELETED
|
@@ -1,406 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* This file is part of the NocoBase (R) project.
|
|
3
|
-
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
-
* Authors: NocoBase Team.
|
|
5
|
-
*
|
|
6
|
-
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
-
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* 统一的安全全局对象代理:window/document/navigator
|
|
12
|
-
* - window:仅允许常用的定时器、console、Math、Date、FormData、Blob、URL、addEventListener、open(安全包装)、location(安全代理)
|
|
13
|
-
* - document:仅允许 createElement/querySelector/querySelectorAll
|
|
14
|
-
* - navigator:仅提供极少量低风险能力(clipboard.writeText、onLine、language、languages)
|
|
15
|
-
* - 不允许随意访问未声明的属性,最小权限原则
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
type RunJSSafeGlobalsRegistry = {
|
|
19
|
-
windowAllow: Set<string>;
|
|
20
|
-
documentAllow: Set<string>;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
function getRunJSSafeGlobalsRegistry(): RunJSSafeGlobalsRegistry {
|
|
24
|
-
const g: any = globalThis as any;
|
|
25
|
-
if (g.__nocobaseRunJSSafeGlobalsRegistry?.windowAllow && g.__nocobaseRunJSSafeGlobalsRegistry?.documentAllow) {
|
|
26
|
-
return g.__nocobaseRunJSSafeGlobalsRegistry as RunJSSafeGlobalsRegistry;
|
|
27
|
-
}
|
|
28
|
-
const reg: RunJSSafeGlobalsRegistry = {
|
|
29
|
-
windowAllow: new Set<string>(),
|
|
30
|
-
documentAllow: new Set<string>(),
|
|
31
|
-
};
|
|
32
|
-
g.__nocobaseRunJSSafeGlobalsRegistry = reg;
|
|
33
|
-
return reg;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function registerRunJSSafeWindowGlobals(keys: Iterable<string> | null | undefined): void {
|
|
37
|
-
if (!keys) return;
|
|
38
|
-
const reg = getRunJSSafeGlobalsRegistry();
|
|
39
|
-
for (const k of keys) {
|
|
40
|
-
if (typeof k !== 'string') continue;
|
|
41
|
-
const key = k.trim();
|
|
42
|
-
if (!key) continue;
|
|
43
|
-
reg.windowAllow.add(key);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export function registerRunJSSafeDocumentGlobals(keys: Iterable<string> | null | undefined): void {
|
|
48
|
-
if (!keys) return;
|
|
49
|
-
const reg = getRunJSSafeGlobalsRegistry();
|
|
50
|
-
for (const k of keys) {
|
|
51
|
-
if (typeof k !== 'string') continue;
|
|
52
|
-
const key = k.trim();
|
|
53
|
-
if (!key) continue;
|
|
54
|
-
reg.documentAllow.add(key);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export function __resetRunJSSafeGlobalsRegistryForTests(): void {
|
|
59
|
-
const g: any = globalThis as any;
|
|
60
|
-
if (g.__nocobaseRunJSSafeGlobalsRegistry) {
|
|
61
|
-
try {
|
|
62
|
-
g.__nocobaseRunJSSafeGlobalsRegistry.windowAllow?.clear?.();
|
|
63
|
-
g.__nocobaseRunJSSafeGlobalsRegistry.documentAllow?.clear?.();
|
|
64
|
-
} catch {
|
|
65
|
-
// ignore
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function isAllowedDynamicWindowKey(key: string): boolean {
|
|
71
|
-
return getRunJSSafeGlobalsRegistry().windowAllow.has(key);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function isAllowedDynamicDocumentKey(key: string): boolean {
|
|
75
|
-
return getRunJSSafeGlobalsRegistry().documentAllow.has(key);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export function createSafeWindow(extra?: Record<string, any>) {
|
|
79
|
-
// 解析相对 URL 使用脱敏 base(不含 query/hash),避免在解析时泄露敏感信息
|
|
80
|
-
const getSafeBaseHref = () => `${window.location.origin}${window.location.pathname}`;
|
|
81
|
-
|
|
82
|
-
// 安全的 window.open 代理
|
|
83
|
-
const safeOpen = (url: string, target?: string, features?: string): Window | null => {
|
|
84
|
-
// 仅允许 http/https 和 about:blank
|
|
85
|
-
const isSafeUrl = (u: string): boolean => {
|
|
86
|
-
try {
|
|
87
|
-
const parsed = new URL(u, getSafeBaseHref()); // 使用脱敏 base
|
|
88
|
-
const protocol = parsed.protocol.toLowerCase();
|
|
89
|
-
if (protocol === 'about:') return parsed.href === 'about:blank';
|
|
90
|
-
return protocol === 'http:' || protocol === 'https:';
|
|
91
|
-
} catch {
|
|
92
|
-
return false;
|
|
93
|
-
}
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
if (!isSafeUrl(url)) {
|
|
97
|
-
throw new Error('Unsafe URL: window.open only allows http/https/about:blank.');
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// 强制在新标签页打开,避免覆盖当前页或父窗口
|
|
101
|
-
const sanitizedTarget = '_blank';
|
|
102
|
-
|
|
103
|
-
// 合并并强制加上安全特性
|
|
104
|
-
const enforceFeatures = (f?: string): string => {
|
|
105
|
-
const set = new Set<string>();
|
|
106
|
-
if (f) {
|
|
107
|
-
f.split(',')
|
|
108
|
-
.map((s) => s.trim())
|
|
109
|
-
.filter(Boolean)
|
|
110
|
-
.forEach((part) => {
|
|
111
|
-
const key = part.split('=')[0].trim().toLowerCase();
|
|
112
|
-
if (key !== 'noopener' && key !== 'noreferrer') set.add(part);
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
set.add('noopener');
|
|
116
|
-
set.add('noreferrer');
|
|
117
|
-
return Array.from(set).join(',');
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
const sanitizedFeatures = enforceFeatures(features);
|
|
121
|
-
|
|
122
|
-
// 调用原生 window.open
|
|
123
|
-
const newWin = window.open.call(window, url, sanitizedTarget, sanitizedFeatures);
|
|
124
|
-
|
|
125
|
-
// 双重保险:禁用 opener(部分浏览器/场景下 features 可能不生效)
|
|
126
|
-
if (newWin && 'opener' in newWin) {
|
|
127
|
-
try {
|
|
128
|
-
(newWin as any).opener = null;
|
|
129
|
-
} catch {
|
|
130
|
-
// ignore
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
return newWin;
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
// 同源在当前页导航;跨域强制用新标签页 + noreferrer/noopener
|
|
137
|
-
const guardedNavigate = (rawUrl: string, opts?: { replace?: boolean }) => {
|
|
138
|
-
const parsed = new URL(rawUrl, getSafeBaseHref());
|
|
139
|
-
const protocol = parsed.protocol.toLowerCase();
|
|
140
|
-
|
|
141
|
-
const isAboutBlank = protocol === 'about:' && parsed.href === 'about:blank';
|
|
142
|
-
const isHttp = protocol === 'http:' || protocol === 'https:';
|
|
143
|
-
if (!isHttp && !isAboutBlank) {
|
|
144
|
-
throw new Error('Unsafe URL: only http/https/about:blank are allowed.');
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
if (isAboutBlank) {
|
|
148
|
-
return opts?.replace ? window.location.replace('about:blank') : window.location.assign('about:blank');
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const sameOrigin =
|
|
152
|
-
parsed.protocol === window.location.protocol &&
|
|
153
|
-
parsed.hostname === window.location.hostname &&
|
|
154
|
-
parsed.port === window.location.port;
|
|
155
|
-
|
|
156
|
-
if (sameOrigin) {
|
|
157
|
-
return opts?.replace ? window.location.replace(parsed.href) : window.location.assign(parsed.href);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const win = safeOpen(parsed.href);
|
|
161
|
-
if (!win) throw new Error('Popup blocked: cross-origin navigation is opened in a new tab.');
|
|
162
|
-
};
|
|
163
|
-
|
|
164
|
-
// 只读/脱敏的 location 代理;支持安全的 href 赋值和 assign/replace
|
|
165
|
-
const safeLocation = new Proxy(
|
|
166
|
-
{},
|
|
167
|
-
{
|
|
168
|
-
get(_t, prop: string) {
|
|
169
|
-
switch (prop) {
|
|
170
|
-
case 'origin':
|
|
171
|
-
return window.location.origin;
|
|
172
|
-
case 'protocol':
|
|
173
|
-
return window.location.protocol;
|
|
174
|
-
case 'host':
|
|
175
|
-
return window.location.host;
|
|
176
|
-
case 'hostname':
|
|
177
|
-
return window.location.hostname;
|
|
178
|
-
case 'port':
|
|
179
|
-
return window.location.port;
|
|
180
|
-
case 'pathname':
|
|
181
|
-
return window.location.pathname;
|
|
182
|
-
case 'assign':
|
|
183
|
-
return (u: string) => guardedNavigate(u, { replace: false });
|
|
184
|
-
case 'replace':
|
|
185
|
-
return (u: string) => guardedNavigate(u, { replace: true });
|
|
186
|
-
case 'reload':
|
|
187
|
-
return window.location.reload.bind(window.location);
|
|
188
|
-
case 'href':
|
|
189
|
-
throw new Error('Reading location.href is not allowed.');
|
|
190
|
-
default:
|
|
191
|
-
throw new Error(`Access to location property "${prop}" is not allowed.`);
|
|
192
|
-
}
|
|
193
|
-
},
|
|
194
|
-
set(_t, prop: string, value: any) {
|
|
195
|
-
if (prop === 'href') {
|
|
196
|
-
guardedNavigate(String(value), { replace: false });
|
|
197
|
-
return true;
|
|
198
|
-
}
|
|
199
|
-
throw new Error('Mutation on location is not allowed.');
|
|
200
|
-
},
|
|
201
|
-
},
|
|
202
|
-
);
|
|
203
|
-
|
|
204
|
-
const allowedGlobals: Record<string, any> = {
|
|
205
|
-
// 需绑定到原始 window,避免严格模式下触发 Illegal invocation
|
|
206
|
-
setTimeout: window.setTimeout.bind(window),
|
|
207
|
-
clearTimeout: window.clearTimeout.bind(window),
|
|
208
|
-
setInterval: window.setInterval.bind(window),
|
|
209
|
-
clearInterval: window.clearInterval.bind(window),
|
|
210
|
-
console,
|
|
211
|
-
Math,
|
|
212
|
-
Date,
|
|
213
|
-
FormData,
|
|
214
|
-
...(typeof Blob !== 'undefined' ? { Blob } : {}),
|
|
215
|
-
...(typeof URL !== 'undefined' ? { URL } : {}),
|
|
216
|
-
// 事件侦听仅绑定到真实 window,便于少量需要的全局监听
|
|
217
|
-
addEventListener: addEventListener.bind(window),
|
|
218
|
-
// 安全的 window.open 代理
|
|
219
|
-
open: safeOpen,
|
|
220
|
-
// 安全的 location 代理
|
|
221
|
-
location: safeLocation,
|
|
222
|
-
...(extra || {}),
|
|
223
|
-
};
|
|
224
|
-
|
|
225
|
-
const target: Record<string, any> = Object.create(null);
|
|
226
|
-
|
|
227
|
-
return new Proxy(target, {
|
|
228
|
-
get(t, prop: string | symbol) {
|
|
229
|
-
if (typeof prop !== 'string') {
|
|
230
|
-
return Reflect.get(t, prop);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
if (prop in allowedGlobals) return allowedGlobals[prop];
|
|
234
|
-
if (Object.prototype.hasOwnProperty.call(t, prop)) return (t as any)[prop];
|
|
235
|
-
if (isAllowedDynamicWindowKey(prop)) {
|
|
236
|
-
const v = (window as any)[prop];
|
|
237
|
-
// Bind functions to the real window to avoid Illegal invocation
|
|
238
|
-
if (typeof v === 'function') return v.bind(window);
|
|
239
|
-
return v;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
throw new Error(`Access to global property "${prop}" is not allowed.`);
|
|
243
|
-
},
|
|
244
|
-
set(t, prop: string | symbol, value: any) {
|
|
245
|
-
if (typeof prop !== 'string') {
|
|
246
|
-
Reflect.set(t, prop, value);
|
|
247
|
-
return true;
|
|
248
|
-
}
|
|
249
|
-
if (prop in allowedGlobals) {
|
|
250
|
-
throw new Error(`Mutation of global property "${prop}" is not allowed.`);
|
|
251
|
-
}
|
|
252
|
-
(t as any)[prop] = value;
|
|
253
|
-
return true;
|
|
254
|
-
},
|
|
255
|
-
has(t, prop: string | symbol) {
|
|
256
|
-
if (typeof prop !== 'string') return Reflect.has(t, prop);
|
|
257
|
-
if (prop in allowedGlobals) return true;
|
|
258
|
-
if (Object.prototype.hasOwnProperty.call(t, prop)) return true;
|
|
259
|
-
if (isAllowedDynamicWindowKey(prop)) return true;
|
|
260
|
-
return false;
|
|
261
|
-
},
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
export function createSafeDocument(extra?: Record<string, any>) {
|
|
266
|
-
const allowed: Record<string, any> = {
|
|
267
|
-
createElement: document.createElement.bind(document),
|
|
268
|
-
querySelector: document.querySelector.bind(document),
|
|
269
|
-
querySelectorAll: document.querySelectorAll.bind(document),
|
|
270
|
-
...(extra || {}),
|
|
271
|
-
};
|
|
272
|
-
const target: Record<string, any> = Object.create(null);
|
|
273
|
-
return new Proxy(target, {
|
|
274
|
-
get(t, prop: string | symbol) {
|
|
275
|
-
if (typeof prop !== 'string') {
|
|
276
|
-
return Reflect.get(t, prop);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
if (prop in allowed) return allowed[prop];
|
|
280
|
-
if (Object.prototype.hasOwnProperty.call(t, prop)) return (t as any)[prop];
|
|
281
|
-
if (isAllowedDynamicDocumentKey(prop)) {
|
|
282
|
-
const v = (document as any)[prop];
|
|
283
|
-
// Bind functions to the real document to avoid Illegal invocation
|
|
284
|
-
if (typeof v === 'function') return v.bind(document);
|
|
285
|
-
return v;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
throw new Error(`Access to document property "${prop}" is not allowed.`);
|
|
289
|
-
},
|
|
290
|
-
set(t, prop: string | symbol, value: any) {
|
|
291
|
-
if (typeof prop !== 'string') {
|
|
292
|
-
Reflect.set(t, prop, value);
|
|
293
|
-
return true;
|
|
294
|
-
}
|
|
295
|
-
if (prop in allowed) {
|
|
296
|
-
throw new Error(`Mutation of document property "${prop}" is not allowed.`);
|
|
297
|
-
}
|
|
298
|
-
(t as any)[prop] = value;
|
|
299
|
-
return true;
|
|
300
|
-
},
|
|
301
|
-
has(t, prop: string | symbol) {
|
|
302
|
-
if (typeof prop !== 'string') return Reflect.has(t, prop);
|
|
303
|
-
if (prop in allowed) return true;
|
|
304
|
-
if (Object.prototype.hasOwnProperty.call(t, prop)) return true;
|
|
305
|
-
if (isAllowedDynamicDocumentKey(prop)) return true;
|
|
306
|
-
return false;
|
|
307
|
-
},
|
|
308
|
-
});
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
export function createSafeNavigator(extra?: Record<string, any>) {
|
|
312
|
-
const nav: any = (typeof window !== 'undefined' && window.navigator) || undefined;
|
|
313
|
-
|
|
314
|
-
// 始终提供 clipboard 对象,避免可选链访问时抛错
|
|
315
|
-
const clipboard: Record<string, any> = {};
|
|
316
|
-
const writeText = nav?.clipboard?.writeText;
|
|
317
|
-
if (typeof writeText === 'function') {
|
|
318
|
-
clipboard.writeText = writeText.bind(nav.clipboard);
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
const allowed: Record<string, any> = {
|
|
322
|
-
clipboard,
|
|
323
|
-
};
|
|
324
|
-
|
|
325
|
-
// 只读常用标识,避免泄露更多指纹信息
|
|
326
|
-
Object.defineProperty(allowed, 'onLine', {
|
|
327
|
-
get: () => !!nav?.onLine,
|
|
328
|
-
enumerable: true,
|
|
329
|
-
configurable: false,
|
|
330
|
-
});
|
|
331
|
-
Object.defineProperty(allowed, 'language', {
|
|
332
|
-
get: () => nav?.language,
|
|
333
|
-
enumerable: true,
|
|
334
|
-
configurable: false,
|
|
335
|
-
});
|
|
336
|
-
Object.defineProperty(allowed, 'languages', {
|
|
337
|
-
get: () => (nav?.languages ? [...nav.languages] : undefined),
|
|
338
|
-
enumerable: true,
|
|
339
|
-
configurable: false,
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
// 允许额外注入(例如自定义能力的受控暴露)
|
|
343
|
-
Object.assign(allowed, extra || {});
|
|
344
|
-
|
|
345
|
-
return new Proxy(
|
|
346
|
-
{},
|
|
347
|
-
{
|
|
348
|
-
get(_t, prop: string) {
|
|
349
|
-
if (prop in allowed) return (allowed as any)[prop];
|
|
350
|
-
throw new Error(`Access to navigator property "${String(prop)}" is not allowed.`);
|
|
351
|
-
},
|
|
352
|
-
},
|
|
353
|
-
);
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
/**
|
|
357
|
-
* Create a safe globals object for RunJS execution.
|
|
358
|
-
*
|
|
359
|
-
* - Always tries to provide `navigator`
|
|
360
|
-
* - Best-effort provides `window` and `document` in browser environments
|
|
361
|
-
* - Never throws (so callers can decide how to handle missing globals)
|
|
362
|
-
*/
|
|
363
|
-
export function createSafeRunJSGlobals(extraGlobals?: Record<string, any>): Record<string, any> {
|
|
364
|
-
const globals: Record<string, any> = {};
|
|
365
|
-
|
|
366
|
-
try {
|
|
367
|
-
const navigator = createSafeNavigator();
|
|
368
|
-
globals.navigator = navigator;
|
|
369
|
-
try {
|
|
370
|
-
globals.window = createSafeWindow({ navigator });
|
|
371
|
-
} catch {
|
|
372
|
-
// ignore when window is not available (e.g. SSR/tests)
|
|
373
|
-
}
|
|
374
|
-
} catch {
|
|
375
|
-
// ignore
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
try {
|
|
379
|
-
globals.document = createSafeDocument();
|
|
380
|
-
} catch {
|
|
381
|
-
// ignore when document is not available (e.g. SSR/tests)
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
return extraGlobals ? { ...globals, ...extraGlobals } : globals;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
/**
|
|
388
|
-
* Execute RunJS with safe globals (window/document/navigator).
|
|
389
|
-
*
|
|
390
|
-
* Keeps `this` binding by calling `ctx.runjs(...)` instead of passing bare function references.
|
|
391
|
-
*/
|
|
392
|
-
export async function runjsWithSafeGlobals(
|
|
393
|
-
ctx: unknown,
|
|
394
|
-
code: string,
|
|
395
|
-
options?: any,
|
|
396
|
-
extraGlobals?: Record<string, any>,
|
|
397
|
-
): Promise<any> {
|
|
398
|
-
if (!ctx || (typeof ctx !== 'object' && typeof ctx !== 'function')) return undefined;
|
|
399
|
-
const runjs = (ctx as { runjs?: unknown }).runjs;
|
|
400
|
-
if (typeof runjs !== 'function') return undefined;
|
|
401
|
-
return (ctx as { runjs: (code: string, variables?: Record<string, any>, options?: any) => Promise<any> }).runjs(
|
|
402
|
-
code,
|
|
403
|
-
createSafeRunJSGlobals(extraGlobals),
|
|
404
|
-
options,
|
|
405
|
-
);
|
|
406
|
-
}
|