@hostwebhook/template-engine 1.2.0 → 1.3.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/dist/index.d.mts +7 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.js +105 -7
- package/dist/index.mjs +104 -7
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -84,6 +84,12 @@ declare function resolveInput(value: string | undefined | null, payload: unknown
|
|
|
84
84
|
*/
|
|
85
85
|
declare function resolveFieldPath(field: string): string;
|
|
86
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
|
+
|
|
87
93
|
/**
|
|
88
94
|
* Resolve a dot-notation path against a TemplateContext.
|
|
89
95
|
* Supports bracket notation for array indexing: `payload.items[0].name`
|
|
@@ -116,4 +122,4 @@ declare function applyPipe(pipe: string, val: unknown, arg?: string, ctx?: Templ
|
|
|
116
122
|
*/
|
|
117
123
|
declare function evaluate(expr: string, ctx: TemplateContext, opts?: TemplateOptions): string;
|
|
118
124
|
|
|
119
|
-
export { type CodeResult, type TemplateContext, type TemplateMode, type TemplateOptions, type TemplateResult, applyPipe, evaluate, renderTemplate, resolveFieldPath, resolveInput, 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
|
@@ -84,6 +84,12 @@ declare function resolveInput(value: string | undefined | null, payload: unknown
|
|
|
84
84
|
*/
|
|
85
85
|
declare function resolveFieldPath(field: string): string;
|
|
86
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
|
+
|
|
87
93
|
/**
|
|
88
94
|
* Resolve a dot-notation path against a TemplateContext.
|
|
89
95
|
* Supports bracket notation for array indexing: `payload.items[0].name`
|
|
@@ -116,4 +122,4 @@ declare function applyPipe(pipe: string, val: unknown, arg?: string, ctx?: Templ
|
|
|
116
122
|
*/
|
|
117
123
|
declare function evaluate(expr: string, ctx: TemplateContext, opts?: TemplateOptions): string;
|
|
118
124
|
|
|
119
|
-
export { type CodeResult, type TemplateContext, type TemplateMode, type TemplateOptions, type TemplateResult, applyPipe, evaluate, renderTemplate, resolveFieldPath, resolveInput, 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
|
@@ -38,6 +38,7 @@ __export(index_exports, {
|
|
|
38
38
|
resolvePath: () => resolvePath,
|
|
39
39
|
tryNullCoalesce: () => tryNullCoalesce,
|
|
40
40
|
tryTernary: () => tryTernary,
|
|
41
|
+
validateCode: () => validateCode,
|
|
41
42
|
valueToString: () => valueToString
|
|
42
43
|
});
|
|
43
44
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -46,6 +47,10 @@ module.exports = __toCommonJS(index_exports);
|
|
|
46
47
|
function resolvePath(path, ctx) {
|
|
47
48
|
const [prefix, ...rest] = path.split(".");
|
|
48
49
|
const key = rest.join(".");
|
|
50
|
+
if (path === "$now") return (/* @__PURE__ */ new Date()).toISOString();
|
|
51
|
+
if (path === "$timestamp") return Date.now();
|
|
52
|
+
if (path === "$date") return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
53
|
+
if (path === "$time") return (/* @__PURE__ */ new Date()).toISOString().slice(11, 19);
|
|
49
54
|
let source;
|
|
50
55
|
switch (prefix) {
|
|
51
56
|
case "payload":
|
|
@@ -90,6 +95,72 @@ function valueToString(val) {
|
|
|
90
95
|
|
|
91
96
|
// src/sandbox.ts
|
|
92
97
|
var vm = __toESM(require("vm"));
|
|
98
|
+
var BLOCKED_PATTERNS = [
|
|
99
|
+
/\bconstructor\b/,
|
|
100
|
+
// this.constructor.constructor('return process')()
|
|
101
|
+
/\b__proto__\b/,
|
|
102
|
+
// prototype chain traversal
|
|
103
|
+
/\bprototype\b/,
|
|
104
|
+
// prototype manipulation
|
|
105
|
+
/\bprocess\b/,
|
|
106
|
+
// Node.js process object
|
|
107
|
+
/\brequire\s*\(/,
|
|
108
|
+
// CommonJS require
|
|
109
|
+
/\bimport\s*\(/,
|
|
110
|
+
// Dynamic import
|
|
111
|
+
/\bglobal\b/,
|
|
112
|
+
// global object
|
|
113
|
+
/\bglobalThis\b/,
|
|
114
|
+
// globalThis reference
|
|
115
|
+
/\beval\s*\(/,
|
|
116
|
+
// eval()
|
|
117
|
+
/\bFunction\s*\(/,
|
|
118
|
+
// new Function()
|
|
119
|
+
/\bProxy\s*\(/,
|
|
120
|
+
// Proxy constructor
|
|
121
|
+
/\bReflect\b/,
|
|
122
|
+
// Reflect API
|
|
123
|
+
/\bBuffer\b/,
|
|
124
|
+
// Node.js Buffer
|
|
125
|
+
/\bSharedArrayBuffer\b/,
|
|
126
|
+
// SharedArrayBuffer
|
|
127
|
+
/\bAtomics\b/,
|
|
128
|
+
// Atomics API
|
|
129
|
+
/\bWebAssembly\b/,
|
|
130
|
+
// WebAssembly
|
|
131
|
+
/\b__defineGetter__\b/,
|
|
132
|
+
// Legacy property definition
|
|
133
|
+
/\b__defineSetter__\b/,
|
|
134
|
+
// Legacy property definition
|
|
135
|
+
/\b__lookupGetter__\b/,
|
|
136
|
+
// Legacy property lookup
|
|
137
|
+
/\b__lookupSetter__\b/
|
|
138
|
+
// Legacy property lookup
|
|
139
|
+
];
|
|
140
|
+
function validateCode(code) {
|
|
141
|
+
for (const pattern of BLOCKED_PATTERNS) {
|
|
142
|
+
if (pattern.test(code)) {
|
|
143
|
+
throw new Error(`Blocked: unsafe pattern "${pattern.source}" detected`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function deepFreeze(obj, seen = /* @__PURE__ */ new WeakSet()) {
|
|
148
|
+
if (obj === null || obj === void 0) return;
|
|
149
|
+
if (typeof obj !== "object" && typeof obj !== "function") return;
|
|
150
|
+
if (seen.has(obj)) return;
|
|
151
|
+
seen.add(obj);
|
|
152
|
+
try {
|
|
153
|
+
Object.freeze(obj);
|
|
154
|
+
for (const key of Object.getOwnPropertyNames(obj)) {
|
|
155
|
+
try {
|
|
156
|
+
const val = obj[key];
|
|
157
|
+
deepFreeze(val, seen);
|
|
158
|
+
} catch {
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
} catch {
|
|
162
|
+
}
|
|
163
|
+
}
|
|
93
164
|
function buildNodeLookup(workspacePayloads) {
|
|
94
165
|
return (nodeName) => {
|
|
95
166
|
if (!workspacePayloads) throw new Error("$() requires workspace context");
|
|
@@ -120,16 +191,23 @@ function buildSandbox(ctx, opts, logs) {
|
|
|
120
191
|
}
|
|
121
192
|
return String(v);
|
|
122
193
|
};
|
|
194
|
+
let safePayload;
|
|
195
|
+
try {
|
|
196
|
+
safePayload = structuredClone(ctx.payload);
|
|
197
|
+
} catch {
|
|
198
|
+
safePayload = JSON.parse(JSON.stringify(ctx.payload ?? null));
|
|
199
|
+
}
|
|
123
200
|
const sandbox = {
|
|
124
|
-
payload:
|
|
201
|
+
payload: safePayload,
|
|
125
202
|
$: buildNodeLookup(opts.workspacePayloads),
|
|
126
|
-
headers: ctx.headers,
|
|
127
|
-
meta: ctx.meta,
|
|
128
|
-
|
|
203
|
+
headers: { ...ctx.headers ?? {} },
|
|
204
|
+
meta: { ...ctx.meta ?? {} },
|
|
205
|
+
// Safe builtins only — no process, require, import, eval, Function, Proxy, Reflect
|
|
206
|
+
JSON: { parse: JSON.parse, stringify: JSON.stringify },
|
|
129
207
|
Math,
|
|
130
208
|
Date,
|
|
131
|
-
Array,
|
|
132
|
-
Object,
|
|
209
|
+
Array: { isArray: Array.isArray, from: Array.from, of: Array.of },
|
|
210
|
+
Object: { keys: Object.keys, values: Object.values, entries: Object.entries, assign: Object.assign, freeze: Object.freeze },
|
|
133
211
|
String,
|
|
134
212
|
Number,
|
|
135
213
|
Boolean,
|
|
@@ -141,7 +219,8 @@ function buildSandbox(ctx, opts, logs) {
|
|
|
141
219
|
encodeURIComponent,
|
|
142
220
|
decodeURIComponent,
|
|
143
221
|
Map,
|
|
144
|
-
Set
|
|
222
|
+
Set,
|
|
223
|
+
undefined: void 0
|
|
145
224
|
};
|
|
146
225
|
if (opts.mode === "code" && logs) {
|
|
147
226
|
sandbox.console = {
|
|
@@ -150,9 +229,21 @@ function buildSandbox(ctx, opts, logs) {
|
|
|
150
229
|
error: (...args) => logs.push(`[error] ${args.map(stringify).join(" ")}`)
|
|
151
230
|
};
|
|
152
231
|
}
|
|
232
|
+
for (const key of Object.keys(sandbox)) {
|
|
233
|
+
const val = sandbox[key];
|
|
234
|
+
if (typeof val === "object" && val !== null && key !== "payload") {
|
|
235
|
+
deepFreeze(val);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
153
238
|
return vm.createContext(sandbox);
|
|
154
239
|
}
|
|
155
240
|
function evalJsExpression(expr, ctx, opts) {
|
|
241
|
+
try {
|
|
242
|
+
validateCode(expr);
|
|
243
|
+
} catch (err) {
|
|
244
|
+
console.error(`[template-engine] ${err instanceof Error ? err.message : err}: "${expr.slice(0, 80)}"`);
|
|
245
|
+
return void 0;
|
|
246
|
+
}
|
|
156
247
|
const context = buildSandbox(ctx, { ...opts, mode: "text" });
|
|
157
248
|
try {
|
|
158
249
|
const script = new vm.Script(`(${expr})`, { filename: "template-expr.js" });
|
|
@@ -446,6 +537,12 @@ var vm2 = __toESM(require("vm"));
|
|
|
446
537
|
function executeCodeTemplate(code, ctx, opts) {
|
|
447
538
|
const logs = [];
|
|
448
539
|
const timeout = opts.timeout ?? 5e3;
|
|
540
|
+
try {
|
|
541
|
+
validateCode(code);
|
|
542
|
+
} catch (err) {
|
|
543
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
544
|
+
return { output: null, statusCode: 500, logs, error: message };
|
|
545
|
+
}
|
|
449
546
|
let sandboxPayload = ctx.payload;
|
|
450
547
|
if (opts.batchMode) {
|
|
451
548
|
const meta = ctx.payload?._meta;
|
|
@@ -565,5 +662,6 @@ function resolveFieldPath(field) {
|
|
|
565
662
|
resolvePath,
|
|
566
663
|
tryNullCoalesce,
|
|
567
664
|
tryTernary,
|
|
665
|
+
validateCode,
|
|
568
666
|
valueToString
|
|
569
667
|
});
|
package/dist/index.mjs
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
function resolvePath(path, ctx) {
|
|
3
3
|
const [prefix, ...rest] = path.split(".");
|
|
4
4
|
const key = rest.join(".");
|
|
5
|
+
if (path === "$now") return (/* @__PURE__ */ new Date()).toISOString();
|
|
6
|
+
if (path === "$timestamp") return Date.now();
|
|
7
|
+
if (path === "$date") return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
8
|
+
if (path === "$time") return (/* @__PURE__ */ new Date()).toISOString().slice(11, 19);
|
|
5
9
|
let source;
|
|
6
10
|
switch (prefix) {
|
|
7
11
|
case "payload":
|
|
@@ -46,6 +50,72 @@ function valueToString(val) {
|
|
|
46
50
|
|
|
47
51
|
// src/sandbox.ts
|
|
48
52
|
import * as vm from "vm";
|
|
53
|
+
var BLOCKED_PATTERNS = [
|
|
54
|
+
/\bconstructor\b/,
|
|
55
|
+
// this.constructor.constructor('return process')()
|
|
56
|
+
/\b__proto__\b/,
|
|
57
|
+
// prototype chain traversal
|
|
58
|
+
/\bprototype\b/,
|
|
59
|
+
// prototype manipulation
|
|
60
|
+
/\bprocess\b/,
|
|
61
|
+
// Node.js process object
|
|
62
|
+
/\brequire\s*\(/,
|
|
63
|
+
// CommonJS require
|
|
64
|
+
/\bimport\s*\(/,
|
|
65
|
+
// Dynamic import
|
|
66
|
+
/\bglobal\b/,
|
|
67
|
+
// global object
|
|
68
|
+
/\bglobalThis\b/,
|
|
69
|
+
// globalThis reference
|
|
70
|
+
/\beval\s*\(/,
|
|
71
|
+
// eval()
|
|
72
|
+
/\bFunction\s*\(/,
|
|
73
|
+
// new Function()
|
|
74
|
+
/\bProxy\s*\(/,
|
|
75
|
+
// Proxy constructor
|
|
76
|
+
/\bReflect\b/,
|
|
77
|
+
// Reflect API
|
|
78
|
+
/\bBuffer\b/,
|
|
79
|
+
// Node.js Buffer
|
|
80
|
+
/\bSharedArrayBuffer\b/,
|
|
81
|
+
// SharedArrayBuffer
|
|
82
|
+
/\bAtomics\b/,
|
|
83
|
+
// Atomics API
|
|
84
|
+
/\bWebAssembly\b/,
|
|
85
|
+
// WebAssembly
|
|
86
|
+
/\b__defineGetter__\b/,
|
|
87
|
+
// Legacy property definition
|
|
88
|
+
/\b__defineSetter__\b/,
|
|
89
|
+
// Legacy property definition
|
|
90
|
+
/\b__lookupGetter__\b/,
|
|
91
|
+
// Legacy property lookup
|
|
92
|
+
/\b__lookupSetter__\b/
|
|
93
|
+
// Legacy property lookup
|
|
94
|
+
];
|
|
95
|
+
function validateCode(code) {
|
|
96
|
+
for (const pattern of BLOCKED_PATTERNS) {
|
|
97
|
+
if (pattern.test(code)) {
|
|
98
|
+
throw new Error(`Blocked: unsafe pattern "${pattern.source}" detected`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function deepFreeze(obj, seen = /* @__PURE__ */ new WeakSet()) {
|
|
103
|
+
if (obj === null || obj === void 0) return;
|
|
104
|
+
if (typeof obj !== "object" && typeof obj !== "function") return;
|
|
105
|
+
if (seen.has(obj)) return;
|
|
106
|
+
seen.add(obj);
|
|
107
|
+
try {
|
|
108
|
+
Object.freeze(obj);
|
|
109
|
+
for (const key of Object.getOwnPropertyNames(obj)) {
|
|
110
|
+
try {
|
|
111
|
+
const val = obj[key];
|
|
112
|
+
deepFreeze(val, seen);
|
|
113
|
+
} catch {
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
} catch {
|
|
117
|
+
}
|
|
118
|
+
}
|
|
49
119
|
function buildNodeLookup(workspacePayloads) {
|
|
50
120
|
return (nodeName) => {
|
|
51
121
|
if (!workspacePayloads) throw new Error("$() requires workspace context");
|
|
@@ -76,16 +146,23 @@ function buildSandbox(ctx, opts, logs) {
|
|
|
76
146
|
}
|
|
77
147
|
return String(v);
|
|
78
148
|
};
|
|
149
|
+
let safePayload;
|
|
150
|
+
try {
|
|
151
|
+
safePayload = structuredClone(ctx.payload);
|
|
152
|
+
} catch {
|
|
153
|
+
safePayload = JSON.parse(JSON.stringify(ctx.payload ?? null));
|
|
154
|
+
}
|
|
79
155
|
const sandbox = {
|
|
80
|
-
payload:
|
|
156
|
+
payload: safePayload,
|
|
81
157
|
$: buildNodeLookup(opts.workspacePayloads),
|
|
82
|
-
headers: ctx.headers,
|
|
83
|
-
meta: ctx.meta,
|
|
84
|
-
|
|
158
|
+
headers: { ...ctx.headers ?? {} },
|
|
159
|
+
meta: { ...ctx.meta ?? {} },
|
|
160
|
+
// Safe builtins only — no process, require, import, eval, Function, Proxy, Reflect
|
|
161
|
+
JSON: { parse: JSON.parse, stringify: JSON.stringify },
|
|
85
162
|
Math,
|
|
86
163
|
Date,
|
|
87
|
-
Array,
|
|
88
|
-
Object,
|
|
164
|
+
Array: { isArray: Array.isArray, from: Array.from, of: Array.of },
|
|
165
|
+
Object: { keys: Object.keys, values: Object.values, entries: Object.entries, assign: Object.assign, freeze: Object.freeze },
|
|
89
166
|
String,
|
|
90
167
|
Number,
|
|
91
168
|
Boolean,
|
|
@@ -97,7 +174,8 @@ function buildSandbox(ctx, opts, logs) {
|
|
|
97
174
|
encodeURIComponent,
|
|
98
175
|
decodeURIComponent,
|
|
99
176
|
Map,
|
|
100
|
-
Set
|
|
177
|
+
Set,
|
|
178
|
+
undefined: void 0
|
|
101
179
|
};
|
|
102
180
|
if (opts.mode === "code" && logs) {
|
|
103
181
|
sandbox.console = {
|
|
@@ -106,9 +184,21 @@ function buildSandbox(ctx, opts, logs) {
|
|
|
106
184
|
error: (...args) => logs.push(`[error] ${args.map(stringify).join(" ")}`)
|
|
107
185
|
};
|
|
108
186
|
}
|
|
187
|
+
for (const key of Object.keys(sandbox)) {
|
|
188
|
+
const val = sandbox[key];
|
|
189
|
+
if (typeof val === "object" && val !== null && key !== "payload") {
|
|
190
|
+
deepFreeze(val);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
109
193
|
return vm.createContext(sandbox);
|
|
110
194
|
}
|
|
111
195
|
function evalJsExpression(expr, ctx, opts) {
|
|
196
|
+
try {
|
|
197
|
+
validateCode(expr);
|
|
198
|
+
} catch (err) {
|
|
199
|
+
console.error(`[template-engine] ${err instanceof Error ? err.message : err}: "${expr.slice(0, 80)}"`);
|
|
200
|
+
return void 0;
|
|
201
|
+
}
|
|
112
202
|
const context = buildSandbox(ctx, { ...opts, mode: "text" });
|
|
113
203
|
try {
|
|
114
204
|
const script = new vm.Script(`(${expr})`, { filename: "template-expr.js" });
|
|
@@ -402,6 +492,12 @@ import * as vm2 from "vm";
|
|
|
402
492
|
function executeCodeTemplate(code, ctx, opts) {
|
|
403
493
|
const logs = [];
|
|
404
494
|
const timeout = opts.timeout ?? 5e3;
|
|
495
|
+
try {
|
|
496
|
+
validateCode(code);
|
|
497
|
+
} catch (err) {
|
|
498
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
499
|
+
return { output: null, statusCode: 500, logs, error: message };
|
|
500
|
+
}
|
|
405
501
|
let sandboxPayload = ctx.payload;
|
|
406
502
|
if (opts.batchMode) {
|
|
407
503
|
const meta = ctx.payload?._meta;
|
|
@@ -520,5 +616,6 @@ export {
|
|
|
520
616
|
resolvePath,
|
|
521
617
|
tryNullCoalesce,
|
|
522
618
|
tryTernary,
|
|
619
|
+
validateCode,
|
|
523
620
|
valueToString
|
|
524
621
|
};
|