@hostwebhook/template-engine 1.1.1 → 1.3.0
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/dist/index.d.mts +36 -1
- package/dist/index.d.ts +36 -1
- package/dist/index.js +125 -7
- package/dist/index.mjs +122 -7
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -55,6 +55,41 @@ interface CodeResult {
|
|
|
55
55
|
*/
|
|
56
56
|
declare function renderTemplate(template: string, ctx: TemplateContext, opts?: TemplateOptions): TemplateResult | CodeResult;
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Resolve a single input value — literal string or {{expression}}.
|
|
60
|
+
*
|
|
61
|
+
* If the value is wrapped in `{{...}}`, it's evaluated as a template expression
|
|
62
|
+
* (supports paths, pipes, $() cross-node references, JS when allowJs).
|
|
63
|
+
* Otherwise, the value is returned as-is (literal).
|
|
64
|
+
*
|
|
65
|
+
* Preserves types: `{{payload.count}}` returns the original number, not a string.
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* resolveInput("active", payload) // → "active"
|
|
69
|
+
* resolveInput("{{payload.status}}", payload) // → "delivered"
|
|
70
|
+
* resolveInput("{{payload.amount}}", payload) // → 1500 (number)
|
|
71
|
+
* resolveInput("{{payload.date | formatDate}}", payload) // → "2024-06-15"
|
|
72
|
+
* resolveInput('{{$("Filter").count}}', payload, { allowJs: true, workspacePayloads }) // → 42
|
|
73
|
+
* resolveInput(undefined, payload) // → undefined
|
|
74
|
+
*/
|
|
75
|
+
declare function resolveInput(value: string | undefined | null, payload: unknown, opts?: Pick<TemplateOptions, 'allowJs' | 'workspacePayloads'>): unknown;
|
|
76
|
+
/**
|
|
77
|
+
* Normalize a field path — strips `{{`, `}}`, and `payload.` prefix.
|
|
78
|
+
*
|
|
79
|
+
* Accepts any of these formats and returns the plain dot-notation path:
|
|
80
|
+
* - `"{{payload.customer.name}}"` → `"customer.name"`
|
|
81
|
+
* - `"payload.customer.name"` → `"customer.name"`
|
|
82
|
+
* - `"customer.name"` → `"customer.name"`
|
|
83
|
+
* - `"{{customer.name}}"` → `"customer.name"`
|
|
84
|
+
*/
|
|
85
|
+
declare function resolveFieldPath(field: string): string;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Validate code/expression against blocked patterns.
|
|
89
|
+
* Throws if any dangerous pattern is detected.
|
|
90
|
+
*/
|
|
91
|
+
declare function validateCode(code: string): void;
|
|
92
|
+
|
|
58
93
|
/**
|
|
59
94
|
* Resolve a dot-notation path against a TemplateContext.
|
|
60
95
|
* Supports bracket notation for array indexing: `payload.items[0].name`
|
|
@@ -87,4 +122,4 @@ declare function applyPipe(pipe: string, val: unknown, arg?: string, ctx?: Templ
|
|
|
87
122
|
*/
|
|
88
123
|
declare function evaluate(expr: string, ctx: TemplateContext, opts?: TemplateOptions): string;
|
|
89
124
|
|
|
90
|
-
export { type CodeResult, type TemplateContext, type TemplateMode, type TemplateOptions, type TemplateResult, applyPipe, evaluate, renderTemplate, resolvePath, tryNullCoalesce, tryTernary, valueToString };
|
|
125
|
+
export { type CodeResult, type TemplateContext, type TemplateMode, type TemplateOptions, type TemplateResult, applyPipe, evaluate, renderTemplate, resolveFieldPath, resolveInput, resolvePath, tryNullCoalesce, tryTernary, validateCode, valueToString };
|
package/dist/index.d.ts
CHANGED
|
@@ -55,6 +55,41 @@ interface CodeResult {
|
|
|
55
55
|
*/
|
|
56
56
|
declare function renderTemplate(template: string, ctx: TemplateContext, opts?: TemplateOptions): TemplateResult | CodeResult;
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Resolve a single input value — literal string or {{expression}}.
|
|
60
|
+
*
|
|
61
|
+
* If the value is wrapped in `{{...}}`, it's evaluated as a template expression
|
|
62
|
+
* (supports paths, pipes, $() cross-node references, JS when allowJs).
|
|
63
|
+
* Otherwise, the value is returned as-is (literal).
|
|
64
|
+
*
|
|
65
|
+
* Preserves types: `{{payload.count}}` returns the original number, not a string.
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* resolveInput("active", payload) // → "active"
|
|
69
|
+
* resolveInput("{{payload.status}}", payload) // → "delivered"
|
|
70
|
+
* resolveInput("{{payload.amount}}", payload) // → 1500 (number)
|
|
71
|
+
* resolveInput("{{payload.date | formatDate}}", payload) // → "2024-06-15"
|
|
72
|
+
* resolveInput('{{$("Filter").count}}', payload, { allowJs: true, workspacePayloads }) // → 42
|
|
73
|
+
* resolveInput(undefined, payload) // → undefined
|
|
74
|
+
*/
|
|
75
|
+
declare function resolveInput(value: string | undefined | null, payload: unknown, opts?: Pick<TemplateOptions, 'allowJs' | 'workspacePayloads'>): unknown;
|
|
76
|
+
/**
|
|
77
|
+
* Normalize a field path — strips `{{`, `}}`, and `payload.` prefix.
|
|
78
|
+
*
|
|
79
|
+
* Accepts any of these formats and returns the plain dot-notation path:
|
|
80
|
+
* - `"{{payload.customer.name}}"` → `"customer.name"`
|
|
81
|
+
* - `"payload.customer.name"` → `"customer.name"`
|
|
82
|
+
* - `"customer.name"` → `"customer.name"`
|
|
83
|
+
* - `"{{customer.name}}"` → `"customer.name"`
|
|
84
|
+
*/
|
|
85
|
+
declare function resolveFieldPath(field: string): string;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Validate code/expression against blocked patterns.
|
|
89
|
+
* Throws if any dangerous pattern is detected.
|
|
90
|
+
*/
|
|
91
|
+
declare function validateCode(code: string): void;
|
|
92
|
+
|
|
58
93
|
/**
|
|
59
94
|
* Resolve a dot-notation path against a TemplateContext.
|
|
60
95
|
* Supports bracket notation for array indexing: `payload.items[0].name`
|
|
@@ -87,4 +122,4 @@ declare function applyPipe(pipe: string, val: unknown, arg?: string, ctx?: Templ
|
|
|
87
122
|
*/
|
|
88
123
|
declare function evaluate(expr: string, ctx: TemplateContext, opts?: TemplateOptions): string;
|
|
89
124
|
|
|
90
|
-
export { type CodeResult, type TemplateContext, type TemplateMode, type TemplateOptions, type TemplateResult, applyPipe, evaluate, renderTemplate, resolvePath, tryNullCoalesce, tryTernary, valueToString };
|
|
125
|
+
export { type CodeResult, type TemplateContext, type TemplateMode, type TemplateOptions, type TemplateResult, applyPipe, evaluate, renderTemplate, resolveFieldPath, resolveInput, resolvePath, tryNullCoalesce, tryTernary, validateCode, valueToString };
|
package/dist/index.js
CHANGED
|
@@ -33,9 +33,12 @@ __export(index_exports, {
|
|
|
33
33
|
applyPipe: () => applyPipe,
|
|
34
34
|
evaluate: () => evaluate,
|
|
35
35
|
renderTemplate: () => renderTemplate,
|
|
36
|
+
resolveFieldPath: () => resolveFieldPath,
|
|
37
|
+
resolveInput: () => resolveInput,
|
|
36
38
|
resolvePath: () => resolvePath,
|
|
37
39
|
tryNullCoalesce: () => tryNullCoalesce,
|
|
38
40
|
tryTernary: () => tryTernary,
|
|
41
|
+
validateCode: () => validateCode,
|
|
39
42
|
valueToString: () => valueToString
|
|
40
43
|
});
|
|
41
44
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -88,6 +91,72 @@ function valueToString(val) {
|
|
|
88
91
|
|
|
89
92
|
// src/sandbox.ts
|
|
90
93
|
var vm = __toESM(require("vm"));
|
|
94
|
+
var BLOCKED_PATTERNS = [
|
|
95
|
+
/\bconstructor\b/,
|
|
96
|
+
// this.constructor.constructor('return process')()
|
|
97
|
+
/\b__proto__\b/,
|
|
98
|
+
// prototype chain traversal
|
|
99
|
+
/\bprototype\b/,
|
|
100
|
+
// prototype manipulation
|
|
101
|
+
/\bprocess\b/,
|
|
102
|
+
// Node.js process object
|
|
103
|
+
/\brequire\s*\(/,
|
|
104
|
+
// CommonJS require
|
|
105
|
+
/\bimport\s*\(/,
|
|
106
|
+
// Dynamic import
|
|
107
|
+
/\bglobal\b/,
|
|
108
|
+
// global object
|
|
109
|
+
/\bglobalThis\b/,
|
|
110
|
+
// globalThis reference
|
|
111
|
+
/\beval\s*\(/,
|
|
112
|
+
// eval()
|
|
113
|
+
/\bFunction\s*\(/,
|
|
114
|
+
// new Function()
|
|
115
|
+
/\bProxy\s*\(/,
|
|
116
|
+
// Proxy constructor
|
|
117
|
+
/\bReflect\b/,
|
|
118
|
+
// Reflect API
|
|
119
|
+
/\bBuffer\b/,
|
|
120
|
+
// Node.js Buffer
|
|
121
|
+
/\bSharedArrayBuffer\b/,
|
|
122
|
+
// SharedArrayBuffer
|
|
123
|
+
/\bAtomics\b/,
|
|
124
|
+
// Atomics API
|
|
125
|
+
/\bWebAssembly\b/,
|
|
126
|
+
// WebAssembly
|
|
127
|
+
/\b__defineGetter__\b/,
|
|
128
|
+
// Legacy property definition
|
|
129
|
+
/\b__defineSetter__\b/,
|
|
130
|
+
// Legacy property definition
|
|
131
|
+
/\b__lookupGetter__\b/,
|
|
132
|
+
// Legacy property lookup
|
|
133
|
+
/\b__lookupSetter__\b/
|
|
134
|
+
// Legacy property lookup
|
|
135
|
+
];
|
|
136
|
+
function validateCode(code) {
|
|
137
|
+
for (const pattern of BLOCKED_PATTERNS) {
|
|
138
|
+
if (pattern.test(code)) {
|
|
139
|
+
throw new Error(`Blocked: unsafe pattern "${pattern.source}" detected`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function deepFreeze(obj, seen = /* @__PURE__ */ new WeakSet()) {
|
|
144
|
+
if (obj === null || obj === void 0) return;
|
|
145
|
+
if (typeof obj !== "object" && typeof obj !== "function") return;
|
|
146
|
+
if (seen.has(obj)) return;
|
|
147
|
+
seen.add(obj);
|
|
148
|
+
try {
|
|
149
|
+
Object.freeze(obj);
|
|
150
|
+
for (const key of Object.getOwnPropertyNames(obj)) {
|
|
151
|
+
try {
|
|
152
|
+
const val = obj[key];
|
|
153
|
+
deepFreeze(val, seen);
|
|
154
|
+
} catch {
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
} catch {
|
|
158
|
+
}
|
|
159
|
+
}
|
|
91
160
|
function buildNodeLookup(workspacePayloads) {
|
|
92
161
|
return (nodeName) => {
|
|
93
162
|
if (!workspacePayloads) throw new Error("$() requires workspace context");
|
|
@@ -118,16 +187,23 @@ function buildSandbox(ctx, opts, logs) {
|
|
|
118
187
|
}
|
|
119
188
|
return String(v);
|
|
120
189
|
};
|
|
190
|
+
let safePayload;
|
|
191
|
+
try {
|
|
192
|
+
safePayload = structuredClone(ctx.payload);
|
|
193
|
+
} catch {
|
|
194
|
+
safePayload = JSON.parse(JSON.stringify(ctx.payload ?? null));
|
|
195
|
+
}
|
|
121
196
|
const sandbox = {
|
|
122
|
-
payload:
|
|
197
|
+
payload: safePayload,
|
|
123
198
|
$: buildNodeLookup(opts.workspacePayloads),
|
|
124
|
-
headers: ctx.headers,
|
|
125
|
-
meta: ctx.meta,
|
|
126
|
-
|
|
199
|
+
headers: { ...ctx.headers ?? {} },
|
|
200
|
+
meta: { ...ctx.meta ?? {} },
|
|
201
|
+
// Safe builtins only — no process, require, import, eval, Function, Proxy, Reflect
|
|
202
|
+
JSON: { parse: JSON.parse, stringify: JSON.stringify },
|
|
127
203
|
Math,
|
|
128
204
|
Date,
|
|
129
|
-
Array,
|
|
130
|
-
Object,
|
|
205
|
+
Array: { isArray: Array.isArray, from: Array.from, of: Array.of },
|
|
206
|
+
Object: { keys: Object.keys, values: Object.values, entries: Object.entries, assign: Object.assign, freeze: Object.freeze },
|
|
131
207
|
String,
|
|
132
208
|
Number,
|
|
133
209
|
Boolean,
|
|
@@ -139,7 +215,8 @@ function buildSandbox(ctx, opts, logs) {
|
|
|
139
215
|
encodeURIComponent,
|
|
140
216
|
decodeURIComponent,
|
|
141
217
|
Map,
|
|
142
|
-
Set
|
|
218
|
+
Set,
|
|
219
|
+
undefined: void 0
|
|
143
220
|
};
|
|
144
221
|
if (opts.mode === "code" && logs) {
|
|
145
222
|
sandbox.console = {
|
|
@@ -148,9 +225,21 @@ function buildSandbox(ctx, opts, logs) {
|
|
|
148
225
|
error: (...args) => logs.push(`[error] ${args.map(stringify).join(" ")}`)
|
|
149
226
|
};
|
|
150
227
|
}
|
|
228
|
+
for (const key of Object.keys(sandbox)) {
|
|
229
|
+
const val = sandbox[key];
|
|
230
|
+
if (typeof val === "object" && val !== null && key !== "payload") {
|
|
231
|
+
deepFreeze(val);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
151
234
|
return vm.createContext(sandbox);
|
|
152
235
|
}
|
|
153
236
|
function evalJsExpression(expr, ctx, opts) {
|
|
237
|
+
try {
|
|
238
|
+
validateCode(expr);
|
|
239
|
+
} catch (err) {
|
|
240
|
+
console.error(`[template-engine] ${err instanceof Error ? err.message : err}: "${expr.slice(0, 80)}"`);
|
|
241
|
+
return void 0;
|
|
242
|
+
}
|
|
154
243
|
const context = buildSandbox(ctx, { ...opts, mode: "text" });
|
|
155
244
|
try {
|
|
156
245
|
const script = new vm.Script(`(${expr})`, { filename: "template-expr.js" });
|
|
@@ -444,6 +533,12 @@ var vm2 = __toESM(require("vm"));
|
|
|
444
533
|
function executeCodeTemplate(code, ctx, opts) {
|
|
445
534
|
const logs = [];
|
|
446
535
|
const timeout = opts.timeout ?? 5e3;
|
|
536
|
+
try {
|
|
537
|
+
validateCode(code);
|
|
538
|
+
} catch (err) {
|
|
539
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
540
|
+
return { output: null, statusCode: 500, logs, error: message };
|
|
541
|
+
}
|
|
447
542
|
let sandboxPayload = ctx.payload;
|
|
448
543
|
if (opts.batchMode) {
|
|
449
544
|
const meta = ctx.payload?._meta;
|
|
@@ -533,13 +628,36 @@ function renderTemplate(template, ctx, opts) {
|
|
|
533
628
|
return resolveTextTemplate(template, ctx, opts);
|
|
534
629
|
}
|
|
535
630
|
}
|
|
631
|
+
|
|
632
|
+
// src/resolve-input.ts
|
|
633
|
+
function resolveInput(value, payload, opts) {
|
|
634
|
+
if (value == null || value === "") return value;
|
|
635
|
+
const str = String(value);
|
|
636
|
+
const match = str.match(/^\{\{([\s\S]+)\}\}$/);
|
|
637
|
+
if (!match) return value;
|
|
638
|
+
const expr = match[1].trim();
|
|
639
|
+
const ctx = { payload, headers: {}, meta: {} };
|
|
640
|
+
const resolved = resolvePath(expr, ctx);
|
|
641
|
+
if (resolved !== void 0) return resolved;
|
|
642
|
+
const evalOpts = opts ? { allowJs: opts.allowJs, workspacePayloads: opts.workspacePayloads } : void 0;
|
|
643
|
+
const result = evaluate(expr, ctx, evalOpts);
|
|
644
|
+
if (result !== "") return result;
|
|
645
|
+
return value;
|
|
646
|
+
}
|
|
647
|
+
function resolveFieldPath(field) {
|
|
648
|
+
if (!field) return "";
|
|
649
|
+
return field.replace(/^\{\{\s*/, "").replace(/\s*\}\}$/, "").replace(/^payload\./, "").trim();
|
|
650
|
+
}
|
|
536
651
|
// Annotate the CommonJS export names for ESM import in node:
|
|
537
652
|
0 && (module.exports = {
|
|
538
653
|
applyPipe,
|
|
539
654
|
evaluate,
|
|
540
655
|
renderTemplate,
|
|
656
|
+
resolveFieldPath,
|
|
657
|
+
resolveInput,
|
|
541
658
|
resolvePath,
|
|
542
659
|
tryNullCoalesce,
|
|
543
660
|
tryTernary,
|
|
661
|
+
validateCode,
|
|
544
662
|
valueToString
|
|
545
663
|
});
|
package/dist/index.mjs
CHANGED
|
@@ -46,6 +46,72 @@ function valueToString(val) {
|
|
|
46
46
|
|
|
47
47
|
// src/sandbox.ts
|
|
48
48
|
import * as vm from "vm";
|
|
49
|
+
var BLOCKED_PATTERNS = [
|
|
50
|
+
/\bconstructor\b/,
|
|
51
|
+
// this.constructor.constructor('return process')()
|
|
52
|
+
/\b__proto__\b/,
|
|
53
|
+
// prototype chain traversal
|
|
54
|
+
/\bprototype\b/,
|
|
55
|
+
// prototype manipulation
|
|
56
|
+
/\bprocess\b/,
|
|
57
|
+
// Node.js process object
|
|
58
|
+
/\brequire\s*\(/,
|
|
59
|
+
// CommonJS require
|
|
60
|
+
/\bimport\s*\(/,
|
|
61
|
+
// Dynamic import
|
|
62
|
+
/\bglobal\b/,
|
|
63
|
+
// global object
|
|
64
|
+
/\bglobalThis\b/,
|
|
65
|
+
// globalThis reference
|
|
66
|
+
/\beval\s*\(/,
|
|
67
|
+
// eval()
|
|
68
|
+
/\bFunction\s*\(/,
|
|
69
|
+
// new Function()
|
|
70
|
+
/\bProxy\s*\(/,
|
|
71
|
+
// Proxy constructor
|
|
72
|
+
/\bReflect\b/,
|
|
73
|
+
// Reflect API
|
|
74
|
+
/\bBuffer\b/,
|
|
75
|
+
// Node.js Buffer
|
|
76
|
+
/\bSharedArrayBuffer\b/,
|
|
77
|
+
// SharedArrayBuffer
|
|
78
|
+
/\bAtomics\b/,
|
|
79
|
+
// Atomics API
|
|
80
|
+
/\bWebAssembly\b/,
|
|
81
|
+
// WebAssembly
|
|
82
|
+
/\b__defineGetter__\b/,
|
|
83
|
+
// Legacy property definition
|
|
84
|
+
/\b__defineSetter__\b/,
|
|
85
|
+
// Legacy property definition
|
|
86
|
+
/\b__lookupGetter__\b/,
|
|
87
|
+
// Legacy property lookup
|
|
88
|
+
/\b__lookupSetter__\b/
|
|
89
|
+
// Legacy property lookup
|
|
90
|
+
];
|
|
91
|
+
function validateCode(code) {
|
|
92
|
+
for (const pattern of BLOCKED_PATTERNS) {
|
|
93
|
+
if (pattern.test(code)) {
|
|
94
|
+
throw new Error(`Blocked: unsafe pattern "${pattern.source}" detected`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function deepFreeze(obj, seen = /* @__PURE__ */ new WeakSet()) {
|
|
99
|
+
if (obj === null || obj === void 0) return;
|
|
100
|
+
if (typeof obj !== "object" && typeof obj !== "function") return;
|
|
101
|
+
if (seen.has(obj)) return;
|
|
102
|
+
seen.add(obj);
|
|
103
|
+
try {
|
|
104
|
+
Object.freeze(obj);
|
|
105
|
+
for (const key of Object.getOwnPropertyNames(obj)) {
|
|
106
|
+
try {
|
|
107
|
+
const val = obj[key];
|
|
108
|
+
deepFreeze(val, seen);
|
|
109
|
+
} catch {
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
} catch {
|
|
113
|
+
}
|
|
114
|
+
}
|
|
49
115
|
function buildNodeLookup(workspacePayloads) {
|
|
50
116
|
return (nodeName) => {
|
|
51
117
|
if (!workspacePayloads) throw new Error("$() requires workspace context");
|
|
@@ -76,16 +142,23 @@ function buildSandbox(ctx, opts, logs) {
|
|
|
76
142
|
}
|
|
77
143
|
return String(v);
|
|
78
144
|
};
|
|
145
|
+
let safePayload;
|
|
146
|
+
try {
|
|
147
|
+
safePayload = structuredClone(ctx.payload);
|
|
148
|
+
} catch {
|
|
149
|
+
safePayload = JSON.parse(JSON.stringify(ctx.payload ?? null));
|
|
150
|
+
}
|
|
79
151
|
const sandbox = {
|
|
80
|
-
payload:
|
|
152
|
+
payload: safePayload,
|
|
81
153
|
$: buildNodeLookup(opts.workspacePayloads),
|
|
82
|
-
headers: ctx.headers,
|
|
83
|
-
meta: ctx.meta,
|
|
84
|
-
|
|
154
|
+
headers: { ...ctx.headers ?? {} },
|
|
155
|
+
meta: { ...ctx.meta ?? {} },
|
|
156
|
+
// Safe builtins only — no process, require, import, eval, Function, Proxy, Reflect
|
|
157
|
+
JSON: { parse: JSON.parse, stringify: JSON.stringify },
|
|
85
158
|
Math,
|
|
86
159
|
Date,
|
|
87
|
-
Array,
|
|
88
|
-
Object,
|
|
160
|
+
Array: { isArray: Array.isArray, from: Array.from, of: Array.of },
|
|
161
|
+
Object: { keys: Object.keys, values: Object.values, entries: Object.entries, assign: Object.assign, freeze: Object.freeze },
|
|
89
162
|
String,
|
|
90
163
|
Number,
|
|
91
164
|
Boolean,
|
|
@@ -97,7 +170,8 @@ function buildSandbox(ctx, opts, logs) {
|
|
|
97
170
|
encodeURIComponent,
|
|
98
171
|
decodeURIComponent,
|
|
99
172
|
Map,
|
|
100
|
-
Set
|
|
173
|
+
Set,
|
|
174
|
+
undefined: void 0
|
|
101
175
|
};
|
|
102
176
|
if (opts.mode === "code" && logs) {
|
|
103
177
|
sandbox.console = {
|
|
@@ -106,9 +180,21 @@ function buildSandbox(ctx, opts, logs) {
|
|
|
106
180
|
error: (...args) => logs.push(`[error] ${args.map(stringify).join(" ")}`)
|
|
107
181
|
};
|
|
108
182
|
}
|
|
183
|
+
for (const key of Object.keys(sandbox)) {
|
|
184
|
+
const val = sandbox[key];
|
|
185
|
+
if (typeof val === "object" && val !== null && key !== "payload") {
|
|
186
|
+
deepFreeze(val);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
109
189
|
return vm.createContext(sandbox);
|
|
110
190
|
}
|
|
111
191
|
function evalJsExpression(expr, ctx, opts) {
|
|
192
|
+
try {
|
|
193
|
+
validateCode(expr);
|
|
194
|
+
} catch (err) {
|
|
195
|
+
console.error(`[template-engine] ${err instanceof Error ? err.message : err}: "${expr.slice(0, 80)}"`);
|
|
196
|
+
return void 0;
|
|
197
|
+
}
|
|
112
198
|
const context = buildSandbox(ctx, { ...opts, mode: "text" });
|
|
113
199
|
try {
|
|
114
200
|
const script = new vm.Script(`(${expr})`, { filename: "template-expr.js" });
|
|
@@ -402,6 +488,12 @@ import * as vm2 from "vm";
|
|
|
402
488
|
function executeCodeTemplate(code, ctx, opts) {
|
|
403
489
|
const logs = [];
|
|
404
490
|
const timeout = opts.timeout ?? 5e3;
|
|
491
|
+
try {
|
|
492
|
+
validateCode(code);
|
|
493
|
+
} catch (err) {
|
|
494
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
495
|
+
return { output: null, statusCode: 500, logs, error: message };
|
|
496
|
+
}
|
|
405
497
|
let sandboxPayload = ctx.payload;
|
|
406
498
|
if (opts.batchMode) {
|
|
407
499
|
const meta = ctx.payload?._meta;
|
|
@@ -491,12 +583,35 @@ function renderTemplate(template, ctx, opts) {
|
|
|
491
583
|
return resolveTextTemplate(template, ctx, opts);
|
|
492
584
|
}
|
|
493
585
|
}
|
|
586
|
+
|
|
587
|
+
// src/resolve-input.ts
|
|
588
|
+
function resolveInput(value, payload, opts) {
|
|
589
|
+
if (value == null || value === "") return value;
|
|
590
|
+
const str = String(value);
|
|
591
|
+
const match = str.match(/^\{\{([\s\S]+)\}\}$/);
|
|
592
|
+
if (!match) return value;
|
|
593
|
+
const expr = match[1].trim();
|
|
594
|
+
const ctx = { payload, headers: {}, meta: {} };
|
|
595
|
+
const resolved = resolvePath(expr, ctx);
|
|
596
|
+
if (resolved !== void 0) return resolved;
|
|
597
|
+
const evalOpts = opts ? { allowJs: opts.allowJs, workspacePayloads: opts.workspacePayloads } : void 0;
|
|
598
|
+
const result = evaluate(expr, ctx, evalOpts);
|
|
599
|
+
if (result !== "") return result;
|
|
600
|
+
return value;
|
|
601
|
+
}
|
|
602
|
+
function resolveFieldPath(field) {
|
|
603
|
+
if (!field) return "";
|
|
604
|
+
return field.replace(/^\{\{\s*/, "").replace(/\s*\}\}$/, "").replace(/^payload\./, "").trim();
|
|
605
|
+
}
|
|
494
606
|
export {
|
|
495
607
|
applyPipe,
|
|
496
608
|
evaluate,
|
|
497
609
|
renderTemplate,
|
|
610
|
+
resolveFieldPath,
|
|
611
|
+
resolveInput,
|
|
498
612
|
resolvePath,
|
|
499
613
|
tryNullCoalesce,
|
|
500
614
|
tryTernary,
|
|
615
|
+
validateCode,
|
|
501
616
|
valueToString
|
|
502
617
|
};
|