data-primals-engine 1.1.0 → 1.1.2

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,17 +4,16 @@ import schedule from "node-schedule";
4
4
  import {ObjectId} from "mongodb";
5
5
  import crypto from "node:crypto";
6
6
 
7
+ import {NodeVM, VM, VMScript} from 'vm2';
7
8
  import {Logger} from "../gameObject.js";
8
- import {deleteData, editData, insertData, patchData, searchData} from "./data.js";
9
- import {maxExecutionsByStep, maxWorkflowSteps} from "../constants.js";
10
- import { ChatOpenAI } from "@langchain/openai";
11
- import { ChatGoogleGenerativeAI } from "@langchain/google-genai";
12
- import { ChatPromptTemplate } from "@langchain/core/prompts";
9
+ import {deleteData, insertData, patchData, searchData} from "./data.js";
10
+ import {maxExecutionsByStep, maxWorkflowSteps, timeoutVM} from "../constants.js";
11
+ import {ChatOpenAI} from "@langchain/openai";
12
+ import {ChatGoogleGenerativeAI} from "@langchain/google-genai";
13
+ import {ChatPromptTemplate} from "@langchain/core/prompts";
13
14
  import i18n from "data-primals-engine/i18n";
14
15
  import {sendEmail} from "../email.js";
15
16
 
16
- // 1. ADD THIS IMPORT AT THE TOP OF THE FILE
17
- // This allows the module to call its own exported functions.
18
17
  import * as workflowModule from './workflow.js';
19
18
 
20
19
  let logger = null;
@@ -181,6 +180,144 @@ export async function scheduleWorkflowTriggers() {
181
180
  }
182
181
 
183
182
 
183
+ /* Remplacement de la fonction executeSafeJavascript
184
+ async function executeWithIsolatedVm(actionDef, context) {
185
+ const code = actionDef.script;
186
+ const isolate = new ivm.Isolate({ memoryLimit: 128 }); // Limite de 128MB de mémoire
187
+
188
+ try {
189
+ const vmContext = await isolate.createContext();
190
+ const jail = vmContext.global;
191
+
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);
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
231
+
232
+ // 1. Construire l'API qui sera exposée au script
233
+ const scriptApi = {
234
+ db: {
235
+ create: (modelName, dataObject) => insertData(modelName, dataObject, {}, user),
236
+ find: async (modelName, filter) => {
237
+ const result = await searchData({ user, query: { model: modelName, filter } });
238
+ return result.data;
239
+ },
240
+ findOne: async (modelName, filter) => {
241
+ const result = await searchData({ user, query: { model: modelName, filter, limit: 1 } });
242
+ return result.data?.[0] || null;
243
+ },
244
+ update: (modelName, filter, updateObject) => patchData(modelName, filter, updateObject, {}, user),
245
+ delete: (modelName, filter) => deleteData(modelName, null, filter, user)
246
+ },
247
+ logger: {
248
+ // Chaque fonction de log utilise maintenant logger.trace() pour générer le message final
249
+ info: (...args) => {
250
+ // On génère le message final formaté une seule fois avec trace()
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);
270
+ collectedLogs.push({
271
+ level: 'error',
272
+ message: finalMessage,
273
+ timestamp: new Date().toISOString()
274
+ });
275
+ }
276
+ },
277
+ env: {
278
+ get: async (variableName) => {
279
+ if (!variableName) return null;
280
+ const result = await searchData({ user, query: { model: 'env', filter: { name: variableName }, limit: 1 } });
281
+ return result.data?.[0]?.value || null;
282
+ },
283
+ getAll: async () => {
284
+ const result = await searchData({ user, query: { model: 'env' } });
285
+ // Retourne un objet { NOM: valeur, ... } plus pratique
286
+ return result.data.reduce((acc, v) => {
287
+ acc[v.name] = v.value;
288
+ return acc;
289
+ }, {});
290
+ }
291
+ },
292
+ ...context
293
+ };
294
+
295
+ const vm = new NodeVM({
296
+ timeout: timeoutVM,
297
+ sandbox: scriptApi,
298
+ console: 'off',
299
+ require: false,
300
+ wasm: false
301
+ });
302
+
303
+ try {
304
+ const script = new VMScript(`const script = () => {${code}}; module.exports = script();`);
305
+ const result= vm.run(script);
306
+ return { success: true, data: result, logs: collectedLogs };
307
+ } catch (error) {
308
+ const errorMessage = `Script execution failed: ${error.message}`;
309
+
310
+ // On utilise aussi trace() pour l'erreur critique
311
+ const finalErrorMessage = logger.trace('critical', `[VM Script] ${errorMessage}\n${error.stack}`);
312
+ collectedLogs.push({
313
+ level: 'critical',
314
+ message: finalErrorMessage,
315
+ timestamp: new Date().toISOString()
316
+ });
317
+
318
+ return { success: false, message: errorMessage, logs: collectedLogs };
319
+ }
320
+ }
184
321
  /**
185
322
  * Handles the 'Webhook' workflow action.
186
323
  * Sends an HTTP request to a specified URL with substituted data using native fetch.
@@ -671,8 +808,9 @@ export async function executeStepAction(actionDef, contextData, user, dbCollecti
671
808
  case 'SendEmail':
672
809
  result = await handleSendEmailAction(actionDef, contextData, user);
673
810
  break;
674
-
675
- // ... autres cases à venir ...
811
+ case 'ExecuteScript':
812
+ result = await executeSafeJavascript(actionDef, contextData, user);
813
+ break;
676
814
  default:
677
815
  logger.error(`[executeStepAction] Unknown action type: ${actionDef.type}`);
678
816
  return { success: false, message: `Unknown action type: ${actionDef.type}` };
@@ -1179,7 +1317,7 @@ export async function processWorkflowRun(workflowRunId, user) {
1179
1317
  );
1180
1318
 
1181
1319
  let stepSucceeded = true;
1182
- let stepError = null;
1320
+ let logInfo = null;
1183
1321
  let conditionsMet = true;
1184
1322
 
1185
1323
  try {
@@ -1201,8 +1339,10 @@ export async function processWorkflowRun(workflowRunId, user) {
1201
1339
  const actionResult = await workflowModule.executeStepAction(actionDef, contextData, user, dbCollection);
1202
1340
  if (!actionResult.success) {
1203
1341
  stepSucceeded = false;
1204
- stepError = actionResult.message || `Action ${actionDef.name || actionId} failed.`;
1342
+ logInfo = actionResult.message || `Action ${actionDef.name || actionId} failed.`;
1205
1343
  break;
1344
+ }else{
1345
+ logInfo = `Action ${actionDef.name || actionId} : ${actionResult.message}`;
1206
1346
  }
1207
1347
  if (actionResult.updatedContext) {
1208
1348
  contextData = { ...contextData, ...actionResult.updatedContext };
@@ -1216,7 +1356,7 @@ export async function processWorkflowRun(workflowRunId, user) {
1216
1356
  } catch (error) {
1217
1357
  logger.error(`[processWorkflowRun] Run ID: ${runId}, Step ID: ${currentStepId}: Error during condition/action execution: ${error.message}`);
1218
1358
  stepSucceeded = false;
1219
- stepError = error.message;
1359
+ logInfo = error.message;
1220
1360
  }
1221
1361
 
1222
1362
  // --- 9. Détermination de la prochaine étape ---
@@ -1233,13 +1373,13 @@ export async function processWorkflowRun(workflowRunId, user) {
1233
1373
  }
1234
1374
  } else {
1235
1375
  // CHEMIN ÉCHEC/BRANCHE : Une action a échoué OU les conditions n'ont pas été remplies.
1236
- const reason = stepError ? `Action failed: ${stepError}` : 'Step conditions not met.';
1376
+ const reason = logInfo ? `Action failed: ${logInfo}` : 'Step conditions not met.';
1237
1377
  logger.warn(`[processWorkflowRun] Run ID: ${runId}, Step ID: ${currentStepId}: Taking failure/branching path. Reason: ${reason}`);
1238
1378
  nextStepId = currentStepDef.onFailureStep;
1239
1379
 
1240
1380
  if (!nextStepId || !isObjectId(nextStepId)) {
1241
1381
  // Fin du workflow. Le statut est 'failed' seulement si une vraie erreur s'est produite.
1242
- finalStatusForRun = stepError ? 'failed' : 'completed';
1382
+ finalStatusForRun = logInfo ? 'failed' : 'completed';
1243
1383
  nextStepId = null;
1244
1384
  }
1245
1385
  }
@@ -1252,9 +1392,7 @@ export async function processWorkflowRun(workflowRunId, user) {
1252
1392
  updatePayload.status = finalStatusForRun;
1253
1393
  updatePayload.completedAt = new Date();
1254
1394
  updatePayload.currentStep = null;
1255
- if (finalStatusForRun === 'failed' && stepError) {
1256
- updatePayload.error = stepError;
1257
- }
1395
+ updatePayload.log = logInfo;
1258
1396
  } else {
1259
1397
  updatePayload.currentStep = currentStepId;
1260
1398
  }
@@ -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);