autotel 3.1.1 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/dist/attribute-redacting-processor.cjs +8 -8
  2. package/dist/attribute-redacting-processor.js +1 -1
  3. package/dist/attributes.cjs +21 -21
  4. package/dist/attributes.js +2 -2
  5. package/dist/auto.cjs +3 -3
  6. package/dist/auto.js +2 -2
  7. package/dist/{chunk-DDXIUZEG.js → chunk-454CH4OV.js} +3 -3
  8. package/dist/{chunk-DDXIUZEG.js.map → chunk-454CH4OV.js.map} +1 -1
  9. package/dist/{chunk-ZPERWNOP.cjs → chunk-4UUEGERM.cjs} +17 -17
  10. package/dist/{chunk-ZPERWNOP.cjs.map → chunk-4UUEGERM.cjs.map} +1 -1
  11. package/dist/{chunk-MXO6LXV5.cjs → chunk-5RZ3NZ2M.cjs} +5 -5
  12. package/dist/{chunk-MXO6LXV5.cjs.map → chunk-5RZ3NZ2M.cjs.map} +1 -1
  13. package/dist/{chunk-FTBBBPT6.js → chunk-7EVW3Z37.js} +13 -22
  14. package/dist/chunk-7EVW3Z37.js.map +1 -0
  15. package/dist/{chunk-KPDIEVVV.cjs → chunk-EEQHQKPP.cjs} +32 -32
  16. package/dist/chunk-EEQHQKPP.cjs.map +1 -0
  17. package/dist/{chunk-T7CPAGOI.js → chunk-FVA2YDEQ.js} +4 -4
  18. package/dist/chunk-FVA2YDEQ.js.map +1 -0
  19. package/dist/{chunk-PEEUMQ3R.js → chunk-IS2QJ44P.js} +3 -3
  20. package/dist/{chunk-PEEUMQ3R.js.map → chunk-IS2QJ44P.js.map} +1 -1
  21. package/dist/{chunk-45B2GD4P.cjs → chunk-KKIYPZOP.cjs} +6 -6
  22. package/dist/{chunk-45B2GD4P.cjs.map → chunk-KKIYPZOP.cjs.map} +1 -1
  23. package/dist/{chunk-DQ2SUROF.cjs → chunk-M3LFHHTN.cjs} +4 -4
  24. package/dist/{chunk-DQ2SUROF.cjs.map → chunk-M3LFHHTN.cjs.map} +1 -1
  25. package/dist/{chunk-NXLRY2CE.cjs → chunk-NEIB3TLD.cjs} +10 -8
  26. package/dist/chunk-NEIB3TLD.cjs.map +1 -0
  27. package/dist/{chunk-6TFJF7SS.js → chunk-NIDUQZIN.js} +3 -3
  28. package/dist/{chunk-6TFJF7SS.js.map → chunk-NIDUQZIN.js.map} +1 -1
  29. package/dist/{chunk-YPQMAE6U.cjs → chunk-NN2GODP4.cjs} +7 -7
  30. package/dist/{chunk-YPQMAE6U.cjs.map → chunk-NN2GODP4.cjs.map} +1 -1
  31. package/dist/{chunk-MHPYLMQS.js → chunk-QVLMGNQF.js} +4 -4
  32. package/dist/{chunk-MHPYLMQS.js.map → chunk-QVLMGNQF.js.map} +1 -1
  33. package/dist/{chunk-6X2GG65S.cjs → chunk-RRTFFAG3.cjs} +5 -5
  34. package/dist/{chunk-6X2GG65S.cjs.map → chunk-RRTFFAG3.cjs.map} +1 -1
  35. package/dist/{chunk-JVWJDHDB.js → chunk-RUPKBKUF.js} +10 -8
  36. package/dist/chunk-RUPKBKUF.js.map +1 -0
  37. package/dist/{chunk-52ALHU7T.js → chunk-RZI5XXAD.js} +3 -3
  38. package/dist/{chunk-52ALHU7T.js.map → chunk-RZI5XXAD.js.map} +1 -1
  39. package/dist/{chunk-LIYNUGML.cjs → chunk-UV64CWMA.cjs} +23 -32
  40. package/dist/chunk-UV64CWMA.cjs.map +1 -0
  41. package/dist/{chunk-MYWQELNY.js → chunk-ZKKJQS6R.js} +3 -3
  42. package/dist/{chunk-MYWQELNY.js.map → chunk-ZKKJQS6R.js.map} +1 -1
  43. package/dist/correlation-id.cjs +11 -11
  44. package/dist/correlation-id.js +3 -3
  45. package/dist/decorators.cjs +5 -5
  46. package/dist/decorators.js +4 -4
  47. package/dist/event-subscriber.d.cts +15 -1
  48. package/dist/event-subscriber.d.ts +15 -1
  49. package/dist/event.cjs +7 -7
  50. package/dist/event.js +4 -4
  51. package/dist/functional.cjs +12 -12
  52. package/dist/functional.js +4 -4
  53. package/dist/http.cjs +4 -4
  54. package/dist/http.js +3 -3
  55. package/dist/index.cjs +135 -94
  56. package/dist/index.cjs.map +1 -1
  57. package/dist/index.d.cts +25 -3
  58. package/dist/index.d.ts +25 -3
  59. package/dist/index.js +54 -14
  60. package/dist/index.js.map +1 -1
  61. package/dist/instrumentation.cjs +9 -9
  62. package/dist/instrumentation.js +2 -2
  63. package/dist/messaging.cjs +8 -8
  64. package/dist/messaging.js +5 -5
  65. package/dist/semantic-helpers.cjs +9 -9
  66. package/dist/semantic-helpers.js +5 -5
  67. package/dist/webhook.cjs +6 -6
  68. package/dist/webhook.js +4 -4
  69. package/dist/workflow-distributed.cjs +6 -6
  70. package/dist/workflow-distributed.js +4 -4
  71. package/dist/workflow.cjs +9 -9
  72. package/dist/workflow.js +5 -5
  73. package/package.json +1 -1
  74. package/src/attribute-redacting-processor.ts +12 -9
  75. package/src/define-event.test.ts +41 -0
  76. package/src/define-event.ts +77 -0
  77. package/src/event-queue.ts +4 -0
  78. package/src/event-subscriber.ts +15 -0
  79. package/src/functional.ts +2 -1
  80. package/src/index.ts +6 -0
  81. package/src/track.ts +3 -0
  82. package/src/validation.test.ts +7 -3
  83. package/src/validation.ts +19 -21
  84. package/dist/chunk-FTBBBPT6.js.map +0 -1
  85. package/dist/chunk-JVWJDHDB.js.map +0 -1
  86. package/dist/chunk-KPDIEVVV.cjs.map +0 -1
  87. package/dist/chunk-LIYNUGML.cjs.map +0 -1
  88. package/dist/chunk-NXLRY2CE.cjs.map +0 -1
  89. package/dist/chunk-T7CPAGOI.js.map +0 -1
package/dist/workflow.cjs CHANGED
@@ -1,17 +1,17 @@
1
1
  'use strict';
2
2
 
3
- var chunkMXO6LXV5_cjs = require('./chunk-MXO6LXV5.cjs');
3
+ var chunk5RZ3NZ2M_cjs = require('./chunk-5RZ3NZ2M.cjs');
4
4
  require('./chunk-4P6ZOARG.cjs');
5
- require('./chunk-KPDIEVVV.cjs');
5
+ require('./chunk-EEQHQKPP.cjs');
6
6
  require('./chunk-2GIBANLB.cjs');
7
7
  require('./chunk-VQTCQKHQ.cjs');
8
- require('./chunk-LIYNUGML.cjs');
9
- require('./chunk-45B2GD4P.cjs');
8
+ require('./chunk-UV64CWMA.cjs');
9
+ require('./chunk-KKIYPZOP.cjs');
10
10
  require('./chunk-FEEVB2GV.cjs');
11
11
  require('./chunk-CEAQK2QY.cjs');
12
12
  require('./chunk-ZNMBW67B.cjs');
13
13
  require('./chunk-IOYFAFHJ.cjs');
14
- require('./chunk-NXLRY2CE.cjs');
14
+ require('./chunk-NEIB3TLD.cjs');
15
15
  require('./chunk-CU6IDACR.cjs');
16
16
  require('./chunk-6S5RUKU3.cjs');
17
17
  require('./chunk-HR5YFXZW.cjs');
@@ -24,19 +24,19 @@ require('./chunk-YREV3LGG.cjs');
24
24
 
25
25
  Object.defineProperty(exports, "getCurrentWorkflowContext", {
26
26
  enumerable: true,
27
- get: function () { return chunkMXO6LXV5_cjs.getCurrentWorkflowContext; }
27
+ get: function () { return chunk5RZ3NZ2M_cjs.getCurrentWorkflowContext; }
28
28
  });
29
29
  Object.defineProperty(exports, "isInWorkflow", {
30
30
  enumerable: true,
31
- get: function () { return chunkMXO6LXV5_cjs.isInWorkflow; }
31
+ get: function () { return chunk5RZ3NZ2M_cjs.isInWorkflow; }
32
32
  });
33
33
  Object.defineProperty(exports, "traceStep", {
34
34
  enumerable: true,
35
- get: function () { return chunkMXO6LXV5_cjs.traceStep; }
35
+ get: function () { return chunk5RZ3NZ2M_cjs.traceStep; }
36
36
  });
37
37
  Object.defineProperty(exports, "traceWorkflow", {
38
38
  enumerable: true,
39
- get: function () { return chunkMXO6LXV5_cjs.traceWorkflow; }
39
+ get: function () { return chunk5RZ3NZ2M_cjs.traceWorkflow; }
40
40
  });
41
41
  //# sourceMappingURL=workflow.cjs.map
42
42
  //# sourceMappingURL=workflow.cjs.map
package/dist/workflow.js CHANGED
@@ -1,15 +1,15 @@
1
- export { getCurrentWorkflowContext, isInWorkflow, traceStep, traceWorkflow } from './chunk-PEEUMQ3R.js';
1
+ export { getCurrentWorkflowContext, isInWorkflow, traceStep, traceWorkflow } from './chunk-IS2QJ44P.js';
2
2
  import './chunk-KIL5CUN6.js';
3
- import './chunk-T7CPAGOI.js';
3
+ import './chunk-FVA2YDEQ.js';
4
4
  import './chunk-HLZ7H3VZ.js';
5
5
  import './chunk-SEO6NAQT.js';
6
- import './chunk-FTBBBPT6.js';
7
- import './chunk-MYWQELNY.js';
6
+ import './chunk-7EVW3Z37.js';
7
+ import './chunk-ZKKJQS6R.js';
8
8
  import './chunk-643PQG3Y.js';
9
9
  import './chunk-A4E5AQFK.js';
10
10
  import './chunk-WGWSHJ2N.js';
11
11
  import './chunk-GYR5K654.js';
12
- import './chunk-JVWJDHDB.js';
12
+ import './chunk-RUPKBKUF.js';
13
13
  import './chunk-6UQRVUN3.js';
14
14
  import './chunk-3QXBFGKP.js';
15
15
  import './chunk-KVDA4HX2.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autotel",
3
- "version": "3.1.1",
3
+ "version": "3.2.0",
4
4
  "description": "Write Once, Observe Anywhere",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -424,19 +424,22 @@ function createRedactorFromConfig(
424
424
  .map((vp) => [cloneRegex(vp.pattern), vp.mask!]);
425
425
 
426
426
  return (key: string, value: AttributeValue): AttributeValue => {
427
- // Check if key matches any sensitive key pattern
428
- for (const pattern of keyPatterns) {
429
- pattern.lastIndex = 0;
430
- if (pattern.test(key)) {
427
+ // Key-pattern and path-based redaction only applies to string values.
428
+ // Numbers, booleans and other non-string attributes are not credentials;
429
+ // replacing them with the string '[REDACTED]' silently changes their
430
+ // type and corrupts downstream consumers (LLM token counters etc.).
431
+ if (typeof value === 'string') {
432
+ for (const pattern of keyPatterns) {
433
+ pattern.lastIndex = 0;
434
+ if (pattern.test(key)) {
435
+ return defaultReplacement;
436
+ }
437
+ }
438
+ if (pathSet.has(key)) {
431
439
  return defaultReplacement;
432
440
  }
433
441
  }
434
442
 
435
- // Check if key matches any path-based redaction
436
- if (pathSet.has(key)) {
437
- return defaultReplacement;
438
- }
439
-
440
443
  // For non-string values, return as-is
441
444
  if (typeof value !== 'string') {
442
445
  if (Array.isArray(value)) {
@@ -0,0 +1,41 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { defineEvent } from './define-event';
3
+
4
+ describe('defineEvent', () => {
5
+ it('validates payload and exposes schema metadata when provided', () => {
6
+ const event = defineEvent(
7
+ 'order.placed',
8
+ {
9
+ safeParse(input: unknown) {
10
+ if (
11
+ typeof input === 'object' &&
12
+ input !== null &&
13
+ 'orderId' in input &&
14
+ typeof (input as Record<string, unknown>).orderId === 'string'
15
+ ) {
16
+ return {
17
+ success: true as const,
18
+ data: input as { orderId: string },
19
+ };
20
+ }
21
+ return { success: false as const, error: new Error('invalid') };
22
+ },
23
+ },
24
+ {
25
+ toJsonSchema: () => ({
26
+ type: 'object',
27
+ properties: { orderId: { type: 'string' } },
28
+ required: ['orderId'],
29
+ }),
30
+ },
31
+ );
32
+
33
+ expect(event.name).toBe('order.placed');
34
+ expect(event.schemaMetadata?.source).toBe('zod');
35
+ expect(event.schemaMetadata?.hash).toMatch(/^[a-f0-9]{64}$/);
36
+ expect(() => event.track({ orderId: 'o-1' })).not.toThrow();
37
+ expect(() => event.track({} as { orderId: string })).toThrow(
38
+ /Schema validation failed/,
39
+ );
40
+ });
41
+ });
@@ -0,0 +1,77 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { track } from './track';
3
+ import type { EventSchemaMetadata } from './event-subscriber';
4
+
5
+ type SafeParseResult<T> =
6
+ | { success: true; data: T }
7
+ | { success: false; error: unknown };
8
+
9
+ export interface SchemaLike<T> {
10
+ safeParse(input: unknown): SafeParseResult<T>;
11
+ }
12
+
13
+ export interface DefineEventOptions<S> {
14
+ toJsonSchema?: (schema: S) => unknown;
15
+ }
16
+
17
+ export interface DefinedEvent<Name extends string, Payload> {
18
+ readonly name: Name;
19
+ readonly schemaMetadata?: EventSchemaMetadata;
20
+ track(payload: Payload): void;
21
+ }
22
+
23
+ export function defineEvent<
24
+ Name extends string,
25
+ Payload,
26
+ S extends SchemaLike<Payload>,
27
+ >(
28
+ name: Name,
29
+ schema: S,
30
+ options: DefineEventOptions<S> = {},
31
+ ): DefinedEvent<Name, Payload> {
32
+ const jsonSchema = options.toJsonSchema?.(schema);
33
+ const schemaMetadata = jsonSchema
34
+ ? {
35
+ source: 'zod' as const,
36
+ jsonSchema,
37
+ hash: hashSchema(jsonSchema),
38
+ }
39
+ : undefined;
40
+
41
+ return {
42
+ name,
43
+ schemaMetadata,
44
+ track(payload: Payload) {
45
+ const parsed = schema.safeParse(payload);
46
+ if (!parsed.success) {
47
+ throw new Error(
48
+ `Invalid payload for event "${name}". Schema validation failed.`,
49
+ );
50
+ }
51
+ track(
52
+ name,
53
+ parsed.data,
54
+ schemaMetadata ? { schema: schemaMetadata } : undefined,
55
+ );
56
+ },
57
+ };
58
+ }
59
+
60
+ function hashSchema(schema: unknown): string {
61
+ return createHash('sha256').update(stableStringify(schema)).digest('hex');
62
+ }
63
+
64
+ function stableStringify(value: unknown): string {
65
+ if (value === null || value === undefined || typeof value !== 'object') {
66
+ return JSON.stringify(value);
67
+ }
68
+ if (Array.isArray(value)) {
69
+ return '[' + value.map((v) => stableStringify(v)).join(',') + ']';
70
+ }
71
+ const obj = value as Record<string, unknown>;
72
+ const body = Object.keys(obj)
73
+ .sort()
74
+ .map((k) => JSON.stringify(k) + ':' + stableStringify(obj[k]))
75
+ .join(',');
76
+ return '{' + body + '}';
77
+ }
@@ -22,6 +22,7 @@ import type {
22
22
  EventSubscriber,
23
23
  EventAttributes,
24
24
  AutotelEventContext,
25
+ EventSchemaMetadata,
25
26
  } from './event-subscriber';
26
27
  import { getLogger } from './init';
27
28
  import { getConfig as getRuntimeConfig } from './config';
@@ -38,6 +39,8 @@ export interface EventData {
38
39
  _traceId?: string;
39
40
  /** Autotel context for trace correlation (passed to subscribers) */
40
41
  autotel?: AutotelEventContext;
42
+ /** Optional schema metadata for contract-aware subscribers. */
43
+ schema?: EventSchemaMetadata;
41
44
  }
42
45
 
43
46
  /**
@@ -591,6 +594,7 @@ export class EventQueue {
591
594
  try {
592
595
  await subscriber.trackEvent(event.name, event.attributes, {
593
596
  autotel: event.autotel,
597
+ schema: event.schema,
594
598
  });
595
599
  this.recordDelivered(event, subscriberName, startTime);
596
600
  return { subscriberName, success: true };
@@ -101,12 +101,27 @@ export interface AutotelEventContext {
101
101
  linked_trace_ids?: string[];
102
102
  }
103
103
 
104
+ /**
105
+ * Optional machine-readable schema metadata attached to an event payload.
106
+ * Intended for contract-aware subscribers (e.g. architecture snapshot capture).
107
+ */
108
+ export interface EventSchemaMetadata {
109
+ /** Schema source format used at the call site. */
110
+ source: 'zod';
111
+ /** JSON Schema representation of the payload contract. */
112
+ jsonSchema: unknown;
113
+ /** Stable schema hash for change detection and cache keys. */
114
+ hash: string;
115
+ }
116
+
104
117
  /**
105
118
  * Options for event tracking methods
106
119
  */
107
120
  export interface EventTrackingOptions {
108
121
  /** Autotel trace context to include in the event */
109
122
  autotel?: AutotelEventContext;
123
+ /** Optional event payload schema metadata */
124
+ schema?: EventSchemaMetadata;
110
125
  }
111
126
 
112
127
  /**
package/src/functional.ts CHANGED
@@ -210,7 +210,8 @@ function hasImmediateExecutionMark(fn: unknown): boolean {
210
210
  */
211
211
  export function markAsImmediate<F>(fn: F): F {
212
212
  if (typeof fn === 'function') {
213
- (fn as unknown as ImmediateExecutionFlag)[IMMEDIATE_EXECUTION_SYMBOL] = true;
213
+ (fn as unknown as ImmediateExecutionFlag)[IMMEDIATE_EXECUTION_SYMBOL] =
214
+ true;
214
215
  }
215
216
  return fn;
216
217
  }
package/src/index.ts CHANGED
@@ -106,6 +106,12 @@ export {
106
106
 
107
107
  // Global track function
108
108
  export { track, getEventQueue } from './track';
109
+ export {
110
+ defineEvent,
111
+ type SchemaLike,
112
+ type DefineEventOptions,
113
+ type DefinedEvent,
114
+ } from './define-event';
109
115
 
110
116
  // Correlation ID utilities
111
117
  export {
package/src/track.ts CHANGED
@@ -15,6 +15,7 @@ import {
15
15
  } from './init';
16
16
  import { validateEvent } from './validation';
17
17
  import { getOrCreateCorrelationId } from './correlation-id';
18
+ import type { EventTrackingOptions } from './event-subscriber';
18
19
  import type { AutotelEventContext } from './event-subscriber';
19
20
 
20
21
  // Global events queue (initialized on first track call)
@@ -167,6 +168,7 @@ function getOrCreateQueue(): EventQueue | null {
167
168
  export function track<Events extends Record<string, any> = Record<string, any>>(
168
169
  event: keyof Events & string,
169
170
  data?: Events[typeof event],
171
+ options?: EventTrackingOptions,
170
172
  ): void {
171
173
  const queue = getOrCreateQueue();
172
174
  if (!queue) return; // No-op if not initialized or no subscribers
@@ -193,6 +195,7 @@ export function track<Events extends Record<string, any> = Record<string, any>>(
193
195
  attributes: enrichedData,
194
196
  timestamp: Date.now(),
195
197
  autotel: autotelContext,
198
+ schema: options?.schema,
196
199
  });
197
200
  }
198
201
 
@@ -298,17 +298,21 @@ describe('Sensitive data patterns', () => {
298
298
  expect(result?.API_KEY).toBe('[REDACTED]');
299
299
  });
300
300
 
301
- it('should redact auth fields', () => {
301
+ it('should redact auth fields (strings only)', () => {
302
302
  const attrs = {
303
303
  auth: 'abc123',
304
304
  authorization: 'Bearer token',
305
- authenticated: true, // Contains "auth" but should still be redacted
305
+ // `authenticated` matches the /auth/i key pattern, but `true` is a
306
+ // boolean status — not a credential — so it passes through unchanged.
307
+ // Redacting it to the string '[REDACTED]' would silently corrupt its
308
+ // type without protecting any secret.
309
+ authenticated: true,
306
310
  };
307
311
 
308
312
  const result = validateAttributes(attrs);
309
313
  expect(result?.auth).toBe('[REDACTED]');
310
314
  expect(result?.authorization).toBe('[REDACTED]');
311
- expect(result?.authenticated).toBe('[REDACTED]');
315
+ expect(result?.authenticated).toBe(true);
312
316
  });
313
317
 
314
318
  it('should not redact non-sensitive fields with similar names', () => {
package/src/validation.ts CHANGED
@@ -130,19 +130,22 @@ export function validateAttributes(
130
130
  );
131
131
  }
132
132
 
133
- // Check for sensitive field
134
- const isSensitive = config.sensitivePatterns.some((pattern) =>
135
- pattern.test(key),
136
- );
133
+ const value = attributes[key];
134
+
135
+ // Redact sensitive *strings* only. Numeric/boolean values are not
136
+ // credentials and replacing them with the literal string '[REDACTED]'
137
+ // both leaks no useful signal and breaks downstream type expectations
138
+ // (e.g. an LLM `promptTokens` counter becoming a string poisons every
139
+ // consumer that treats it as a number).
140
+ const isSensitive =
141
+ typeof value === 'string' &&
142
+ config.sensitivePatterns.some((pattern) => pattern.test(key));
137
143
 
138
144
  if (isSensitive) {
139
- // Redact sensitive data
140
145
  sanitized[key] = '[REDACTED]';
141
146
  continue;
142
147
  }
143
148
 
144
- // Sanitize value
145
- const value = attributes[key];
146
149
  sanitized[key] = sanitizeValue(value, config, 1) as
147
150
  | string
148
151
  | number
@@ -196,20 +199,15 @@ function sanitizeValue(
196
199
  const sanitized: Record<string, unknown> = {};
197
200
  for (const key in value) {
198
201
  if (Object.prototype.hasOwnProperty.call(value, key)) {
199
- // Check for sensitive field in nested objects
200
- const isSensitive = config.sensitivePatterns.some((pattern) =>
201
- pattern.test(key),
202
- );
203
-
204
- if (isSensitive) {
205
- sanitized[key] = '[REDACTED]';
206
- } else {
207
- sanitized[key] = sanitizeValue(
208
- (value as Record<string, unknown>)[key],
209
- config,
210
- depth + 1,
211
- );
212
- }
202
+ const nested = (value as Record<string, unknown>)[key];
203
+ // See top-level branch above: only string values are redacted.
204
+ const isSensitive =
205
+ typeof nested === 'string' &&
206
+ config.sensitivePatterns.some((pattern) => pattern.test(key));
207
+
208
+ sanitized[key] = isSensitive
209
+ ? '[REDACTED]'
210
+ : sanitizeValue(nested, config, depth + 1);
213
211
  }
214
212
  }
215
213
  return sanitized;