@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 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: ctx.payload,
201
+ payload: safePayload,
125
202
  $: buildNodeLookup(opts.workspacePayloads),
126
- headers: ctx.headers,
127
- meta: ctx.meta,
128
- JSON,
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: ctx.payload,
156
+ payload: safePayload,
81
157
  $: buildNodeLookup(opts.workspacePayloads),
82
- headers: ctx.headers,
83
- meta: ctx.meta,
84
- JSON,
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
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hostwebhook/template-engine",
3
- "version": "1.2.0",
3
+ "version": "1.3.1",
4
4
  "description": "Template interpolation and sandboxed JS execution engine for HostWebhook",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",