@api3/commons 0.4.1 → 0.5.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/dist/blockchain-utilities/derivation.d.ts +78 -0
- package/dist/blockchain-utilities/derivation.d.ts.map +1 -0
- package/dist/blockchain-utilities/derivation.js +97 -0
- package/dist/blockchain-utilities/derivation.js.map +1 -0
- package/dist/blockchain-utilities/schema.d.ts +12 -0
- package/dist/blockchain-utilities/schema.d.ts.map +1 -0
- package/dist/blockchain-utilities/schema.js +13 -0
- package/dist/blockchain-utilities/schema.js.map +1 -0
- package/dist/processing/processing.d.ts +70 -18
- package/dist/processing/processing.d.ts.map +1 -1
- package/dist/processing/processing.js +145 -37
- package/dist/processing/processing.js.map +1 -1
- package/dist/processing/schema.d.ts +25 -2
- package/dist/processing/schema.d.ts.map +1 -1
- package/dist/processing/schema.js +10 -9
- package/dist/processing/schema.js.map +1 -1
- package/dist/processing/unsafe-evaluate.d.ts +1 -0
- package/dist/processing/unsafe-evaluate.d.ts.map +1 -1
- package/dist/processing/unsafe-evaluate.js +31 -1
- package/dist/processing/unsafe-evaluate.js.map +1 -1
- package/package.json +5 -3
- package/src/blockchain-utilities/README.md +5 -0
- package/src/blockchain-utilities/derivation.test.ts +147 -0
- package/src/blockchain-utilities/derivation.ts +116 -0
- package/src/blockchain-utilities/schema.test.ts +23 -0
- package/src/blockchain-utilities/schema.ts +14 -0
- package/src/processing/README.md +89 -14
- package/src/processing/processing.test.ts +429 -111
- package/src/processing/processing.ts +196 -47
- package/src/processing/schema.ts +21 -8
- package/src/processing/unsafe-evaluate.test.ts +220 -1
- package/src/processing/unsafe-evaluate.ts +39 -0
|
@@ -1,20 +1,29 @@
|
|
|
1
1
|
import { type Endpoint, RESERVED_PARAMETERS } from '@api3/ois';
|
|
2
2
|
import { type GoAsyncOptions, go } from '@api3/promise-utils';
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
|
|
4
|
+
import {
|
|
5
|
+
type EndpointParameters,
|
|
6
|
+
postProcessingV2ResponseSchema,
|
|
7
|
+
endpointParametersSchema,
|
|
8
|
+
preProcessingV2ResponseSchema,
|
|
9
|
+
type PreProcessingV2Response,
|
|
10
|
+
type PostProcessingV2Response,
|
|
11
|
+
type ProcessingSpecificationV2,
|
|
12
|
+
type ProcessingSpecifications,
|
|
13
|
+
} from './schema';
|
|
14
|
+
import { unsafeEvaluate, unsafeEvaluateAsync, unsafeEvaluateV2 } from './unsafe-evaluate';
|
|
6
15
|
|
|
7
16
|
export const DEFAULT_PROCESSING_TIMEOUT_MS = 10_000;
|
|
8
17
|
|
|
9
18
|
const reservedParameters = RESERVED_PARAMETERS as string[]; // To avoid strict TS checks.
|
|
10
19
|
|
|
11
20
|
/**
|
|
12
|
-
* Removes reserved parameters from the parameters
|
|
13
|
-
* @param parameters The
|
|
14
|
-
* @returns The parameters
|
|
21
|
+
* Removes reserved parameters from the endpoint parameters.
|
|
22
|
+
* @param parameters The endpoint parameters from which reserved parameters will be removed.
|
|
23
|
+
* @returns The endpoint parameters without reserved parameters.
|
|
15
24
|
*/
|
|
16
|
-
export const removeReservedParameters = (parameters:
|
|
17
|
-
const result:
|
|
25
|
+
export const removeReservedParameters = (parameters: EndpointParameters): EndpointParameters => {
|
|
26
|
+
const result: EndpointParameters = {};
|
|
18
27
|
|
|
19
28
|
for (const key in parameters) {
|
|
20
29
|
if (!reservedParameters.includes(key)) {
|
|
@@ -26,15 +35,15 @@ export const removeReservedParameters = (parameters: ApiCallParameters): ApiCall
|
|
|
26
35
|
};
|
|
27
36
|
|
|
28
37
|
/**
|
|
29
|
-
* Re-inserts reserved parameters from the initial parameters
|
|
30
|
-
* @param initialParameters The initial
|
|
31
|
-
* @param modifiedParameters The modified
|
|
32
|
-
* @returns The modified parameters
|
|
38
|
+
* Re-inserts reserved parameters from the initial endpoint parameters into the modified endpoint parameters.
|
|
39
|
+
* @param initialParameters The initial endpoint parameters that might contain reserved parameters.
|
|
40
|
+
* @param modifiedParameters The modified endpoint parameters to which reserved parameters will be added.
|
|
41
|
+
* @returns The modified endpoint parameters with re-inserted reserved parameters.
|
|
33
42
|
*/
|
|
34
43
|
export const addReservedParameters = (
|
|
35
|
-
initialParameters:
|
|
36
|
-
modifiedParameters:
|
|
37
|
-
):
|
|
44
|
+
initialParameters: EndpointParameters,
|
|
45
|
+
modifiedParameters: EndpointParameters
|
|
46
|
+
): EndpointParameters => {
|
|
38
47
|
for (const key in initialParameters) {
|
|
39
48
|
if (reservedParameters.includes(key)) {
|
|
40
49
|
modifiedParameters[key] = initialParameters[key];
|
|
@@ -45,38 +54,38 @@ export const addReservedParameters = (
|
|
|
45
54
|
};
|
|
46
55
|
|
|
47
56
|
/**
|
|
48
|
-
* Pre-processes
|
|
57
|
+
* Pre-processes endpoint parameters based on the provided endpoint's processing specifications.
|
|
49
58
|
*
|
|
50
|
-
* @param
|
|
51
|
-
* @param
|
|
59
|
+
* @param preProcessingSpecifications The v1 pre-processing specifications.
|
|
60
|
+
* @param endpointParameters The parameters to be pre-processed.
|
|
52
61
|
* @param processingOptions Options to control the async processing behavior like retries and timeouts.
|
|
53
62
|
*
|
|
54
63
|
* @returns A promise that resolves to the pre-processed parameters.
|
|
55
64
|
*/
|
|
56
|
-
export const
|
|
57
|
-
|
|
58
|
-
|
|
65
|
+
export const preProcessEndpointParametersV1 = async (
|
|
66
|
+
preProcessingSpecifications: ProcessingSpecifications | undefined,
|
|
67
|
+
endpointParameters: EndpointParameters,
|
|
59
68
|
processingOptions: GoAsyncOptions = { retries: 0, totalTimeoutMs: DEFAULT_PROCESSING_TIMEOUT_MS }
|
|
60
|
-
): Promise<
|
|
61
|
-
const { preProcessingSpecifications } = endpoint;
|
|
69
|
+
): Promise<EndpointParameters> => {
|
|
62
70
|
if (!preProcessingSpecifications || preProcessingSpecifications.length === 0) {
|
|
63
|
-
return
|
|
71
|
+
return endpointParameters;
|
|
64
72
|
}
|
|
65
73
|
|
|
66
|
-
// We only wrap the code through "go" utils because of the timeout and retry logic.
|
|
74
|
+
// We only wrap the code through "go" utils because of the timeout and retry logic. In case of error, the function
|
|
75
|
+
// just re-throws.
|
|
67
76
|
const goProcessedParameters = await go(async () => {
|
|
68
|
-
let currentValue: unknown = removeReservedParameters(
|
|
77
|
+
let currentValue: unknown = removeReservedParameters(endpointParameters);
|
|
69
78
|
|
|
70
79
|
for (const processing of preProcessingSpecifications) {
|
|
71
80
|
// Provide endpoint parameters without reserved parameters immutably between steps. Recompute them for each
|
|
72
81
|
// snippet independently because processing snippets can modify the parameters.
|
|
73
|
-
const
|
|
82
|
+
const nonReservedEndpointParameters = removeReservedParameters(endpointParameters);
|
|
74
83
|
|
|
75
84
|
switch (processing.environment) {
|
|
76
85
|
case 'Node': {
|
|
77
86
|
currentValue = await unsafeEvaluate(
|
|
78
87
|
processing.value,
|
|
79
|
-
{ input: currentValue, endpointParameters },
|
|
88
|
+
{ input: currentValue, endpointParameters: nonReservedEndpointParameters },
|
|
80
89
|
processing.timeoutMs
|
|
81
90
|
);
|
|
82
91
|
break;
|
|
@@ -84,7 +93,7 @@ export const preProcessApiCallParameters = async (
|
|
|
84
93
|
case 'Node async': {
|
|
85
94
|
currentValue = await unsafeEvaluateAsync(
|
|
86
95
|
processing.value,
|
|
87
|
-
{ input: currentValue, endpointParameters },
|
|
96
|
+
{ input: currentValue, endpointParameters: nonReservedEndpointParameters },
|
|
88
97
|
processing.timeoutMs
|
|
89
98
|
);
|
|
90
99
|
break;
|
|
@@ -97,46 +106,48 @@ export const preProcessApiCallParameters = async (
|
|
|
97
106
|
if (!goProcessedParameters.success) throw goProcessedParameters.error;
|
|
98
107
|
|
|
99
108
|
// Let this throw if the processed parameters are invalid.
|
|
100
|
-
const parsedParameters =
|
|
109
|
+
const parsedParameters = endpointParametersSchema.parse(goProcessedParameters.data);
|
|
101
110
|
|
|
102
|
-
// Having removed reserved parameters for pre-processing, we need to re-insert them
|
|
103
|
-
return addReservedParameters(
|
|
111
|
+
// Having removed reserved parameters for pre-processing, we need to re-insert them after pre-processing.
|
|
112
|
+
return addReservedParameters(endpointParameters, parsedParameters);
|
|
104
113
|
};
|
|
105
114
|
|
|
106
115
|
/**
|
|
107
|
-
* Post-processes the
|
|
116
|
+
* Post-processes the response based on the provided endpoint's processing specifications. The response is usually the
|
|
117
|
+
* API call response, but this logic depends on how the processing is used by the target service. For example, Airnode
|
|
118
|
+
* allows skipping API calls in which case the response is the result of pre-processing.
|
|
108
119
|
*
|
|
109
|
-
* @param
|
|
110
|
-
* @param
|
|
111
|
-
* @param
|
|
120
|
+
* @param response The response to be post-processed.
|
|
121
|
+
* @param postProcessingSpecifications The v1 post-processing specifications.
|
|
122
|
+
* @param endpointParameters The endpoint parameters.
|
|
112
123
|
* @param processingOptions Options to control the async processing behavior like retries and timeouts.
|
|
113
124
|
*
|
|
114
|
-
* @returns A promise that resolves to the post-processed
|
|
125
|
+
* @returns A promise that resolves to the post-processed response.
|
|
115
126
|
*/
|
|
116
|
-
export const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
127
|
+
export const postProcessResponseV1 = async (
|
|
128
|
+
response: unknown,
|
|
129
|
+
postProcessingSpecifications: ProcessingSpecifications | undefined,
|
|
130
|
+
endpointParameters: EndpointParameters,
|
|
120
131
|
processingOptions: GoAsyncOptions = { retries: 0, totalTimeoutMs: DEFAULT_PROCESSING_TIMEOUT_MS }
|
|
121
132
|
) => {
|
|
122
|
-
const { postProcessingSpecifications } = endpoint;
|
|
123
133
|
if (!postProcessingSpecifications || postProcessingSpecifications?.length === 0) {
|
|
124
|
-
return
|
|
134
|
+
return response;
|
|
125
135
|
}
|
|
126
136
|
|
|
127
|
-
// We only wrap the code through "go" utils because of the timeout and retry logic.
|
|
137
|
+
// We only wrap the code through "go" utils because of the timeout and retry logic. In case of error, the function
|
|
138
|
+
// just re-throws.
|
|
128
139
|
const goResult = await go(async () => {
|
|
129
|
-
let currentValue: unknown =
|
|
140
|
+
let currentValue: unknown = response;
|
|
130
141
|
|
|
131
142
|
for (const processing of postProcessingSpecifications) {
|
|
132
143
|
// Provide endpoint parameters without reserved parameters immutably between steps. Recompute them for each
|
|
133
144
|
// snippet independently because processing snippets can modify the parameters.
|
|
134
|
-
const
|
|
145
|
+
const nonReservedEndpointParameters = removeReservedParameters(endpointParameters);
|
|
135
146
|
switch (processing.environment) {
|
|
136
147
|
case 'Node': {
|
|
137
148
|
currentValue = await unsafeEvaluate(
|
|
138
149
|
processing.value,
|
|
139
|
-
{ input: currentValue, endpointParameters },
|
|
150
|
+
{ input: currentValue, endpointParameters: nonReservedEndpointParameters },
|
|
140
151
|
processing.timeoutMs
|
|
141
152
|
);
|
|
142
153
|
break;
|
|
@@ -144,7 +155,7 @@ export const postProcessApiCallResponse = async (
|
|
|
144
155
|
case 'Node async': {
|
|
145
156
|
currentValue = await unsafeEvaluateAsync(
|
|
146
157
|
processing.value,
|
|
147
|
-
{ input: currentValue, endpointParameters },
|
|
158
|
+
{ input: currentValue, endpointParameters: nonReservedEndpointParameters },
|
|
148
159
|
processing.timeoutMs
|
|
149
160
|
);
|
|
150
161
|
break;
|
|
@@ -158,3 +169,141 @@ export const postProcessApiCallResponse = async (
|
|
|
158
169
|
|
|
159
170
|
return goResult.data;
|
|
160
171
|
};
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Pre-processes endpoint parameters based on the provided endpoint's processing specifications.
|
|
175
|
+
*
|
|
176
|
+
* @param preProcessingSpecificationV2 The v2 pre-processing specification.
|
|
177
|
+
* @param endpointParameters The parameters to be pre-processed.
|
|
178
|
+
* @param processingOptions Options to control the async processing behavior like retries and timeouts.
|
|
179
|
+
*
|
|
180
|
+
* @returns A promise that resolves to the pre-processed parameters.
|
|
181
|
+
*/
|
|
182
|
+
export const preProcessEndpointParametersV2 = async (
|
|
183
|
+
preProcessingSpecificationV2: ProcessingSpecificationV2 | undefined,
|
|
184
|
+
endpointParameters: EndpointParameters,
|
|
185
|
+
processingOptions: GoAsyncOptions = { retries: 0, totalTimeoutMs: DEFAULT_PROCESSING_TIMEOUT_MS }
|
|
186
|
+
): Promise<PreProcessingV2Response> => {
|
|
187
|
+
if (!preProcessingSpecificationV2) return { endpointParameters };
|
|
188
|
+
|
|
189
|
+
// We only wrap the code through "go" utils because of the timeout and retry logic. In case of error, the function
|
|
190
|
+
// just re-throws.
|
|
191
|
+
const goProcessedParameters = await go(async () => {
|
|
192
|
+
const { environment, timeoutMs, value } = preProcessingSpecificationV2;
|
|
193
|
+
|
|
194
|
+
switch (environment) {
|
|
195
|
+
case 'Node': {
|
|
196
|
+
return unsafeEvaluateV2(value, { endpointParameters: removeReservedParameters(endpointParameters) }, timeoutMs);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}, processingOptions);
|
|
200
|
+
if (!goProcessedParameters.success) throw goProcessedParameters.error;
|
|
201
|
+
|
|
202
|
+
// Let this throw if the processed parameters are invalid.
|
|
203
|
+
const preProcessingResponse = preProcessingV2ResponseSchema.parse(goProcessedParameters.data);
|
|
204
|
+
|
|
205
|
+
// Having removed reserved parameters for pre-processing, we need to re-insert them after pre-processing.
|
|
206
|
+
return { endpointParameters: addReservedParameters(endpointParameters, preProcessingResponse.endpointParameters) };
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Post-processes the response based on the provided endpoint's processing specifications. The response is usually the
|
|
211
|
+
* API call response, but this logic depends on how the processing is used by the target service. For example, Airnode
|
|
212
|
+
* allows skipping API calls in which case the response is the result of pre-processing.
|
|
213
|
+
*
|
|
214
|
+
* @param response The response to be post-processed.
|
|
215
|
+
* @param postProcessingSpecificationV2 The v2 post-processing specification.
|
|
216
|
+
* @param endpointParameters The endpoint parameters.
|
|
217
|
+
* @param processingOptions Options to control the async processing behavior like retries and timeouts.
|
|
218
|
+
*
|
|
219
|
+
* @returns A promise that resolves to the post-processed response.
|
|
220
|
+
*/
|
|
221
|
+
export const postProcessResponseV2 = async (
|
|
222
|
+
response: unknown,
|
|
223
|
+
postProcessingSpecificationV2: ProcessingSpecificationV2 | undefined,
|
|
224
|
+
endpointParameters: EndpointParameters,
|
|
225
|
+
processingOptions: GoAsyncOptions = { retries: 0, totalTimeoutMs: DEFAULT_PROCESSING_TIMEOUT_MS }
|
|
226
|
+
): Promise<PostProcessingV2Response> => {
|
|
227
|
+
if (!postProcessingSpecificationV2) return { response };
|
|
228
|
+
|
|
229
|
+
// We only wrap the code through "go" utils because of the timeout and retry logic. In case of error, the function
|
|
230
|
+
// just re-throws.
|
|
231
|
+
const goResult = await go(async () => {
|
|
232
|
+
const { environment, timeoutMs, value } = postProcessingSpecificationV2;
|
|
233
|
+
// Provide endpoint parameters without reserved parameters immutably between steps. Recompute them for each
|
|
234
|
+
// snippet independently because processing snippets can modify the parameters.
|
|
235
|
+
const nonReservedEndpointParameters = removeReservedParameters(endpointParameters);
|
|
236
|
+
|
|
237
|
+
switch (environment) {
|
|
238
|
+
case 'Node': {
|
|
239
|
+
return unsafeEvaluateV2(value, { response, endpointParameters: nonReservedEndpointParameters }, timeoutMs);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}, processingOptions);
|
|
243
|
+
if (!goResult.success) throw goResult.error;
|
|
244
|
+
|
|
245
|
+
return postProcessingV2ResponseSchema.parse(goResult.data);
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Pre-processes endpoint parameters based on the provided endpoint's processing specifications. Internally it
|
|
250
|
+
* determines what processing implementation should be used.
|
|
251
|
+
*
|
|
252
|
+
* @param endpoint The endpoint containing processing specifications.
|
|
253
|
+
* @param endpointParameters The parameters to be pre-processed.
|
|
254
|
+
* @param processingOptions Options to control the async processing behavior like retries and timeouts.
|
|
255
|
+
*
|
|
256
|
+
* @returns A promise that resolves to the pre-processed parameters.
|
|
257
|
+
*/
|
|
258
|
+
export const preProcessEndpointParameters = async (
|
|
259
|
+
endpoint: Endpoint,
|
|
260
|
+
endpointParameters: EndpointParameters,
|
|
261
|
+
processingOptions: GoAsyncOptions = { retries: 0, totalTimeoutMs: DEFAULT_PROCESSING_TIMEOUT_MS }
|
|
262
|
+
): Promise<PreProcessingV2Response> => {
|
|
263
|
+
const { preProcessingSpecificationV2, preProcessingSpecifications } = endpoint;
|
|
264
|
+
if (preProcessingSpecificationV2) {
|
|
265
|
+
return preProcessEndpointParametersV2(preProcessingSpecificationV2, endpointParameters);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const preProcessV1Response = await preProcessEndpointParametersV1(
|
|
269
|
+
preProcessingSpecifications,
|
|
270
|
+
endpointParameters,
|
|
271
|
+
processingOptions
|
|
272
|
+
);
|
|
273
|
+
return { endpointParameters: preProcessV1Response };
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Post-processes the response based on the provided endpoint's processing specifications. The response is usually the
|
|
278
|
+
* API call response, but this logic depends on how the processing is used by the target service. For example, Airnode
|
|
279
|
+
* allows skipping API calls in which case the response is the result of pre-processing.
|
|
280
|
+
*
|
|
281
|
+
* This function determines what processing version should be used and provides a common interface. This is useful for
|
|
282
|
+
* services that want to use processing and don't care which processing version is used.
|
|
283
|
+
*
|
|
284
|
+
* @param response The response to be post-processed.
|
|
285
|
+
* @param endpoint The endpoint containing processing specifications.
|
|
286
|
+
* @param endpointParameters The endpoint parameters.
|
|
287
|
+
* @param processingOptions Options to control the async processing behavior like retries and timeouts.
|
|
288
|
+
*
|
|
289
|
+
* @returns A promise that resolves to the post-processed response.
|
|
290
|
+
*/
|
|
291
|
+
export const postProcessResponse = async (
|
|
292
|
+
response: unknown,
|
|
293
|
+
endpoint: Endpoint,
|
|
294
|
+
endpointParameters: EndpointParameters,
|
|
295
|
+
processingOptions: GoAsyncOptions = { retries: 0, totalTimeoutMs: DEFAULT_PROCESSING_TIMEOUT_MS }
|
|
296
|
+
): Promise<PostProcessingV2Response> => {
|
|
297
|
+
const { postProcessingSpecificationV2, postProcessingSpecifications } = endpoint;
|
|
298
|
+
if (postProcessingSpecificationV2) {
|
|
299
|
+
return postProcessResponseV2(response, postProcessingSpecificationV2, endpointParameters);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const postProcessV1Response = await postProcessResponseV1(
|
|
303
|
+
response,
|
|
304
|
+
postProcessingSpecifications,
|
|
305
|
+
endpointParameters,
|
|
306
|
+
processingOptions
|
|
307
|
+
);
|
|
308
|
+
return { response: postProcessV1Response };
|
|
309
|
+
};
|
package/src/processing/schema.ts
CHANGED
|
@@ -1,10 +1,23 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
if (typeof apiCallParameters !== 'object' || apiCallParameters === null) {
|
|
4
|
-
throw new TypeError('Invalid API call parameters');
|
|
5
|
-
}
|
|
1
|
+
import type { processingSpecificationSchemaV2, ProcessingSpecification } from '@api3/ois';
|
|
2
|
+
import { z } from 'zod';
|
|
6
3
|
|
|
7
|
-
|
|
8
|
-
};
|
|
4
|
+
export type ProcessingSpecificationV2 = z.infer<typeof processingSpecificationSchemaV2>;
|
|
9
5
|
|
|
10
|
-
export type
|
|
6
|
+
export type ProcessingSpecifications = ProcessingSpecification[];
|
|
7
|
+
|
|
8
|
+
export const endpointParametersSchema = z.record(z.any());
|
|
9
|
+
|
|
10
|
+
export type EndpointParameters = z.infer<typeof endpointParametersSchema>;
|
|
11
|
+
|
|
12
|
+
export const preProcessingV2ResponseSchema = z.object({
|
|
13
|
+
endpointParameters: endpointParametersSchema,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export type PreProcessingV2Response = z.infer<typeof preProcessingV2ResponseSchema>;
|
|
17
|
+
|
|
18
|
+
export const postProcessingV2ResponseSchema = z.object({
|
|
19
|
+
response: z.any(),
|
|
20
|
+
timestamp: z.number().nonnegative().int().optional(),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export type PostProcessingV2Response = z.infer<typeof postProcessingV2ResponseSchema>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* eslint-disable jest/prefer-strict-equal */ // Because the errors are thrown from the "vm" module (different context), they are not strictly equal.
|
|
2
|
-
import { unsafeEvaluate, unsafeEvaluateAsync } from './unsafe-evaluate';
|
|
2
|
+
import { unsafeEvaluate, unsafeEvaluateAsync, unsafeEvaluateV2 } from './unsafe-evaluate';
|
|
3
3
|
|
|
4
4
|
describe('unsafe evaluate - sync', () => {
|
|
5
5
|
it('executes harmless code', () => {
|
|
@@ -101,3 +101,222 @@ describe('unsafe evaluate - async', () => {
|
|
|
101
101
|
);
|
|
102
102
|
});
|
|
103
103
|
});
|
|
104
|
+
|
|
105
|
+
describe(unsafeEvaluateV2.name, () => {
|
|
106
|
+
it('has access to node modules and vm context', async () => {
|
|
107
|
+
const res = await unsafeEvaluateV2(
|
|
108
|
+
`
|
|
109
|
+
async () => {
|
|
110
|
+
return Object.keys(this);
|
|
111
|
+
}
|
|
112
|
+
`,
|
|
113
|
+
{},
|
|
114
|
+
100
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
expect(res).toEqual([
|
|
118
|
+
'assert',
|
|
119
|
+
'async_hooks',
|
|
120
|
+
'buffer',
|
|
121
|
+
'child_process',
|
|
122
|
+
'cluster',
|
|
123
|
+
'console',
|
|
124
|
+
'constants',
|
|
125
|
+
'crypto',
|
|
126
|
+
'dgram',
|
|
127
|
+
'dns',
|
|
128
|
+
'events',
|
|
129
|
+
'fs',
|
|
130
|
+
'http',
|
|
131
|
+
'http2',
|
|
132
|
+
'https',
|
|
133
|
+
'inspector',
|
|
134
|
+
'module',
|
|
135
|
+
'net',
|
|
136
|
+
'os',
|
|
137
|
+
'path',
|
|
138
|
+
'perf_hooks',
|
|
139
|
+
'process',
|
|
140
|
+
'readline',
|
|
141
|
+
'repl',
|
|
142
|
+
'stream',
|
|
143
|
+
'string_decoder',
|
|
144
|
+
'timers',
|
|
145
|
+
'tls',
|
|
146
|
+
'trace_events',
|
|
147
|
+
'tty',
|
|
148
|
+
'url',
|
|
149
|
+
'util',
|
|
150
|
+
'v8',
|
|
151
|
+
'vm',
|
|
152
|
+
'worker_threads',
|
|
153
|
+
'zlib',
|
|
154
|
+
'setTimeout',
|
|
155
|
+
'setInterval',
|
|
156
|
+
'clearTimeout',
|
|
157
|
+
'clearInterval',
|
|
158
|
+
'payload',
|
|
159
|
+
]);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('can access vm context values as global variables', async () => {
|
|
163
|
+
const res = await unsafeEvaluateV2(
|
|
164
|
+
`
|
|
165
|
+
async (payload) => {
|
|
166
|
+
return [this.payload, payload];
|
|
167
|
+
}
|
|
168
|
+
`,
|
|
169
|
+
123,
|
|
170
|
+
100
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
expect(res).toEqual([123, 123]);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('works with sync function as well', async () => {
|
|
177
|
+
const res = await unsafeEvaluateV2(
|
|
178
|
+
`
|
|
179
|
+
(payload) => {
|
|
180
|
+
return payload + 500;
|
|
181
|
+
}
|
|
182
|
+
`,
|
|
183
|
+
123,
|
|
184
|
+
100
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
expect(res).toBe(623);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('can use setTimeout and setInterval', async () => {
|
|
191
|
+
const res = await unsafeEvaluateV2(
|
|
192
|
+
`
|
|
193
|
+
async (payload) => {
|
|
194
|
+
const intervalId = setInterval(() => payload++, 50);
|
|
195
|
+
setTimeout(() => payload++, 50);
|
|
196
|
+
await new Promise((resolve) => {
|
|
197
|
+
clearInterval(intervalId);
|
|
198
|
+
setTimeout(resolve, 120);
|
|
199
|
+
});
|
|
200
|
+
return payload;
|
|
201
|
+
}
|
|
202
|
+
`,
|
|
203
|
+
0,
|
|
204
|
+
250
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
expect(res).toBe(3);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('applies timeout', async () => {
|
|
211
|
+
await expect(async () =>
|
|
212
|
+
unsafeEvaluateV2(
|
|
213
|
+
`
|
|
214
|
+
async () => {
|
|
215
|
+
await new Promise((res) => setTimeout(res, 100));
|
|
216
|
+
}
|
|
217
|
+
`,
|
|
218
|
+
0,
|
|
219
|
+
50
|
|
220
|
+
)
|
|
221
|
+
).rejects.toEqual(new Error('Full timeout exceeded'));
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('rejects on sync error', async () => {
|
|
225
|
+
await expect(async () =>
|
|
226
|
+
unsafeEvaluateV2(
|
|
227
|
+
`
|
|
228
|
+
() => {
|
|
229
|
+
return nonDefinedPayload + 500;
|
|
230
|
+
}
|
|
231
|
+
`,
|
|
232
|
+
{},
|
|
233
|
+
100
|
|
234
|
+
)
|
|
235
|
+
).rejects.toEqual(new ReferenceError('nonDefinedPayload is not defined'));
|
|
236
|
+
|
|
237
|
+
await expect(async () =>
|
|
238
|
+
unsafeEvaluateV2(
|
|
239
|
+
`
|
|
240
|
+
() => {
|
|
241
|
+
throw new Error('some error');
|
|
242
|
+
}
|
|
243
|
+
`,
|
|
244
|
+
{},
|
|
245
|
+
100
|
|
246
|
+
)
|
|
247
|
+
).rejects.toEqual(new Error('some error'));
|
|
248
|
+
|
|
249
|
+
await expect(async () =>
|
|
250
|
+
unsafeEvaluateV2(
|
|
251
|
+
`
|
|
252
|
+
() => {
|
|
253
|
+
throw 'non-error-value';
|
|
254
|
+
}
|
|
255
|
+
`,
|
|
256
|
+
{},
|
|
257
|
+
100
|
|
258
|
+
)
|
|
259
|
+
).rejects.toBe('non-error-value');
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('allows using closures for modular code', async () => {
|
|
263
|
+
const res = await unsafeEvaluateV2(
|
|
264
|
+
`
|
|
265
|
+
async (payload) => {
|
|
266
|
+
const isDivisible = (n, k) => n % k === 0;
|
|
267
|
+
|
|
268
|
+
const isPrime = (n) => {
|
|
269
|
+
for (let i = 2; i < n; i++) {
|
|
270
|
+
if (isDivisible(n, i)) return false;
|
|
271
|
+
}
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const ans = []
|
|
276
|
+
for (let i = 2; i < payload; i++) {
|
|
277
|
+
if (isPrime(i)) ans.push(i);
|
|
278
|
+
}
|
|
279
|
+
return ans;
|
|
280
|
+
}
|
|
281
|
+
`,
|
|
282
|
+
50,
|
|
283
|
+
1000
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
expect(res).toEqual([2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47]);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('can use any async function syntax', async () => {
|
|
290
|
+
const anonymousArrowFnResult = await unsafeEvaluateV2(
|
|
291
|
+
`
|
|
292
|
+
async (payload) => {
|
|
293
|
+
return await Promise.resolve(payload + 500);
|
|
294
|
+
}
|
|
295
|
+
`,
|
|
296
|
+
123,
|
|
297
|
+
100
|
|
298
|
+
);
|
|
299
|
+
const regularFnResult = await unsafeEvaluateV2(
|
|
300
|
+
`
|
|
301
|
+
async function(payload) {
|
|
302
|
+
return await Promise.resolve(payload + 500);
|
|
303
|
+
}
|
|
304
|
+
`,
|
|
305
|
+
123,
|
|
306
|
+
100
|
|
307
|
+
);
|
|
308
|
+
const regularFnReturningPromiseResult = await unsafeEvaluateV2(
|
|
309
|
+
`
|
|
310
|
+
function(payload) {
|
|
311
|
+
return Promise.resolve(payload + 500);
|
|
312
|
+
}
|
|
313
|
+
`,
|
|
314
|
+
123,
|
|
315
|
+
100
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
expect(anonymousArrowFnResult).toBe(623);
|
|
319
|
+
expect(regularFnResult).toBe(623);
|
|
320
|
+
expect(regularFnReturningPromiseResult).toBe(623);
|
|
321
|
+
});
|
|
322
|
+
});
|
|
@@ -36,6 +36,8 @@ import vm from 'node:vm';
|
|
|
36
36
|
import worker_threads from 'node:worker_threads';
|
|
37
37
|
import zlib from 'node:zlib';
|
|
38
38
|
|
|
39
|
+
import { type GoWrappedError, go } from '@api3/promise-utils';
|
|
40
|
+
|
|
39
41
|
import { createTimers } from './vm-timers';
|
|
40
42
|
|
|
41
43
|
const builtInNodeModules = {
|
|
@@ -113,6 +115,43 @@ export const unsafeEvaluate = (code: string, globalVariables: Record<string, unk
|
|
|
113
115
|
return vmContext.deferredOutput;
|
|
114
116
|
};
|
|
115
117
|
|
|
118
|
+
export const unsafeEvaluateV2 = async (code: string, payload: unknown, timeout: number) => {
|
|
119
|
+
const timers = createTimers();
|
|
120
|
+
|
|
121
|
+
const goEvaluate = await go<Promise<any>, GoWrappedError>(
|
|
122
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
123
|
+
async () =>
|
|
124
|
+
vm.runInNewContext(
|
|
125
|
+
`
|
|
126
|
+
(async () => {
|
|
127
|
+
return await (${code})(payload)
|
|
128
|
+
})();
|
|
129
|
+
`,
|
|
130
|
+
{
|
|
131
|
+
...builtInNodeModules,
|
|
132
|
+
setTimeout: timers.customSetTimeout,
|
|
133
|
+
setInterval: timers.customSetInterval,
|
|
134
|
+
clearTimeout: timers.customClearTimeout,
|
|
135
|
+
clearInterval: timers.customClearInterval,
|
|
136
|
+
payload,
|
|
137
|
+
},
|
|
138
|
+
{ displayErrors: true, timeout }
|
|
139
|
+
),
|
|
140
|
+
// Make sure the timeout is applied. When the processing snippet uses setTimeout or setInterval, the timeout option
|
|
141
|
+
// from VM is broken. See: https://github.com/nodejs/node/issues/3020.
|
|
142
|
+
{ totalTimeoutMs: timeout }
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
// We need to manually clear all timers and reject the processing manually.
|
|
146
|
+
timers.clearAll();
|
|
147
|
+
|
|
148
|
+
if (goEvaluate.success) {
|
|
149
|
+
return goEvaluate.data;
|
|
150
|
+
} else {
|
|
151
|
+
throw (goEvaluate.error.reason as Error) ?? goEvaluate.error;
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
116
155
|
/**
|
|
117
156
|
* Asynchronously evaluates the provided code in a new VM context with the specified global variables.
|
|
118
157
|
*
|