@nocobase/flow-engine 2.0.0-beta.21 → 2.0.0-beta.22
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.js +23 -1
- package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.js +3 -3
- package/lib/flowContext.js +2 -1
- package/lib/resources/sqlResource.d.ts +3 -3
- package/lib/utils/safeGlobals.js +2 -0
- package/package.json +4 -4
- package/src/JSRunner.ts +29 -1
- package/src/__tests__/JSRunner.test.ts +64 -0
- package/src/__tests__/flowContext.test.ts +78 -0
- package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +3 -3
- package/src/flowContext.ts +2 -1
- package/src/resources/sqlResource.ts +3 -3
- package/src/utils/__tests__/safeGlobals.test.ts +8 -0
- package/src/utils/safeGlobals.ts +3 -1
package/lib/JSRunner.js
CHANGED
|
@@ -35,6 +35,7 @@ const _JSRunner = class _JSRunner {
|
|
|
35
35
|
globals;
|
|
36
36
|
timeoutMs;
|
|
37
37
|
constructor(options = {}) {
|
|
38
|
+
var _a, _b;
|
|
38
39
|
const bindWindowFn = /* @__PURE__ */ __name((key) => {
|
|
39
40
|
if (typeof window !== "undefined" && typeof window[key] === "function") {
|
|
40
41
|
return window[key].bind(window);
|
|
@@ -42,13 +43,34 @@ const _JSRunner = class _JSRunner {
|
|
|
42
43
|
const fn = globalThis[key];
|
|
43
44
|
return typeof fn === "function" ? fn.bind(globalThis) : fn;
|
|
44
45
|
}, "bindWindowFn");
|
|
46
|
+
const providedGlobals = options.globals || {};
|
|
47
|
+
const liftedGlobals = {};
|
|
48
|
+
if (!Object.prototype.hasOwnProperty.call(providedGlobals, "Blob")) {
|
|
49
|
+
try {
|
|
50
|
+
const blobCtor = (_a = providedGlobals.window) == null ? void 0 : _a.Blob;
|
|
51
|
+
if (typeof blobCtor !== "undefined") {
|
|
52
|
+
liftedGlobals.Blob = blobCtor;
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (!Object.prototype.hasOwnProperty.call(providedGlobals, "URL")) {
|
|
58
|
+
try {
|
|
59
|
+
const urlCtor = (_b = providedGlobals.window) == null ? void 0 : _b.URL;
|
|
60
|
+
if (typeof urlCtor !== "undefined") {
|
|
61
|
+
liftedGlobals.URL = urlCtor;
|
|
62
|
+
}
|
|
63
|
+
} catch {
|
|
64
|
+
}
|
|
65
|
+
}
|
|
45
66
|
this.globals = {
|
|
46
67
|
console,
|
|
47
68
|
setTimeout: bindWindowFn("setTimeout"),
|
|
48
69
|
clearTimeout: bindWindowFn("clearTimeout"),
|
|
49
70
|
setInterval: bindWindowFn("setInterval"),
|
|
50
71
|
clearInterval: bindWindowFn("clearInterval"),
|
|
51
|
-
...
|
|
72
|
+
...liftedGlobals,
|
|
73
|
+
...providedGlobals
|
|
52
74
|
};
|
|
53
75
|
this.timeoutMs = options.timeoutMs ?? 5e3;
|
|
54
76
|
}
|
|
@@ -521,7 +521,7 @@ const DefaultSettingsIcon = /* @__PURE__ */ __name(({
|
|
|
521
521
|
};
|
|
522
522
|
items.push({
|
|
523
523
|
key: uniqueKey,
|
|
524
|
-
label: /* @__PURE__ */ import_react.default.createElement(MenuLabelItem, { title:
|
|
524
|
+
label: /* @__PURE__ */ import_react.default.createElement(MenuLabelItem, { title: stepInfo.title, uiMode, itemProps })
|
|
525
525
|
});
|
|
526
526
|
});
|
|
527
527
|
if (flow.options.divider === "bottom") {
|
|
@@ -555,7 +555,7 @@ const DefaultSettingsIcon = /* @__PURE__ */ __name(({
|
|
|
555
555
|
const uniqueKey = generateUniqueKey(`${flow.key}:${stepInfo.stepKey}`);
|
|
556
556
|
items.push({
|
|
557
557
|
key: uniqueKey,
|
|
558
|
-
label:
|
|
558
|
+
label: stepInfo.title
|
|
559
559
|
});
|
|
560
560
|
});
|
|
561
561
|
});
|
|
@@ -567,7 +567,7 @@ const DefaultSettingsIcon = /* @__PURE__ */ __name(({
|
|
|
567
567
|
const uniqueKey = generateUniqueKey(`${modelKey}:${flow.key}:${stepInfo.stepKey}`);
|
|
568
568
|
subMenuChildren.push({
|
|
569
569
|
key: uniqueKey,
|
|
570
|
-
label:
|
|
570
|
+
label: stepInfo.title
|
|
571
571
|
});
|
|
572
572
|
});
|
|
573
573
|
});
|
package/lib/flowContext.js
CHANGED
|
@@ -830,7 +830,8 @@ const _FlowEngineContext = class _FlowEngineContext extends BaseFlowEngineContex
|
|
|
830
830
|
value: this.engine
|
|
831
831
|
});
|
|
832
832
|
this.defineProperty("sql", {
|
|
833
|
-
get: /* @__PURE__ */ __name(() => new import_resources.FlowSQLRepository(
|
|
833
|
+
get: /* @__PURE__ */ __name((ctx) => new import_resources.FlowSQLRepository(ctx), "get"),
|
|
834
|
+
cache: false
|
|
834
835
|
});
|
|
835
836
|
this.defineProperty("dataSourceManager", {
|
|
836
837
|
value: dataSourceManager
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
|
-
import { FlowEngineContext } from '../flowContext';
|
|
9
|
+
import { FlowContext, FlowEngineContext } from '../flowContext';
|
|
10
10
|
import { BaseRecordResource } from './baseRecordResource';
|
|
11
11
|
type SQLRunOptions = {
|
|
12
12
|
bind?: Record<string, any>;
|
|
@@ -20,8 +20,8 @@ type SQLSaveOptions = {
|
|
|
20
20
|
dataSourceKey?: string;
|
|
21
21
|
};
|
|
22
22
|
export declare class FlowSQLRepository {
|
|
23
|
-
protected ctx:
|
|
24
|
-
constructor(ctx:
|
|
23
|
+
protected ctx: FlowContext;
|
|
24
|
+
constructor(ctx: FlowContext);
|
|
25
25
|
run(sql: string, options?: SQLRunOptions): Promise<any>;
|
|
26
26
|
save(data: SQLSaveOptions): Promise<void>;
|
|
27
27
|
runById(uid: string, options?: SQLRunOptions): Promise<any>;
|
package/lib/utils/safeGlobals.js
CHANGED
|
@@ -196,6 +196,8 @@ function createSafeWindow(extra) {
|
|
|
196
196
|
Math,
|
|
197
197
|
Date,
|
|
198
198
|
FormData,
|
|
199
|
+
...typeof Blob !== "undefined" ? { Blob } : {},
|
|
200
|
+
...typeof URL !== "undefined" ? { URL } : {},
|
|
199
201
|
// 事件侦听仅绑定到真实 window,便于少量需要的全局监听
|
|
200
202
|
addEventListener: addEventListener.bind(window),
|
|
201
203
|
// 安全的 window.open 代理
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/flow-engine",
|
|
3
|
-
"version": "2.0.0-beta.
|
|
3
|
+
"version": "2.0.0-beta.22",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "A standalone flow engine for NocoBase, managing workflows, models, and actions.",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
"dependencies": {
|
|
9
9
|
"@formily/antd-v5": "1.x",
|
|
10
10
|
"@formily/reactive": "2.x",
|
|
11
|
-
"@nocobase/sdk": "2.0.0-beta.
|
|
12
|
-
"@nocobase/shared": "2.0.0-beta.
|
|
11
|
+
"@nocobase/sdk": "2.0.0-beta.22",
|
|
12
|
+
"@nocobase/shared": "2.0.0-beta.22",
|
|
13
13
|
"ahooks": "^3.7.2",
|
|
14
14
|
"dayjs": "^1.11.9",
|
|
15
15
|
"dompurify": "^3.0.2",
|
|
@@ -36,5 +36,5 @@
|
|
|
36
36
|
],
|
|
37
37
|
"author": "NocoBase Team",
|
|
38
38
|
"license": "AGPL-3.0",
|
|
39
|
-
"gitHead": "
|
|
39
|
+
"gitHead": "a49874a59e48eda08c1a1570466e8baff00b8a24"
|
|
40
40
|
}
|
package/src/JSRunner.ts
CHANGED
|
@@ -34,13 +34,41 @@ export class JSRunner {
|
|
|
34
34
|
return typeof fn === 'function' ? fn.bind(globalThis) : fn;
|
|
35
35
|
};
|
|
36
36
|
|
|
37
|
+
const providedGlobals = options.globals || {};
|
|
38
|
+
const liftedGlobals: Record<string, any> = {};
|
|
39
|
+
|
|
40
|
+
// Auto-lift selected globals from safe window into top-level sandbox globals
|
|
41
|
+
// so user code can access them directly (e.g. `new Blob(...)`).
|
|
42
|
+
if (!Object.prototype.hasOwnProperty.call(providedGlobals, 'Blob')) {
|
|
43
|
+
try {
|
|
44
|
+
const blobCtor = (providedGlobals as any).window?.Blob;
|
|
45
|
+
if (typeof blobCtor !== 'undefined') {
|
|
46
|
+
liftedGlobals.Blob = blobCtor;
|
|
47
|
+
}
|
|
48
|
+
} catch {
|
|
49
|
+
// ignore when window proxy blocks property access
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!Object.prototype.hasOwnProperty.call(providedGlobals, 'URL')) {
|
|
54
|
+
try {
|
|
55
|
+
const urlCtor = (providedGlobals as any).window?.URL;
|
|
56
|
+
if (typeof urlCtor !== 'undefined') {
|
|
57
|
+
liftedGlobals.URL = urlCtor;
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
// ignore when window proxy blocks property access
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
37
64
|
this.globals = {
|
|
38
65
|
console,
|
|
39
66
|
setTimeout: bindWindowFn('setTimeout'),
|
|
40
67
|
clearTimeout: bindWindowFn('clearTimeout'),
|
|
41
68
|
setInterval: bindWindowFn('setInterval'),
|
|
42
69
|
clearInterval: bindWindowFn('clearInterval'),
|
|
43
|
-
...
|
|
70
|
+
...liftedGlobals,
|
|
71
|
+
...providedGlobals,
|
|
44
72
|
};
|
|
45
73
|
this.timeoutMs = options.timeoutMs ?? 5000; // 默认 5 秒超时
|
|
46
74
|
}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
11
11
|
import { JSRunner } from '../JSRunner';
|
|
12
|
+
import { createSafeWindow } from '../utils';
|
|
12
13
|
|
|
13
14
|
describe('JSRunner', () => {
|
|
14
15
|
let originalSearch: string;
|
|
@@ -48,6 +49,69 @@ describe('JSRunner', () => {
|
|
|
48
49
|
expect(res2.value).toBe('baz');
|
|
49
50
|
});
|
|
50
51
|
|
|
52
|
+
it('auto-lifts Blob from injected window to top-level globals', async () => {
|
|
53
|
+
if (typeof Blob === 'undefined') {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const runner = new JSRunner({
|
|
58
|
+
globals: {
|
|
59
|
+
window: createSafeWindow(),
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const result = await runner.run('return new Blob(["x"]).size');
|
|
64
|
+
expect(result.success).toBe(true);
|
|
65
|
+
expect(result.value).toBe(1);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('keeps explicit globals.Blob higher priority than auto-lifted Blob', async () => {
|
|
69
|
+
const explicitBlob = function ExplicitBlob(this: any, chunks: any[]) {
|
|
70
|
+
this.size = Array.isArray(chunks) ? chunks.length : 0;
|
|
71
|
+
} as any;
|
|
72
|
+
|
|
73
|
+
const runner = new JSRunner({
|
|
74
|
+
globals: {
|
|
75
|
+
window: createSafeWindow(),
|
|
76
|
+
Blob: explicitBlob,
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const result = await runner.run('const b = new Blob([1,2,3]); return b.size;');
|
|
81
|
+
expect(result.success).toBe(true);
|
|
82
|
+
expect(result.value).toBe(3);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('auto-lifts URL from injected window to top-level globals', async () => {
|
|
86
|
+
const runner = new JSRunner({
|
|
87
|
+
globals: {
|
|
88
|
+
window: createSafeWindow(),
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const result = await runner.run('return typeof URL.createObjectURL === "function"');
|
|
93
|
+
expect(result.success).toBe(true);
|
|
94
|
+
expect(result.value).toBe(true);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('keeps explicit globals.URL higher priority than auto-lifted URL', async () => {
|
|
98
|
+
const explicitURL = {
|
|
99
|
+
createObjectURL: () => 'explicit://url',
|
|
100
|
+
revokeObjectURL: (_url: string) => undefined,
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const runner = new JSRunner({
|
|
104
|
+
globals: {
|
|
105
|
+
window: createSafeWindow(),
|
|
106
|
+
URL: explicitURL,
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const result = await runner.run('return URL.createObjectURL(new Blob(["x"]))');
|
|
111
|
+
expect(result.success).toBe(true);
|
|
112
|
+
expect(result.value).toBe('explicit://url');
|
|
113
|
+
});
|
|
114
|
+
|
|
51
115
|
it('exposes console in sandbox by default', async () => {
|
|
52
116
|
const runner = new JSRunner();
|
|
53
117
|
const result = await runner.run('return typeof console !== "undefined"');
|
|
@@ -759,6 +759,84 @@ describe('FlowEngine context', () => {
|
|
|
759
759
|
expect(engine.context.appName).toBe('NocoBase');
|
|
760
760
|
});
|
|
761
761
|
|
|
762
|
+
it('ctx.sql should resolve template variables from caller context in delegate chain', async () => {
|
|
763
|
+
const engine = new FlowEngine();
|
|
764
|
+
const request = vi.fn(async () => ({ data: { data: [] } }));
|
|
765
|
+
engine.context.defineProperty('api', {
|
|
766
|
+
value: { request },
|
|
767
|
+
});
|
|
768
|
+
engine.context.defineProperty('minId', {
|
|
769
|
+
get: () => 999,
|
|
770
|
+
cache: false,
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
const callerCtx = new FlowContext();
|
|
774
|
+
callerCtx.addDelegate(engine.context);
|
|
775
|
+
callerCtx.defineProperty('minId', {
|
|
776
|
+
get: () => 1,
|
|
777
|
+
cache: false,
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
await callerCtx.sql.run('SELECT * FROM users WHERE id > {{ctx.minId}}', { type: 'selectRows' });
|
|
781
|
+
|
|
782
|
+
expect(request).toHaveBeenCalledTimes(1);
|
|
783
|
+
const config = request.mock.calls[0]?.[0];
|
|
784
|
+
expect(config?.url).toBe('flowSql:run');
|
|
785
|
+
expect(config?.data.bind.__var1).toBe(1);
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
it('ctx.sql should not share repository instance between different caller contexts', async () => {
|
|
789
|
+
const engine = new FlowEngine();
|
|
790
|
+
const request = vi.fn(async () => ({ data: { data: [] } }));
|
|
791
|
+
engine.context.defineProperty('api', {
|
|
792
|
+
value: { request },
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
const caller1 = new FlowContext();
|
|
796
|
+
caller1.addDelegate(engine.context);
|
|
797
|
+
caller1.defineProperty('minId', {
|
|
798
|
+
get: () => 1,
|
|
799
|
+
cache: false,
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
const caller2 = new FlowContext();
|
|
803
|
+
caller2.addDelegate(engine.context);
|
|
804
|
+
caller2.defineProperty('minId', {
|
|
805
|
+
get: () => 2,
|
|
806
|
+
cache: false,
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
expect(caller1.sql).not.toBe(caller2.sql);
|
|
810
|
+
|
|
811
|
+
await caller1.sql.run('SELECT * FROM users WHERE id > {{ctx.minId}}', { type: 'selectRows' });
|
|
812
|
+
await caller2.sql.run('SELECT * FROM users WHERE id > {{ctx.minId}}', { type: 'selectRows' });
|
|
813
|
+
|
|
814
|
+
expect(request).toHaveBeenCalledTimes(2);
|
|
815
|
+
const config1 = request.mock.calls[0]?.[0];
|
|
816
|
+
const config2 = request.mock.calls[1]?.[0];
|
|
817
|
+
expect(config1?.data.bind.__var1).toBe(1);
|
|
818
|
+
expect(config2?.data.bind.__var1).toBe(2);
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
it('engine.context.sql should keep working when accessed directly', async () => {
|
|
822
|
+
const engine = new FlowEngine();
|
|
823
|
+
const request = vi.fn(async () => ({ data: { data: [] } }));
|
|
824
|
+
engine.context.defineProperty('api', {
|
|
825
|
+
value: { request },
|
|
826
|
+
});
|
|
827
|
+
engine.context.defineProperty('minId', {
|
|
828
|
+
get: () => 3,
|
|
829
|
+
cache: false,
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
await engine.context.sql.run('SELECT * FROM users WHERE id > {{ctx.minId}}', { type: 'selectRows' });
|
|
833
|
+
|
|
834
|
+
expect(request).toHaveBeenCalledTimes(1);
|
|
835
|
+
const config = request.mock.calls[0]?.[0];
|
|
836
|
+
expect(config?.url).toBe('flowSql:run');
|
|
837
|
+
expect(config?.data.bind.__var1).toBe(3);
|
|
838
|
+
});
|
|
839
|
+
|
|
762
840
|
it('engine.context.getVar should resolve variable by path', async () => {
|
|
763
841
|
const engine = new FlowEngine();
|
|
764
842
|
engine.context.defineProperty('foo', { value: { bar: 1 } });
|
|
@@ -614,7 +614,7 @@ export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
|
|
|
614
614
|
};
|
|
615
615
|
items.push({
|
|
616
616
|
key: uniqueKey,
|
|
617
|
-
label: <MenuLabelItem title={
|
|
617
|
+
label: <MenuLabelItem title={stepInfo.title} uiMode={uiMode} itemProps={itemProps} />,
|
|
618
618
|
});
|
|
619
619
|
});
|
|
620
620
|
if (flow.options.divider === 'bottom') {
|
|
@@ -656,7 +656,7 @@ export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
|
|
|
656
656
|
|
|
657
657
|
items.push({
|
|
658
658
|
key: uniqueKey,
|
|
659
|
-
label:
|
|
659
|
+
label: stepInfo.title,
|
|
660
660
|
});
|
|
661
661
|
});
|
|
662
662
|
});
|
|
@@ -671,7 +671,7 @@ export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
|
|
|
671
671
|
|
|
672
672
|
subMenuChildren.push({
|
|
673
673
|
key: uniqueKey,
|
|
674
|
-
label:
|
|
674
|
+
label: stepInfo.title,
|
|
675
675
|
});
|
|
676
676
|
});
|
|
677
677
|
});
|
package/src/flowContext.ts
CHANGED
|
@@ -1144,7 +1144,8 @@ export class FlowEngineContext extends BaseFlowEngineContext {
|
|
|
1144
1144
|
value: this.engine,
|
|
1145
1145
|
});
|
|
1146
1146
|
this.defineProperty('sql', {
|
|
1147
|
-
get: () => new FlowSQLRepository(
|
|
1147
|
+
get: (ctx) => new FlowSQLRepository(ctx),
|
|
1148
|
+
cache: false,
|
|
1148
1149
|
});
|
|
1149
1150
|
this.defineProperty('dataSourceManager', {
|
|
1150
1151
|
value: dataSourceManager,
|
|
@@ -27,10 +27,10 @@ type SQLSaveOptions = {
|
|
|
27
27
|
};
|
|
28
28
|
|
|
29
29
|
export class FlowSQLRepository {
|
|
30
|
-
protected ctx:
|
|
30
|
+
protected ctx: FlowContext;
|
|
31
31
|
|
|
32
|
-
constructor(ctx:
|
|
33
|
-
this.ctx = new FlowContext()
|
|
32
|
+
constructor(ctx: FlowContext) {
|
|
33
|
+
this.ctx = new FlowContext();
|
|
34
34
|
this.ctx.addDelegate(ctx);
|
|
35
35
|
this.ctx.defineProperty('offset', {
|
|
36
36
|
get: () => 0,
|
|
@@ -28,6 +28,14 @@ describe('safeGlobals', () => {
|
|
|
28
28
|
expect(win.console).toBeDefined();
|
|
29
29
|
expect(win.foo).toBe(123);
|
|
30
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
|
+
}
|
|
31
39
|
// access to location proxy is allowed, but sensitive props throw
|
|
32
40
|
expect(() => win.location.href).toThrow(/not allowed/);
|
|
33
41
|
});
|
package/src/utils/safeGlobals.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* 统一的安全全局对象代理:window/document/navigator
|
|
12
|
-
* - window:仅允许常用的定时器、console、Math、Date、FormData、addEventListener、open(安全包装)、location(安全代理)
|
|
12
|
+
* - window:仅允许常用的定时器、console、Math、Date、FormData、Blob、URL、addEventListener、open(安全包装)、location(安全代理)
|
|
13
13
|
* - document:仅允许 createElement/querySelector/querySelectorAll
|
|
14
14
|
* - navigator:仅提供极少量低风险能力(clipboard.writeText、onLine、language、languages)
|
|
15
15
|
* - 不允许随意访问未声明的属性,最小权限原则
|
|
@@ -211,6 +211,8 @@ export function createSafeWindow(extra?: Record<string, any>) {
|
|
|
211
211
|
Math,
|
|
212
212
|
Date,
|
|
213
213
|
FormData,
|
|
214
|
+
...(typeof Blob !== 'undefined' ? { Blob } : {}),
|
|
215
|
+
...(typeof URL !== 'undefined' ? { URL } : {}),
|
|
214
216
|
// 事件侦听仅绑定到真实 window,便于少量需要的全局监听
|
|
215
217
|
addEventListener: addEventListener.bind(window),
|
|
216
218
|
// 安全的 window.open 代理
|