jsfe 0.9.0 → 0.9.1
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/README.md +131 -9
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +391 -839
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1214,6 +1214,7 @@ function evaluateCondition(data, condition) {
|
|
|
1214
1214
|
if (!condition || typeof condition !== 'object')
|
|
1215
1215
|
return false;
|
|
1216
1216
|
const { field, operator, value } = condition;
|
|
1217
|
+
logger.debug(`evaluateCondition called with field: ${field}, operator: ${operator}, value: ${value}`);
|
|
1217
1218
|
try {
|
|
1218
1219
|
let fieldValue = undefined;
|
|
1219
1220
|
if (isPathTraversableObject(data)) {
|
|
@@ -1222,44 +1223,61 @@ function evaluateCondition(data, condition) {
|
|
|
1222
1223
|
switch (operator) {
|
|
1223
1224
|
case 'equals':
|
|
1224
1225
|
case 'eq':
|
|
1226
|
+
logger.debug(`evaluateCondition: checking equality for field ${field} with value ${value}`);
|
|
1225
1227
|
return fieldValue === value;
|
|
1226
1228
|
case 'notEquals':
|
|
1227
1229
|
case 'ne':
|
|
1230
|
+
logger.debug(`evaluateCondition: checking inequality for field ${field} with value ${value}`);
|
|
1228
1231
|
return fieldValue !== value;
|
|
1229
1232
|
case 'contains':
|
|
1233
|
+
logger.debug(`evaluateCondition: checking containment for field ${field} with value ${value}`);
|
|
1230
1234
|
return String(fieldValue).includes(String(value));
|
|
1231
1235
|
case 'exists':
|
|
1236
|
+
logger.debug(`evaluateCondition: checking existence for field ${field}`);
|
|
1232
1237
|
return fieldValue !== null && fieldValue !== undefined;
|
|
1233
1238
|
case 'notExists':
|
|
1239
|
+
logger.debug(`evaluateCondition: checking non-existence for field ${field}`);
|
|
1234
1240
|
return fieldValue === null || fieldValue === undefined;
|
|
1235
1241
|
case 'greaterThan':
|
|
1236
1242
|
case 'gt':
|
|
1243
|
+
logger.debug(`evaluateCondition: checking greaterThan for field ${field} with value ${value}`);
|
|
1237
1244
|
return Number(fieldValue) > Number(value);
|
|
1238
1245
|
case 'lessThan':
|
|
1239
1246
|
case 'lt':
|
|
1247
|
+
logger.debug(`evaluateCondition: checking lessThan for field ${field} with value ${value}`);
|
|
1240
1248
|
return Number(fieldValue) < Number(value);
|
|
1241
1249
|
case 'greaterThanOrEqual':
|
|
1242
1250
|
case 'gte':
|
|
1251
|
+
logger.debug(`evaluateCondition: checking greaterThanOrEqual for field ${field} with value ${value}`);
|
|
1243
1252
|
return Number(fieldValue) >= Number(value);
|
|
1244
1253
|
case 'lessThanOrEqual':
|
|
1245
1254
|
case 'lte':
|
|
1255
|
+
logger.debug(`evaluateCondition: checking lessThanOrEqual for field ${field} with value ${value}`);
|
|
1246
1256
|
return Number(fieldValue) <= Number(value);
|
|
1247
1257
|
case 'startsWith':
|
|
1258
|
+
logger.debug(`evaluateCondition: checking startsWith for field ${field} with value ${value}`);
|
|
1248
1259
|
return String(fieldValue).startsWith(String(value));
|
|
1249
1260
|
case 'endsWith':
|
|
1261
|
+
logger.debug(`evaluateCondition: checking endsWith for field ${field} with value ${value}`);
|
|
1250
1262
|
return String(fieldValue).endsWith(String(value));
|
|
1251
1263
|
case 'matches':
|
|
1264
|
+
logger.debug(`evaluateCondition: checking regex match for field ${field} with value ${value}`);
|
|
1252
1265
|
return new RegExp(String(value)).test(String(fieldValue));
|
|
1253
1266
|
case 'in':
|
|
1267
|
+
logger.debug(`evaluateCondition: checking inclusion for field ${field} with value ${value}`);
|
|
1254
1268
|
return Array.isArray(value) && value.includes(fieldValue);
|
|
1255
1269
|
case 'hasLength':
|
|
1256
1270
|
const length = Array.isArray(fieldValue) ? fieldValue.length : String(fieldValue).length;
|
|
1271
|
+
logger.debug(`evaluateCondition: checking length for field ${field} with value ${value}, actual length: ${length}`);
|
|
1257
1272
|
return length === Number(value);
|
|
1258
1273
|
case 'isArray':
|
|
1274
|
+
logger.debug(`evaluateCondition: checking if field ${field} is an array`);
|
|
1259
1275
|
return Array.isArray(fieldValue);
|
|
1260
1276
|
case 'isObject':
|
|
1277
|
+
logger.debug(`evaluateCondition: checking if field ${field} is an object`);
|
|
1261
1278
|
return typeof fieldValue === 'object' && fieldValue !== null && !Array.isArray(fieldValue);
|
|
1262
1279
|
case 'isString':
|
|
1280
|
+
logger.debug(`evaluateCondition: checking if field ${field} is a string`);
|
|
1263
1281
|
return typeof fieldValue === 'string';
|
|
1264
1282
|
case 'isNumber':
|
|
1265
1283
|
return typeof fieldValue === 'number' && !isNaN(fieldValue);
|
|
@@ -1284,14 +1302,19 @@ function interpolateObject(obj, data, args = {}, engine) {
|
|
|
1284
1302
|
let value = undefined;
|
|
1285
1303
|
if (isPathTraversableObject(data)) {
|
|
1286
1304
|
value = extractByPath(data, path);
|
|
1305
|
+
// Handle user input wrapper objects
|
|
1306
|
+
if (isUserInputVariable(value)) {
|
|
1307
|
+
value = value.value;
|
|
1308
|
+
}
|
|
1287
1309
|
}
|
|
1288
|
-
// If not found in data and engine is available,
|
|
1310
|
+
// If not found in data and engine is available, use the new unified resolver
|
|
1289
1311
|
if ((value === undefined || value === null)) {
|
|
1290
1312
|
try {
|
|
1291
|
-
|
|
1313
|
+
// Use the new resolveVariablePath function which handles all variable types
|
|
1314
|
+
value = resolveVariablePath(path, args, [], engine);
|
|
1292
1315
|
}
|
|
1293
1316
|
catch (error) {
|
|
1294
|
-
// Ignore
|
|
1317
|
+
// Ignore variable resolution errors
|
|
1295
1318
|
}
|
|
1296
1319
|
}
|
|
1297
1320
|
return value !== undefined && value !== null ? value : '';
|
|
@@ -1306,14 +1329,19 @@ function interpolateObject(obj, data, args = {}, engine) {
|
|
|
1306
1329
|
let value = undefined;
|
|
1307
1330
|
if (isPathTraversableObject(data)) {
|
|
1308
1331
|
value = extractByPath(data, path.trim());
|
|
1332
|
+
// Handle user input wrapper objects
|
|
1333
|
+
if (isUserInputVariable(value)) {
|
|
1334
|
+
value = value.value;
|
|
1335
|
+
}
|
|
1309
1336
|
}
|
|
1310
|
-
// If not found in data and engine is available,
|
|
1337
|
+
// If not found in data and engine is available, use the new unified resolver
|
|
1311
1338
|
if ((value === undefined || value === null)) {
|
|
1312
1339
|
try {
|
|
1313
|
-
|
|
1340
|
+
// Use the new resolveVariablePath function which handles all variable types
|
|
1341
|
+
value = resolveVariablePath(path.trim(), args, [], engine);
|
|
1314
1342
|
}
|
|
1315
1343
|
catch (error) {
|
|
1316
|
-
// Ignore
|
|
1344
|
+
// Ignore variable resolution errors
|
|
1317
1345
|
}
|
|
1318
1346
|
}
|
|
1319
1347
|
return value !== null && value !== undefined ? String(value) : '';
|
|
@@ -1501,11 +1529,8 @@ function checkRateLimit(engine, userId, toolId) {
|
|
|
1501
1529
|
function sanitizeInput(input) {
|
|
1502
1530
|
if (typeof input !== 'string')
|
|
1503
1531
|
return input;
|
|
1504
|
-
//
|
|
1505
|
-
return input.trim()
|
|
1506
|
-
const escapeMap = { '<': '<', '>': '>', '"': '"', "'": ''', '&': '&' };
|
|
1507
|
-
return escapeMap[char];
|
|
1508
|
-
});
|
|
1532
|
+
// Simple trim - no HTML encoding since user input is treated as literal data
|
|
1533
|
+
return input.trim();
|
|
1509
1534
|
}
|
|
1510
1535
|
function validateToolArgs(tool, args) {
|
|
1511
1536
|
if (!tool.parameters)
|
|
@@ -1807,10 +1832,11 @@ async function playFlowFrame(engine) {
|
|
|
1807
1832
|
delete currentFlowFrame.pendingVariable;
|
|
1808
1833
|
}
|
|
1809
1834
|
else {
|
|
1810
|
-
// Store user input as variable value
|
|
1835
|
+
// Store user input as variable value with proper sanitization
|
|
1811
1836
|
// (System commands like 'cancel' are already handled before this point)
|
|
1812
|
-
currentFlowFrame.variables
|
|
1813
|
-
|
|
1837
|
+
setUserInputVariable(currentFlowFrame.variables, currentFlowFrame.pendingVariable, userInput, true // Sanitize user input
|
|
1838
|
+
);
|
|
1839
|
+
logger.info(`Stored sanitized user input in variable '${currentFlowFrame.pendingVariable}': "${userInput}"`);
|
|
1814
1840
|
delete currentFlowFrame.pendingVariable;
|
|
1815
1841
|
// Pop the SAY-GET step now that variable assignment is complete
|
|
1816
1842
|
currentFlowFrame.flowStepsStack.pop();
|
|
@@ -2124,7 +2150,7 @@ async function getFlowForInput(input, engine) {
|
|
|
2124
2150
|
// === SMART DEFAULT ONFAIL GENERATOR ===
|
|
2125
2151
|
function generateSmartRetryDefaultOnFail(step, error, currentFlowFrame) {
|
|
2126
2152
|
const toolName = step.tool || 'unknown';
|
|
2127
|
-
const errorMessage = error.message || '
|
|
2153
|
+
const errorMessage = error.message.toLocaleLowerCase() || 'unknown error';
|
|
2128
2154
|
const flowName = currentFlowFrame.flowName;
|
|
2129
2155
|
logger.info(`Generating smart default onFail for tool ${toolName}, in flow ${flowName} for error: ${errorMessage}`);
|
|
2130
2156
|
// Categorize error types for intelligent handling
|
|
@@ -2152,7 +2178,8 @@ function generateSmartRetryDefaultOnFail(step, error, currentFlowFrame) {
|
|
|
2152
2178
|
errorMessage.includes('bad request') ||
|
|
2153
2179
|
errorMessage.includes('invalid request') ||
|
|
2154
2180
|
errorMessage.includes('malformed request') ||
|
|
2155
|
-
errorMessage.includes('syntax error')
|
|
2181
|
+
errorMessage.includes('syntax error') ||
|
|
2182
|
+
errorMessage.includes('rate limit');
|
|
2156
2183
|
// Provoke cancelation of the current flow if unrecoverable
|
|
2157
2184
|
const isAuthError = errorMessage.includes('401') || // Unauthorized
|
|
2158
2185
|
errorMessage.includes('402') || // Payment Required
|
|
@@ -2178,7 +2205,7 @@ function generateSmartRetryDefaultOnFail(step, error, currentFlowFrame) {
|
|
|
2178
2205
|
doCancel = true;
|
|
2179
2206
|
}
|
|
2180
2207
|
else {
|
|
2181
|
-
logger.
|
|
2208
|
+
logger.debug(`Unrecognized error type for tool ${toolName} in flow ${flowName}: ${errorMessage}`);
|
|
2182
2209
|
// Default to Cancel for unexpected errors
|
|
2183
2210
|
doCancel = true;
|
|
2184
2211
|
}
|
|
@@ -2304,7 +2331,7 @@ async function performStepInputValidation(step, currentFlowFrame, engine) {
|
|
|
2304
2331
|
// Custom validation function
|
|
2305
2332
|
if (step.inputValidation.customValidator && engine.APPROVED_FUNCTIONS) {
|
|
2306
2333
|
try {
|
|
2307
|
-
const validator = engine.APPROVED_FUNCTIONS
|
|
2334
|
+
const validator = engine.APPROVED_FUNCTIONS[step.inputValidation.customValidator];
|
|
2308
2335
|
if (typeof validator === 'function') {
|
|
2309
2336
|
const customResult = await validator(currentFlowFrame.variables, currentFlowFrame);
|
|
2310
2337
|
if (customResult && typeof customResult === 'object') {
|
|
@@ -2822,8 +2849,13 @@ function handleSetStep(currentFlowFrame, engine) {
|
|
|
2822
2849
|
throw new Error(`SET step requires both 'variable' and 'value' attributes`);
|
|
2823
2850
|
}
|
|
2824
2851
|
// Support interpolation in SET values
|
|
2852
|
+
// Use JavaScript evaluation context for security - strings will be properly quoted
|
|
2825
2853
|
const interpolatedValue = typeof step.value === 'string'
|
|
2826
|
-
?
|
|
2854
|
+
? evaluateExpression(step.value, currentFlowFrame?.variables || {}, [], {
|
|
2855
|
+
securityLevel: 'basic',
|
|
2856
|
+
context: 'javascript-evaluation', // Force JavaScript context for SET values
|
|
2857
|
+
returnType: 'auto'
|
|
2858
|
+
}, engine)
|
|
2827
2859
|
: step.value;
|
|
2828
2860
|
if (currentFlowFrame && currentFlowFrame.variables !== undefined) {
|
|
2829
2861
|
currentFlowFrame.variables[step.variable] = interpolatedValue;
|
|
@@ -2838,7 +2870,11 @@ async function handleSwitchStep(currentFlowFrame, engine) {
|
|
|
2838
2870
|
throw new Error(`SWITCH step requires both 'variable' and 'branches' attributes`);
|
|
2839
2871
|
}
|
|
2840
2872
|
// Get the variable value to switch on
|
|
2841
|
-
|
|
2873
|
+
let switchValue = currentFlowFrame.variables ? currentFlowFrame.variables[step.variable] : undefined;
|
|
2874
|
+
// Handle user input wrapper objects
|
|
2875
|
+
if (isUserInputVariable(switchValue)) {
|
|
2876
|
+
switchValue = switchValue.value;
|
|
2877
|
+
}
|
|
2842
2878
|
logger.info(`SWITCH step: evaluating variable '${step.variable}' with value '${switchValue}'`);
|
|
2843
2879
|
// Find the matching branch (exact value matching only)
|
|
2844
2880
|
let selectedStep = null;
|
|
@@ -2938,7 +2974,7 @@ async function handleSubFlowStep(currentFlowFrame, engine) {
|
|
|
2938
2974
|
const input = currentFlowFrame.inputStack[currentFlowFrame.inputStack.length - 1];
|
|
2939
2975
|
const flowsMenu = engine.flowsMenu; // Access the global flows menu
|
|
2940
2976
|
const subFlowName = step.value || step.name || step.nextFlow;
|
|
2941
|
-
const subFlow = flowsMenu?.find(f => f.name === subFlowName);
|
|
2977
|
+
const subFlow = flowsMenu?.find(f => f.name === subFlowName || f.id === subFlowName);
|
|
2942
2978
|
if (!subFlow) {
|
|
2943
2979
|
return getSystemMessage(engine, 'subflow_not_found', { subFlowName });
|
|
2944
2980
|
}
|
|
@@ -3212,6 +3248,11 @@ function generateEnhancedFallbackArgs(schema, input, flowFrame) {
|
|
|
3212
3248
|
// Direct variable match
|
|
3213
3249
|
if (flowFrame.variables[key] !== undefined) {
|
|
3214
3250
|
let value = flowFrame.variables[key];
|
|
3251
|
+
// Handle user input wrapper objects
|
|
3252
|
+
if (isUserInputVariable(value)) {
|
|
3253
|
+
const userInputObj = value;
|
|
3254
|
+
value = userInputObj.value;
|
|
3255
|
+
}
|
|
3215
3256
|
// Type conversion if needed
|
|
3216
3257
|
if (prop.type === 'number' && typeof value === 'string') {
|
|
3217
3258
|
const num = parseFloat(value);
|
|
@@ -3222,37 +3263,6 @@ function generateEnhancedFallbackArgs(schema, input, flowFrame) {
|
|
|
3222
3263
|
args[key] = value;
|
|
3223
3264
|
continue;
|
|
3224
3265
|
}
|
|
3225
|
-
// Smart matching for account numbers
|
|
3226
|
-
if (key.includes('account')) {
|
|
3227
|
-
const accountVar = Object.keys(flowFrame.variables).find(varName => varName.includes('account') || (typeof flowFrame.variables[varName] === 'object' &&
|
|
3228
|
-
flowFrame.variables[varName] !== null &&
|
|
3229
|
-
flowFrame.variables[varName]?.accountId));
|
|
3230
|
-
if (accountVar) {
|
|
3231
|
-
const accountData = flowFrame.variables[accountVar];
|
|
3232
|
-
if (typeof accountData === 'object' && accountData !== null && 'accountId' in accountData) {
|
|
3233
|
-
args[key] = accountData.accountId;
|
|
3234
|
-
}
|
|
3235
|
-
else if (typeof accountData === 'string') {
|
|
3236
|
-
args[key] = accountData;
|
|
3237
|
-
}
|
|
3238
|
-
}
|
|
3239
|
-
}
|
|
3240
|
-
// Smart matching for amounts
|
|
3241
|
-
if (key.includes('amount')) {
|
|
3242
|
-
const amountVar = Object.keys(flowFrame.variables).find(varName => varName.includes('amount') || varName.includes('payment'));
|
|
3243
|
-
if (amountVar) {
|
|
3244
|
-
const amountData = flowFrame.variables[amountVar];
|
|
3245
|
-
if (typeof amountData === 'number') {
|
|
3246
|
-
args[key] = amountData;
|
|
3247
|
-
}
|
|
3248
|
-
else if (typeof amountData === 'string') {
|
|
3249
|
-
const num = parseFloat(amountData);
|
|
3250
|
-
if (!isNaN(num)) {
|
|
3251
|
-
args[key] = num;
|
|
3252
|
-
}
|
|
3253
|
-
}
|
|
3254
|
-
}
|
|
3255
|
-
}
|
|
3256
3266
|
}
|
|
3257
3267
|
}
|
|
3258
3268
|
// If enhanced fallback didn't find anything useful, try simple pattern matching
|
|
@@ -3315,7 +3325,7 @@ async function callTool(engine, tool, args, userId = 'anonymous', transactionId
|
|
|
3315
3325
|
if (!APPROVED_FUNCTIONS) {
|
|
3316
3326
|
throw new Error(`Approved functions registry not available`);
|
|
3317
3327
|
}
|
|
3318
|
-
const fn = APPROVED_FUNCTIONS
|
|
3328
|
+
const fn = APPROVED_FUNCTIONS[tool.implementation.function];
|
|
3319
3329
|
if (!fn) {
|
|
3320
3330
|
throw new Error(`Function "${tool.implementation.function}" not found in approved functions registry`);
|
|
3321
3331
|
}
|
|
@@ -3885,10 +3895,115 @@ export async function portableHash(data, secret, algorithm = 'SHA-256', encoding
|
|
|
3885
3895
|
return encoding === 'base64' ? arrayBufferToBase64(hash) : Array.from(new Uint8Array(hash)).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
3886
3896
|
}
|
|
3887
3897
|
}
|
|
3888
|
-
|
|
3898
|
+
/**
|
|
3899
|
+
* Resolve variable path like "cargo.callerId" or "user.profile.name"
|
|
3900
|
+
* Supports variables, context stack, engine session variables, and global variables
|
|
3901
|
+
*/
|
|
3902
|
+
function resolveVariablePath(path, variables, contextStack, engine) {
|
|
3903
|
+
const parts = path.split('.');
|
|
3904
|
+
const rootVar = parts[0];
|
|
3905
|
+
// 1. Try to find in flow variables first
|
|
3906
|
+
let rootValue = variables[rootVar];
|
|
3907
|
+
// Handle user input variables as pure literals
|
|
3908
|
+
if (rootValue && typeof rootValue === 'object' && rootValue !== null) {
|
|
3909
|
+
const userInputObj = rootValue;
|
|
3910
|
+
if (userInputObj.__userInput === true && userInputObj.__literal === true) {
|
|
3911
|
+
// Extract the literal value and treat it as a normal variable
|
|
3912
|
+
// This allows normal property access like userInput.length
|
|
3913
|
+
rootValue = userInputObj.value;
|
|
3914
|
+
}
|
|
3915
|
+
else {
|
|
3916
|
+
logger.debug(`resolveVariablePath - Found user input object, but not a literal: ${JSON.stringify(userInputObj)}`);
|
|
3917
|
+
}
|
|
3918
|
+
}
|
|
3919
|
+
// 2. If not found in variables, try context stack
|
|
3920
|
+
if (rootValue === undefined && contextStack.length > 0) {
|
|
3921
|
+
for (const context of contextStack) {
|
|
3922
|
+
if (context && typeof context === 'object' && rootVar in context) {
|
|
3923
|
+
rootValue = context[rootVar];
|
|
3924
|
+
break;
|
|
3925
|
+
}
|
|
3926
|
+
}
|
|
3927
|
+
}
|
|
3928
|
+
// 3. If not found and engine available, try engine session variables
|
|
3929
|
+
if (rootValue === undefined && engine) {
|
|
3930
|
+
// Use the centralized session variable logic
|
|
3931
|
+
const sessionVars = getEngineSessionVariables(engine, contextStack);
|
|
3932
|
+
// Handle property access (e.g., cargo.someVar)
|
|
3933
|
+
if (path.includes('.')) {
|
|
3934
|
+
const parts = path.split('.');
|
|
3935
|
+
const baseVariable = parts[0];
|
|
3936
|
+
const propertyPath = parts.slice(1).join('.');
|
|
3937
|
+
// Get the base session variable
|
|
3938
|
+
const baseValue = sessionVars[baseVariable];
|
|
3939
|
+
// If we found a base value, navigate the remaining path
|
|
3940
|
+
if (baseValue !== undefined) {
|
|
3941
|
+
return resolveVariablePath(propertyPath, { root: baseValue }, [], engine);
|
|
3942
|
+
}
|
|
3943
|
+
}
|
|
3944
|
+
else {
|
|
3945
|
+
// Handle direct variable access (no dots)
|
|
3946
|
+
rootValue = sessionVars[path];
|
|
3947
|
+
}
|
|
3948
|
+
}
|
|
3949
|
+
// 4. If not found and engine available, try global variables
|
|
3950
|
+
if (rootValue === undefined && engine?.globalVariables) {
|
|
3951
|
+
rootValue = engine.globalVariables[rootVar];
|
|
3952
|
+
}
|
|
3953
|
+
// If still not found, return undefined
|
|
3954
|
+
if (rootValue === undefined) {
|
|
3955
|
+
return undefined;
|
|
3956
|
+
}
|
|
3957
|
+
// Navigate the remaining path safely
|
|
3958
|
+
let current = rootValue;
|
|
3959
|
+
for (let i = 1; i < parts.length; i++) {
|
|
3960
|
+
if (current === null || current === undefined) {
|
|
3961
|
+
return undefined;
|
|
3962
|
+
}
|
|
3963
|
+
if (typeof current === 'object' && current !== null) {
|
|
3964
|
+
current = current[parts[i]];
|
|
3965
|
+
}
|
|
3966
|
+
else {
|
|
3967
|
+
return undefined;
|
|
3968
|
+
}
|
|
3969
|
+
}
|
|
3970
|
+
return current;
|
|
3971
|
+
}
|
|
3972
|
+
/**
|
|
3973
|
+
* Check if a variable value is marked as user input (literal data)
|
|
3974
|
+
* User input should never be evaluated as code for security
|
|
3975
|
+
*/
|
|
3976
|
+
function isUserInputVariable(value) {
|
|
3977
|
+
return (value !== null &&
|
|
3978
|
+
typeof value === 'object' &&
|
|
3979
|
+
value.__userInput === true &&
|
|
3980
|
+
value.__literal === true);
|
|
3981
|
+
}
|
|
3982
|
+
/**
|
|
3983
|
+
* Input sanitization for user-provided values
|
|
3984
|
+
* Escapes special characters that could break expression syntax
|
|
3985
|
+
* NOTE: This is secondary defense - primary security is treating user input as literal data
|
|
3986
|
+
*/
|
|
3987
|
+
function sanitizeUserInput(input) {
|
|
3988
|
+
if (typeof input !== 'string') {
|
|
3989
|
+
return String(input);
|
|
3990
|
+
}
|
|
3991
|
+
// Escape special characters that could break expression syntax
|
|
3992
|
+
return input
|
|
3993
|
+
.replace(/\\/g, '\\\\') // Escape backslashes
|
|
3994
|
+
.replace(/'/g, "\\'") // Escape single quotes
|
|
3995
|
+
.replace(/"/g, '\\"') // Escape double quotes
|
|
3996
|
+
.replace(/\n/g, '\\n') // Escape newlines
|
|
3997
|
+
.replace(/\r/g, '\\r') // Escape carriage returns
|
|
3998
|
+
.replace(/\t/g, '\\t'); // Escape tabs
|
|
3999
|
+
}
|
|
4000
|
+
/**
|
|
4001
|
+
* Simplified Expression Evaluator
|
|
4002
|
+
* Replaced complex two-phase parser with regex + JavaScript evaluation for 100% reliability
|
|
4003
|
+
*/
|
|
3889
4004
|
function evaluateExpression(expression, variables = {}, contextStack = [], options = {}, engine) {
|
|
3890
4005
|
const opts = {
|
|
3891
|
-
securityLevel: '
|
|
4006
|
+
securityLevel: 'basic',
|
|
3892
4007
|
allowLogicalOperators: true,
|
|
3893
4008
|
allowMathOperators: true,
|
|
3894
4009
|
allowComparisons: true,
|
|
@@ -3898,335 +4013,56 @@ function evaluateExpression(expression, variables = {}, contextStack = [], optio
|
|
|
3898
4013
|
...options
|
|
3899
4014
|
};
|
|
3900
4015
|
try {
|
|
3901
|
-
logger.debug(`
|
|
3902
|
-
//
|
|
3903
|
-
if (
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
}
|
|
3907
|
-
|
|
3908
|
-
//
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
}
|
|
3924
|
-
// Now check the processed expression for operators (after template interpolation)
|
|
3925
|
-
// Handle logical AND/OR expressions (if allowed)
|
|
3926
|
-
if (opts.allowLogicalOperators && (processedExpression.includes('&&') || processedExpression.includes('||'))) {
|
|
3927
|
-
const result = evaluateLogicalExpression(processedExpression, variables, contextStack, opts, engine);
|
|
3928
|
-
return convertReturnType(result, opts.returnType);
|
|
3929
|
-
}
|
|
3930
|
-
// Handle comparison expressions (if allowed)
|
|
3931
|
-
if (opts.allowComparisons && containsComparisonOperators(processedExpression)) {
|
|
3932
|
-
const result = evaluateComparisonExpression(processedExpression, variables, contextStack, opts, engine);
|
|
3933
|
-
return convertReturnType(result, opts.returnType);
|
|
3934
|
-
}
|
|
3935
|
-
// Handle ternary expressions (if allowed)
|
|
3936
|
-
if (opts.allowTernary && processedExpression.includes('?') && processedExpression.includes(':')) {
|
|
3937
|
-
const result = evaluateSafeTernaryExpression(processedExpression, variables, contextStack, engine);
|
|
3938
|
-
return convertReturnType(result, opts.returnType);
|
|
3939
|
-
}
|
|
3940
|
-
// Handle mathematical expressions (if allowed)
|
|
3941
|
-
if (opts.allowMathOperators && isMathematicalExpression(processedExpression)) {
|
|
3942
|
-
const result = evaluateSafeMathematicalExpression(processedExpression, variables, contextStack, engine);
|
|
3943
|
-
return convertReturnType(result, opts.returnType);
|
|
3944
|
-
}
|
|
3945
|
-
// Handle typeof operator (e.g., "typeof variable", "typeof container")
|
|
3946
|
-
if (processedExpression.trim().startsWith('typeof ')) {
|
|
3947
|
-
const result = evaluateTypeofExpression(processedExpression, variables, contextStack, engine);
|
|
3948
|
-
return convertReturnType(result, opts.returnType);
|
|
3949
|
-
}
|
|
3950
|
-
// Handle simple variable paths (e.g., "user.name", "data.items.length")
|
|
3951
|
-
if (isSimpleVariablePath(processedExpression)) {
|
|
3952
|
-
const result = resolveSimpleVariable(processedExpression, variables, contextStack, engine);
|
|
3953
|
-
return convertReturnType(result, opts.returnType);
|
|
3954
|
-
}
|
|
3955
|
-
// Handle function calls (e.g., "currentTime()", "extractCryptoFromInput(...)")
|
|
3956
|
-
if (processedExpression.includes('(') && processedExpression.includes(')')) {
|
|
3957
|
-
const result = evaluateFunctionCall(processedExpression, variables, contextStack, engine);
|
|
3958
|
-
if (result !== undefined) {
|
|
3959
|
-
return convertReturnType(result, opts.returnType);
|
|
3960
|
-
}
|
|
3961
|
-
}
|
|
3962
|
-
// If no pattern matches and we had template variables, return the interpolated result
|
|
3963
|
-
if (processedExpression !== expression) {
|
|
3964
|
-
return convertReturnType(processedExpression, opts.returnType);
|
|
3965
|
-
}
|
|
3966
|
-
// Fallback: treat as literal
|
|
3967
|
-
const result = expression;
|
|
3968
|
-
return convertReturnType(result, opts.returnType);
|
|
3969
|
-
}
|
|
3970
|
-
catch (error) {
|
|
3971
|
-
logger.warn(`Expression evaluation error in ${opts.context}: ${error.message}`);
|
|
3972
|
-
return opts.returnType === 'boolean' ? false : `[error: ${expression}]`;
|
|
3973
|
-
}
|
|
3974
|
-
}
|
|
3975
|
-
// Unified security pattern checking with configurable levels
|
|
3976
|
-
function containsUnsafePatterns(expression, options, engine) {
|
|
3977
|
-
const { securityLevel = 'standard', allowLogicalOperators = true, allowComparisons = true } = options;
|
|
3978
|
-
// Safe string methods that are allowed in expressions
|
|
3979
|
-
const safeStringMethods = [
|
|
3980
|
-
'toLowerCase', 'toUpperCase', 'trim', 'charAt', 'charCodeAt', 'indexOf', 'lastIndexOf',
|
|
3981
|
-
'substring', 'substr', 'slice', 'split', 'replace', 'match', 'search', 'includes',
|
|
3982
|
-
'startsWith', 'endsWith', 'padStart', 'padEnd', 'repeat', 'toString', 'valueOf',
|
|
3983
|
-
'length', 'concat', 'localeCompare', 'normalize'
|
|
3984
|
-
];
|
|
3985
|
-
// Safe array methods that are allowed in expressions
|
|
3986
|
-
const safeArrayMethods = [
|
|
3987
|
-
'length', 'join', 'indexOf', 'lastIndexOf', 'includes', 'slice', 'toString', 'valueOf'
|
|
3988
|
-
];
|
|
3989
|
-
// Safe math methods that are allowed in expressions
|
|
3990
|
-
const safeMathMethods = [
|
|
3991
|
-
'abs', 'ceil', 'floor', 'round', 'max', 'min', 'pow', 'sqrt', 'random'
|
|
3992
|
-
];
|
|
3993
|
-
// Safe standalone functions that are allowed
|
|
3994
|
-
const safeStandaloneFunctions = [
|
|
3995
|
-
'isNaN', 'isFinite', 'parseInt', 'parseFloat',
|
|
3996
|
-
'encodeURIComponent', 'decodeURIComponent', 'encodeURI', 'decodeURI',
|
|
3997
|
-
'String', 'Number', 'Boolean' // Type conversion functions (not new constructors)
|
|
3998
|
-
];
|
|
3999
|
-
// Check if expression contains only safe function calls
|
|
4000
|
-
const functionCallPattern = /(\w+)\.(\w+)\s*\(/g;
|
|
4001
|
-
const standaloneFunctionPattern = /(?:^|[^.\w])(\w+)\s*\(/g;
|
|
4002
|
-
let match;
|
|
4003
|
-
const foundFunctionCalls = [];
|
|
4004
|
-
// First, check for any standalone function calls (not methods)
|
|
4005
|
-
standaloneFunctionPattern.lastIndex = 0; // Reset regex
|
|
4006
|
-
while ((match = standaloneFunctionPattern.exec(expression)) !== null) {
|
|
4007
|
-
const functionName = match[1];
|
|
4008
|
-
// Skip if this is actually a method call (preceded by a dot)
|
|
4009
|
-
const beforeFunction = expression.substring(0, match.index + match[0].indexOf(functionName));
|
|
4010
|
-
if (!beforeFunction.endsWith('.')) {
|
|
4011
|
-
// Check if this standalone function is in our safe list or approved functions
|
|
4012
|
-
const isApprovedFunction = engine.APPROVED_FUNCTIONS?.get &&
|
|
4013
|
-
typeof engine.APPROVED_FUNCTIONS.get(functionName) === 'function';
|
|
4014
|
-
if (!safeStandaloneFunctions.includes(functionName) && !isApprovedFunction) {
|
|
4015
|
-
logger.debug(`Blocking unsafe standalone function call: ${functionName}`);
|
|
4016
|
-
return true; // Unsafe standalone function calls are not allowed
|
|
4017
|
-
}
|
|
4018
|
-
logger.debug(`Allowing safe standalone function call: ${functionName}${isApprovedFunction ? ' (approved)' : ' (built-in)'}`);
|
|
4019
|
-
}
|
|
4020
|
-
}
|
|
4021
|
-
// Then, check method calls and verify they're safe
|
|
4022
|
-
functionCallPattern.lastIndex = 0; // Reset regex
|
|
4023
|
-
while ((match = functionCallPattern.exec(expression)) !== null) {
|
|
4024
|
-
const methodName = match[2];
|
|
4025
|
-
foundFunctionCalls.push(methodName);
|
|
4026
|
-
// If this method is not in our safe lists, it's potentially unsafe
|
|
4027
|
-
if (!safeStringMethods.includes(methodName) &&
|
|
4028
|
-
!safeArrayMethods.includes(methodName) &&
|
|
4029
|
-
!safeMathMethods.includes(methodName)) {
|
|
4030
|
-
logger.debug(`Blocking unsafe method call: ${methodName}`);
|
|
4031
|
-
return true; // Unsafe method found
|
|
4032
|
-
}
|
|
4033
|
-
}
|
|
4034
|
-
// Core dangerous patterns (blocked at all levels) - removed the problematic function call pattern
|
|
4035
|
-
const coreDangerousPatterns = [
|
|
4036
|
-
/eval\s*\(/, // eval() calls
|
|
4037
|
-
/Function\s*\(/, // Function constructor
|
|
4038
|
-
/constructor/, // Constructor access
|
|
4039
|
-
/prototype/, // Prototype manipulation
|
|
4040
|
-
/__proto__/, // Prototype access
|
|
4041
|
-
/import\s*\(/, // Dynamic imports
|
|
4042
|
-
/require\s*\(/, // CommonJS requires
|
|
4043
|
-
/process\./, // Process access
|
|
4044
|
-
/global\./, // Global access
|
|
4045
|
-
/window\./, // Window access (browser)
|
|
4046
|
-
/document\./, // Document access (browser)
|
|
4047
|
-
/console\./, // Console access
|
|
4048
|
-
/setTimeout/, // Timer functions
|
|
4049
|
-
/setInterval/, // Timer functions
|
|
4050
|
-
/fetch\s*\(/, // Network requests
|
|
4051
|
-
/XMLHttpRequest/, // Network requests
|
|
4052
|
-
/localStorage/, // Storage access
|
|
4053
|
-
/sessionStorage/, // Storage access
|
|
4054
|
-
/\+\s*\+/, // Increment operators
|
|
4055
|
-
/--/, // Decrement operators
|
|
4056
|
-
/(?<![=!<>])=(?!=)/, // Assignment operators = (but not ==, !=, <=, >=)
|
|
4057
|
-
/delete\s+/, // Delete operator
|
|
4058
|
-
/new\s+/, // Constructor calls
|
|
4059
|
-
/throw\s+/, // Throw statements
|
|
4060
|
-
/try\s*\{/, // Try blocks
|
|
4061
|
-
/catch\s*\(/, // Catch blocks
|
|
4062
|
-
/finally\s*\{/, // Finally blocks
|
|
4063
|
-
/for\s*\(/, // For loops
|
|
4064
|
-
/while\s*\(/, // While loops
|
|
4065
|
-
/do\s*\{/, // Do-while loops
|
|
4066
|
-
/switch\s*\(/, // Switch statements
|
|
4067
|
-
/return\s+/, // Return statements
|
|
4068
|
-
];
|
|
4069
|
-
// Additional strict patterns (only blocked in strict mode)
|
|
4070
|
-
const strictOnlyPatterns = [
|
|
4071
|
-
/\[.*\]/, // Array/object bracket notation
|
|
4072
|
-
/this\./, // This access
|
|
4073
|
-
/arguments\./, // Arguments access
|
|
4074
|
-
];
|
|
4075
|
-
// Check core patterns (function call validation is handled separately above)
|
|
4076
|
-
if (coreDangerousPatterns.some(pattern => pattern.test(expression))) {
|
|
4077
|
-
return true;
|
|
4078
|
-
}
|
|
4079
|
-
// Check strict-only patterns if in strict mode
|
|
4080
|
-
if (securityLevel === 'strict' && strictOnlyPatterns.some(pattern => pattern.test(expression))) {
|
|
4081
|
-
return true;
|
|
4082
|
-
}
|
|
4083
|
-
return false;
|
|
4084
|
-
}
|
|
4085
|
-
// Template variable interpolation ({{variable}} format) with nested support
|
|
4086
|
-
// Inside-out approach: process innermost {{}} expressions first, then repeat until no more changes
|
|
4087
|
-
function interpolateTemplateVariables(template, variables, contextStack, options, engine) {
|
|
4088
|
-
logger.debug(`Starting template interpolation: ${template}`);
|
|
4089
|
-
let result = template;
|
|
4090
|
-
let iterations = 0;
|
|
4091
|
-
const maxIterations = 10; // Prevent infinite loops
|
|
4092
|
-
while (result.includes('{{') && result.includes('}}') && iterations < maxIterations) {
|
|
4093
|
-
iterations++;
|
|
4094
|
-
logger.debug(`Template interpolation iteration ${iterations}: ${result}`);
|
|
4095
|
-
// Find the LAST (rightmost) {{ - this is the innermost opening
|
|
4096
|
-
const lastOpenIndex = result.lastIndexOf('{{');
|
|
4097
|
-
if (lastOpenIndex === -1)
|
|
4098
|
-
break;
|
|
4099
|
-
// From that position, find the FIRST }} - this is the matching closing
|
|
4100
|
-
const closeIndex = result.indexOf('}}', lastOpenIndex);
|
|
4101
|
-
if (closeIndex === -1) {
|
|
4102
|
-
logger.error(`Found {{ at ${lastOpenIndex} but no matching }} in: ${result}`);
|
|
4103
|
-
return 'Invalid template!';
|
|
4104
|
-
}
|
|
4105
|
-
// Extract the innermost expression
|
|
4106
|
-
const expression = result.substring(lastOpenIndex + 2, closeIndex).trim();
|
|
4107
|
-
logger.debug(`Found innermost template expression: {{${expression}}}`);
|
|
4108
|
-
// Evaluate the innermost expression
|
|
4109
|
-
const evaluatedContent = evaluateExpression(expression, variables, contextStack, {
|
|
4110
|
-
...options,
|
|
4111
|
-
returnType: 'string'
|
|
4112
|
-
}, engine);
|
|
4113
|
-
const replacement = typeof evaluatedContent === 'string' ? evaluatedContent : String(evaluatedContent);
|
|
4114
|
-
logger.debug(`Evaluated to: ${replacement}`);
|
|
4115
|
-
// Replace the template expression with its evaluated result
|
|
4116
|
-
result = result.substring(0, lastOpenIndex) + replacement + result.substring(closeIndex + 2);
|
|
4117
|
-
logger.debug(`After replacement: ${result}`);
|
|
4118
|
-
}
|
|
4119
|
-
if (iterations >= maxIterations) {
|
|
4120
|
-
logger.warn(`Template interpolation stopped after ${maxIterations} iterations to prevent infinite loop`);
|
|
4121
|
-
}
|
|
4122
|
-
logger.debug(`Final template result: ${result}`);
|
|
4123
|
-
return result;
|
|
4124
|
-
}
|
|
4125
|
-
// Logical expression evaluator (&&, ||)
|
|
4126
|
-
function evaluateLogicalExpression(expression, variables, contextStack, options, engine) {
|
|
4127
|
-
// Handle OR expressions
|
|
4128
|
-
if (expression.includes('||')) {
|
|
4129
|
-
return evaluateSafeOrExpression(expression, variables, contextStack, engine);
|
|
4130
|
-
}
|
|
4131
|
-
// Handle AND expressions
|
|
4132
|
-
if (expression.includes('&&')) {
|
|
4133
|
-
const parts = expression.split('&&').map(part => part.trim());
|
|
4134
|
-
for (const part of parts) {
|
|
4135
|
-
const partResult = evaluateExpression(part, variables, contextStack, {
|
|
4136
|
-
...options,
|
|
4137
|
-
returnType: 'boolean'
|
|
4138
|
-
}, engine);
|
|
4139
|
-
if (!partResult) {
|
|
4140
|
-
return false;
|
|
4016
|
+
logger.debug(`Simplified evaluation starting: ${expression}`);
|
|
4017
|
+
// If not a string, return as-is
|
|
4018
|
+
if (typeof expression !== 'string') {
|
|
4019
|
+
return expression;
|
|
4020
|
+
}
|
|
4021
|
+
// Simple regex to find {{expression}} patterns
|
|
4022
|
+
const expressionRegex = /\{\{([^}]+)\}\}/g;
|
|
4023
|
+
// Create evaluation context by merging all available variables
|
|
4024
|
+
const context = createSimplifiedEvaluationContext(variables, contextStack, engine);
|
|
4025
|
+
// Check if the entire expression is a single interpolation
|
|
4026
|
+
const singleExpressionMatch = expression.match(/^\{\{([^}]+)\}\}$/);
|
|
4027
|
+
if (singleExpressionMatch) {
|
|
4028
|
+
// Single expression - return the evaluated result directly to preserve type
|
|
4029
|
+
try {
|
|
4030
|
+
const evaluationResult = evaluateJavaScriptExpression(singleExpressionMatch[1].trim(), context);
|
|
4031
|
+
logger.debug(`Simplified evaluation complete (single): ${expression} -> ${evaluationResult}`);
|
|
4032
|
+
return convertReturnType(evaluationResult, opts.returnType);
|
|
4033
|
+
}
|
|
4034
|
+
catch (error) {
|
|
4035
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
4036
|
+
logger.warn(`Single expression evaluation failed: ${singleExpressionMatch[1]} - ${errorMessage}`);
|
|
4037
|
+
return opts.returnType === 'boolean' ? false : `[error: ${expression}]`;
|
|
4141
4038
|
}
|
|
4142
4039
|
}
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
// For comparison expressions, we need to handle variable substitution properly
|
|
4150
|
-
let processedExpression = expression;
|
|
4151
|
-
// Find all variable references (not in template format) and replace them
|
|
4152
|
-
// This handles cases like "input.toLowerCase()" where input is a variable
|
|
4153
|
-
const variableNames = Object.keys(variables);
|
|
4154
|
-
for (const varName of variableNames) {
|
|
4155
|
-
const varValue = variables[varName];
|
|
4156
|
-
// Create a regex to match the variable name as a whole word
|
|
4157
|
-
const varRegex = new RegExp(`\\b${varName}\\b`, 'g');
|
|
4158
|
-
// Replace variable references with their values, properly quoted for strings
|
|
4159
|
-
processedExpression = processedExpression.replace(varRegex, (match) => {
|
|
4160
|
-
if (typeof varValue === 'string') {
|
|
4161
|
-
logger.debug(`Replacing variable '${varName}' with string value: ${varValue}`);
|
|
4162
|
-
return `"${varValue}"`;
|
|
4163
|
-
}
|
|
4164
|
-
else if (typeof varValue === 'boolean' || typeof varValue === 'number') {
|
|
4165
|
-
logger.debug(`Replacing variable '${varName}' with value: ${varValue}`);
|
|
4166
|
-
return varValue.toString();
|
|
4167
|
-
}
|
|
4168
|
-
else if (varValue === null || varValue === undefined) {
|
|
4169
|
-
logger.debug(`Replacing variable '${varName}' with null value`);
|
|
4170
|
-
return 'null';
|
|
4040
|
+
// Multiple expressions or mixed content - convert to string
|
|
4041
|
+
const result = expression.replace(expressionRegex, (match, expr) => {
|
|
4042
|
+
try {
|
|
4043
|
+
// Direct JavaScript evaluation with injected variables
|
|
4044
|
+
const evaluationResult = evaluateJavaScriptExpression(expr.trim(), context);
|
|
4045
|
+
return String(evaluationResult);
|
|
4171
4046
|
}
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4047
|
+
catch (error) {
|
|
4048
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
4049
|
+
logger.warn(`Expression evaluation failed: ${expr} - ${errorMessage}`);
|
|
4050
|
+
return match; // Return original if evaluation fails
|
|
4175
4051
|
}
|
|
4176
4052
|
});
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
const variableMatches = processedExpression.match(/\{\{([^}]+)\}\}/g);
|
|
4180
|
-
if (variableMatches) {
|
|
4181
|
-
for (const match of variableMatches) {
|
|
4182
|
-
const varName = match.slice(2, -2).trim();
|
|
4183
|
-
let varValue = getNestedValue(variables, varName);
|
|
4184
|
-
logger.debug(`Resolving template variable '${varName}' with value:`, varValue);
|
|
4185
|
-
// Check engine session variables if not found
|
|
4186
|
-
if (varValue === undefined && engine) {
|
|
4187
|
-
varValue = resolveEngineSessionVariable(varName, engine);
|
|
4188
|
-
}
|
|
4189
|
-
// Convert value to appropriate type for comparison
|
|
4190
|
-
let replacementValue;
|
|
4191
|
-
if (varValue === undefined || varValue === null) {
|
|
4192
|
-
replacementValue = 'null';
|
|
4193
|
-
}
|
|
4194
|
-
else if (typeof varValue === 'string') {
|
|
4195
|
-
replacementValue = `"${varValue}"`;
|
|
4196
|
-
}
|
|
4197
|
-
else if (typeof varValue === 'boolean') {
|
|
4198
|
-
replacementValue = varValue.toString();
|
|
4199
|
-
}
|
|
4200
|
-
else {
|
|
4201
|
-
replacementValue = varValue.toString();
|
|
4202
|
-
}
|
|
4203
|
-
processedExpression = processedExpression.replace(match, replacementValue);
|
|
4204
|
-
}
|
|
4205
|
-
}
|
|
4206
|
-
logger.debug(`Comparison expression after variable substitution: ${processedExpression}`);
|
|
4207
|
-
try {
|
|
4208
|
-
// Use Function constructor for safe evaluation
|
|
4209
|
-
const result = new Function('return ' + processedExpression)();
|
|
4210
|
-
logger.debug(`Comparison evaluation result: ${result}`);
|
|
4211
|
-
return !!result; // Convert to boolean
|
|
4053
|
+
logger.debug(`Simplified evaluation complete: ${expression} -> ${result}`);
|
|
4054
|
+
return convertReturnType(result, opts.returnType);
|
|
4212
4055
|
}
|
|
4213
4056
|
catch (error) {
|
|
4214
|
-
|
|
4215
|
-
|
|
4057
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
4058
|
+
logger.warn(`Simplified evaluation error: ${errorMessage}`);
|
|
4059
|
+
return opts.returnType === 'boolean' ? false : `[error: ${expression}]`;
|
|
4216
4060
|
}
|
|
4217
4061
|
}
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
}
|
|
4223
|
-
// Helper function to check for mathematical expressions
|
|
4224
|
-
function isMathematicalExpression(expression) {
|
|
4225
|
-
return /[\+\-\*\/\%]/.test(expression) &&
|
|
4226
|
-
!expression.includes('++') &&
|
|
4227
|
-
!expression.includes('--'); // Exclude increment/decrement
|
|
4228
|
-
}
|
|
4229
|
-
// Convert result to requested return type
|
|
4062
|
+
/**
|
|
4063
|
+
* Convert result to requested return type
|
|
4064
|
+
* Enhanced version with better type handling
|
|
4065
|
+
*/
|
|
4230
4066
|
function convertReturnType(value, returnType) {
|
|
4231
4067
|
switch (returnType) {
|
|
4232
4068
|
case 'boolean':
|
|
@@ -4238,478 +4074,158 @@ function convertReturnType(value, returnType) {
|
|
|
4238
4074
|
return value;
|
|
4239
4075
|
}
|
|
4240
4076
|
}
|
|
4241
|
-
|
|
4077
|
+
/**
|
|
4078
|
+
* Get engine session variables for simplified evaluator
|
|
4079
|
+
* This is the SINGLE SOURCE OF TRUTH for session variable logic
|
|
4080
|
+
*/
|
|
4081
|
+
function getEngineSessionVariables(engine, contextStack) {
|
|
4082
|
+
const sessionVars = {};
|
|
4083
|
+
try {
|
|
4084
|
+
const currentFlowFrame = getCurrentFlowFrame(engine);
|
|
4085
|
+
// Get userInput from the most recent user entry in context stack
|
|
4086
|
+
const userEntries = currentFlowFrame.contextStack.filter(entry => entry.role === 'user');
|
|
4087
|
+
if (userEntries.length > 0) {
|
|
4088
|
+
const lastUserEntry = userEntries[userEntries.length - 1];
|
|
4089
|
+
const userInputValue = typeof lastUserEntry.content === 'string' ? lastUserEntry.content : String(lastUserEntry.content);
|
|
4090
|
+
sessionVars.userInput = userInputValue;
|
|
4091
|
+
sessionVars.lastUserInput = userInputValue; // alias
|
|
4092
|
+
}
|
|
4093
|
+
// Add other engine session variables
|
|
4094
|
+
sessionVars.cargo = engine.cargo || {};
|
|
4095
|
+
sessionVars.sessionId = engine.sessionId;
|
|
4096
|
+
sessionVars.userId = currentFlowFrame.userId;
|
|
4097
|
+
sessionVars.flowName = currentFlowFrame.flowName;
|
|
4098
|
+
// Special function-like variables
|
|
4099
|
+
sessionVars['currentTime()'] = new Date().toISOString();
|
|
4100
|
+
// Add global variables if available
|
|
4101
|
+
if (engine.globalVariables) {
|
|
4102
|
+
Object.assign(sessionVars, engine.globalVariables);
|
|
4103
|
+
}
|
|
4104
|
+
}
|
|
4105
|
+
catch (error) {
|
|
4106
|
+
logger.debug(`Error getting engine session variables: ${error}`);
|
|
4107
|
+
}
|
|
4108
|
+
return sessionVars;
|
|
4109
|
+
}
|
|
4110
|
+
/**
|
|
4111
|
+
* Create evaluation context with all available variables for simplified evaluator
|
|
4112
|
+
*/
|
|
4113
|
+
function createSimplifiedEvaluationContext(variables, contextStack, engine) {
|
|
4114
|
+
// Just use the provided variables - contextStack is primarily for conversation history
|
|
4115
|
+
const allVars = variables || {};
|
|
4116
|
+
// Get engine session variables
|
|
4117
|
+
const engineSessionVars = getEngineSessionVariables(engine, contextStack);
|
|
4118
|
+
const context = {
|
|
4119
|
+
// All variables as top-level properties
|
|
4120
|
+
...allVars,
|
|
4121
|
+
// Engine session variables (userInput, cargo, sessionId, etc.)
|
|
4122
|
+
...engineSessionVars,
|
|
4123
|
+
// Special variables that should always be available
|
|
4124
|
+
$args: variables?.$args || {},
|
|
4125
|
+
xml_result: variables?.xml_result,
|
|
4126
|
+
json_result: variables?.json_result,
|
|
4127
|
+
// Add context object for Object.keys() patterns (avoid 'this' keyword issues)
|
|
4128
|
+
currentContext: { ...allVars, ...engineSessionVars },
|
|
4129
|
+
// Include approved functions so they can be called in expressions
|
|
4130
|
+
...engine.APPROVED_FUNCTIONS,
|
|
4131
|
+
// Utility objects available in expressions
|
|
4132
|
+
Object,
|
|
4133
|
+
Date,
|
|
4134
|
+
Math,
|
|
4135
|
+
String,
|
|
4136
|
+
Number,
|
|
4137
|
+
Boolean,
|
|
4138
|
+
Array,
|
|
4139
|
+
JSON
|
|
4140
|
+
};
|
|
4141
|
+
return context;
|
|
4142
|
+
}
|
|
4143
|
+
/**
|
|
4144
|
+
* Safely evaluate JavaScript expression with injected context
|
|
4145
|
+
*/
|
|
4146
|
+
function evaluateJavaScriptExpression(expression, context) {
|
|
4147
|
+
// Filter to only valid JavaScript identifiers (root variables only)
|
|
4148
|
+
const validParamNames = Object.keys(context).filter(key => /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key));
|
|
4149
|
+
// Unwrap user input variables before passing to function
|
|
4150
|
+
const paramValues = validParamNames.map(name => {
|
|
4151
|
+
const value = context[name];
|
|
4152
|
+
// Unwrap user input wrapper objects
|
|
4153
|
+
if (value && typeof value === 'object' &&
|
|
4154
|
+
value.__userInput === true &&
|
|
4155
|
+
value.__literal === true) {
|
|
4156
|
+
return value.value;
|
|
4157
|
+
}
|
|
4158
|
+
return value;
|
|
4159
|
+
});
|
|
4160
|
+
// Build function body that evaluates the expression
|
|
4161
|
+
const functionBody = `
|
|
4162
|
+
"use strict";
|
|
4163
|
+
try {
|
|
4164
|
+
const result = (${expression});
|
|
4165
|
+
// Auto-stringify complex data structures when used in string contexts
|
|
4166
|
+
if (typeof result === 'object' && result !== null &&
|
|
4167
|
+
!(result.__userInput === true && result.__literal === true)) {
|
|
4168
|
+
return JSON.stringify(result);
|
|
4169
|
+
}
|
|
4170
|
+
return result;
|
|
4171
|
+
} catch (e) {
|
|
4172
|
+
throw new Error('Expression evaluation failed: ' + e.message);
|
|
4173
|
+
}
|
|
4174
|
+
`;
|
|
4175
|
+
// Create and execute function with only valid parameter names
|
|
4176
|
+
const evaluatorFunction = new Function(...validParamNames, functionBody);
|
|
4177
|
+
return evaluatorFunction(...paramValues);
|
|
4178
|
+
}
|
|
4179
|
+
/**
|
|
4180
|
+
* Security-aware variable setter for user input
|
|
4181
|
+
* Integrates with the variable resolution system
|
|
4182
|
+
*/
|
|
4183
|
+
function setUserInputVariable(variables, key, value, sanitize = true) {
|
|
4184
|
+
// Mark user input as static literal data that should never be evaluated as code
|
|
4185
|
+
variables[key] = {
|
|
4186
|
+
__userInput: true,
|
|
4187
|
+
__literal: true,
|
|
4188
|
+
value: sanitize && typeof value === 'string' ? sanitizeUserInput(value) : value
|
|
4189
|
+
};
|
|
4190
|
+
}
|
|
4191
|
+
// ===============================================
|
|
4192
|
+
// LEGACY COMPATIBILITY FUNCTIONS
|
|
4193
|
+
// ===============================================
|
|
4194
|
+
/**
|
|
4195
|
+
* Clean unified expression interface functions
|
|
4196
|
+
* Maintains backward compatibility with existing code
|
|
4197
|
+
*/
|
|
4242
4198
|
function interpolateMessage(template, contextStack, variables = {}, engine) {
|
|
4243
4199
|
logger.debug(`Interpolating message template: ${template} with variables: ${JSON.stringify(variables)}`);
|
|
4244
4200
|
if (!template)
|
|
4245
4201
|
return template;
|
|
4246
|
-
//
|
|
4247
|
-
|
|
4248
|
-
|
|
4202
|
+
// For template interpolation, we should NOT apply security restrictions
|
|
4203
|
+
// Templates are developer-controlled, not user input
|
|
4204
|
+
const result = evaluateExpression(template, variables, contextStack, {
|
|
4205
|
+
securityLevel: 'none', // No security restrictions for templates
|
|
4249
4206
|
allowLogicalOperators: true,
|
|
4250
4207
|
allowMathOperators: true,
|
|
4251
4208
|
allowComparisons: true,
|
|
4252
4209
|
allowTernary: true,
|
|
4253
4210
|
context: 'template-interpolation',
|
|
4254
|
-
returnType: '
|
|
4211
|
+
returnType: 'string'
|
|
4255
4212
|
}, engine);
|
|
4213
|
+
return String(result);
|
|
4256
4214
|
}
|
|
4215
|
+
/**
|
|
4216
|
+
* Safe condition evaluation for boolean contexts
|
|
4217
|
+
* Maintains backward compatibility
|
|
4218
|
+
*/
|
|
4257
4219
|
function evaluateSafeCondition(condition, variables, engine) {
|
|
4258
4220
|
logger.debug(`Evaluating safe condition: ${condition} with variables: ${JSON.stringify(variables)}`);
|
|
4259
|
-
|
|
4260
|
-
securityLevel: '
|
|
4221
|
+
const result = evaluateExpression(condition, variables, [], {
|
|
4222
|
+
securityLevel: 'basic',
|
|
4261
4223
|
context: 'condition-evaluation',
|
|
4262
4224
|
returnType: 'boolean',
|
|
4263
4225
|
allowComparisons: true,
|
|
4264
4226
|
allowLogicalOperators: true
|
|
4265
4227
|
}, engine);
|
|
4266
|
-
|
|
4267
|
-
// Essential helper functions for the unified evaluator
|
|
4268
|
-
function isSimpleVariablePath(expression) {
|
|
4269
|
-
logger.debug(`Checking if expression is a simple variable path: ${expression}`);
|
|
4270
|
-
return /^[a-zA-Z_$][a-zA-Z0-9_$.]*$/.test(expression);
|
|
4271
|
-
}
|
|
4272
|
-
function resolveSimpleVariable(expression, variables, contextStack, engine) {
|
|
4273
|
-
// Debug logging to track variable resolution issues
|
|
4274
|
-
logger.debug(`Resolving variable '${expression}' - Available variables: ${JSON.stringify(Object.keys(variables || {}))}`);
|
|
4275
|
-
// Try variables first
|
|
4276
|
-
if (variables && Object.keys(variables).length > 0) {
|
|
4277
|
-
const val = expression.split('.').reduce((obj, part) => obj?.[part], variables);
|
|
4278
|
-
if (val !== undefined) {
|
|
4279
|
-
logger.debug(`Variable '${expression}' resolved to: ${val}`);
|
|
4280
|
-
return typeof val === 'object' ? JSON.stringify(val) : String(val);
|
|
4281
|
-
}
|
|
4282
|
-
}
|
|
4283
|
-
// If not found in variables, check for engine session variables
|
|
4284
|
-
if (engine) {
|
|
4285
|
-
const sessionValue = resolveEngineSessionVariable(expression, engine);
|
|
4286
|
-
if (sessionValue !== undefined) {
|
|
4287
|
-
logger.debug(`Variable '${expression}' resolved from engine session: ${sessionValue}`);
|
|
4288
|
-
return typeof sessionValue === 'object' ? JSON.stringify(sessionValue) : String(sessionValue);
|
|
4289
|
-
}
|
|
4290
|
-
}
|
|
4291
|
-
logger.debug(`Variable '${expression}' not found in variables or engine session`);
|
|
4292
|
-
return `[undefined: ${expression}]`;
|
|
4293
|
-
}
|
|
4294
|
-
// Helper function to resolve engine session variables
|
|
4295
|
-
function resolveEngineSessionVariable(expression, engine) {
|
|
4296
|
-
try {
|
|
4297
|
-
const currentFlowFrame = getCurrentFlowFrame(engine);
|
|
4298
|
-
logger.debug(`Resolving engine session variable: ${expression} in flow frame: ${currentFlowFrame.flowName}`);
|
|
4299
|
-
// Handle property access (e.g., cargo.someVar)
|
|
4300
|
-
if (expression.includes('.')) {
|
|
4301
|
-
const parts = expression.split('.');
|
|
4302
|
-
const baseVariable = parts[0];
|
|
4303
|
-
const propertyPath = parts.slice(1).join('.');
|
|
4304
|
-
let baseValue;
|
|
4305
|
-
// Get the base engine session variable
|
|
4306
|
-
switch (baseVariable) {
|
|
4307
|
-
case 'cargo':
|
|
4308
|
-
baseValue = engine.cargo || {};
|
|
4309
|
-
break;
|
|
4310
|
-
case 'userInput':
|
|
4311
|
-
case 'lastUserInput':
|
|
4312
|
-
const userEntries = currentFlowFrame.contextStack.filter(entry => entry.role === 'user');
|
|
4313
|
-
if (userEntries.length > 0) {
|
|
4314
|
-
const lastUserEntry = userEntries[userEntries.length - 1];
|
|
4315
|
-
baseValue = typeof lastUserEntry.content === 'string' ? lastUserEntry.content : String(lastUserEntry.content);
|
|
4316
|
-
}
|
|
4317
|
-
else {
|
|
4318
|
-
baseValue = undefined;
|
|
4319
|
-
}
|
|
4320
|
-
break;
|
|
4321
|
-
case 'sessionId':
|
|
4322
|
-
baseValue = engine.sessionId;
|
|
4323
|
-
break;
|
|
4324
|
-
case 'userId':
|
|
4325
|
-
baseValue = currentFlowFrame.userId;
|
|
4326
|
-
break;
|
|
4327
|
-
case 'flowName':
|
|
4328
|
-
baseValue = currentFlowFrame.flowName;
|
|
4329
|
-
break;
|
|
4330
|
-
default:
|
|
4331
|
-
logger.debug(`Unknown base engine session variable: ${baseVariable}`);
|
|
4332
|
-
return undefined;
|
|
4333
|
-
}
|
|
4334
|
-
// If base value is undefined, return undefined
|
|
4335
|
-
if (baseValue === undefined || baseValue === null) {
|
|
4336
|
-
logger.debug(`Base engine session variable '${baseVariable}' is undefined/null`);
|
|
4337
|
-
return undefined;
|
|
4338
|
-
}
|
|
4339
|
-
// Navigate through the property path using getNestedValue
|
|
4340
|
-
const result = getNestedValue(baseValue, propertyPath);
|
|
4341
|
-
logger.debug(`Engine session variable '${expression}' resolved to:`, result);
|
|
4342
|
-
return result;
|
|
4343
|
-
}
|
|
4344
|
-
// Handle direct variable access (no dots)
|
|
4345
|
-
switch (expression) {
|
|
4346
|
-
case 'cargo':
|
|
4347
|
-
logger.debug(`Returning cargo from engine session variable: ${expression}`);
|
|
4348
|
-
return engine.cargo || {};
|
|
4349
|
-
case 'userInput':
|
|
4350
|
-
case 'lastUserInput':
|
|
4351
|
-
// Get the most recent user input from context stack
|
|
4352
|
-
const userEntries = currentFlowFrame.contextStack.filter(entry => entry.role === 'user');
|
|
4353
|
-
if (userEntries.length > 0) {
|
|
4354
|
-
logger.debug(`Returning last user input from engine session variable: ${expression}`);
|
|
4355
|
-
const lastUserEntry = userEntries[userEntries.length - 1];
|
|
4356
|
-
return typeof lastUserEntry.content === 'string' ? lastUserEntry.content : String(lastUserEntry.content);
|
|
4357
|
-
}
|
|
4358
|
-
logger.debug(`No user input found in context stack for engine session variable: ${expression}`);
|
|
4359
|
-
return undefined;
|
|
4360
|
-
case 'currentTime()':
|
|
4361
|
-
logger.debug(`Returning current time for engine session variable: ${expression}`);
|
|
4362
|
-
return new Date().toISOString();
|
|
4363
|
-
case 'sessionId':
|
|
4364
|
-
logger.debug(`Returning session ID for engine session variable: ${expression}`);
|
|
4365
|
-
return engine.sessionId;
|
|
4366
|
-
case 'userId':
|
|
4367
|
-
logger.debug(`Returning user ID for engine session variable: ${expression}`);
|
|
4368
|
-
return currentFlowFrame.userId;
|
|
4369
|
-
case 'flowName':
|
|
4370
|
-
logger.debug(`Returning flow name for engine session variable: ${expression}`);
|
|
4371
|
-
return currentFlowFrame.flowName;
|
|
4372
|
-
default:
|
|
4373
|
-
logger.debug(`No specific engine session variable found for: ${expression}`);
|
|
4374
|
-
return undefined;
|
|
4375
|
-
}
|
|
4376
|
-
}
|
|
4377
|
-
catch (error) {
|
|
4378
|
-
logger.debug(`Failed to resolve engine session variable '${expression}': ${error}`);
|
|
4379
|
-
return undefined;
|
|
4380
|
-
}
|
|
4381
|
-
}
|
|
4382
|
-
// Helper function to evaluate function calls
|
|
4383
|
-
function evaluateFunctionCall(expression, variables, contextStack, engine) {
|
|
4384
|
-
try {
|
|
4385
|
-
logger.debug(`Evaluating function call: ${expression}`);
|
|
4386
|
-
// Handle currentTime() function call
|
|
4387
|
-
if (expression === 'currentTime()') {
|
|
4388
|
-
logger.debug(`Returning current time for function call: ${expression}`);
|
|
4389
|
-
return new Date().toISOString();
|
|
4390
|
-
}
|
|
4391
|
-
// Handle safe standalone functions
|
|
4392
|
-
const safeStandaloneFunctions = ['isNaN', 'parseInt', 'parseFloat', 'Boolean', 'Number', 'String'];
|
|
4393
|
-
// Parse function call with arguments
|
|
4394
|
-
const functionMatch = expression.match(/^(\w+)\((.*)\)$/);
|
|
4395
|
-
if (functionMatch) {
|
|
4396
|
-
const funcName = functionMatch[1];
|
|
4397
|
-
const argsString = functionMatch[2];
|
|
4398
|
-
if (safeStandaloneFunctions.includes(funcName)) {
|
|
4399
|
-
logger.debug(`Evaluating safe standalone function: ${funcName}`);
|
|
4400
|
-
// Parse and evaluate arguments
|
|
4401
|
-
const args = [];
|
|
4402
|
-
if (argsString.trim()) {
|
|
4403
|
-
// Simple argument parsing (supports literals and variables)
|
|
4404
|
-
const argParts = argsString.split(',').map(arg => arg.trim());
|
|
4405
|
-
for (const argPart of argParts) {
|
|
4406
|
-
let argValue;
|
|
4407
|
-
// Handle string literals
|
|
4408
|
-
if ((argPart.startsWith('"') && argPart.endsWith('"')) ||
|
|
4409
|
-
(argPart.startsWith("'") && argPart.endsWith("'"))) {
|
|
4410
|
-
argValue = argPart.slice(1, -1);
|
|
4411
|
-
}
|
|
4412
|
-
// Handle number literals
|
|
4413
|
-
else if (!isNaN(Number(argPart))) {
|
|
4414
|
-
argValue = Number(argPart);
|
|
4415
|
-
}
|
|
4416
|
-
// Handle boolean literals
|
|
4417
|
-
else if (argPart === 'true' || argPart === 'false') {
|
|
4418
|
-
argValue = argPart === 'true';
|
|
4419
|
-
}
|
|
4420
|
-
// Handle variables
|
|
4421
|
-
else {
|
|
4422
|
-
const varResult = resolveSimpleVariable(argPart, variables, contextStack, engine);
|
|
4423
|
-
if (varResult && !varResult.startsWith('[undefined:')) {
|
|
4424
|
-
argValue = varResult;
|
|
4425
|
-
}
|
|
4426
|
-
else if (engine) {
|
|
4427
|
-
const sessionResult = resolveEngineSessionVariable(argPart, engine);
|
|
4428
|
-
argValue = sessionResult !== undefined ? sessionResult : argPart;
|
|
4429
|
-
}
|
|
4430
|
-
else {
|
|
4431
|
-
argValue = argPart;
|
|
4432
|
-
}
|
|
4433
|
-
}
|
|
4434
|
-
args.push(argValue);
|
|
4435
|
-
}
|
|
4436
|
-
}
|
|
4437
|
-
// Execute the safe function
|
|
4438
|
-
try {
|
|
4439
|
-
switch (funcName) {
|
|
4440
|
-
case 'isNaN':
|
|
4441
|
-
logger.debug(`Executing isNaN with args: ${args}`);
|
|
4442
|
-
return isNaN(args[0]);
|
|
4443
|
-
case 'parseInt':
|
|
4444
|
-
logger.debug(`Executing parseInt with args: ${args}`);
|
|
4445
|
-
return parseInt(args[0], args[1] || 10);
|
|
4446
|
-
case 'parseFloat':
|
|
4447
|
-
logger.debug(`Executing parseFloat with args: ${args}`);
|
|
4448
|
-
return parseFloat(args[0]);
|
|
4449
|
-
case 'Boolean':
|
|
4450
|
-
logger.debug(`Executing Boolean with args: ${args}`);
|
|
4451
|
-
return Boolean(args[0]);
|
|
4452
|
-
case 'Number':
|
|
4453
|
-
logger.debug(`Executing Number with args: ${args}`);
|
|
4454
|
-
return Number(args[0]);
|
|
4455
|
-
case 'String':
|
|
4456
|
-
logger.debug(`Executing String with args: ${args}`);
|
|
4457
|
-
return String(args[0]);
|
|
4458
|
-
default:
|
|
4459
|
-
logger.warn(`Safe function ${funcName} not implemented`);
|
|
4460
|
-
return undefined;
|
|
4461
|
-
}
|
|
4462
|
-
}
|
|
4463
|
-
catch (error) {
|
|
4464
|
-
logger.debug(`Error executing safe function ${funcName}: ${error.message}`);
|
|
4465
|
-
return undefined;
|
|
4466
|
-
}
|
|
4467
|
-
}
|
|
4468
|
-
}
|
|
4469
|
-
// Handle extractCryptoFromInput(...) function call
|
|
4470
|
-
const extractCryptoMatch = expression.match(/^extractCryptoFromInput\((.+)\)$/);
|
|
4471
|
-
if (extractCryptoMatch) {
|
|
4472
|
-
const argExpression = extractCryptoMatch[1].trim();
|
|
4473
|
-
// Evaluate the argument expression (could be a variable or literal)
|
|
4474
|
-
let argValue;
|
|
4475
|
-
if (argExpression.startsWith('"') && argExpression.endsWith('"')) {
|
|
4476
|
-
argValue = argExpression.slice(1, -1);
|
|
4477
|
-
}
|
|
4478
|
-
else if (argExpression.startsWith("'") && argExpression.endsWith("'")) {
|
|
4479
|
-
argValue = argExpression.slice(1, -1);
|
|
4480
|
-
}
|
|
4481
|
-
else {
|
|
4482
|
-
// Try to resolve as variable (could be userInput or other variable)
|
|
4483
|
-
const varResult = resolveSimpleVariable(argExpression, variables, contextStack, engine);
|
|
4484
|
-
if (varResult && !varResult.startsWith('[undefined:')) {
|
|
4485
|
-
argValue = varResult;
|
|
4486
|
-
}
|
|
4487
|
-
else if (engine) {
|
|
4488
|
-
// Try engine session variables
|
|
4489
|
-
const sessionResult = resolveEngineSessionVariable(argExpression, engine);
|
|
4490
|
-
if (sessionResult !== undefined) {
|
|
4491
|
-
argValue = String(sessionResult);
|
|
4492
|
-
}
|
|
4493
|
-
else {
|
|
4494
|
-
argValue = argExpression; // Use as literal if can't resolve
|
|
4495
|
-
}
|
|
4496
|
-
}
|
|
4497
|
-
else {
|
|
4498
|
-
argValue = argExpression; // Use as literal if can't resolve
|
|
4499
|
-
}
|
|
4500
|
-
}
|
|
4501
|
-
logger.debug(`extractCryptoFromInput called with: ${argValue}`);
|
|
4502
|
-
// Extract crypto name from input text
|
|
4503
|
-
const cryptoNames = {
|
|
4504
|
-
'bitcoin': 'bitcoin',
|
|
4505
|
-
'btc': 'bitcoin',
|
|
4506
|
-
'ethereum': 'ethereum',
|
|
4507
|
-
'eth': 'ethereum',
|
|
4508
|
-
'litecoin': 'litecoin',
|
|
4509
|
-
'ltc': 'litecoin',
|
|
4510
|
-
'dogecoin': 'dogecoin',
|
|
4511
|
-
'doge': 'dogecoin',
|
|
4512
|
-
'cardano': 'cardano',
|
|
4513
|
-
'ada': 'cardano'
|
|
4514
|
-
};
|
|
4515
|
-
const lowerInput = argValue.toLowerCase();
|
|
4516
|
-
for (const [key, value] of Object.entries(cryptoNames)) {
|
|
4517
|
-
if (lowerInput.includes(key)) {
|
|
4518
|
-
logger.debug(`Extracted crypto: ${value}`);
|
|
4519
|
-
return value;
|
|
4520
|
-
}
|
|
4521
|
-
}
|
|
4522
|
-
logger.debug(`No crypto found in input, defaulting to bitcoin`);
|
|
4523
|
-
return 'bitcoin'; // Default fallback
|
|
4524
|
-
}
|
|
4525
|
-
return undefined; // Function not recognized
|
|
4526
|
-
}
|
|
4527
|
-
catch (error) {
|
|
4528
|
-
logger.warn(`Function call evaluation error: ${error.message}`);
|
|
4529
|
-
return undefined;
|
|
4530
|
-
}
|
|
4531
|
-
}
|
|
4532
|
-
function getNestedValue(obj, path) {
|
|
4533
|
-
if (!obj || typeof path !== 'string') {
|
|
4534
|
-
return undefined;
|
|
4535
|
-
}
|
|
4536
|
-
if (!path.includes('.')) {
|
|
4537
|
-
return obj[path];
|
|
4538
|
-
}
|
|
4539
|
-
const keys = path.split('.');
|
|
4540
|
-
let current = obj;
|
|
4541
|
-
for (const key of keys) {
|
|
4542
|
-
if (current === null || current === undefined) {
|
|
4543
|
-
return undefined;
|
|
4544
|
-
}
|
|
4545
|
-
current = current[key];
|
|
4546
|
-
}
|
|
4547
|
-
return current;
|
|
4548
|
-
}
|
|
4549
|
-
function evaluateSafeOrExpression(expression, variables, contextStack, engine) {
|
|
4550
|
-
const parts = expression.split('||').map(part => part.trim());
|
|
4551
|
-
for (const part of parts) {
|
|
4552
|
-
if ((part.startsWith('"') && part.endsWith('"')) ||
|
|
4553
|
-
(part.startsWith("'") && part.endsWith("'"))) {
|
|
4554
|
-
return part.slice(1, -1);
|
|
4555
|
-
}
|
|
4556
|
-
if (isSimpleVariablePath(part)) {
|
|
4557
|
-
if (variables && Object.keys(variables).length > 0) {
|
|
4558
|
-
const val = part.split('.').reduce((obj, partKey) => obj?.[partKey], variables);
|
|
4559
|
-
if (val !== undefined && val !== null && String(val) !== '') {
|
|
4560
|
-
return typeof val === 'object' ? JSON.stringify(val) : String(val);
|
|
4561
|
-
}
|
|
4562
|
-
}
|
|
4563
|
-
// Check engine session variables if not found in variables
|
|
4564
|
-
if (engine) {
|
|
4565
|
-
const sessionValue = resolveEngineSessionVariable(part, engine);
|
|
4566
|
-
if (sessionValue !== undefined && sessionValue !== null && String(sessionValue) !== '') {
|
|
4567
|
-
return typeof sessionValue === 'object' ? JSON.stringify(sessionValue) : String(sessionValue);
|
|
4568
|
-
}
|
|
4569
|
-
}
|
|
4570
|
-
}
|
|
4571
|
-
}
|
|
4572
|
-
const lastPart = parts[parts.length - 1];
|
|
4573
|
-
if ((lastPart.startsWith('"') && lastPart.endsWith('"')) ||
|
|
4574
|
-
(lastPart.startsWith("'") && lastPart.endsWith("'"))) {
|
|
4575
|
-
return lastPart.slice(1, -1);
|
|
4576
|
-
}
|
|
4577
|
-
// Final fallback - check for session variables in the last part
|
|
4578
|
-
if (engine && isSimpleVariablePath(lastPart)) {
|
|
4579
|
-
const sessionValue = resolveEngineSessionVariable(lastPart, engine);
|
|
4580
|
-
if (sessionValue !== undefined) {
|
|
4581
|
-
return typeof sessionValue === 'object' ? JSON.stringify(sessionValue) : String(sessionValue);
|
|
4582
|
-
}
|
|
4583
|
-
}
|
|
4584
|
-
return '';
|
|
4585
|
-
}
|
|
4586
|
-
function evaluateSafeTernaryExpression(expression, variables, contextStack, engine) {
|
|
4587
|
-
const ternaryMatch = expression.match(/^([a-zA-Z_$.]+)\s*(===|!==|==|!=|>=|<=|>|<)\s*(\d+|'[^']*'|"[^"]*"|true|false)\s*\?\s*('([^']*)'|"([^"]*)"|[a-zA-Z_$.]+)\s*:\s*('([^']*)'|"([^"]*)"|[a-zA-Z_$.]+)$/);
|
|
4588
|
-
if (!ternaryMatch) {
|
|
4589
|
-
return `[invalid-ternary: ${expression}]`;
|
|
4590
|
-
}
|
|
4591
|
-
const [, leftVar, operator, rightValue, , trueStr1, trueStr2, , falseStr1, falseStr2] = ternaryMatch;
|
|
4592
|
-
let leftVal;
|
|
4593
|
-
if (variables && Object.keys(variables).length > 0) {
|
|
4594
|
-
leftVal = leftVar.split('.').reduce((obj, part) => obj?.[part], variables);
|
|
4595
|
-
}
|
|
4596
|
-
// Check engine session variables if not found
|
|
4597
|
-
if (leftVal === undefined && engine) {
|
|
4598
|
-
leftVal = resolveEngineSessionVariable(leftVar, engine);
|
|
4599
|
-
}
|
|
4600
|
-
if (leftVal === undefined) {
|
|
4601
|
-
return `[undefined: ${leftVar}]`;
|
|
4602
|
-
}
|
|
4603
|
-
let rightVal;
|
|
4604
|
-
if (rightValue === 'true')
|
|
4605
|
-
rightVal = true;
|
|
4606
|
-
else if (rightValue === 'false')
|
|
4607
|
-
rightVal = false;
|
|
4608
|
-
else if (/^\d+$/.test(rightValue))
|
|
4609
|
-
rightVal = parseInt(rightValue);
|
|
4610
|
-
else if (/^\d*\.\d+$/.test(rightValue))
|
|
4611
|
-
rightVal = parseFloat(rightValue);
|
|
4612
|
-
else if (rightValue.startsWith("'") && rightValue.endsWith("'"))
|
|
4613
|
-
rightVal = rightValue.slice(1, -1);
|
|
4614
|
-
else if (rightValue.startsWith('"') && rightValue.endsWith('"'))
|
|
4615
|
-
rightVal = rightValue.slice(1, -1);
|
|
4616
|
-
else
|
|
4617
|
-
rightVal = rightValue;
|
|
4618
|
-
let condition = false;
|
|
4619
|
-
switch (operator) {
|
|
4620
|
-
case '===':
|
|
4621
|
-
condition = leftVal === rightVal;
|
|
4622
|
-
break;
|
|
4623
|
-
case '!==':
|
|
4624
|
-
condition = leftVal !== rightVal;
|
|
4625
|
-
break;
|
|
4626
|
-
case '==':
|
|
4627
|
-
condition = leftVal == rightVal;
|
|
4628
|
-
break;
|
|
4629
|
-
case '!=':
|
|
4630
|
-
condition = leftVal != rightVal;
|
|
4631
|
-
break;
|
|
4632
|
-
case '>':
|
|
4633
|
-
condition = leftVal > rightVal;
|
|
4634
|
-
break;
|
|
4635
|
-
case '<':
|
|
4636
|
-
condition = leftVal < rightVal;
|
|
4637
|
-
break;
|
|
4638
|
-
case '>=':
|
|
4639
|
-
condition = leftVal >= rightVal;
|
|
4640
|
-
break;
|
|
4641
|
-
case '<=':
|
|
4642
|
-
condition = leftVal <= rightVal;
|
|
4643
|
-
break;
|
|
4644
|
-
default: return `[invalid-operator: ${operator}]`;
|
|
4645
|
-
}
|
|
4646
|
-
if (condition) {
|
|
4647
|
-
return trueStr1 || trueStr2 || resolveSimpleVariable(trueStr2, variables, contextStack, engine) || '';
|
|
4648
|
-
}
|
|
4649
|
-
else {
|
|
4650
|
-
return falseStr1 || falseStr2 || resolveSimpleVariable(falseStr2, variables, contextStack, engine) || '';
|
|
4651
|
-
}
|
|
4652
|
-
}
|
|
4653
|
-
function evaluateSafeMathematicalExpression(expression, variables, contextStack, engine) {
|
|
4654
|
-
try {
|
|
4655
|
-
let evaluatedExpression = expression;
|
|
4656
|
-
const variablePattern = /[a-zA-Z_$][a-zA-Z0-9_$]*(\.[a-zA-Z_$][a-zA-Z0-9_$]*)*/g;
|
|
4657
|
-
const variables_found = expression.match(variablePattern) || [];
|
|
4658
|
-
for (const varPath of [...new Set(variables_found)]) {
|
|
4659
|
-
if (/^\d+(\.\d+)?$/.test(varPath))
|
|
4660
|
-
continue;
|
|
4661
|
-
let value = resolveSimpleVariable(varPath, variables, contextStack, engine);
|
|
4662
|
-
const numValue = Number(value);
|
|
4663
|
-
if (!isNaN(numValue)) {
|
|
4664
|
-
value = String(numValue);
|
|
4665
|
-
}
|
|
4666
|
-
else if (value === undefined || value === null) {
|
|
4667
|
-
value = '0';
|
|
4668
|
-
}
|
|
4669
|
-
const regex = new RegExp(`\\b${varPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'g');
|
|
4670
|
-
evaluatedExpression = evaluatedExpression.replace(regex, String(value));
|
|
4671
|
-
}
|
|
4672
|
-
if (!/^[\d+\-*/%().\s]+$/.test(evaluatedExpression)) {
|
|
4673
|
-
return `[unsafe-math: ${expression}]`;
|
|
4674
|
-
}
|
|
4675
|
-
const result = new Function(`"use strict"; return (${evaluatedExpression})`)();
|
|
4676
|
-
return String(result);
|
|
4677
|
-
}
|
|
4678
|
-
catch (error) {
|
|
4679
|
-
logger.warn(`Mathematical expression evaluation error: ${error.message}`);
|
|
4680
|
-
return `[math-error: ${expression}]`;
|
|
4681
|
-
}
|
|
4682
|
-
}
|
|
4683
|
-
// Handle typeof operator expressions (e.g., "typeof variable", "typeof container.prop")
|
|
4684
|
-
function evaluateTypeofExpression(expression, variables, contextStack, engine) {
|
|
4685
|
-
try {
|
|
4686
|
-
// Extract the variable name after "typeof "
|
|
4687
|
-
const typeofMatch = expression.trim().match(/^typeof\s+(.+)$/);
|
|
4688
|
-
if (!typeofMatch) {
|
|
4689
|
-
logger.warn(`Invalid typeof expression: ${expression}`);
|
|
4690
|
-
return 'undefined';
|
|
4691
|
-
}
|
|
4692
|
-
const variablePath = typeofMatch[1].trim();
|
|
4693
|
-
logger.debug(`Evaluating typeof for variable path: ${variablePath}`);
|
|
4694
|
-
// Resolve the variable value
|
|
4695
|
-
let value;
|
|
4696
|
-
// Try to resolve from variables first
|
|
4697
|
-
if (variables && Object.keys(variables).length > 0) {
|
|
4698
|
-
value = variablePath.split('.').reduce((obj, part) => obj?.[part], variables);
|
|
4699
|
-
}
|
|
4700
|
-
// If not found in variables, check engine session variables
|
|
4701
|
-
if (value === undefined && engine) {
|
|
4702
|
-
value = resolveEngineSessionVariable(variablePath, engine);
|
|
4703
|
-
}
|
|
4704
|
-
// Return the JavaScript typeof result
|
|
4705
|
-
const typeResult = typeof value;
|
|
4706
|
-
logger.debug(`typeof ${variablePath} = ${typeResult} (value: ${JSON.stringify(value)})`);
|
|
4707
|
-
return typeResult;
|
|
4708
|
-
}
|
|
4709
|
-
catch (error) {
|
|
4710
|
-
logger.warn(`Typeof expression evaluation error: ${error.message}`);
|
|
4711
|
-
return 'undefined';
|
|
4712
|
-
}
|
|
4228
|
+
return Boolean(result);
|
|
4713
4229
|
}
|
|
4714
4230
|
// === ENHANCED FLOW CONTROL COMMANDS ===
|
|
4715
4231
|
// Universal commands that work during any flow
|
|
@@ -5071,10 +4587,28 @@ async function processActivity(input, userId, engine) {
|
|
|
5071
4587
|
if (flowControlResult) {
|
|
5072
4588
|
return flowControlResult;
|
|
5073
4589
|
}
|
|
5074
|
-
// Check for strong intent interruption (new flows)
|
|
5075
|
-
const
|
|
5076
|
-
|
|
5077
|
-
|
|
4590
|
+
// Check for strong intent interruption (new flows) - only if current flow AND all parent flows are interruptable
|
|
4591
|
+
const flowStacks = engine.flowStacks;
|
|
4592
|
+
const currentStackIndex = flowStacks.length - 1;
|
|
4593
|
+
const currentStack = flowStacks[currentStackIndex] || [];
|
|
4594
|
+
let isAnyFlowNonInterruptable = false;
|
|
4595
|
+
for (const flowFrame of currentStack) {
|
|
4596
|
+
const flowDefinition = engine.flowsMenu.find((f) => f.name === flowFrame.flowName);
|
|
4597
|
+
const isFlowInterruptable = flowDefinition?.interruptable ?? false; // Default to false
|
|
4598
|
+
if (!isFlowInterruptable) {
|
|
4599
|
+
isAnyFlowNonInterruptable = true;
|
|
4600
|
+
logger.debug(`Flow "${flowFrame.flowName}" in stack is not interruptable`);
|
|
4601
|
+
break;
|
|
4602
|
+
}
|
|
4603
|
+
}
|
|
4604
|
+
if (!isAnyFlowNonInterruptable) {
|
|
4605
|
+
const interruptionResult = await handleIntentInterruption(String(sanitizedInput), engine, userId);
|
|
4606
|
+
if (interruptionResult) {
|
|
4607
|
+
return interruptionResult;
|
|
4608
|
+
}
|
|
4609
|
+
}
|
|
4610
|
+
else {
|
|
4611
|
+
logger.debug(`Skipping intent interruption check - one or more flows in the stack are not interruptable`);
|
|
5078
4612
|
}
|
|
5079
4613
|
// Clear the last SAY message if we had one
|
|
5080
4614
|
if (currentFlowFrame.lastSayMessage) {
|
|
@@ -5509,7 +5043,8 @@ export class WorkflowEngine {
|
|
|
5509
5043
|
flowCallGraph: new Map(),
|
|
5510
5044
|
currentDepth: 0,
|
|
5511
5045
|
variableScopes: new Map(),
|
|
5512
|
-
toolRegistry: new Set(this.toolsRegistry.map((t) => t.id))
|
|
5046
|
+
toolRegistry: new Set(this.toolsRegistry.map((t) => t.id)),
|
|
5047
|
+
flowCallStack: [] // Track parent flow chain for variable inheritance
|
|
5513
5048
|
};
|
|
5514
5049
|
try {
|
|
5515
5050
|
this._validateFlowRecursive(flowName, validationState, opts);
|
|
@@ -5549,7 +5084,7 @@ export class WorkflowEngine {
|
|
|
5549
5084
|
return;
|
|
5550
5085
|
}
|
|
5551
5086
|
// Find the flow definition
|
|
5552
|
-
const flowDef = this.flowsMenu.find((f) => f.name === flowName);
|
|
5087
|
+
const flowDef = this.flowsMenu.find((f) => f.id === flowName || f.name === flowName);
|
|
5553
5088
|
if (!flowDef) {
|
|
5554
5089
|
state.errors.push(`Flow not found: ${flowName}`);
|
|
5555
5090
|
return;
|
|
@@ -5557,6 +5092,8 @@ export class WorkflowEngine {
|
|
|
5557
5092
|
// Mark as visited
|
|
5558
5093
|
state.visitedFlows.add(flowName);
|
|
5559
5094
|
state.currentDepth++;
|
|
5095
|
+
// Add to flow call stack for variable inheritance tracking
|
|
5096
|
+
state.flowCallStack.push(flowName);
|
|
5560
5097
|
// Initialize flow call graph
|
|
5561
5098
|
if (!state.flowCallGraph.has(flowName)) {
|
|
5562
5099
|
state.flowCallGraph.set(flowName, []);
|
|
@@ -5586,6 +5123,8 @@ export class WorkflowEngine {
|
|
|
5586
5123
|
this._validateFlowRecursive(calledFlow, state, opts);
|
|
5587
5124
|
}
|
|
5588
5125
|
}
|
|
5126
|
+
// Remove from flow call stack when exiting this flow
|
|
5127
|
+
state.flowCallStack.pop();
|
|
5589
5128
|
state.currentDepth--;
|
|
5590
5129
|
}
|
|
5591
5130
|
/**
|
|
@@ -5975,7 +5514,7 @@ export class WorkflowEngine {
|
|
|
5975
5514
|
// PHASE 2: Validate only top-level flows (not referenced as sub-flows)
|
|
5976
5515
|
logger.info('🔍 Phase 2: Validating top-level flows...');
|
|
5977
5516
|
for (const flow of this.flowsMenu) {
|
|
5978
|
-
if (referencedSubFlows.has(flow.name)) {
|
|
5517
|
+
if (referencedSubFlows.has(flow.name) || referencedSubFlows.has(flow.id)) {
|
|
5979
5518
|
// Skip validation of sub-flows - they'll be validated in parent context
|
|
5980
5519
|
logger.info(`⏭️ Skipping sub-flow "${flow.name}" - will be validated in parent context`);
|
|
5981
5520
|
results.skippedSubFlows.push(flow.name);
|
|
@@ -6024,11 +5563,24 @@ export class WorkflowEngine {
|
|
|
6024
5563
|
// === ENHANCED VARIABLE SCOPE TRACKING ===
|
|
6025
5564
|
/**
|
|
6026
5565
|
* Gets the current variable scope available at a specific step
|
|
6027
|
-
* This includes variables defined in flow definition + variables created by previous steps
|
|
5566
|
+
* This includes variables defined in flow definition + variables created by previous steps + parent flow variables
|
|
6028
5567
|
*/
|
|
6029
5568
|
_getCurrentStepScope(flowDef, stepIndex, state) {
|
|
6030
5569
|
const scope = new Set();
|
|
6031
|
-
// Add flow
|
|
5570
|
+
// Add parent flow variables (variable inheritance from call stack)
|
|
5571
|
+
if (state.flowCallStack && state.flowCallStack.length > 0) {
|
|
5572
|
+
// Iterate through parent flows in the call stack (excluding current flow)
|
|
5573
|
+
for (let i = 0; i < state.flowCallStack.length - 1; i++) {
|
|
5574
|
+
const parentFlowName = state.flowCallStack[i];
|
|
5575
|
+
const parentFlowDef = this.flowsMenu.find((f) => f.id === parentFlowName || f.name === parentFlowName);
|
|
5576
|
+
if (parentFlowDef && parentFlowDef.variables) {
|
|
5577
|
+
for (const varName of Object.keys(parentFlowDef.variables)) {
|
|
5578
|
+
scope.add(varName);
|
|
5579
|
+
}
|
|
5580
|
+
}
|
|
5581
|
+
}
|
|
5582
|
+
}
|
|
5583
|
+
// Add flow-defined variables (current flow)
|
|
6032
5584
|
if (flowDef.variables) {
|
|
6033
5585
|
for (const varName of Object.keys(flowDef.variables)) {
|
|
6034
5586
|
scope.add(varName);
|
|
@@ -6197,7 +5749,7 @@ export class WorkflowEngine {
|
|
|
6197
5749
|
const variableNames = extractVariableNames(varPath);
|
|
6198
5750
|
for (const rootVar of variableNames) {
|
|
6199
5751
|
// Check if it's a registered function first (global scope)
|
|
6200
|
-
const isApprovedFunction = this.APPROVED_FUNCTIONS && this.APPROVED_FUNCTIONS
|
|
5752
|
+
const isApprovedFunction = this.APPROVED_FUNCTIONS && this.APPROVED_FUNCTIONS[rootVar];
|
|
6201
5753
|
// If not an approved function, check if it's in current scope as a variable
|
|
6202
5754
|
if (!isApprovedFunction && !currentScope.has(rootVar)) {
|
|
6203
5755
|
state.errors.push(`${context} in step "${step.id}" references undefined variable: ${rootVar} (full path: ${varPath})`);
|