-
Notifications
You must be signed in to change notification settings - Fork 58.5k
Expand file tree
/
Copy pathJavaScriptSandbox.ts
More file actions
147 lines (121 loc) · 4.33 KB
/
Copy pathJavaScriptSandbox.ts
File metadata and controls
147 lines (121 loc) · 4.33 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
import { NodeVM, makeResolverFromLegacyOptions, type Resolver } from '@n8n/vm2';
import type { IExecuteFunctions, INodeExecutionData } from 'n8n-workflow';
import { ValidationError } from './ValidationError';
import { ExecutionError } from './ExecutionError';
import type { SandboxContext } from './Sandbox';
import { Sandbox } from './Sandbox';
const { NODE_FUNCTION_ALLOW_BUILTIN: builtIn, NODE_FUNCTION_ALLOW_EXTERNAL: external } =
process.env;
export const vmResolver = makeResolverFromLegacyOptions({
external: external
? {
modules: external.split(','),
transitive: false,
}
: false,
builtin: builtIn?.split(',') ?? [],
});
export class JavaScriptSandbox extends Sandbox {
private readonly vm: NodeVM;
constructor(
context: SandboxContext,
private jsCode: string,
itemIndex: number | undefined,
helpers: IExecuteFunctions['helpers'],
options?: { resolver?: Resolver },
) {
super(
{
object: {
singular: 'object',
plural: 'objects',
},
},
itemIndex,
helpers,
);
this.vm = new NodeVM({
console: 'redirect',
sandbox: context,
require: options?.resolver ?? vmResolver,
wasm: false,
});
this.vm.on('console.log', (...args: unknown[]) => this.emit('output', ...args));
}
async runCode(): Promise<unknown> {
const script = `module.exports = async function() {${this.jsCode}\n}()`;
try {
const executionResult = await this.vm.run(script, __dirname);
return executionResult;
} catch (error) {
throw new ExecutionError(error);
}
}
async runCodeAllItems(options?: {
multiOutput?: boolean;
}): Promise<INodeExecutionData[] | INodeExecutionData[][]> {
const script = `module.exports = async function() {${this.jsCode}\n}()`;
let executionResult: INodeExecutionData | INodeExecutionData[] | INodeExecutionData[][];
try {
executionResult = await this.vm.run(script, __dirname);
} catch (error) {
// anticipate user expecting `items` to pre-exist as in Function Item node
if (error.message === 'items is not defined' && !/(let|const|var) items =/.test(script)) {
const quoted = error.message.replace('items', '`items`');
error.message = (quoted as string) + '. Did you mean `$input.all()`?';
}
throw new ExecutionError(error);
}
if (executionResult === null) return [];
if (options?.multiOutput === true) {
// Check if executionResult is an array of arrays
if (!Array.isArray(executionResult) || executionResult.some((item) => !Array.isArray(item))) {
throw new ValidationError({
message: "The code doesn't return an array of arrays",
description:
'Please return an array of arrays. One array for the different outputs and one for the different items that get returned.',
itemIndex: this.itemIndex,
});
}
return executionResult.map((data) => {
return this.validateRunCodeAllItems(data);
});
}
return this.validateRunCodeAllItems(
executionResult as INodeExecutionData | INodeExecutionData[],
);
}
async runCodeEachItem(): Promise<INodeExecutionData | undefined> {
const script = `module.exports = async function() {${this.jsCode}\n}()`;
const match = this.jsCode.match(/\$input\.(?<disallowedMethod>first|last|all|itemMatching)/);
if (match?.groups?.disallowedMethod) {
const { disallowedMethod } = match.groups;
const lineNumber =
this.jsCode.split('\n').findIndex((line) => {
return line.includes(disallowedMethod) && !line.startsWith('//') && !line.startsWith('*');
}) + 1;
const disallowedMethodFound = lineNumber !== 0;
if (disallowedMethodFound) {
throw new ValidationError({
message: `Can't use .${disallowedMethod}() here`,
description: "This is only available in 'Run Once for All Items' mode",
itemIndex: this.itemIndex,
lineNumber,
});
}
}
let executionResult: INodeExecutionData;
try {
executionResult = await this.vm.run(script, __dirname);
} catch (error) {
// anticipate user expecting `item` to pre-exist as in Function Item node
if (error.message === 'item is not defined' && !/(let|const|var) item =/.test(script)) {
const quoted = error.message.replace('item', '`item`');
error.message = (quoted as string) + '. Did you mean `$input.item.json`?';
}
throw new ExecutionError(error, this.itemIndex);
}
if (executionResult === null) return;
return this.validateRunCodeEachItem(executionResult);
}
}