autotel 2.26.0 → 2.26.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.
Files changed (117) hide show
  1. package/dist/attribute-redacting-processor.cjs +14 -6
  2. package/dist/attribute-redacting-processor.d.cts +63 -1
  3. package/dist/attribute-redacting-processor.d.ts +63 -1
  4. package/dist/attribute-redacting-processor.js +1 -1
  5. package/dist/attributes.cjs +21 -21
  6. package/dist/attributes.js +2 -2
  7. package/dist/auto.cjs +8 -8
  8. package/dist/auto.js +6 -6
  9. package/dist/{chunk-RUD7KS4R.js → chunk-3SDILILG.js} +3 -3
  10. package/dist/{chunk-RUD7KS4R.js.map → chunk-3SDILILG.js.map} +1 -1
  11. package/dist/{chunk-B33XPEKY.js → chunk-55ER2KD5.js} +4 -4
  12. package/dist/chunk-55ER2KD5.js.map +1 -0
  13. package/dist/{chunk-UJJPTSEI.cjs → chunk-563EL6O6.cjs} +81 -14
  14. package/dist/chunk-563EL6O6.cjs.map +1 -0
  15. package/dist/{chunk-TS7IHIRW.cjs → chunk-6YGUN7IY.cjs} +5 -5
  16. package/dist/{chunk-TS7IHIRW.cjs.map → chunk-6YGUN7IY.cjs.map} +1 -1
  17. package/dist/{chunk-XDKK53OL.js → chunk-A4E5AQFK.js} +3 -3
  18. package/dist/{chunk-XDKK53OL.js.map → chunk-A4E5AQFK.js.map} +1 -1
  19. package/dist/{chunk-WAB4CHBU.js → chunk-BJ2XPN77.js} +3 -3
  20. package/dist/{chunk-WAB4CHBU.js.map → chunk-BJ2XPN77.js.map} +1 -1
  21. package/dist/{chunk-KZEC4CHV.cjs → chunk-CEAQK2QY.cjs} +5 -5
  22. package/dist/{chunk-KZEC4CHV.cjs.map → chunk-CEAQK2QY.cjs.map} +1 -1
  23. package/dist/chunk-CMNGGTQL.cjs +349 -0
  24. package/dist/chunk-CMNGGTQL.cjs.map +1 -0
  25. package/dist/{chunk-VYA6QDNA.js → chunk-DPSA4QLA.js} +4 -2
  26. package/dist/chunk-DPSA4QLA.js.map +1 -0
  27. package/dist/{chunk-M4US3P4K.js → chunk-ER43K7ES.js} +3 -3
  28. package/dist/{chunk-M4US3P4K.js.map → chunk-ER43K7ES.js.map} +1 -1
  29. package/dist/{chunk-AZ24DJAG.cjs → chunk-FU6R566Y.cjs} +4 -4
  30. package/dist/chunk-FU6R566Y.cjs.map +1 -0
  31. package/dist/{chunk-4PTCDOZY.js → chunk-HPUGKUMZ.js} +4 -4
  32. package/dist/{chunk-4PTCDOZY.js.map → chunk-HPUGKUMZ.js.map} +1 -1
  33. package/dist/{chunk-XRBP4RYL.cjs → chunk-JKIMEPI2.cjs} +4 -4
  34. package/dist/{chunk-XRBP4RYL.cjs.map → chunk-JKIMEPI2.cjs.map} +1 -1
  35. package/dist/{chunk-N344PVE5.cjs → chunk-OBWXM4NN.cjs} +9 -9
  36. package/dist/{chunk-N344PVE5.cjs.map → chunk-OBWXM4NN.cjs.map} +1 -1
  37. package/dist/{chunk-OFPZULMQ.cjs → chunk-OC6X2VIN.cjs} +8 -8
  38. package/dist/{chunk-OFPZULMQ.cjs.map → chunk-OC6X2VIN.cjs.map} +1 -1
  39. package/dist/{chunk-GTD3NXOS.js → chunk-QC5MNKVF.js} +4 -4
  40. package/dist/{chunk-GTD3NXOS.js.map → chunk-QC5MNKVF.js.map} +1 -1
  41. package/dist/chunk-TDNKIHKT.js +341 -0
  42. package/dist/chunk-TDNKIHKT.js.map +1 -0
  43. package/dist/{chunk-DGPUZ6TE.js → chunk-U54FTVFH.js} +3 -3
  44. package/dist/{chunk-DGPUZ6TE.js.map → chunk-U54FTVFH.js.map} +1 -1
  45. package/dist/{chunk-ZJ5GXCOT.cjs → chunk-UTZR7P7E.cjs} +36 -36
  46. package/dist/{chunk-ZJ5GXCOT.cjs.map → chunk-UTZR7P7E.cjs.map} +1 -1
  47. package/dist/{chunk-7FIGORWI.cjs → chunk-VH77IPJN.cjs} +4 -2
  48. package/dist/chunk-VH77IPJN.cjs.map +1 -0
  49. package/dist/{chunk-EXOXDI5A.js → chunk-W35FVJBC.js} +73 -8
  50. package/dist/chunk-W35FVJBC.js.map +1 -0
  51. package/dist/{chunk-II7GFVAF.cjs → chunk-WZOKY3PW.cjs} +13 -13
  52. package/dist/{chunk-II7GFVAF.cjs.map → chunk-WZOKY3PW.cjs.map} +1 -1
  53. package/dist/{chunk-CMADDTHY.cjs → chunk-YEVCD6DR.cjs} +7 -7
  54. package/dist/{chunk-CMADDTHY.cjs.map → chunk-YEVCD6DR.cjs.map} +1 -1
  55. package/dist/{chunk-RXFZKLRQ.js → chunk-YN7USLHW.js} +3 -3
  56. package/dist/{chunk-RXFZKLRQ.js.map → chunk-YN7USLHW.js.map} +1 -1
  57. package/dist/decorators.cjs +7 -7
  58. package/dist/decorators.js +7 -7
  59. package/dist/event.cjs +10 -10
  60. package/dist/event.js +7 -7
  61. package/dist/functional.cjs +14 -14
  62. package/dist/functional.js +7 -7
  63. package/dist/index.cjs +214 -97
  64. package/dist/index.cjs.map +1 -1
  65. package/dist/index.d.cts +8 -2
  66. package/dist/index.d.ts +8 -2
  67. package/dist/index.js +141 -33
  68. package/dist/index.js.map +1 -1
  69. package/dist/{init-QSj7X6zU.d.cts → init-CMuTaFAV.d.cts} +26 -1
  70. package/dist/{init-FiR_glVc.d.ts → init-D6JfWEjL.d.ts} +26 -1
  71. package/dist/instrumentation.cjs +14 -14
  72. package/dist/instrumentation.js +6 -6
  73. package/dist/logger.cjs +8 -8
  74. package/dist/logger.js +1 -1
  75. package/dist/messaging.cjs +11 -11
  76. package/dist/messaging.js +8 -8
  77. package/dist/metric.cjs +1 -1
  78. package/dist/metric.js +1 -1
  79. package/dist/sampling.cjs +15 -15
  80. package/dist/sampling.js +2 -2
  81. package/dist/semantic-helpers.cjs +12 -12
  82. package/dist/semantic-helpers.js +8 -8
  83. package/dist/tail-sampling-processor.cjs +4 -4
  84. package/dist/tail-sampling-processor.js +3 -3
  85. package/dist/testing.cjs +1 -1
  86. package/dist/testing.js +1 -1
  87. package/dist/webhook.cjs +9 -8
  88. package/dist/webhook.cjs.map +1 -1
  89. package/dist/webhook.js +8 -7
  90. package/dist/webhook.js.map +1 -1
  91. package/dist/workflow-distributed.cjs +9 -9
  92. package/dist/workflow-distributed.js +7 -7
  93. package/dist/workflow.cjs +12 -12
  94. package/dist/workflow.js +8 -8
  95. package/dist/yaml-config.cjs +6 -6
  96. package/dist/yaml-config.d.cts +1 -1
  97. package/dist/yaml-config.d.ts +1 -1
  98. package/dist/yaml-config.js +3 -3
  99. package/package.json +1 -1
  100. package/src/attribute-redacting-processor.test.ts +81 -16
  101. package/src/attribute-redacting-processor.ts +278 -24
  102. package/src/autotel-logger.ts +2 -2
  103. package/src/index.ts +2 -1
  104. package/src/init.ts +117 -2
  105. package/src/request-logger.test.ts +266 -1
  106. package/src/request-logger.ts +115 -16
  107. package/src/structured-error.ts +54 -1
  108. package/dist/chunk-7FIGORWI.cjs.map +0 -1
  109. package/dist/chunk-AZ24DJAG.cjs.map +0 -1
  110. package/dist/chunk-B33XPEKY.js.map +0 -1
  111. package/dist/chunk-ELW34S4C.cjs +0 -173
  112. package/dist/chunk-ELW34S4C.cjs.map +0 -1
  113. package/dist/chunk-EXOXDI5A.js.map +0 -1
  114. package/dist/chunk-SNINLBEE.js +0 -167
  115. package/dist/chunk-SNINLBEE.js.map +0 -1
  116. package/dist/chunk-UJJPTSEI.cjs.map +0 -1
  117. package/dist/chunk-VYA6QDNA.js.map +0 -1
@@ -46,6 +46,11 @@ export type AttributeRedactorFn = (
46
46
  */
47
47
  export type AttributeRedactorPreset = 'default' | 'strict' | 'pci-dss';
48
48
 
49
+ /**
50
+ * Masker function type - receives the matched string and returns a masked version
51
+ */
52
+ export type MaskFn = (match: string) => string;
53
+
49
54
  /**
50
55
  * Value pattern configuration
51
56
  */
@@ -56,8 +61,15 @@ export interface ValuePatternConfig {
56
61
  pattern: RegExp;
57
62
  /** Custom replacement (default: uses global replacement) */
58
63
  replacement?: string;
64
+ /** Mask function for smart partial masking (overrides replacement) */
65
+ mask?: MaskFn;
59
66
  }
60
67
 
68
+ /**
69
+ * Built-in PII pattern names
70
+ */
71
+ export type BuiltinPatternName = keyof typeof builtinPatterns;
72
+
61
73
  /**
62
74
  * Attribute redactor configuration
63
75
  */
@@ -68,6 +80,15 @@ export interface AttributeRedactorConfig {
68
80
  /** Patterns to match against attribute values (redacts matched portion) */
69
81
  valuePatterns?: ValuePatternConfig[];
70
82
 
83
+ /** Dot-notation paths to redact (e.g. 'user.password', 'payment.card') */
84
+ paths?: string[];
85
+
86
+ /** Built-in PII patterns to enable. `true` enables all, `false` disables all, array selects specific ones. */
87
+ builtins?: boolean | BuiltinPatternName[];
88
+
89
+ /** Custom RegExp patterns for string-level redaction */
90
+ patterns?: RegExp[];
91
+
71
92
  /** Default replacement string (default: '[REDACTED]') */
72
93
  replacement?: string;
73
94
 
@@ -100,14 +121,112 @@ export const REDACTOR_PATTERNS = {
100
121
  /^(password|passwd|pwd|secret|token|api[_-]?key|auth|credential|private[_-]?key|authorization)$/i,
101
122
  } as const;
102
123
 
124
+ /**
125
+ * Built-in PII detection patterns with smart masking.
126
+ * Each builtin preserves just enough signal for debugging while scrubbing PII.
127
+ */
128
+ export const builtinPatterns = {
129
+ /** Credit card numbers → ****1111 (PCI DSS: last 4 allowed) */
130
+ creditCard: {
131
+ pattern: /\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/g,
132
+ mask: (m: string) => `****${m.replace(/[\s-]/g, '').slice(-4)}`,
133
+ },
134
+ /** Email addresses → a***@***.com */
135
+ email: {
136
+ pattern: /[\w.+-]+@[\w-]+\.[\w.]+/g,
137
+ mask: (m: string) => {
138
+ const at = m.indexOf('@');
139
+ if (at < 1) return '***@***';
140
+ const tld = m.slice(m.lastIndexOf('.'));
141
+ return `${m[0]}***@***${tld}`;
142
+ },
143
+ },
144
+ /** IPv4 addresses → ***.***.***.100 (last octet only) */
145
+ ipv4: {
146
+ pattern:
147
+ /\b(?!0\.0\.0\.0\b)(?!127\.0\.0\.1\b)\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g,
148
+ mask: (m: string) => `***.***.***.${m.split('.').pop()}`,
149
+ },
150
+ /** International phone numbers → +33******78 (country code + last 2 digits) */
151
+ phone: {
152
+ pattern:
153
+ /(?:\+\d{1,3}[\s.-]?)?\(?\d{1,4}\)?[\s.-]?\d{2,4}[\s.-]?\d{2,4}[\s.-]?\d{2,4}\b/g,
154
+ mask: (m: string) => {
155
+ const digits = m.replace(/[^\d]/g, '');
156
+ const hasPlus = m.startsWith('+');
157
+ if (hasPlus && digits.length > 4) {
158
+ const ccMatch = m.match(/^\+\d{1,3}/);
159
+ const cc = ccMatch ? ccMatch[0] : '+';
160
+ return `${cc}******${digits.slice(-2)}`;
161
+ }
162
+ if (digits.length > 2) {
163
+ return `${'*'.repeat(digits.length - 2)}${digits.slice(-2)}`;
164
+ }
165
+ return '***';
166
+ },
167
+ },
168
+ /** JWT tokens → eyJ***.*** */
169
+ jwt: {
170
+ pattern: /\beyJ[\w-]*\.[\w-]*\.[\w-]*\b/g,
171
+ mask: () => 'eyJ***.***',
172
+ },
173
+ /** Bearer tokens → Bearer *** */
174
+ bearer: {
175
+ pattern: /\bBearer\s+[\w\-.~+/]{8,}=*/gi,
176
+ mask: () => 'Bearer ***',
177
+ },
178
+ /** IBAN → FR76****189 (country + check digits + last 3) */
179
+ iban: {
180
+ pattern:
181
+ /\b[A-Z]{2}\d{2}[\s-]?[\dA-Z]{4}[\s-]?[\dA-Z]{4}[\s-]?[\dA-Z]{4}[\s-]?[\dA-Z]{0,4}[\s-]?[\dA-Z]{0,4}[\s-]?[\dA-Z]{0,4}\b/g,
182
+ mask: (m: string) => {
183
+ const clean = m.replace(/[\s-]/g, '');
184
+ return `${clean.slice(0, 4)}****${clean.slice(-3)}`;
185
+ },
186
+ },
187
+ } as const;
188
+
189
+ function cloneRegex(re: RegExp): RegExp {
190
+ return new RegExp(re.source, re.flags);
191
+ }
192
+
193
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
194
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
195
+ }
196
+
197
+ function toRegExp(value: unknown): RegExp | undefined {
198
+ if (value instanceof RegExp) return value;
199
+ if (typeof value === 'string') return new RegExp(value, 'g');
200
+ if (isPlainObject(value) && typeof value.source === 'string') {
201
+ const flags = typeof value.flags === 'string' ? value.flags : 'g';
202
+ return new RegExp(value.source, flags);
203
+ }
204
+ return undefined;
205
+ }
206
+
207
+ function toRegExpArray(value: unknown): RegExp[] | undefined {
208
+ if (!Array.isArray(value)) return undefined;
209
+ const out: RegExp[] = [];
210
+ for (const item of value) {
211
+ const re = toRegExp(item);
212
+ if (re) out.push(re);
213
+ }
214
+ return out.length > 0 ? out : [];
215
+ }
216
+
217
+ function builtinToValuePattern(name: BuiltinPatternName): ValuePatternConfig {
218
+ const b = builtinPatterns[name];
219
+ return { name, pattern: cloneRegex(b.pattern), mask: b.mask };
220
+ }
221
+
103
222
  /**
104
223
  * Default value patterns for the 'default' preset
105
224
  */
106
225
  const DEFAULT_VALUE_PATTERNS: ValuePatternConfig[] = [
107
- { name: 'email', pattern: REDACTOR_PATTERNS.email },
108
- { name: 'phone', pattern: REDACTOR_PATTERNS.phone },
226
+ builtinToValuePattern('email'),
227
+ builtinToValuePattern('phone'),
109
228
  { name: 'ssn', pattern: REDACTOR_PATTERNS.ssn },
110
- { name: 'creditCard', pattern: REDACTOR_PATTERNS.creditCard },
229
+ builtinToValuePattern('creditCard'),
111
230
  ];
112
231
 
113
232
  /**
@@ -118,61 +237,158 @@ export const REDACTOR_PRESETS: Record<
118
237
  AttributeRedactorConfig
119
238
  > = {
120
239
  /**
121
- * Default preset - covers common PII patterns
122
- * Detects: emails, phone numbers, SSNs, credit cards
240
+ * Default preset - covers common PII patterns with smart masking
241
+ * Detects: emails (a***@***.com), phone numbers, SSNs, credit cards (****1111)
123
242
  * Redacts keys: password, secret, token, apiKey, auth, credential
124
243
  */
125
244
  default: {
126
245
  keyPatterns: [REDACTOR_PATTERNS.sensitiveKey],
127
246
  valuePatterns: DEFAULT_VALUE_PATTERNS,
247
+ builtins: true,
128
248
  replacement: '[REDACTED]',
129
249
  },
130
250
 
131
251
  /**
132
252
  * Strict preset - more aggressive redaction for high-security environments
133
- * Includes everything in default plus: Bearer tokens, JWTs, API keys in values
253
+ * Includes everything in default plus: Bearer tokens, JWTs, IBAN, API keys in values
134
254
  */
135
255
  strict: {
136
256
  keyPatterns: [REDACTOR_PATTERNS.sensitiveKey, /bearer/i, /jwt/i],
137
257
  valuePatterns: [
138
258
  ...DEFAULT_VALUE_PATTERNS,
139
- { name: 'bearerToken', pattern: REDACTOR_PATTERNS.bearerToken },
259
+ builtinToValuePattern('jwt'),
260
+ builtinToValuePattern('bearer'),
261
+ builtinToValuePattern('iban'),
140
262
  { name: 'apiKeyInValue', pattern: REDACTOR_PATTERNS.apiKeyInValue },
141
- { name: 'jwt', pattern: REDACTOR_PATTERNS.jwt },
142
263
  ],
264
+ builtins: true,
143
265
  replacement: '[REDACTED]',
144
266
  },
145
267
 
146
268
  /**
147
269
  * PCI-DSS preset - focused on payment card industry compliance
148
- * Redacts: credit card numbers, CVV-like patterns, card-related keys
270
+ * Redacts: credit card numbers (****1111), CVV-like patterns, card-related keys
149
271
  */
150
272
  'pci-dss': {
151
273
  keyPatterns: [/card/i, /cvv/i, /cvc/i, /pan/i, /expir/i, /ccn/i],
152
- valuePatterns: [
153
- { name: 'creditCard', pattern: REDACTOR_PATTERNS.creditCard },
154
- ],
274
+ valuePatterns: [builtinToValuePattern('creditCard')],
275
+ builtins: ['creditCard'],
155
276
  replacement: '[REDACTED]',
156
277
  },
157
278
  };
158
279
 
280
+ /**
281
+ * Normalize redactor config that may have been deserialized from JSON/YAML.
282
+ * Converts regex-like values back to RegExp instances.
283
+ */
284
+ export function normalizeAttributeRedactorConfig(
285
+ raw: AttributeRedactorConfig | AttributeRedactorPreset | unknown,
286
+ ): AttributeRedactorConfig | AttributeRedactorPreset | undefined {
287
+ if (raw === undefined || raw === null) return undefined;
288
+ if (typeof raw === 'string') return raw as AttributeRedactorPreset;
289
+ if (!isPlainObject(raw)) return undefined;
290
+
291
+ const config: AttributeRedactorConfig = {};
292
+
293
+ if (Array.isArray(raw.paths)) {
294
+ config.paths = raw.paths.filter(
295
+ (value): value is string => typeof value === 'string',
296
+ );
297
+ }
298
+
299
+ if (typeof raw.replacement === 'string') {
300
+ config.replacement = raw.replacement;
301
+ }
302
+
303
+ if (typeof raw.builtins === 'boolean') {
304
+ config.builtins = raw.builtins;
305
+ } else if (Array.isArray(raw.builtins)) {
306
+ config.builtins = raw.builtins.filter(
307
+ (name): name is BuiltinPatternName => typeof name === 'string',
308
+ );
309
+ }
310
+
311
+ if (typeof raw.redactor === 'function') {
312
+ config.redactor = raw.redactor as AttributeRedactorFn;
313
+ }
314
+
315
+ const keyPatterns = toRegExpArray(raw.keyPatterns);
316
+ if (keyPatterns) config.keyPatterns = keyPatterns;
317
+
318
+ const patterns = toRegExpArray(raw.patterns);
319
+ if (patterns) config.patterns = patterns;
320
+
321
+ if (Array.isArray(raw.valuePatterns)) {
322
+ const valuePatterns: ValuePatternConfig[] = [];
323
+ for (const item of raw.valuePatterns) {
324
+ if (!isPlainObject(item) || typeof item.name !== 'string') continue;
325
+ const pattern = toRegExp(item.pattern);
326
+ if (!pattern) continue;
327
+ valuePatterns.push({
328
+ name: item.name,
329
+ pattern,
330
+ replacement:
331
+ typeof item.replacement === 'string' ? item.replacement : undefined,
332
+ mask:
333
+ typeof item.mask === 'function' ? (item.mask as MaskFn) : undefined,
334
+ });
335
+ }
336
+ config.valuePatterns = valuePatterns;
337
+ }
338
+
339
+ return config;
340
+ }
341
+
159
342
  /**
160
343
  * Resolve config to a normalized form
161
344
  */
162
345
  function resolveConfig(
163
346
  config: AttributeRedactorConfig | AttributeRedactorPreset,
164
347
  ): AttributeRedactorConfig {
165
- if (typeof config === 'string') {
166
- const preset = REDACTOR_PRESETS[config];
348
+ const normalized = normalizeAttributeRedactorConfig(config);
349
+ if (!normalized) {
350
+ throw new Error('Invalid attribute redactor config');
351
+ }
352
+
353
+ if (typeof normalized === 'string') {
354
+ const preset = REDACTOR_PRESETS[normalized];
167
355
  if (!preset) {
168
356
  throw new Error(
169
- `Unknown attribute redactor preset: "${config}". ` +
357
+ `Unknown attribute redactor preset: "${normalized}". ` +
170
358
  `Available presets: ${Object.keys(REDACTOR_PRESETS).join(', ')}`,
171
359
  );
172
360
  }
173
361
  return preset;
174
362
  }
175
- return config;
363
+
364
+ const resolvedConfig: AttributeRedactorConfig = {
365
+ ...normalized,
366
+ keyPatterns: normalized.keyPatterns
367
+ ? [...normalized.keyPatterns]
368
+ : undefined,
369
+ valuePatterns: normalized.valuePatterns
370
+ ? [...normalized.valuePatterns]
371
+ : undefined,
372
+ paths: normalized.paths ? [...normalized.paths] : undefined,
373
+ patterns: normalized.patterns ? [...normalized.patterns] : undefined,
374
+ };
375
+
376
+ // Merge built-in patterns if enabled
377
+ if (resolvedConfig.builtins !== false) {
378
+ const builtinNames = Array.isArray(resolvedConfig.builtins)
379
+ ? resolvedConfig.builtins
380
+ : (Object.keys(builtinPatterns) as BuiltinPatternName[]);
381
+ const builtinValuePatterns = builtinNames
382
+ .filter((name) => name in builtinPatterns)
383
+ .map(builtinToValuePattern);
384
+
385
+ resolvedConfig.valuePatterns = [
386
+ ...(resolvedConfig.valuePatterns ?? []),
387
+ ...builtinValuePatterns,
388
+ ];
389
+ }
390
+
391
+ return resolvedConfig;
176
392
  }
177
393
 
178
394
  /**
@@ -188,27 +404,40 @@ function createRedactorFromConfig(
188
404
 
189
405
  const keyPatterns = config.keyPatterns ?? [];
190
406
  const valuePatterns = config.valuePatterns ?? [];
407
+ const paths = config.paths ?? [];
408
+ const pathSet = new Set(paths);
409
+ const customPatterns = config.patterns ?? [];
191
410
  const defaultReplacement = config.replacement ?? '[REDACTED]';
192
411
 
412
+ // Build masker list from valuePatterns that have mask functions
413
+ const maskers: [RegExp, MaskFn][] = valuePatterns
414
+ .filter((vp) => vp.mask)
415
+ .map((vp) => [cloneRegex(vp.pattern), vp.mask!]);
416
+
193
417
  return (key: string, value: AttributeValue): AttributeValue => {
194
418
  // Check if key matches any sensitive key pattern
195
419
  for (const pattern of keyPatterns) {
196
- // Reset lastIndex for global regexes
197
420
  pattern.lastIndex = 0;
198
421
  if (pattern.test(key)) {
199
422
  return defaultReplacement;
200
423
  }
201
424
  }
202
425
 
203
- // For non-string values, return as-is (can't pattern match)
426
+ // Check if key matches any path-based redaction
427
+ if (pathSet.has(key)) {
428
+ return defaultReplacement;
429
+ }
430
+
431
+ // For non-string values, return as-is
204
432
  if (typeof value !== 'string') {
205
- // Handle arrays of strings
206
433
  if (Array.isArray(value)) {
207
434
  return value.map((item) => {
208
435
  if (typeof item === 'string') {
209
436
  return redactStringValue(
210
437
  item,
211
438
  valuePatterns,
439
+ maskers,
440
+ customPatterns,
212
441
  defaultReplacement,
213
442
  ) as string;
214
443
  }
@@ -218,25 +447,50 @@ function createRedactorFromConfig(
218
447
  return value;
219
448
  }
220
449
 
221
- // Apply value patterns to string values
222
- return redactStringValue(value, valuePatterns, defaultReplacement);
450
+ // Three-tier strategy: path-based masker-based → pattern-based
451
+ return redactStringValue(
452
+ value,
453
+ valuePatterns,
454
+ maskers,
455
+ customPatterns,
456
+ defaultReplacement,
457
+ );
223
458
  };
224
459
  }
225
460
 
226
461
  /**
227
- * Apply value patterns to a string
462
+ * Apply three-tier redaction strategy to a string
463
+ * 1. Masker-based: built-in patterns with smart partial masking
464
+ * 2. Pattern-based: custom RegExp patterns replaced with replacement
228
465
  */
229
466
  function redactStringValue(
230
467
  value: string,
231
468
  patterns: ValuePatternConfig[],
469
+ maskers: [RegExp, MaskFn][],
470
+ customPatterns: RegExp[],
232
471
  defaultReplacement: string,
233
472
  ): string {
234
473
  let result = value;
235
- for (const { pattern, replacement } of patterns) {
236
- // Reset lastIndex for global regexes
474
+
475
+ // Tier 1: Apply maskers (smart partial masking)
476
+ for (const [pattern, mask] of maskers) {
477
+ pattern.lastIndex = 0;
478
+ result = result.replace(pattern, mask);
479
+ }
480
+
481
+ // Tier 2: Apply value patterns without mask (full replacement)
482
+ for (const { pattern, replacement, mask } of patterns) {
483
+ if (mask) continue; // Already handled by maskers
237
484
  pattern.lastIndex = 0;
238
485
  result = result.replaceAll(pattern, replacement ?? defaultReplacement);
239
486
  }
487
+
488
+ // Tier 3: Apply custom patterns
489
+ for (const pattern of customPatterns) {
490
+ pattern.lastIndex = 0;
491
+ result = result.replaceAll(pattern, defaultReplacement);
492
+ }
493
+
240
494
  return result;
241
495
  }
242
496
 
@@ -232,7 +232,7 @@ export function createBuiltinLogger(
232
232
  log(level, extraOrMessage, message as Record<string, unknown>);
233
233
  } else {
234
234
  // Pure string-only call: logger.info('message')
235
- log(level, extraOrMessage, undefined);
235
+ log(level, extraOrMessage);
236
236
  }
237
237
  } else {
238
238
  // Pino style: logger.info({ extra }, 'message')
@@ -303,7 +303,7 @@ export function createBuiltinLogger(
303
303
  }
304
304
 
305
305
  // Pure string-only call: logger.error('message')
306
- log('error', extraOrMessage, undefined);
306
+ log('error', extraOrMessage);
307
307
  return;
308
308
  }
309
309
 
package/src/index.ts CHANGED
@@ -29,7 +29,7 @@
29
29
  */
30
30
 
31
31
  // Core initialization
32
- export { init, type AutotelConfig } from './init';
32
+ export { init, lockLogger, isLoggerLocked, type AutotelConfig } from './init';
33
33
 
34
34
  // Baggage span processor
35
35
  export {
@@ -126,6 +126,7 @@ export {
126
126
  // Structured errors
127
127
  export {
128
128
  createStructuredError,
129
+ structuredErrorToJSON,
129
130
  getStructuredErrorAttributes,
130
131
  recordStructuredError,
131
132
  type StructuredError,
package/src/init.ts CHANGED
@@ -60,6 +60,7 @@ import {
60
60
  } from './span-name-normalizer';
61
61
  import {
62
62
  AttributeRedactingProcessor,
63
+ normalizeAttributeRedactorConfig,
63
64
  type AttributeRedactorConfig,
64
65
  type AttributeRedactorPreset,
65
66
  } from './attribute-redacting-processor';
@@ -1142,10 +1143,28 @@ export interface AutotelConfig {
1142
1143
  */
1143
1144
  pretty?: boolean;
1144
1145
  };
1146
+
1147
+ /**
1148
+ * Suppress console output while keeping OTel exporters running.
1149
+ * Useful for platforms like GCP Cloud Run / AWS Lambda where stdout
1150
+ * is managed externally by the platform's log collector.
1151
+ *
1152
+ * @default false
1153
+ */
1154
+ silent?: boolean;
1155
+
1156
+ /**
1157
+ * Minimum log level for internal autotel diagnostic messages.
1158
+ * Messages below this level are dropped before processing.
1159
+ *
1160
+ * @default 'info'
1161
+ */
1162
+ minLevel?: 'debug' | 'info' | 'warn' | 'error';
1145
1163
  }
1146
1164
 
1147
1165
  // Internal state
1148
1166
  let initialized = false;
1167
+ let locked = false;
1149
1168
  let config: AutotelConfig | null = null;
1150
1169
  let sdk: NodeSDK | null = null;
1151
1170
  let warnedOnce = false;
@@ -1156,6 +1175,85 @@ let _stringRedactor: StringRedactor | null = null;
1156
1175
  let _optionalRequire: typeof safeRequire = safeRequire;
1157
1176
  let _devtoolsClose: (() => Promise<void> | void) | null = null;
1158
1177
 
1178
+ const LOG_LEVELS = { debug: 0, info: 1, warn: 2, error: 3 } as const;
1179
+ type LogLevelKey = keyof typeof LOG_LEVELS;
1180
+
1181
+ /**
1182
+ * Lock the logger to prevent further `init()` calls.
1183
+ * Use this when framework plugins set up instrumentation and you want
1184
+ * to prevent accidental re-initialization from user code.
1185
+ */
1186
+ export function lockLogger(): void {
1187
+ locked = true;
1188
+ }
1189
+
1190
+ /**
1191
+ * Check if the logger has been locked.
1192
+ */
1193
+ export function isLoggerLocked(): boolean {
1194
+ return locked;
1195
+ }
1196
+
1197
+ function createSilentLogger(): Logger {
1198
+ return {
1199
+ info: () => {},
1200
+ warn: () => {},
1201
+ error: () => {},
1202
+ debug: () => {},
1203
+ };
1204
+ }
1205
+
1206
+ function wrapLogger(
1207
+ base: Logger,
1208
+ silent: boolean,
1209
+ minLevel: LogLevelKey,
1210
+ ): Logger {
1211
+ if (silent) return createSilentLogger();
1212
+ const threshold = LOG_LEVELS[minLevel];
1213
+ const wrap = (fn: Logger['info'], level: LogLevelKey): Logger['info'] => {
1214
+ if (LOG_LEVELS[level] < threshold) {
1215
+ return (() => {}) as Logger['info'];
1216
+ }
1217
+ return ((...args: Parameters<Logger['info']>) =>
1218
+ fn(...args)) as Logger['info'];
1219
+ };
1220
+ return {
1221
+ debug: wrap(base.debug, 'debug'),
1222
+ info: wrap(base.info, 'info'),
1223
+ warn: wrap(base.warn, 'warn'),
1224
+ error: wrap(base.error, 'error'),
1225
+ };
1226
+ }
1227
+
1228
+ function detectEnvironmentAttributes(): Record<string, string> {
1229
+ const attrs: Record<string, string> = {};
1230
+
1231
+ const commitSha =
1232
+ process.env.COMMIT_SHA ||
1233
+ process.env.GITHUB_SHA ||
1234
+ process.env.VERCEL_GIT_COMMIT_SHA ||
1235
+ process.env.CF_PAGES_COMMIT_SHA ||
1236
+ process.env.AWS_CODEPIPELINE_EXECUTION_ID;
1237
+ if (commitSha) attrs['service.commit.sha'] = commitSha;
1238
+
1239
+ const region =
1240
+ process.env.VERCEL_REGION ||
1241
+ process.env.AWS_REGION ||
1242
+ process.env.AWS_DEFAULT_REGION ||
1243
+ process.env.FLY_REGION ||
1244
+ process.env.CF_REGION ||
1245
+ process.env.GOOGLE_CLOUD_REGION;
1246
+ if (region) attrs['service.region'] = region;
1247
+
1248
+ const version =
1249
+ process.env.APP_VERSION ||
1250
+ process.env.HEROKU_RELEASE_VERSION ||
1251
+ process.env.VERCEL_GIT_COMMIT_REF;
1252
+ if (version) attrs['service.deploy.version'] = version;
1253
+
1254
+ return attrs;
1255
+ }
1256
+
1159
1257
  /**
1160
1258
  * Resolve metrics flag with env var override support
1161
1259
  */
@@ -1295,6 +1393,10 @@ function normalizeOtlpHeaders(
1295
1393
  */
1296
1394
 
1297
1395
  export function init(cfg: AutotelConfig): void {
1396
+ if (locked) {
1397
+ return;
1398
+ }
1399
+
1298
1400
  // Resolve configs in priority order: explicit > yaml > env > defaults
1299
1401
  const envConfig = resolveConfigFromEnv();
1300
1402
  const yamlConfig = loadYamlConfig() ?? {};
@@ -1308,19 +1410,32 @@ export function init(cfg: AutotelConfig): void {
1308
1410
  resourceAttributes: {
1309
1411
  ...envConfig.resourceAttributes,
1310
1412
  ...yamlConfig.resourceAttributes,
1413
+ ...detectEnvironmentAttributes(),
1311
1414
  ...cfg.resourceAttributes,
1312
1415
  },
1313
1416
  // Handle headers merge (can be string or object)
1314
1417
  headers: cfg.headers ?? yamlConfig.headers ?? envConfig.headers,
1315
1418
  } as AutotelConfig;
1316
1419
 
1420
+ if (mergedConfig.attributeRedactor !== undefined) {
1421
+ const normalizedRedactor = normalizeAttributeRedactorConfig(
1422
+ mergedConfig.attributeRedactor,
1423
+ );
1424
+ if (!normalizedRedactor) {
1425
+ throw new Error('Invalid attributeRedactor config');
1426
+ }
1427
+ mergedConfig.attributeRedactor = normalizedRedactor;
1428
+ }
1429
+
1317
1430
  const devtoolsConfig = resolveDevtoolsConfig(mergedConfig.devtools);
1318
1431
  if (devtoolsConfig.enabled && mergedConfig.logs === undefined) {
1319
1432
  mergedConfig.logs = true;
1320
1433
  }
1321
1434
 
1322
- // Set logger (use provided or default to silent - no spam)
1323
- logger = mergedConfig.logger || silentLogger;
1435
+ const silent = mergedConfig.silent ?? false;
1436
+ const minLevel = mergedConfig.minLevel ?? 'info';
1437
+ const baseLogger = mergedConfig.logger || silentLogger;
1438
+ logger = wrapLogger(baseLogger, silent, minLevel);
1324
1439
 
1325
1440
  // Warn if re-initializing (same behavior in all environments)
1326
1441
  if (initialized) {