2026-04-14 12:24:30 -04:00
|
|
|
'use strict';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Validate a webhook URL.
|
|
|
|
|
* Returns { ok: true, url } or { ok: false, reason }.
|
|
|
|
|
*/
|
|
|
|
|
function validateWebhookUrl(raw) {
|
|
|
|
|
if (!raw) return { ok: true, url: null };
|
|
|
|
|
let url;
|
|
|
|
|
try { url = new URL(raw); } catch { return { ok: false, reason: 'Invalid URL format' }; }
|
|
|
|
|
if (!['http:', 'https:'].includes(url.protocol)) {
|
|
|
|
|
return { ok: false, reason: 'Webhook URL must use http or https' };
|
|
|
|
|
}
|
|
|
|
|
const host = url.hostname.toLowerCase();
|
|
|
|
|
if (
|
|
|
|
|
host === 'localhost' ||
|
|
|
|
|
host === '::1' ||
|
|
|
|
|
/^127\./.test(host) ||
|
|
|
|
|
/^10\./.test(host) ||
|
|
|
|
|
/^192\.168\./.test(host) ||
|
|
|
|
|
/^172\.(1[6-9]|2\d|3[01])\./.test(host) ||
|
|
|
|
|
/^169\.254\./.test(host) ||
|
|
|
|
|
/^fe80:/i.test(host)
|
|
|
|
|
) {
|
|
|
|
|
return { ok: false, reason: 'Webhook URL must not point to a private/internal address' };
|
|
|
|
|
}
|
|
|
|
|
return { ok: true, url };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Replace {{param}} placeholders in a command string.
|
|
|
|
|
* Throws if a substituted value contains unsafe characters.
|
|
|
|
|
*/
|
|
|
|
|
function applyParams(command, params) {
|
|
|
|
|
return command.replace(/\{\{(\w+)\}\}/g, (match, key) => {
|
|
|
|
|
if (!(key in params)) return match;
|
|
|
|
|
const val = String(params[key]).trim();
|
2026-04-14 12:41:09 -04:00
|
|
|
if (!/^[a-zA-Z0-9._:@/-]+$/.test(val)) {
|
2026-04-14 12:24:30 -04:00
|
|
|
throw new Error(`Unsafe value for workflow parameter "${key}"`);
|
|
|
|
|
}
|
|
|
|
|
return val;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Evaluate a condition expression safely using vm.runInNewContext.
|
|
|
|
|
* Returns boolean (false on error).
|
|
|
|
|
*/
|
|
|
|
|
function evalCondition(condition, state, params) {
|
|
|
|
|
const vm = require('vm');
|
|
|
|
|
try {
|
|
|
|
|
const context = vm.createContext({ state, params, promptResponse: state.promptResponse });
|
|
|
|
|
return !!vm.runInNewContext(condition, context, { timeout: 100 });
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.warn(`[Workflow] evalCondition error (treated as false): ${e.message} — condition: ${condition}`);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Calculate the next run date for a scheduled command.
|
|
|
|
|
*/
|
|
|
|
|
function calculateNextRun(scheduleType, scheduleValue) {
|
|
|
|
|
const cronParser = require('cron-parser');
|
|
|
|
|
const now = new Date();
|
|
|
|
|
|
|
|
|
|
if (scheduleType === 'interval') {
|
|
|
|
|
const minutes = parseInt(scheduleValue);
|
|
|
|
|
if (isNaN(minutes) || minutes <= 0) throw new Error(`Invalid interval value: ${scheduleValue}`);
|
|
|
|
|
return new Date(now.getTime() + minutes * 60000);
|
|
|
|
|
} else if (scheduleType === 'daily') {
|
|
|
|
|
const [hours, minutes] = scheduleValue.split(':').map(Number);
|
|
|
|
|
if (isNaN(hours) || isNaN(minutes)) throw new Error(`Invalid daily time format: ${scheduleValue}`);
|
|
|
|
|
const next = new Date(now);
|
|
|
|
|
next.setHours(hours, minutes, 0, 0);
|
|
|
|
|
if (next <= now) next.setDate(next.getDate() + 1);
|
|
|
|
|
return next;
|
|
|
|
|
} else if (scheduleType === 'hourly') {
|
|
|
|
|
const hours = parseInt(scheduleValue);
|
|
|
|
|
if (isNaN(hours) || hours <= 0) throw new Error(`Invalid hourly value: ${scheduleValue}`);
|
|
|
|
|
return new Date(now.getTime() + hours * 3600000);
|
|
|
|
|
} else if (scheduleType === 'cron') {
|
|
|
|
|
const interval = cronParser.CronExpressionParser.parse(scheduleValue, { currentDate: now });
|
|
|
|
|
return interval.next().toDate();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
throw new Error(`Unknown schedule type: ${scheduleType}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
module.exports = { validateWebhookUrl, applyParams, evalCondition, calculateNextRun };
|