Files
pulse/lib/utils.js
T

91 lines
3.1 KiB
JavaScript
Raw Normal View History

'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();
if (!/^[a-zA-Z0-9._:@\-\/]+$/.test(val)) {
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 };