data-primals-engine 1.1.2 → 1.1.3
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.
|
@@ -14,7 +14,7 @@ jobs:
|
|
|
14
14
|
|
|
15
15
|
runs-on: ubuntu-latest
|
|
16
16
|
container:
|
|
17
|
-
image: node:
|
|
17
|
+
image: node:20 # Specify the Docker version you needx
|
|
18
18
|
env:
|
|
19
19
|
NODE_OPTIONS: "--max_old_space_size=4096"
|
|
20
20
|
NODE_ENV: development
|
|
@@ -23,7 +23,7 @@ jobs:
|
|
|
23
23
|
- 27017
|
|
24
24
|
strategy:
|
|
25
25
|
matrix:
|
|
26
|
-
node-version: [
|
|
26
|
+
node-version: [20.x]
|
|
27
27
|
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
|
28
28
|
services:
|
|
29
29
|
mongodb:
|
|
@@ -45,11 +45,6 @@ jobs:
|
|
|
45
45
|
apt-get update
|
|
46
46
|
apt-get install -y mongodb-org-tools
|
|
47
47
|
|
|
48
|
-
- name: Set up Node.js
|
|
49
|
-
uses: actions/setup-node@v2
|
|
50
|
-
with:
|
|
51
|
-
node-version: '18.18.2'
|
|
52
|
-
|
|
53
48
|
- name: Install dependencies
|
|
54
49
|
run: npm install
|
|
55
50
|
|
|
@@ -7,7 +7,7 @@ import {getFieldPathValue} from "data-primals-engine/data";
|
|
|
7
7
|
import {getFieldDefinitionFromPath} from "./core/data.js";
|
|
8
8
|
import {Dialog, DialogProvider} from "./Dialog.jsx";
|
|
9
9
|
import {FaPlay} from "react-icons/fa";
|
|
10
|
-
|
|
10
|
+
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Récupère une valeur imbriquée dans un objet (ex: 'user.address.city').
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "data-primals-engine",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.3",
|
|
4
4
|
"description": "data-primals-engine is the package responsible from handling large amount of data in a practical and performant way. It can handle large amount of data in a practical and performant way. It can also get workflow models working (for automation), and fully supports internationalisation.",
|
|
5
5
|
"main": "src/engine.js",
|
|
6
6
|
"type": "module",
|
|
@@ -90,7 +90,7 @@
|
|
|
90
90
|
"tar": "^7.4.3",
|
|
91
91
|
"uniqid": "^5.4.0",
|
|
92
92
|
"vitest": "^3.2.4",
|
|
93
|
-
"
|
|
93
|
+
"isolated-vm": "^4.7.2",
|
|
94
94
|
"yaml": "^2.8.0"
|
|
95
95
|
},
|
|
96
96
|
"devDependencies": {
|
|
@@ -115,6 +115,6 @@
|
|
|
115
115
|
"author": "anonympins",
|
|
116
116
|
"license": "MIT",
|
|
117
117
|
"engines": {
|
|
118
|
-
"node": ">=
|
|
118
|
+
"node": ">=20.0.0"
|
|
119
119
|
}
|
|
120
120
|
}
|
package/src/modules/workflow.js
CHANGED
|
@@ -4,7 +4,8 @@ import schedule from "node-schedule";
|
|
|
4
4
|
import {ObjectId} from "mongodb";
|
|
5
5
|
import crypto from "node:crypto";
|
|
6
6
|
|
|
7
|
-
import
|
|
7
|
+
import ivm from 'isolated-vm';
|
|
8
|
+
|
|
8
9
|
import {Logger} from "../gameObject.js";
|
|
9
10
|
import {deleteData, insertData, patchData, searchData} from "./data.js";
|
|
10
11
|
import {maxExecutionsByStep, maxWorkflowSteps, timeoutVM} from "../constants.js";
|
|
@@ -180,144 +181,100 @@ export async function scheduleWorkflowTriggers() {
|
|
|
180
181
|
}
|
|
181
182
|
|
|
182
183
|
|
|
183
|
-
|
|
184
|
-
async function executeWithIsolatedVm(actionDef, context) {
|
|
184
|
+
export async function executeSafeJavascript(actionDef, context, user) {
|
|
185
185
|
const code = actionDef.script;
|
|
186
|
-
const
|
|
186
|
+
const collectedLogs = [];
|
|
187
|
+
const isolate = new ivm.Isolate({ memoryLimit: 128 }); // 128MB memory limit
|
|
187
188
|
|
|
188
189
|
try {
|
|
189
190
|
const vmContext = await isolate.createContext();
|
|
190
191
|
const jail = vmContext.global;
|
|
191
192
|
|
|
192
|
-
//
|
|
193
|
-
|
|
194
|
-
await jail.set('log', new ivm.Reference(function(...args) {
|
|
195
|
-
// On peut préfixer les logs pour savoir qu'ils viennent de la VM
|
|
196
|
-
logger.info('[VM Script Log]', ...args);
|
|
197
|
-
}));
|
|
198
|
-
|
|
199
|
-
// On injecte les données du contexte. Elles sont COPIÉES, pas référencées.
|
|
200
|
-
// C'est une caractéristique de sécurité clé.
|
|
201
|
-
await jail.set('contextData', new ivm.ExternalCopy(context).copyInto());
|
|
202
|
-
// On peut aussi exposer des fonctions spécifiques de votre application
|
|
203
|
-
// await jail.set('myApiFunction', new ivm.Reference(async (params) => { ... }));
|
|
204
|
-
|
|
205
|
-
// On compile le script
|
|
206
|
-
const script = await isolate.compileScript(code);
|
|
207
|
-
|
|
208
|
-
// On exécute le script avec un timeout
|
|
209
|
-
const result = await script.run(vmContext, { timeout: 1000 });
|
|
210
|
-
|
|
211
|
-
// Le résultat est une référence, on le copie pour l'utiliser dans notre contexte principal.
|
|
212
|
-
return result;
|
|
213
|
-
|
|
214
|
-
} catch (error) {
|
|
215
|
-
logger.error("Error executing script with isolated-vm:", error.stack);
|
|
216
|
-
// On propage une erreur propre
|
|
217
|
-
throw new Error(`Script execution failed: ${error.message}`);
|
|
218
|
-
} finally {
|
|
219
|
-
// TRÈS IMPORTANT : Toujours libérer l'isolate pour éviter les fuites de mémoire.
|
|
220
|
-
if (isolate && !isolate.isDisposed) {
|
|
221
|
-
isolate.dispose();
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
REQUIRES NODE >=20
|
|
226
|
-
*/
|
|
227
|
-
|
|
228
|
-
export async function executeSafeJavascript(actionDef, context, user) {
|
|
229
|
-
const code = actionDef.script;
|
|
230
|
-
const collectedLogs = []; // Tableau pour capturer les logs de cette exécution
|
|
193
|
+
// Helper to create a secure reference for our API functions
|
|
194
|
+
const createJailFunction = (fn) => new ivm.Reference(fn);
|
|
231
195
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
db: {
|
|
196
|
+
// 1. Build the sandboxed API
|
|
197
|
+
await jail.set('db', createJailFunction({
|
|
235
198
|
create: (modelName, dataObject) => insertData(modelName, dataObject, {}, user),
|
|
236
199
|
find: async (modelName, filter) => {
|
|
237
200
|
const result = await searchData({ user, query: { model: modelName, filter } });
|
|
238
|
-
return result.data;
|
|
201
|
+
return new ivm.ExternalCopy(result.data).copyInto();
|
|
239
202
|
},
|
|
240
203
|
findOne: async (modelName, filter) => {
|
|
241
204
|
const result = await searchData({ user, query: { model: modelName, filter, limit: 1 } });
|
|
242
|
-
return result.data?.[0] || null;
|
|
205
|
+
return new ivm.ExternalCopy(result.data?.[0] || null).copyInto();
|
|
243
206
|
},
|
|
244
207
|
update: (modelName, filter, updateObject) => patchData(modelName, filter, updateObject, {}, user),
|
|
245
208
|
delete: (modelName, filter) => deleteData(modelName, null, filter, user)
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
const finalMessage = logger.trace('info', '[VM Script]', ...args);
|
|
252
|
-
// On l'affiche en couleur dans la console du serveur
|
|
253
|
-
// On stocke ce même message final dans les logs collectés
|
|
254
|
-
collectedLogs.push({
|
|
255
|
-
level: 'info',
|
|
256
|
-
message: finalMessage, // Le message complet est stocké
|
|
257
|
-
timestamp: new Date().toISOString()
|
|
258
|
-
});
|
|
259
|
-
},
|
|
260
|
-
warn: (...args) => {
|
|
261
|
-
const finalMessage = logger.trace('warn', '[VM Script]', ...args);
|
|
262
|
-
collectedLogs.push({
|
|
263
|
-
level: 'warn',
|
|
264
|
-
message: finalMessage,
|
|
265
|
-
timestamp: new Date().toISOString()
|
|
266
|
-
});
|
|
267
|
-
},
|
|
268
|
-
error: (...args) => {
|
|
269
|
-
const finalMessage = logger.trace('error', '[VM Script]', ...args);
|
|
209
|
+
}));
|
|
210
|
+
|
|
211
|
+
const createLoggerFunction = (level) => {
|
|
212
|
+
return (...args) => {
|
|
213
|
+
const finalMessage = logger.trace(level, '[VM Script]', ...args);
|
|
270
214
|
collectedLogs.push({
|
|
271
|
-
level:
|
|
215
|
+
level: level,
|
|
272
216
|
message: finalMessage,
|
|
273
217
|
timestamp: new Date().toISOString()
|
|
274
218
|
});
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
|
|
219
|
+
};
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
await jail.set('logger', createJailFunction({
|
|
223
|
+
info: createLoggerFunction('info'),
|
|
224
|
+
warn: createLoggerFunction('warn'),
|
|
225
|
+
error: createLoggerFunction('error')
|
|
226
|
+
}));
|
|
227
|
+
|
|
228
|
+
await jail.set('env', createJailFunction({
|
|
278
229
|
get: async (variableName) => {
|
|
279
230
|
if (!variableName) return null;
|
|
280
231
|
const result = await searchData({ user, query: { model: 'env', filter: { name: variableName }, limit: 1 } });
|
|
281
|
-
return result.data?.[0]?.value || null;
|
|
232
|
+
return new ivm.ExternalCopy(result.data?.[0]?.value || null).copyInto();
|
|
282
233
|
},
|
|
283
234
|
getAll: async () => {
|
|
284
235
|
const result = await searchData({ user, query: { model: 'env' } });
|
|
285
|
-
|
|
286
|
-
return result.data.reduce((acc, v) => {
|
|
236
|
+
const envObject = result.data.reduce((acc, v) => {
|
|
287
237
|
acc[v.name] = v.value;
|
|
288
238
|
return acc;
|
|
289
239
|
}, {});
|
|
240
|
+
return new ivm.ExternalCopy(envObject).copyInto();
|
|
290
241
|
}
|
|
291
|
-
}
|
|
292
|
-
...context
|
|
293
|
-
};
|
|
242
|
+
}));
|
|
294
243
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
sandbox: scriptApi,
|
|
298
|
-
console: 'off',
|
|
299
|
-
require: false,
|
|
300
|
-
wasm: false
|
|
301
|
-
});
|
|
244
|
+
// Inject the context data securely
|
|
245
|
+
await jail.set('context', new ivm.ExternalCopy(context).copyInto());
|
|
302
246
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
const
|
|
247
|
+
// 2. Prepare and run the script
|
|
248
|
+
// The script is wrapped in an async IIFE to handle promises correctly
|
|
249
|
+
const fullScript = `
|
|
250
|
+
(async () => {
|
|
251
|
+
${code}
|
|
252
|
+
})();
|
|
253
|
+
`;
|
|
254
|
+
|
|
255
|
+
const script = await isolate.compileScript(fullScript);
|
|
256
|
+
const result = await script.run(vmContext, { timeout: timeoutVM, promise: true });
|
|
257
|
+
|
|
258
|
+
// The result is returned as an ExternalCopy, we need to copy it out
|
|
306
259
|
return { success: true, data: result, logs: collectedLogs };
|
|
260
|
+
|
|
307
261
|
} catch (error) {
|
|
308
262
|
const errorMessage = `Script execution failed: ${error.message}`;
|
|
309
|
-
|
|
310
|
-
// On utilise aussi trace() pour l'erreur critique
|
|
311
263
|
const finalErrorMessage = logger.trace('critical', `[VM Script] ${errorMessage}\n${error.stack}`);
|
|
312
264
|
collectedLogs.push({
|
|
313
265
|
level: 'critical',
|
|
314
266
|
message: finalErrorMessage,
|
|
315
267
|
timestamp: new Date().toISOString()
|
|
316
268
|
});
|
|
317
|
-
|
|
318
269
|
return { success: false, message: errorMessage, logs: collectedLogs };
|
|
270
|
+
} finally {
|
|
271
|
+
// 3. CRUCIAL: Dispose of the isolate to prevent memory leaks
|
|
272
|
+
if (isolate && !isolate.isDisposed) {
|
|
273
|
+
isolate.dispose();
|
|
274
|
+
}
|
|
319
275
|
}
|
|
320
276
|
}
|
|
277
|
+
|
|
321
278
|
/**
|
|
322
279
|
* Handles the 'Webhook' workflow action.
|
|
323
280
|
* Sends an HTTP request to a specified URL with substituted data using native fetch.
|