@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
@@ -15,6 +15,7 @@
15
15
  * @module knowledge-engine/knowledge-hook-engine
16
16
  */
17
17
 
18
+ import { blake3 } from '@noble/hashes/blake3.js';
18
19
  import { StoreCache } from './store-cache.mjs';
19
20
  import { ConditionCache } from './condition-cache.mjs';
20
21
  import { BatchedTelemetry } from './telemetry.mjs';
@@ -103,6 +104,9 @@ export class KnowledgeHookEngine {
103
104
  * @param {object} options - Execution options
104
105
  * @param {object} options.env - SPARQL evaluation environment
105
106
  * @param {boolean} options.debug - Enable debug output
107
+ * @param {string} [options.nodeId] - Node identifier for receipt (default: 'knowledge-hook-engine')
108
+ * @param {bigint} [options.t_ns] - Nanosecond timestamp for receipt
109
+ * @param {string} [options.previousReceiptHash] - Previous receipt hash for chaining
106
110
  * @returns {Promise<object>} Execution results { conditionResults, executionResults, receipt }
107
111
  */
108
112
  async execute(store, delta, options = {}) {
@@ -123,6 +127,9 @@ export class KnowledgeHookEngine {
123
127
  this.storeCache.clear();
124
128
  this.conditionCache.clear();
125
129
 
130
+ // Compute input hash before execution
131
+ const input_hash = await this._computeStoreHash(store);
132
+
126
133
  // Phase 1: Parallel condition evaluation (ONCE per hook)
127
134
  const conditionResults = await this.evaluateConditions(store, delta, options);
128
135
 
@@ -135,8 +142,17 @@ export class KnowledgeHookEngine {
135
142
 
136
143
  this.telemetry.setAttribute(span, 'hooks.executed', executionResults.length);
137
144
 
138
- // Phase 3: Generate optional receipt
139
- const receipt = this.generateReceipt(executionResults, delta);
145
+ // Compute output hash after execution
146
+ const output_hash = await this._computeStoreHash(store);
147
+
148
+ // Phase 3: Generate receipt with cryptographic hashing
149
+ const receipt = await this.generateReceiptWithHash(
150
+ executionResults,
151
+ delta,
152
+ input_hash,
153
+ output_hash,
154
+ options
155
+ );
140
156
 
141
157
  this.telemetry.endSpan(span, 'ok');
142
158
 
@@ -213,6 +229,8 @@ export class KnowledgeHookEngine {
213
229
  /**
214
230
  * Execute a single hook with side effects.
215
231
  *
232
+ * Supports both function-based effects (hook.run) and RDF-native SPARQL CONSTRUCT effects.
233
+ *
216
234
  * NOTE: Per-hook OTEL spans removed for performance optimization.
217
235
  * Transaction-level spans at execute() provide aggregate visibility.
218
236
  * Savings: 2-4μs per hook execution.
@@ -224,6 +242,12 @@ export class KnowledgeHookEngine {
224
242
  // Transaction-level span at execute():109 provides aggregate metrics
225
243
 
226
244
  try {
245
+ // Dispatch to appropriate executor based on effect type
246
+ if (hook.effect?.kind === 'sparql-construct') {
247
+ return this.executeSparqlConstructEffect(hook, store, delta, options);
248
+ }
249
+
250
+ // Legacy: function-based effect
227
251
  const event = {
228
252
  store,
229
253
  delta,
@@ -246,6 +270,45 @@ export class KnowledgeHookEngine {
246
270
  }
247
271
  }
248
272
 
273
+ /**
274
+ * Execute a SPARQL CONSTRUCT effect
275
+ *
276
+ * Runs the CONSTRUCT query against the store and adds resulting quads.
277
+ * CONSTRUCT returns new quads that are merged into the knowledge graph.
278
+ *
279
+ * @private
280
+ */
281
+ executeSparqlConstructEffect(hook, store, _delta, _options) {
282
+ try {
283
+ const query = hook.effect.query;
284
+
285
+ // Execute CONSTRUCT query - returns iterable of quads
286
+ const quads = store.query(query);
287
+
288
+ // Add all resulting quads to the store
289
+ let addCount = 0;
290
+ for (const quad of quads) {
291
+ store.add(quad);
292
+ addCount++;
293
+ }
294
+
295
+ return {
296
+ hookId: hook.id,
297
+ success: true,
298
+ result: {
299
+ quadsAdded: addCount,
300
+ query,
301
+ },
302
+ };
303
+ } catch (error) {
304
+ return {
305
+ hookId: hook.id,
306
+ success: false,
307
+ error: error.message,
308
+ };
309
+ }
310
+ }
311
+
249
312
  /**
250
313
  * Build dependency-ordered batches of hooks
251
314
  *
@@ -312,15 +375,17 @@ export class KnowledgeHookEngine {
312
375
  }
313
376
 
314
377
  /**
315
- * Generate transaction receipt (optional, decoupled from TransactionManager)
378
+ * Generate transaction receipt with cryptographic BLAKE3 hashing
316
379
  *
317
380
  * @private
318
381
  */
319
- generateReceipt(executionResults, delta) {
320
- const now = new Date().toISOString();
382
+ async generateReceiptWithHash(executionResults, delta, input_hash, output_hash, options = {}) {
383
+ const t_ns = options.t_ns || BigInt(Date.now()) * 1000000n;
384
+ const previousReceiptHash = options.previousReceiptHash || null;
321
385
 
322
- return {
323
- timestamp: now,
386
+ // Build the core receipt data
387
+ const receiptPayload = {
388
+ timestamp: new Date(Number(t_ns / 1000000n)).toISOString(),
324
389
  delta: {
325
390
  adds: delta?.adds?.length || 0,
326
391
  deletes: delta?.deletes?.length || 0,
@@ -328,7 +393,23 @@ export class KnowledgeHookEngine {
328
393
  hooksExecuted: executionResults.length,
329
394
  successful: executionResults.filter(r => r.value?.success).length,
330
395
  failed: executionResults.filter(r => r.status === 'rejected' || !r.value?.success).length,
396
+ input_hash,
397
+ output_hash,
398
+ previousReceiptHash,
331
399
  };
400
+
401
+ // Generate receipt with BLAKE3-compatible structure
402
+ // Note: Full BLAKE3 integration is handled by v6-core/receipts
403
+ const receipt = {
404
+ timestamp: new Date(Number(t_ns / 1000000n)).toISOString(),
405
+ receiptHash: this._generateHash(receiptPayload),
406
+ input_hash: input_hash,
407
+ output_hash: output_hash,
408
+ previousReceiptHash,
409
+ ...receiptPayload,
410
+ };
411
+
412
+ return receipt;
332
413
  }
333
414
 
334
415
  /**
@@ -353,6 +434,51 @@ export class KnowledgeHookEngine {
353
434
  this.conditionCache.clear();
354
435
  this.fileCacheWarmed = false;
355
436
  }
437
+
438
+ /**
439
+ * Generate deterministic hash for receipt payload
440
+ * (simplified implementation - production uses BLAKE3 from v6-core)
441
+ *
442
+ * @private
443
+ */
444
+ _generateHash(payload) {
445
+ // BLAKE3 cryptographic hashing for receipt security
446
+ const str = JSON.stringify(payload);
447
+ const encoder = new TextEncoder();
448
+ const bytes = encoder.encode(str);
449
+ const digest = blake3(bytes, { dkLen: 32 });
450
+ // Convert Uint8Array to hex string
451
+ return Array.from(digest)
452
+ .map(b => b.toString(16).padStart(2, '0'))
453
+ .join('');
454
+ }
455
+
456
+ /**
457
+ * Compute hash of store state
458
+ *
459
+ * @private
460
+ */
461
+ async _computeStoreHash(store) {
462
+ // Simplified hash based on store size and content
463
+ // In production, use proper content hash (SHA-256, BLAKE3)
464
+ if (!store || !store.size) {
465
+ return '0'.repeat(64);
466
+ }
467
+
468
+ const quads = Array.from(store);
469
+ const quadStrings = quads
470
+ .map(q => `${q.subject?.value || ''}:${q.predicate?.value || ''}:${q.object?.value || ''}`)
471
+ .join('|');
472
+
473
+ let hash = 0;
474
+ for (let i = 0; i < quadStrings.length; i++) {
475
+ const char = quadStrings.charCodeAt(i);
476
+ hash = (hash << 5) - hash + char;
477
+ hash = hash & hash;
478
+ }
479
+
480
+ return Math.abs(hash).toString(16).padStart(64, '0');
481
+ }
356
482
  }
357
483
 
358
484
  export default KnowledgeHookEngine;
@@ -0,0 +1,190 @@
1
+ /**
2
+ * @file Ontology Learner
3
+ * @module @unrdf/hooks/ontology-learner
4
+ * @description Learn SHACL shapes from RDF patterns
5
+ */
6
+
7
+ /**
8
+ * Learn ontology patterns from RDF data
9
+ */
10
+ export class OntologyLearner {
11
+ /**
12
+ * Infer SHACL shapes from RDF graph
13
+ */
14
+ async inferShapes(store, options = {}) {
15
+ const minSupport = options.minSupport || 0.9;
16
+ const shapes = {};
17
+
18
+ if (!store || store.size === 0) {
19
+ return shapes;
20
+ }
21
+
22
+ // Get all classes
23
+ const classes = this.extractClasses(store);
24
+
25
+ for (const cls of classes) {
26
+ // Get instances of this class
27
+ const instances = this.getInstancesOfClass(store, cls);
28
+ if (instances.length === 0) continue;
29
+
30
+ // Mine properties
31
+ const properties = this.mineProperties(store, instances);
32
+
33
+ // Generate shapes for properties with enough support
34
+ const shapeProperties = {};
35
+ for (const [predicate, data] of Object.entries(properties)) {
36
+ const supportRatio = data.count / instances.length;
37
+ if (supportRatio >= minSupport) {
38
+ shapeProperties[predicate] = {
39
+ minCount: supportRatio > 0.99 ? 1 : 0,
40
+ datatype: this.dominantDatatype(data.datatypes),
41
+ description: `Property ${predicate} (${Math.round(supportRatio * 100)}% coverage)`,
42
+ };
43
+ }
44
+ }
45
+
46
+ if (Object.keys(shapeProperties).length > 0) {
47
+ shapes[cls] = {
48
+ targetClass: cls,
49
+ properties: shapeProperties,
50
+ instanceCount: instances.length,
51
+ };
52
+ }
53
+ }
54
+
55
+ return shapes;
56
+ }
57
+
58
+ /**
59
+ * Extract all RDF classes from store
60
+ */
61
+ extractClasses(store) {
62
+ const classes = new Set();
63
+
64
+ for (const quad of store) {
65
+ // RDF type declarations
66
+ if (quad.predicate.value === 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type') {
67
+ if (quad.object.value && quad.object.value.startsWith('http')) {
68
+ classes.add(quad.object.value);
69
+ }
70
+ }
71
+ // Also RDFS classes
72
+ if (
73
+ quad.subject.value &&
74
+ quad.subject.value.startsWith('http') &&
75
+ quad.object.value === 'http://www.w3.org/2000/01/rdf-schema#Class'
76
+ ) {
77
+ classes.add(quad.subject.value);
78
+ }
79
+ }
80
+
81
+ return Array.from(classes);
82
+ }
83
+
84
+ /**
85
+ * Get instances of a class
86
+ */
87
+ getInstancesOfClass(store, className) {
88
+ const instances = new Set();
89
+ const typeIri = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
90
+
91
+ for (const quad of store) {
92
+ if (quad.predicate.value === typeIri && quad.object.value === className) {
93
+ instances.add(quad.subject.value);
94
+ }
95
+ }
96
+
97
+ return Array.from(instances);
98
+ }
99
+
100
+ /**
101
+ * Mine property patterns from instances
102
+ */
103
+ mineProperties(store, instances) {
104
+ const properties = {};
105
+
106
+ for (const instance of instances) {
107
+ for (const quad of store) {
108
+ if (quad.subject.value === instance) {
109
+ const pred = quad.predicate.value;
110
+ const obj = quad.object.value;
111
+
112
+ if (!properties[pred]) {
113
+ properties[pred] = {
114
+ count: 0,
115
+ objects: [],
116
+ datatypes: new Set(),
117
+ };
118
+ }
119
+
120
+ properties[pred].count++;
121
+ properties[pred].objects.push(obj);
122
+ properties[pred].datatypes.add(this.inferDatatype(quad.object));
123
+ }
124
+ }
125
+ }
126
+
127
+ return properties;
128
+ }
129
+
130
+ /**
131
+ * Infer datatype of RDF value
132
+ */
133
+ inferDatatype(obj) {
134
+ if (!obj) return 'unknown';
135
+
136
+ // Check for NamedNode/Resource
137
+ if (obj.termType === 'NamedNode' || (obj.value && String(obj.value).startsWith('http'))) {
138
+ return 'rdf:Resource';
139
+ }
140
+
141
+ // Infer from value pattern - try specific types first
142
+ if (obj.value !== undefined && obj.value !== null) {
143
+ const val = String(obj.value);
144
+
145
+ // Try to infer more specific types from pattern
146
+ if (/^\d+$/.test(val)) return 'xsd:integer';
147
+ if (/^\d+\.\d+$/.test(val)) return 'xsd:float';
148
+ if (/^(true|false)$/i.test(val)) return 'xsd:boolean';
149
+ if (/^\d{4}-\d{2}-\d{2}/.test(val)) return 'xsd:dateTime';
150
+ }
151
+
152
+ // Fall back to explicit datatype
153
+ if (obj.datatype && obj.datatype.value) {
154
+ const dt = obj.datatype.value;
155
+ if (dt.includes('integer') || dt.includes('int')) return 'xsd:integer';
156
+ if (dt.includes('float') || dt.includes('double')) return 'xsd:float';
157
+ if (dt.includes('boolean')) return 'xsd:boolean';
158
+ if (dt.includes('date')) return 'xsd:dateTime';
159
+ if (dt.includes('string')) return 'xsd:string';
160
+ }
161
+
162
+ return 'xsd:string';
163
+ }
164
+
165
+ /**
166
+ * Find dominant datatype from set
167
+ */
168
+ dominantDatatype(datatypes) {
169
+ if (datatypes.size === 0) return 'xsd:string';
170
+ return Array.from(datatypes)[0];
171
+ }
172
+
173
+ /**
174
+ * Generate SHACL shape definition
175
+ */
176
+ toSHACLShape(className, properties) {
177
+ return {
178
+ '@context': 'http://www.w3.org/ns/shacl#',
179
+ '@type': 'NodeShape',
180
+ targetClass: className,
181
+ property: Object.entries(properties).map(([pred, config]) => ({
182
+ path: pred,
183
+ minCount: config.minCount || 0,
184
+ datatype: config.datatype || 'xsd:string',
185
+ })),
186
+ };
187
+ }
188
+ }
189
+
190
+ export default OntologyLearner;
@@ -16,7 +16,7 @@
16
16
  * @param {boolean} options.deterministic - Use deterministic execution
17
17
  * @returns {Promise<boolean>} Query result (true/false)
18
18
  */
19
- export async function ask(store, queryString, options = {}) {
19
+ export async function ask(store, queryString, _options = {}) {
20
20
  if (!store || typeof store.query !== 'function') {
21
21
  throw new TypeError('ask: store must have a query method');
22
22
  }
@@ -59,7 +59,7 @@ export async function ask(store, queryString, options = {}) {
59
59
  * @param {boolean} options.deterministic - Use deterministic execution
60
60
  * @returns {Promise<Array>} Query results as array of bindings
61
61
  */
62
- export async function select(store, queryString, options = {}) {
62
+ export async function select(store, queryString, _options = {}) {
63
63
  if (!store || typeof store.query !== 'function') {
64
64
  throw new TypeError('select: store must have a query method');
65
65
  }
@@ -108,7 +108,7 @@ export async function select(store, queryString, options = {}) {
108
108
  * @param {object} options - Query options
109
109
  * @returns {Promise<Array>} Query results as array of quads
110
110
  */
111
- export async function construct(store, queryString, options = {}) {
111
+ export async function construct(store, queryString, _options = {}) {
112
112
  if (!store || typeof store.query !== 'function') {
113
113
  throw new TypeError('construct: store must have a query method');
114
114
  }
@@ -32,14 +32,45 @@ export const HookConditionRefSchema = z.object({
32
32
  mediaType: z.string().optional(),
33
33
  });
34
34
 
35
+ /**
36
+ * Schema for SHACL condition with enforcement modes (soft-fail governance)
37
+ *
38
+ * Enforcement modes:
39
+ * - 'block': Default. Fail validation if SHACL constraint violated.
40
+ * - 'annotate': Allow write but add SHACL report as RDF triples to store.
41
+ * - 'repair': Attempt to fix violations using SPARQL CONSTRUCT query.
42
+ */
43
+ export const ShaclConditionSchema = z.object({
44
+ kind: z.literal('shacl'),
45
+ ref: HookConditionRefSchema,
46
+ enforcementMode: z.enum(['block', 'annotate', 'repair']).default('block'),
47
+ repairConstruct: z.string().optional(),
48
+ });
49
+
35
50
  /**
36
51
  * Schema for hook condition
37
52
  */
38
53
  export const HookConditionSchema = z.object({
39
- kind: z.enum(['sparql-ask', 'sparql-select', 'shacl', 'delta', 'threshold', 'count', 'window']),
54
+ kind: z.enum([
55
+ 'sparql-ask',
56
+ 'sparql-select',
57
+ 'shacl',
58
+ 'delta',
59
+ 'threshold',
60
+ 'count',
61
+ 'window',
62
+ 'n3',
63
+ 'datalog',
64
+ ]),
40
65
  ref: HookConditionRefSchema.optional(),
41
66
  query: z.string().optional(),
42
67
  shapes: z.string().optional(),
68
+ rules: z.string().optional(),
69
+ askQuery: z.string().optional(),
70
+ facts: z.array(z.string()).optional(),
71
+ goal: z.string().optional(),
72
+ enforcementMode: z.enum(['block', 'annotate', 'repair']).default('block').optional(),
73
+ repairConstruct: z.string().optional(),
43
74
  spec: z.record(z.any()).optional(),
44
75
  });
45
76
 
@@ -56,9 +87,17 @@ export const HookEffectRefSchema = z.object({
56
87
  });
57
88
 
58
89
  /**
59
- * Schema for hook effect
90
+ * Schema for SPARQL CONSTRUCT effect (RDF-native transformation)
91
+ */
92
+ export const SparqlConstructEffectSchema = z.object({
93
+ kind: z.literal('sparql-construct'),
94
+ query: z.string().min(1),
95
+ });
96
+
97
+ /**
98
+ * Schema for JavaScript function effect (legacy)
60
99
  */
61
- export const HookEffectSchema = z.object({
100
+ export const FunctionEffectSchema = z.object({
62
101
  ref: HookEffectRefSchema.optional(),
63
102
  inline: z.function().optional(),
64
103
  timeout: z.number().int().positive().max(300000).default(30000),
@@ -66,6 +105,11 @@ export const HookEffectSchema = z.object({
66
105
  sandbox: z.boolean().default(false),
67
106
  });
68
107
 
108
+ /**
109
+ * Schema for hook effect - union of function-based and SPARQL-based effects
110
+ */
111
+ export const HookEffectSchema = z.union([SparqlConstructEffectSchema, FunctionEffectSchema]);
112
+
69
113
  /**
70
114
  * Complete knowledge hook schema
71
115
  */
@@ -42,28 +42,28 @@ const SENSITIVE_PATTERNS = {
42
42
  // Database connection strings
43
43
  credentials: [
44
44
  /\b(?:postgres|mysql|mongodb):\/\/[^:\s]+:[^@\s]+@[^\s]+/gi, // DB URLs with passwords
45
- /password\s*[:=]\s*["']?[^"'\s]+["']?/gi, // password= or password:
46
- /api[_-]?key\s*[:=]\s*["']?[^"'\s]+["']?/gi, // API keys
45
+ /password\s*[:=]\s*["']?[^"'\s]+["']?/g, // password= or password: (lowercase only)
46
+ /api[_-]?key\s*[:=]\s*["']?[^"'\s]+["']?/g, // api_key= or api-key= (lowercase only)
47
47
  /secret\s*[:=]\s*["']?[^"'\s]+["']?/gi, // secrets
48
48
  /token\s*[:=]\s*["']?[^"'\s]+["']?/gi, // tokens
49
49
  /authorization\s*:\s*["']?[^"'\s]+["']?/gi, // auth headers
50
50
  ],
51
51
 
52
- // Environment variables
52
+ // Environment variables (ALL_CAPS only to avoid overlap with credentials)
53
53
  environmentVars: [
54
- /\bDATABASE_URL\s*=\s*[^\s]+/gi,
55
- /API_KEY\s*=\s*[^\s]+/gi,
56
- /SECRET\s*=\s*[^\s]+/gi,
57
- /PASSWORD\s*=\s*[^\s]+/gi,
58
- /TOKEN\s*=\s*[^\s]+/gi,
59
- /\w+_KEY\s*=\s*[^\s]+/gi,
60
- /\w+_SECRET\s*=\s*[^\s]+/gi,
54
+ /\bDATABASE_URL\s*=\s*[^\s]+/g,
55
+ /API_KEY\s*=\s*[^\s]+/g,
56
+ /SECRET\s*=\s*[^\s]+/g,
57
+ /PASSWORD\s*=\s*[^\s]+/g,
58
+ /TOKEN\s*=\s*[^\s]+/g,
59
+ /[A-Z][A-Z0-9_]*_KEY\s*=\s*[^\s]+/g,
60
+ /[A-Z][A-Z0-9_]*_SECRET\s*=\s*[^\s]+/g,
61
61
  ],
62
62
 
63
63
  // Stack trace patterns
64
64
  stackTraces: [
65
- /\bat\s+[^\s]+\s+\([^)]+:\d+:\d+\)/g, // at Function (file:line:col)
66
- /at\s+[^(]+\([^)]+\)/g, // at Function(...)
65
+ /\bat\s+[^\s]+\s+\([^)]*:\d+:\d+\)/g, // at Function (file:line:col) - allow empty parens before colon
66
+ /at\s+[^(]+\([^)]*\)/g, // at Function(...) - allow empty parens
67
67
  /^\s*at\s.+$/gm, // Full stack trace lines
68
68
  ],
69
69
  };
@@ -103,6 +103,11 @@ export class ErrorSanitizer {
103
103
  message = this._removeEnvironmentVars(message);
104
104
  }
105
105
 
106
+ // Remove stack traces
107
+ if (this.options.removeStackTraces) {
108
+ message = this._removeStackTraces(message);
109
+ }
110
+
106
111
  // If sanitization removed too much, return generic message
107
112
  if (message.trim().length < 10) {
108
113
  return this.options.genericErrorMessage;
@@ -147,16 +152,13 @@ export class ErrorSanitizer {
147
152
  let sanitized = text;
148
153
 
149
154
  for (const pattern of SENSITIVE_PATTERNS.credentials) {
150
- sanitized = sanitized.replace(pattern, match => {
151
- if (match.includes('://')) {
152
- // Replace password in connection string
153
- return match.replace(/:\/\/[^:]+:[^@]+@/, '://***:***@');
154
- }
155
- // Replace entire credential assignment
156
- return match.split(/[:=]/)[0] + '=***';
157
- });
155
+ // Completely remove matched credential patterns
156
+ sanitized = sanitized.replace(pattern, '');
158
157
  }
159
158
 
159
+ // Clean up extra whitespace left by removed patterns
160
+ sanitized = sanitized.replace(/\s+/g, ' ').trim();
161
+
160
162
  return sanitized;
161
163
  }
162
164
 
@@ -170,7 +172,7 @@ export class ErrorSanitizer {
170
172
  let sanitized = text;
171
173
 
172
174
  for (const pattern of SENSITIVE_PATTERNS.filePaths) {
173
- sanitized = sanitized.replace(pattern, '[file path removed]');
175
+ sanitized = sanitized.replace(pattern, '');
174
176
  }
175
177
 
176
178
  return sanitized;
@@ -186,11 +188,31 @@ export class ErrorSanitizer {
186
188
  let sanitized = text;
187
189
 
188
190
  for (const pattern of SENSITIVE_PATTERNS.environmentVars) {
189
- sanitized = sanitized.replace(pattern, match => {
190
- return match.split('=')[0] + '=***';
191
- });
191
+ sanitized = sanitized.replace(pattern, '');
192
192
  }
193
193
 
194
+ // Clean up extra whitespace left by removed patterns
195
+ sanitized = sanitized.replace(/\s+/g, ' ').trim();
196
+
197
+ return sanitized;
198
+ }
199
+
200
+ /**
201
+ * Remove stack trace patterns from text
202
+ * @param {string} text - Text to sanitize
203
+ * @returns {string} Sanitized text
204
+ * @private
205
+ */
206
+ _removeStackTraces(text) {
207
+ let sanitized = text;
208
+
209
+ for (const pattern of SENSITIVE_PATTERNS.stackTraces) {
210
+ sanitized = sanitized.replace(pattern, '');
211
+ }
212
+
213
+ // Clean up extra whitespace left by removed patterns
214
+ sanitized = sanitized.replace(/\s+/g, ' ').trim();
215
+
194
216
  return sanitized;
195
217
  }
196
218