@hostwebhook/template-engine 1.2.0 → 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 +7 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.js +101 -7
- package/dist/index.mjs +100 -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);
|
|
@@ -90,6 +91,72 @@ function valueToString(val) {
|
|
|
90
91
|
|
|
91
92
|
// src/sandbox.ts
|
|
92
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
|
+
}
|
|
93
160
|
function buildNodeLookup(workspacePayloads) {
|
|
94
161
|
return (nodeName) => {
|
|
95
162
|
if (!workspacePayloads) throw new Error("$() requires workspace context");
|
|
@@ -120,16 +187,23 @@ function buildSandbox(ctx, opts, logs) {
|
|
|
120
187
|
}
|
|
121
188
|
return String(v);
|
|
122
189
|
};
|
|
190
|
+
let safePayload;
|
|
191
|
+
try {
|
|
192
|
+
safePayload = structuredClone(ctx.payload);
|
|
193
|
+
} catch {
|
|
194
|
+
safePayload = JSON.parse(JSON.stringify(ctx.payload ?? null));
|
|
195
|
+
}
|
|
123
196
|
const sandbox = {
|
|
124
|
-
payload:
|
|
197
|
+
payload: safePayload,
|
|
125
198
|
$: buildNodeLookup(opts.workspacePayloads),
|
|
126
|
-
headers: ctx.headers,
|
|
127
|
-
meta: ctx.meta,
|
|
128
|
-
|
|
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 },
|
|
129
203
|
Math,
|
|
130
204
|
Date,
|
|
131
|
-
Array,
|
|
132
|
-
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 },
|
|
133
207
|
String,
|
|
134
208
|
Number,
|
|
135
209
|
Boolean,
|
|
@@ -141,7 +215,8 @@ function buildSandbox(ctx, opts, logs) {
|
|
|
141
215
|
encodeURIComponent,
|
|
142
216
|
decodeURIComponent,
|
|
143
217
|
Map,
|
|
144
|
-
Set
|
|
218
|
+
Set,
|
|
219
|
+
undefined: void 0
|
|
145
220
|
};
|
|
146
221
|
if (opts.mode === "code" && logs) {
|
|
147
222
|
sandbox.console = {
|
|
@@ -150,9 +225,21 @@ function buildSandbox(ctx, opts, logs) {
|
|
|
150
225
|
error: (...args) => logs.push(`[error] ${args.map(stringify).join(" ")}`)
|
|
151
226
|
};
|
|
152
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
|
+
}
|
|
153
234
|
return vm.createContext(sandbox);
|
|
154
235
|
}
|
|
155
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
|
+
}
|
|
156
243
|
const context = buildSandbox(ctx, { ...opts, mode: "text" });
|
|
157
244
|
try {
|
|
158
245
|
const script = new vm.Script(`(${expr})`, { filename: "template-expr.js" });
|
|
@@ -446,6 +533,12 @@ var vm2 = __toESM(require("vm"));
|
|
|
446
533
|
function executeCodeTemplate(code, ctx, opts) {
|
|
447
534
|
const logs = [];
|
|
448
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
|
+
}
|
|
449
542
|
let sandboxPayload = ctx.payload;
|
|
450
543
|
if (opts.batchMode) {
|
|
451
544
|
const meta = ctx.payload?._meta;
|
|
@@ -565,5 +658,6 @@ function resolveFieldPath(field) {
|
|
|
565
658
|
resolvePath,
|
|
566
659
|
tryNullCoalesce,
|
|
567
660
|
tryTernary,
|
|
661
|
+
validateCode,
|
|
568
662
|
valueToString
|
|
569
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;
|
|
@@ -520,5 +612,6 @@ export {
|
|
|
520
612
|
resolvePath,
|
|
521
613
|
tryNullCoalesce,
|
|
522
614
|
tryTernary,
|
|
615
|
+
validateCode,
|
|
523
616
|
valueToString
|
|
524
617
|
};
|