data-primals-engine 1.1.1 → 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.
@@ -4,10 +4,11 @@ import schedule from "node-schedule";
4
4
  import {ObjectId} from "mongodb";
5
5
  import crypto from "node:crypto";
6
6
 
7
- import {VM, VMScript} from 'vm2';
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
- import {maxExecutionsByStep, maxWorkflowSteps} from "../constants.js";
11
+ import {maxExecutionsByStep, maxWorkflowSteps, timeoutVM} from "../constants.js";
11
12
  import {ChatOpenAI} from "@langchain/openai";
12
13
  import {ChatGoogleGenerativeAI} from "@langchain/google-genai";
13
14
  import {ChatPromptTemplate} from "@langchain/core/prompts";
@@ -180,68 +181,99 @@ export async function scheduleWorkflowTriggers() {
180
181
  }
181
182
 
182
183
 
183
- /* Remplacement de la fonction executeSafeJavascript
184
- async function executeWithIsolatedVm(actionDef, context) {
184
+ export async function executeSafeJavascript(actionDef, context, user) {
185
185
  const code = actionDef.script;
186
- const isolate = new ivm.Isolate({ memoryLimit: 128 }); // Limite de 128MB de mémoire
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
- // On expose une fonction 'log' sécurisée à l'intérieur de la VM.
193
- // C'est une référence, pas la fonction elle-même.
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);
193
+ // Helper to create a secure reference for our API functions
194
+ const createJailFunction = (fn) => new ivm.Reference(fn);
195
+
196
+ // 1. Build the sandboxed API
197
+ await jail.set('db', createJailFunction({
198
+ create: (modelName, dataObject) => insertData(modelName, dataObject, {}, user),
199
+ find: async (modelName, filter) => {
200
+ const result = await searchData({ user, query: { model: modelName, filter } });
201
+ return new ivm.ExternalCopy(result.data).copyInto();
202
+ },
203
+ findOne: async (modelName, filter) => {
204
+ const result = await searchData({ user, query: { model: modelName, filter, limit: 1 } });
205
+ return new ivm.ExternalCopy(result.data?.[0] || null).copyInto();
206
+ },
207
+ update: (modelName, filter, updateObject) => patchData(modelName, filter, updateObject, {}, user),
208
+ delete: (modelName, filter) => deleteData(modelName, null, filter, user)
197
209
  }));
198
210
 
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) => { ... }));
211
+ const createLoggerFunction = (level) => {
212
+ return (...args) => {
213
+ const finalMessage = logger.trace(level, '[VM Script]', ...args);
214
+ collectedLogs.push({
215
+ level: level,
216
+ message: finalMessage,
217
+ timestamp: new Date().toISOString()
218
+ });
219
+ };
220
+ };
204
221
 
205
- // On compile le script
206
- const script = await isolate.compileScript(code);
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({
229
+ get: async (variableName) => {
230
+ if (!variableName) return null;
231
+ const result = await searchData({ user, query: { model: 'env', filter: { name: variableName }, limit: 1 } });
232
+ return new ivm.ExternalCopy(result.data?.[0]?.value || null).copyInto();
233
+ },
234
+ getAll: async () => {
235
+ const result = await searchData({ user, query: { model: 'env' } });
236
+ const envObject = result.data.reduce((acc, v) => {
237
+ acc[v.name] = v.value;
238
+ return acc;
239
+ }, {});
240
+ return new ivm.ExternalCopy(envObject).copyInto();
241
+ }
242
+ }));
207
243
 
208
- // On exécute le script avec un timeout
209
- const result = await script.run(vmContext, { timeout: 1000 });
244
+ // Inject the context data securely
245
+ await jail.set('context', new ivm.ExternalCopy(context).copyInto());
210
246
 
211
- // Le résultat est une référence, on le copie pour l'utiliser dans notre contexte principal.
212
- return result;
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
259
+ return { success: true, data: result, logs: collectedLogs };
213
260
 
214
261
  } 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}`);
262
+ const errorMessage = `Script execution failed: ${error.message}`;
263
+ const finalErrorMessage = logger.trace('critical', `[VM Script] ${errorMessage}\n${error.stack}`);
264
+ collectedLogs.push({
265
+ level: 'critical',
266
+ message: finalErrorMessage,
267
+ timestamp: new Date().toISOString()
268
+ });
269
+ return { success: false, message: errorMessage, logs: collectedLogs };
218
270
  } finally {
219
- // TRÈS IMPORTANT : Toujours libérer l'isolate pour éviter les fuites de mémoire.
271
+ // 3. CRUCIAL: Dispose of the isolate to prevent memory leaks
220
272
  if (isolate && !isolate.isDisposed) {
221
273
  isolate.dispose();
222
274
  }
223
275
  }
224
276
  }
225
- REQUIRES NODE >=20
226
- */
227
- async function executeSafeJavascript(actionDef, context) {
228
- const code = actionDef.script;
229
- const vm = new VM({
230
- timeout: 1000, // Time out after 1 second
231
- sandbox: context, // Pass the context object
232
- console: 'redirect', // Redirect console output
233
- require: false, // Disable require
234
- wasm: false // disable WebAssembly
235
- });
236
-
237
- try {
238
- const script = new VMScript(code);
239
- return vm.run(script);
240
- } catch (error) {
241
- console.error("Error executing script:", error);
242
- throw error; // or return an error object
243
- }
244
- }
245
277
 
246
278
  /**
247
279
  * Handles the 'Webhook' workflow action.
@@ -734,10 +766,8 @@ export async function executeStepAction(actionDef, contextData, user, dbCollecti
734
766
  result = await handleSendEmailAction(actionDef, contextData, user);
735
767
  break;
736
768
  case 'ExecuteScript':
737
- result = await executeSafeJavascript(actionDef, contextData);
769
+ result = await executeSafeJavascript(actionDef, contextData, user);
738
770
  break;
739
-
740
- // ... autres cases à venir ...
741
771
  default:
742
772
  logger.error(`[executeStepAction] Unknown action type: ${actionDef.type}`);
743
773
  return { success: false, message: `Unknown action type: ${actionDef.type}` };
@@ -1244,7 +1274,7 @@ export async function processWorkflowRun(workflowRunId, user) {
1244
1274
  );
1245
1275
 
1246
1276
  let stepSucceeded = true;
1247
- let stepError = null;
1277
+ let logInfo = null;
1248
1278
  let conditionsMet = true;
1249
1279
 
1250
1280
  try {
@@ -1266,8 +1296,10 @@ export async function processWorkflowRun(workflowRunId, user) {
1266
1296
  const actionResult = await workflowModule.executeStepAction(actionDef, contextData, user, dbCollection);
1267
1297
  if (!actionResult.success) {
1268
1298
  stepSucceeded = false;
1269
- stepError = actionResult.message || `Action ${actionDef.name || actionId} failed.`;
1299
+ logInfo = actionResult.message || `Action ${actionDef.name || actionId} failed.`;
1270
1300
  break;
1301
+ }else{
1302
+ logInfo = `Action ${actionDef.name || actionId} : ${actionResult.message}`;
1271
1303
  }
1272
1304
  if (actionResult.updatedContext) {
1273
1305
  contextData = { ...contextData, ...actionResult.updatedContext };
@@ -1281,7 +1313,7 @@ export async function processWorkflowRun(workflowRunId, user) {
1281
1313
  } catch (error) {
1282
1314
  logger.error(`[processWorkflowRun] Run ID: ${runId}, Step ID: ${currentStepId}: Error during condition/action execution: ${error.message}`);
1283
1315
  stepSucceeded = false;
1284
- stepError = error.message;
1316
+ logInfo = error.message;
1285
1317
  }
1286
1318
 
1287
1319
  // --- 9. Détermination de la prochaine étape ---
@@ -1298,13 +1330,13 @@ export async function processWorkflowRun(workflowRunId, user) {
1298
1330
  }
1299
1331
  } else {
1300
1332
  // CHEMIN ÉCHEC/BRANCHE : Une action a échoué OU les conditions n'ont pas été remplies.
1301
- const reason = stepError ? `Action failed: ${stepError}` : 'Step conditions not met.';
1333
+ const reason = logInfo ? `Action failed: ${logInfo}` : 'Step conditions not met.';
1302
1334
  logger.warn(`[processWorkflowRun] Run ID: ${runId}, Step ID: ${currentStepId}: Taking failure/branching path. Reason: ${reason}`);
1303
1335
  nextStepId = currentStepDef.onFailureStep;
1304
1336
 
1305
1337
  if (!nextStepId || !isObjectId(nextStepId)) {
1306
1338
  // Fin du workflow. Le statut est 'failed' seulement si une vraie erreur s'est produite.
1307
- finalStatusForRun = stepError ? 'failed' : 'completed';
1339
+ finalStatusForRun = logInfo ? 'failed' : 'completed';
1308
1340
  nextStepId = null;
1309
1341
  }
1310
1342
  }
@@ -1317,9 +1349,7 @@ export async function processWorkflowRun(workflowRunId, user) {
1317
1349
  updatePayload.status = finalStatusForRun;
1318
1350
  updatePayload.completedAt = new Date();
1319
1351
  updatePayload.currentStep = null;
1320
- if (finalStatusForRun === 'failed' && stepError) {
1321
- updatePayload.error = stepError;
1322
- }
1352
+ updatePayload.log = logInfo;
1323
1353
  } else {
1324
1354
  updatePayload.currentStep = currentStepId;
1325
1355
  }
@@ -723,7 +723,7 @@ describe('Intégration des fonctions CRUD de données avec validation complète'
723
723
  const packId = packInsertResult.insertedId;
724
724
 
725
725
  // 3. Appeler la fonction à tester avec la nouvelle signature
726
- const result = await installPack(console, packId, currentTestUser, 'en');
726
+ const result = await installPack(packId, currentTestUser, 'en');
727
727
 
728
728
  // 4. Assertions sur le résumé de l'installation
729
729
  expect(result.success, `L'installation du pack a échoué: ${result.errors?.join('; ')}`).toBe(true);
@@ -776,7 +776,7 @@ describe('Intégration des fonctions CRUD de données avec validation complète'
776
776
  const packInsertResult = await testPacksColInstance.insertOne(mockPackWithInvalidModel);
777
777
  const packId = packInsertResult.insertedId;
778
778
 
779
- const result = await installPack(console, packId, currentTestUser, 'en');
779
+ const result = await installPack(packId, currentTestUser, 'en');
780
780
 
781
781
  // Le succès global est faux s'il y a des erreurs
782
782
  expect(result.success).toBe(false);