@unrdf/hooks 26.4.3 → 26.4.4

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.
Files changed (56) hide show
  1. package/LICENSE +24 -0
  2. package/README.md +562 -53
  3. package/examples/atomvm-fibo-hooks-demo.mjs +323 -0
  4. package/examples/delta-monitoring-example.mjs +213 -0
  5. package/examples/fibo-jtbd-governance.mjs +388 -0
  6. package/examples/hook-chains/node_modules/.bin/jiti +0 -0
  7. package/examples/hook-chains/node_modules/.bin/msw +0 -0
  8. package/examples/hook-chains/node_modules/.bin/terser +0 -0
  9. package/examples/hook-chains/node_modules/.bin/tsc +0 -0
  10. package/examples/hook-chains/node_modules/.bin/tsserver +0 -0
  11. package/examples/hook-chains/node_modules/.bin/tsx +0 -0
  12. package/examples/hook-chains/node_modules/.bin/validate-hooks +0 -0
  13. package/examples/hook-chains/node_modules/.bin/vite +0 -0
  14. package/examples/hook-chains/node_modules/.bin/vitest +0 -0
  15. package/examples/hook-chains/node_modules/.bin/yaml +0 -0
  16. package/examples/hooks-marketplace.mjs +261 -0
  17. package/examples/n3-reasoning-example.mjs +279 -0
  18. package/examples/policy-hooks/node_modules/.bin/jiti +0 -0
  19. package/examples/policy-hooks/node_modules/.bin/msw +0 -0
  20. package/examples/policy-hooks/node_modules/.bin/terser +0 -0
  21. package/examples/policy-hooks/node_modules/.bin/tsc +0 -0
  22. package/examples/policy-hooks/node_modules/.bin/tsserver +0 -0
  23. package/examples/policy-hooks/node_modules/.bin/tsx +0 -0
  24. package/examples/policy-hooks/node_modules/.bin/validate-hooks +0 -0
  25. package/examples/policy-hooks/node_modules/.bin/vite +0 -0
  26. package/examples/policy-hooks/node_modules/.bin/vitest +0 -0
  27. package/examples/policy-hooks/node_modules/.bin/yaml +0 -0
  28. package/examples/shacl-repair-example.mjs +191 -0
  29. package/examples/window-condition-example.mjs +285 -0
  30. package/package.json +26 -23
  31. package/src/atomvm.mjs +9 -0
  32. package/src/define.mjs +114 -0
  33. package/src/executor.mjs +23 -0
  34. package/src/hooks/atomvm-bridge.mjs +332 -0
  35. package/src/hooks/builtin-hooks.mjs +13 -7
  36. package/src/hooks/condition-evaluator.mjs +684 -77
  37. package/src/hooks/define-hook.mjs +23 -23
  38. package/src/hooks/effect-executor.mjs +630 -0
  39. package/src/hooks/effect-sandbox.mjs +19 -9
  40. package/src/hooks/file-resolver.mjs +155 -1
  41. package/src/hooks/hook-chain-compiler.mjs +11 -1
  42. package/src/hooks/hook-executor.mjs +98 -73
  43. package/src/hooks/knowledge-hook-engine.mjs +133 -7
  44. package/src/hooks/ontology-learner.mjs +190 -0
  45. package/src/hooks/query.mjs +3 -3
  46. package/src/hooks/schemas.mjs +47 -3
  47. package/src/hooks/security/error-sanitizer.mjs +46 -24
  48. package/src/hooks/self-play-autonomics.mjs +423 -0
  49. package/src/hooks/telemetry.mjs +32 -9
  50. package/src/hooks/validate.mjs +100 -33
  51. package/src/index.mjs +2 -0
  52. package/src/lib/admit-hook.mjs +615 -0
  53. package/src/policy-compiler.mjs +12 -2
  54. package/dist/index.d.mts +0 -1738
  55. package/dist/index.d.ts +0 -1738
  56. package/dist/index.mjs +0 -1738
@@ -94,23 +94,27 @@ export const HookTriggerSchema = z.enum([
94
94
  'audit-trail',
95
95
  ]);
96
96
 
97
- export const HookConfigSchema = z.object({
98
- name: z.string().min(1, 'Hook name is required').optional(),
99
- trigger: HookTriggerSchema,
100
- // Note: No return type enforcement - runtime POKA-YOKE guard handles non-boolean returns
101
- validate: z.function().optional(),
102
- transform: z.function().optional(),
103
- metadata: z.record(z.string(), z.any()).optional(),
104
- // Old format compatibility
105
- meta: z
106
- .object({
107
- name: z.string(),
108
- description: z.string().optional(),
109
- })
110
- .optional(),
111
- pattern: z.string().optional(),
112
- run: z.function().optional(),
113
- });
97
+ export const HookConfigSchema = z
98
+ .object({
99
+ name: z.string().min(1, 'Hook name is required'),
100
+ trigger: HookTriggerSchema,
101
+ // Note: No return type enforcement - runtime POKA-YOKE guard handles non-boolean returns
102
+ validate: z.function().optional(),
103
+ transform: z.function().optional(),
104
+ metadata: z.record(z.string(), z.any()).optional(),
105
+ // Old format compatibility
106
+ meta: z
107
+ .object({
108
+ name: z.string(),
109
+ description: z.string().optional(),
110
+ })
111
+ .optional(),
112
+ pattern: z.string().optional(),
113
+ run: z.function().optional(),
114
+ })
115
+ .refine(obj => obj.validate || obj.transform || obj.run, {
116
+ message: 'Hook must define either validate, transform, or run function',
117
+ });
114
118
 
115
119
  export const HookSchema = z.object({
116
120
  name: z.string(),
@@ -143,8 +147,8 @@ export const HookSchema = z.object({
143
147
  export function defineHook(config) {
144
148
  const validated = HookConfigSchema.parse(config);
145
149
 
146
- // Support old format with meta.name and run()
147
- const name = validated.name || validated.meta?.name;
150
+ // name is now required by schema, validate and transform checked via refine
151
+ const name = validated.name;
148
152
  const validate = validated.validate;
149
153
  const transform = validated.transform || validated.run;
150
154
  const metadata = validated.metadata || {
@@ -152,10 +156,6 @@ export function defineHook(config) {
152
156
  pattern: validated.pattern,
153
157
  };
154
158
 
155
- if (!validate && !transform) {
156
- throw new Error('Hook must define either validate, transform, or run function');
157
- }
158
-
159
159
  return {
160
160
  name,
161
161
  trigger: validated.trigger,
@@ -0,0 +1,630 @@
1
+ /**
2
+ * @file Effect Executor - Sandboxed file operations with path traversal defense
3
+ * @module effect-executor
4
+ *
5
+ * @description
6
+ * Provides safe file read/write/mkdir operations that defend against:
7
+ * - Symlink traversal (resolves real paths before access)
8
+ * - Path traversal via ../ sequences
9
+ * - Absolute path escapes outside sandbox
10
+ * - Encoded traversal patterns (%2e%2e, %252f, etc.)
11
+ * - Null byte injection
12
+ * - Oversized file reads (memory DoS)
13
+ */
14
+
15
+ import { promises as fs } from 'fs';
16
+ import { resolve, normalize, isAbsolute } from 'path';
17
+
18
+ /** @type {string} Root directory for all sandboxed file operations */
19
+ export const SANDBOX_ROOT = '/tmp/hooks-sandbox';
20
+
21
+ /** @type {number} Maximum file size in bytes (10 MB) */
22
+ export const MAX_FILE_SIZE = 10 * 1024 * 1024;
23
+
24
+ /** @type {number} Maximum path length */
25
+ const MAX_PATH_LENGTH = 4096;
26
+
27
+ /**
28
+ * Cache for the real (symlink-resolved) sandbox root path.
29
+ * On macOS, /tmp is a symlink to /private/tmp, so we must compare
30
+ * against the resolved path when checking fs.realpath() results.
31
+ * @type {string|null}
32
+ */
33
+ let _resolvedSandboxRoot = null;
34
+
35
+ /**
36
+ * Get the real sandbox root path, resolving symlinks (e.g., /tmp → /private/tmp on macOS).
37
+ * @returns {Promise<string>}
38
+ */
39
+ async function getResolvedSandboxRoot() {
40
+ if (_resolvedSandboxRoot === null) {
41
+ await fs.mkdir(SANDBOX_ROOT, { recursive: true });
42
+ _resolvedSandboxRoot = await fs.realpath(SANDBOX_ROOT);
43
+ }
44
+ return _resolvedSandboxRoot;
45
+ }
46
+
47
+ /**
48
+ * Reset the resolved sandbox root cache (for testing).
49
+ */
50
+ export function _resetSandboxRootCache() {
51
+ _resolvedSandboxRoot = null;
52
+ }
53
+
54
+ /**
55
+ * Dangerous patterns that indicate path traversal attempts.
56
+ * Checked against the raw input before any normalization.
57
+ */
58
+ const TRAVERSAL_PATTERNS = [
59
+ /\.\.\//, // ../
60
+ /\.\.\\/, // ..\
61
+ /\.\.%2f/i, // ..%2f (URL encoded /)
62
+ /\.\.%5c/i, // ..%5c (URL encoded \)
63
+ /\.\.%252f/i, // ..%252f (double URL encoded)
64
+ /\.\.%255c/i, // ..%255c (double URL encoded)
65
+ /\.\.%c0%af/i, // overlong UTF-8 /
66
+ /\.\.%c1%9c/i, // overlong UTF-8 \
67
+ /%2e%2e/i, // encoded ..
68
+ /%252e%252e/i, // double-encoded ..
69
+ ];
70
+
71
+ /**
72
+ * Validate and resolve a path to ensure it stays within SANDBOX_ROOT.
73
+ *
74
+ * Steps:
75
+ * 1. Reject null bytes, encoded traversal, and overly long paths
76
+ * 2. Reject absolute paths (must be relative to sandbox)
77
+ * 3. Normalize and resolve against SANDBOX_ROOT
78
+ * 4. Resolve symlinks via fs.realpath (if target exists)
79
+ * 5. Confirm the final real path is still under SANDBOX_ROOT
80
+ *
81
+ * @param {string} untrustedPath - The user-supplied path to validate
82
+ * @returns {Promise<string>} The resolved, safe absolute path
83
+ * @throws {Error} On any policy violation
84
+ */
85
+ export async function validatePath(untrustedPath) {
86
+ // --- input type check ---
87
+ if (typeof untrustedPath !== 'string') {
88
+ throw new Error('Path must be a string');
89
+ }
90
+
91
+ // --- length check ---
92
+ if (untrustedPath.length === 0) {
93
+ throw new Error('Path must not be empty');
94
+ }
95
+ if (untrustedPath.length > MAX_PATH_LENGTH) {
96
+ throw new Error('Path exceeds maximum length');
97
+ }
98
+
99
+ // --- null byte injection ---
100
+ if (untrustedPath.includes('\x00')) {
101
+ throw new Error('Path traversal detected: null byte');
102
+ }
103
+
104
+ // --- encoded traversal patterns (check raw input) ---
105
+ for (const pattern of TRAVERSAL_PATTERNS) {
106
+ if (pattern.test(untrustedPath)) {
107
+ throw new Error('Path traversal detected');
108
+ }
109
+ }
110
+
111
+ // --- reject absolute paths (must be relative to sandbox) ---
112
+ if (isAbsolute(untrustedPath)) {
113
+ throw new Error('Absolute paths are not allowed');
114
+ }
115
+
116
+ // --- normalize and resolve ---
117
+ const normalized = normalize(untrustedPath);
118
+
119
+ // After normalization, check again for traversal (handles edge cases)
120
+ if (normalized.startsWith('..') || normalized.includes('/..') || normalized.includes('\\..')) {
121
+ throw new Error('Path traversal detected');
122
+ }
123
+
124
+ const resolved = resolve(SANDBOX_ROOT, normalized);
125
+
126
+ // --- boundary check (pre-symlink) ---
127
+ if (!resolved.startsWith(SANDBOX_ROOT + '/') && resolved !== SANDBOX_ROOT) {
128
+ throw new Error('Path traversal detected: escapes sandbox');
129
+ }
130
+
131
+ // --- symlink resolution ---
132
+ // Resolve the real sandbox root (handles /tmp → /private/tmp on macOS)
133
+ const realRoot = await getResolvedSandboxRoot();
134
+
135
+ // If the file/directory exists, resolve symlinks and re-check
136
+ try {
137
+ const realPath = await fs.realpath(resolved);
138
+ if (!realPath.startsWith(realRoot + '/') && realPath !== realRoot) {
139
+ throw new Error('Path traversal detected: symlink escapes sandbox');
140
+ }
141
+ return realPath;
142
+ } catch (err) {
143
+ if (err.code === 'ENOENT') {
144
+ // File doesn't exist yet (e.g., for writes) — check parent exists and is safe
145
+ const parentResolved = resolve(resolved, '..');
146
+ try {
147
+ const parentReal = await fs.realpath(parentResolved);
148
+ if (!parentReal.startsWith(realRoot + '/') && parentReal !== realRoot) {
149
+ throw new Error('Path traversal detected: parent symlink escapes sandbox');
150
+ }
151
+ } catch (parentErr) {
152
+ if (parentErr.code === 'ENOENT') {
153
+ // Parent doesn't exist either — that's OK for mkdir -p style usage
154
+ // The resolved path already passed the prefix check above
155
+ return resolved;
156
+ }
157
+ throw parentErr;
158
+ }
159
+ return resolved;
160
+ }
161
+ // Re-throw symlink escape errors
162
+ if (err.message.includes('Path traversal detected')) {
163
+ throw err;
164
+ }
165
+ throw err;
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Safely read a file from within the sandbox.
171
+ *
172
+ * @param {string} filePath - Relative path within SANDBOX_ROOT
173
+ * @param {Object} [options] - Read options
174
+ * @param {string} [options.encoding='utf-8'] - File encoding
175
+ * @param {number} [options.maxSize] - Maximum file size in bytes (default: MAX_FILE_SIZE)
176
+ * @returns {Promise<string|Buffer>} File contents
177
+ * @throws {Error} On path violation or oversized file
178
+ */
179
+ export async function safeFileRead(filePath, options = {}) {
180
+ const safePath = await validatePath(filePath);
181
+ const maxSize = options.maxSize ?? MAX_FILE_SIZE;
182
+ const encoding = options.encoding ?? 'utf-8';
183
+
184
+ // Check file size before reading to prevent memory DoS
185
+ const stat = await fs.stat(safePath);
186
+ if (stat.size > maxSize) {
187
+ throw new Error(`File size ${stat.size} exceeds limit of ${maxSize} bytes`);
188
+ }
189
+
190
+ return fs.readFile(safePath, { encoding });
191
+ }
192
+
193
+ /**
194
+ * Safely write a file within the sandbox.
195
+ *
196
+ * @param {string} filePath - Relative path within SANDBOX_ROOT
197
+ * @param {string|Buffer} data - Data to write
198
+ * @param {Object} [options] - Write options
199
+ * @param {string} [options.encoding='utf-8'] - File encoding
200
+ * @param {number} [options.maxSize] - Maximum data size in bytes (default: MAX_FILE_SIZE)
201
+ * @returns {Promise<void>}
202
+ * @throws {Error} On path violation or oversized data
203
+ */
204
+ export async function safeFileWrite(filePath, data, options = {}) {
205
+ const maxSize = options.maxSize ?? MAX_FILE_SIZE;
206
+ const encoding = options.encoding ?? 'utf-8';
207
+
208
+ // Check data size before writing
209
+ const dataSize = Buffer.byteLength(data);
210
+ if (dataSize > maxSize) {
211
+ throw new Error(`Data size ${dataSize} exceeds limit of ${maxSize} bytes`);
212
+ }
213
+
214
+ const safePath = await validatePath(filePath);
215
+ await fs.writeFile(safePath, data, { encoding });
216
+ }
217
+
218
+ /**
219
+ * Safely create a directory within the sandbox.
220
+ *
221
+ * @param {string} dirPath - Relative path within SANDBOX_ROOT
222
+ * @param {Object} [options] - mkdir options
223
+ * @param {boolean} [options.recursive=true] - Create parent directories
224
+ * @returns {Promise<string|undefined>} The first directory path created, or undefined
225
+ * @throws {Error} On path violation
226
+ */
227
+ export async function safeMkdir(dirPath, options = {}) {
228
+ const safePath = await validatePath(dirPath);
229
+ return fs.mkdir(safePath, { recursive: options.recursive ?? true });
230
+ }
231
+
232
+ /**
233
+ * Safely stat a file within the sandbox.
234
+ *
235
+ * @param {string} filePath - Relative path within SANDBOX_ROOT
236
+ * @returns {Promise<import('fs').Stats>} File stats
237
+ * @throws {Error} On path violation
238
+ */
239
+ export async function safeStat(filePath) {
240
+ const safePath = await validatePath(filePath);
241
+ return fs.stat(safePath);
242
+ }
243
+
244
+ /**
245
+ * Safely list directory contents within the sandbox.
246
+ *
247
+ * @param {string} dirPath - Relative path within SANDBOX_ROOT
248
+ * @returns {Promise<string[]>} Directory entries
249
+ * @throws {Error} On path violation
250
+ */
251
+ export async function safeReaddir(dirPath) {
252
+ const safePath = await validatePath(dirPath);
253
+ return fs.readdir(safePath);
254
+ }
255
+
256
+ /**
257
+ * Safely delete a file within the sandbox.
258
+ *
259
+ * @param {string} filePath - Relative path within SANDBOX_ROOT
260
+ * @returns {Promise<void>}
261
+ * @throws {Error} On path violation
262
+ */
263
+ export async function safeUnlink(filePath) {
264
+ const safePath = await validatePath(filePath);
265
+ return fs.unlink(safePath);
266
+ }
267
+
268
+ /**
269
+ * Ensure the sandbox root directory exists.
270
+ *
271
+ * @returns {Promise<void>}
272
+ */
273
+ export async function ensureSandboxRoot() {
274
+ await fs.mkdir(SANDBOX_ROOT, { recursive: true });
275
+ }
276
+
277
+ // ============================================================================
278
+ // Code Injection Defense Layer
279
+ // ============================================================================
280
+
281
+ /**
282
+ * Dangerous code patterns that indicate injection attempts.
283
+ * Each entry has a regex pattern and a human-readable description.
284
+ */
285
+ const DANGEROUS_CODE_PATTERNS = [
286
+ // Function constructor variants
287
+ { pattern: /\bFunction\s*\(/, desc: 'Function constructor call' },
288
+ { pattern: /\bnew\s+Function\b/, desc: 'new Function constructor' },
289
+ { pattern: /\bFunction\s*\[/, desc: 'Function bracket access' },
290
+ { pattern: /\bFunction\.prototype\b/, desc: 'Function.prototype access' },
291
+ { pattern: /\bFunction\.constructor\b/, desc: 'Function.constructor access' },
292
+ { pattern: /=\s*Function\b/, desc: 'Function constructor assignment' },
293
+
294
+ // eval variants
295
+ { pattern: /\beval\s*\(/, desc: 'eval() call' },
296
+ { pattern: /\beval\s*\[/, desc: 'eval bracket access' },
297
+ { pattern: /\(['"]eval['"]\)/, desc: 'indirect eval reference' },
298
+
299
+ // Constructor escape
300
+ { pattern: /\.constructor\s*\(/, desc: 'constructor invocation' },
301
+ { pattern: /\.constructor\s*\[/, desc: 'constructor bracket access' },
302
+ { pattern: /\['constructor'\]/, desc: 'constructor string access' },
303
+ { pattern: /\["constructor"\]/, desc: 'constructor string access (double quotes)' },
304
+ { pattern: /\[`constructor`\]/, desc: 'constructor template literal access' },
305
+
306
+ // Prototype pollution
307
+ { pattern: /__proto__/, desc: '__proto__ access' },
308
+ { pattern: /\.prototype\s*\[/, desc: 'prototype bracket access' },
309
+ { pattern: /\.prototype\.constructor/, desc: 'prototype.constructor chain' },
310
+ { pattern: /Object\s*\.\s*setPrototypeOf/, desc: 'Object.setPrototypeOf' },
311
+ { pattern: /Object\s*\.\s*getPrototypeOf/, desc: 'Object.getPrototypeOf' },
312
+ { pattern: /Object\s*\.\s*defineProperty/, desc: 'Object.defineProperty' },
313
+ { pattern: /Object\s*\.\s*defineProperties/, desc: 'Object.defineProperties' },
314
+ { pattern: /Reflect\s*\.\s*setPrototypeOf/, desc: 'Reflect.setPrototypeOf' },
315
+ { pattern: /Reflect\s*\.\s*defineProperty/, desc: 'Reflect.defineProperty' },
316
+ { pattern: /Reflect\s*\.\s*construct/, desc: 'Reflect.construct' },
317
+
318
+ // require/import injection
319
+ { pattern: /\brequire\s*\(/, desc: 'require() call' },
320
+ { pattern: /\bimport\s*\(/, desc: 'dynamic import()' },
321
+ { pattern: /\bimport\.meta\b/, desc: 'import.meta access' },
322
+
323
+ // Process/global access
324
+ { pattern: /\bprocess\s*\./, desc: 'process object access' },
325
+ { pattern: /\bprocess\s*\[/, desc: 'process bracket access' },
326
+ { pattern: /\bglobalThis\b/, desc: 'globalThis access' },
327
+
328
+ // Dangerous object access
329
+ { pattern: /\bnew\s+Proxy\b/, desc: 'new Proxy constructor' },
330
+ { pattern: /\bWeakRef\s*\(/, desc: 'WeakRef constructor' },
331
+ { pattern: /\bFinalizationRegistry\b/, desc: 'FinalizationRegistry access' },
332
+
333
+ // WebAssembly
334
+ { pattern: /\bWebAssembly\b/, desc: 'WebAssembly access' },
335
+
336
+ // SharedArrayBuffer (Spectre-class attacks)
337
+ { pattern: /\bSharedArrayBuffer\b/, desc: 'SharedArrayBuffer access' },
338
+ { pattern: /\bAtomics\b/, desc: 'Atomics access' },
339
+
340
+ // Indirect eval via various global accessors
341
+ { pattern: /\(0,\s*eval\)/, desc: 'indirect eval via comma operator' },
342
+ { pattern: /\bthis\s*\.\s*constructor/, desc: 'this.constructor access' },
343
+ { pattern: /\bthis\s*\[\s*['"`]constructor/, desc: 'this["constructor"] access' },
344
+
345
+ // String-based code execution
346
+ { pattern: /\bsetTimeout\s*\(\s*['"`]/, desc: 'setTimeout with string code' },
347
+ { pattern: /\bsetInterval\s*\(\s*['"`]/, desc: 'setInterval with string code' },
348
+ { pattern: /\bsetTimeout\b/, desc: 'setTimeout access' },
349
+ { pattern: /\bsetInterval\b/, desc: 'setInterval access' },
350
+ { pattern: /\bsetImmediate\b/, desc: 'setImmediate access' },
351
+
352
+ // Symbol manipulation for sandbox escape
353
+ { pattern: /Symbol\s*\.\s*unscopables/, desc: 'Symbol.unscopables manipulation' },
354
+ { pattern: /Symbol\s*\.\s*hasInstance/, desc: 'Symbol.hasInstance manipulation' },
355
+ { pattern: /Symbol\s*\.\s*toPrimitive/, desc: 'Symbol.toPrimitive manipulation' },
356
+ ];
357
+
358
+ /** @type {number} Maximum allowed code length */
359
+ const MAX_CODE_LENGTH = 100000;
360
+
361
+ /**
362
+ * Validate code string against dangerous injection patterns
363
+ * @param {string} codeString - Code to validate
364
+ * @returns {{ valid: boolean, violations: string[] }} Validation result
365
+ */
366
+ export function validateCodeInjection(codeString) {
367
+ if (typeof codeString !== 'string') {
368
+ return { valid: false, violations: ['Code must be a string'] };
369
+ }
370
+
371
+ if (codeString.length > MAX_CODE_LENGTH) {
372
+ return { valid: false, violations: [`Code exceeds maximum length of ${MAX_CODE_LENGTH}`] };
373
+ }
374
+
375
+ const violations = [];
376
+
377
+ for (const { pattern, desc } of DANGEROUS_CODE_PATTERNS) {
378
+ if (pattern.test(codeString)) {
379
+ violations.push(`Blocked: ${desc}`);
380
+ }
381
+ }
382
+
383
+ return {
384
+ valid: violations.length === 0,
385
+ violations,
386
+ };
387
+ }
388
+
389
+ /**
390
+ * Validate a function by converting to string and checking patterns
391
+ * @param {Function} fn - Function to validate
392
+ * @returns {{ valid: boolean, violations: string[] }} Validation result
393
+ */
394
+ export function validateFunction(fn) {
395
+ if (typeof fn !== 'function') {
396
+ return { valid: false, violations: ['Input must be a function'] };
397
+ }
398
+
399
+ return validateCodeInjection(fn.toString());
400
+ }
401
+
402
+ /**
403
+ * Create a hardened execution context with all dangerous
404
+ * constructors and functions neutralized
405
+ * @returns {Object} Hardened context (frozen)
406
+ */
407
+ export function createHardenedContext() {
408
+ const context = Object.create(null);
409
+
410
+ // Safe globals
411
+ context.Math = Math;
412
+ context.JSON = JSON;
413
+ context.Array = Array;
414
+ context.String = String;
415
+ context.Object = Object;
416
+ context.Number = Number;
417
+ context.Boolean = Boolean;
418
+ context.Date = Date;
419
+ context.RegExp = RegExp;
420
+ context.Map = Map;
421
+ context.Set = Set;
422
+ context.parseInt = parseInt;
423
+ context.parseFloat = parseFloat;
424
+ context.isNaN = isNaN;
425
+ context.isFinite = isFinite;
426
+ context.encodeURI = encodeURI;
427
+ context.decodeURI = decodeURI;
428
+ context.encodeURIComponent = encodeURIComponent;
429
+ context.decodeURIComponent = decodeURIComponent;
430
+
431
+ // Disable dangerous constructors
432
+ context.Function = undefined;
433
+ context.eval = undefined;
434
+ context.constructor = undefined;
435
+
436
+ // Disable process/global/require
437
+ context.process = undefined;
438
+ context.global = undefined;
439
+ context.globalThis = undefined;
440
+ context.require = undefined;
441
+
442
+ // Disable timers (DoS prevention)
443
+ context.setTimeout = undefined;
444
+ context.setInterval = undefined;
445
+ context.setImmediate = undefined;
446
+ context.clearTimeout = undefined;
447
+ context.clearInterval = undefined;
448
+ context.clearImmediate = undefined;
449
+
450
+ // Disable dangerous APIs
451
+ context.Proxy = undefined;
452
+ context.Reflect = undefined;
453
+ context.WebAssembly = undefined;
454
+ context.SharedArrayBuffer = undefined;
455
+ context.Atomics = undefined;
456
+ context.WeakRef = undefined;
457
+ context.FinalizationRegistry = undefined;
458
+ context.Buffer = undefined;
459
+
460
+ // Safe console (write-only logging)
461
+ context.console = Object.freeze({
462
+ log: (...args) => console.log('[Sandbox]', ...args),
463
+ warn: (...args) => console.warn('[Sandbox]', ...args),
464
+ error: (...args) => console.error('[Sandbox]', ...args),
465
+ info: (...args) => console.info('[Sandbox]', ...args),
466
+ });
467
+
468
+ return Object.freeze(context);
469
+ }
470
+
471
+ /**
472
+ * Create a SafeFunction proxy that rejects all construction
473
+ * and execution attempts. Used as a drop-in replacement for
474
+ * the Function constructor in sandboxed contexts.
475
+ * @returns {Proxy} SafeFunction proxy
476
+ */
477
+ export function createSafeFunctionProxy() {
478
+ const handler = {
479
+ construct() {
480
+ throw new Error(
481
+ 'SecurityError: Function constructor is disabled in sandbox'
482
+ );
483
+ },
484
+ apply() {
485
+ throw new Error(
486
+ 'SecurityError: Function execution is disabled in sandbox'
487
+ );
488
+ },
489
+ get(target, prop) {
490
+ if (prop === 'constructor' || prop === 'prototype' || prop === '__proto__') {
491
+ return undefined;
492
+ }
493
+ if (prop === Symbol.hasInstance) {
494
+ return () => false;
495
+ }
496
+ return undefined;
497
+ },
498
+ set() {
499
+ throw new Error(
500
+ 'SecurityError: Cannot modify SafeFunction proxy'
501
+ );
502
+ },
503
+ defineProperty() {
504
+ throw new Error(
505
+ 'SecurityError: Cannot define properties on SafeFunction proxy'
506
+ );
507
+ },
508
+ deleteProperty() {
509
+ throw new Error(
510
+ 'SecurityError: Cannot delete properties on SafeFunction proxy'
511
+ );
512
+ },
513
+ getPrototypeOf() {
514
+ return null;
515
+ },
516
+ setPrototypeOf() {
517
+ throw new Error(
518
+ 'SecurityError: Cannot set prototype on SafeFunction proxy'
519
+ );
520
+ },
521
+ };
522
+
523
+ // Use arrow function (no .prototype property) with null prototype
524
+ // to satisfy Proxy invariants for non-configurable properties
525
+ const target = () => {
526
+ throw new Error('SecurityError: SafeFunction cannot be called');
527
+ };
528
+ Object.setPrototypeOf(target, null);
529
+ Object.freeze(target);
530
+
531
+ return new Proxy(target, handler);
532
+ }
533
+
534
+ /**
535
+ * Log an injection attempt with full context
536
+ * @param {string} code - The code that was blocked (truncated)
537
+ * @param {string[]} violations - List of violations detected
538
+ * @param {object[]} log - Array to append the attempt to
539
+ */
540
+ export function logInjectionAttempt(code, violations, log = []) {
541
+ const truncated = typeof code === 'string' ? code.substring(0, 500) : '';
542
+ let hash = 0;
543
+ for (let i = 0; i < truncated.length; i++) {
544
+ hash = ((hash << 5) - hash + truncated.charCodeAt(i)) | 0;
545
+ }
546
+
547
+ const attempt = {
548
+ code: truncated,
549
+ violations,
550
+ timestamp: new Date(),
551
+ codeHash: hash.toString(16),
552
+ };
553
+
554
+ log.push(attempt);
555
+
556
+ console.warn(
557
+ `[EffectExecutor] INJECTION ATTEMPT BLOCKED:\n` +
558
+ ` Violations: ${violations.join(', ')}\n` +
559
+ ` Code hash: ${attempt.codeHash}\n` +
560
+ ` Timestamp: ${attempt.timestamp.toISOString()}`
561
+ );
562
+
563
+ return attempt;
564
+ }
565
+
566
+ /**
567
+ * Execute a function with full code injection validation and
568
+ * hardened context. Combines validation + context creation + execution.
569
+ * @param {Function} effect - Effect function to execute
570
+ * @param {Object} [context={}] - Execution context (event, store, delta)
571
+ * @param {Object} [options={}] - Options
572
+ * @param {number} [options.timeout=30000] - Execution timeout in ms
573
+ * @param {boolean} [options.logAttempts=true] - Whether to log blocked attempts
574
+ * @returns {Promise<Object>} Execution result
575
+ */
576
+ export async function executeHardened(effect, context = {}, options = {}) {
577
+ const timeout = options.timeout ?? 30000;
578
+ const startTime = Date.now();
579
+ const injectionLog = options._injectionLog ?? [];
580
+
581
+ // Step 1: Validate the function code
582
+ const validation = validateFunction(effect);
583
+
584
+ if (!validation.valid) {
585
+ if (options.logAttempts !== false) {
586
+ logInjectionAttempt(effect.toString(), validation.violations, injectionLog);
587
+ }
588
+
589
+ return {
590
+ success: false,
591
+ error: `SecurityError: Code injection detected - ${validation.violations.join('; ')}`,
592
+ violations: validation.violations,
593
+ duration: Date.now() - startTime,
594
+ blocked: true,
595
+ };
596
+ }
597
+
598
+ // Step 2: Create hardened context
599
+ const hardenedContext = createHardenedContext();
600
+
601
+ // Step 3: Execute with timeout
602
+ try {
603
+ const timeoutPromise = new Promise((_, reject) => {
604
+ const timer = setTimeout(() => {
605
+ reject(new Error(`Execution timeout after ${timeout}ms`));
606
+ }, timeout);
607
+ if (timer.unref) timer.unref();
608
+ });
609
+
610
+ const executionPromise = Promise.resolve().then(() => {
611
+ return effect.call(hardenedContext, context);
612
+ });
613
+
614
+ const result = await Promise.race([executionPromise, timeoutPromise]);
615
+
616
+ return {
617
+ success: true,
618
+ result,
619
+ duration: Date.now() - startTime,
620
+ blocked: false,
621
+ };
622
+ } catch (error) {
623
+ return {
624
+ success: false,
625
+ error: error.message,
626
+ duration: Date.now() - startTime,
627
+ blocked: false,
628
+ };
629
+ }
630
+ }