autotel 2.1.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 (272) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1946 -0
  3. package/dist/chunk-2LNRY4QK.js +273 -0
  4. package/dist/chunk-2LNRY4QK.js.map +1 -0
  5. package/dist/chunk-3HENGDW2.js +587 -0
  6. package/dist/chunk-3HENGDW2.js.map +1 -0
  7. package/dist/chunk-4OAT42CA.cjs +73 -0
  8. package/dist/chunk-4OAT42CA.cjs.map +1 -0
  9. package/dist/chunk-5GWX5LFW.js +70 -0
  10. package/dist/chunk-5GWX5LFW.js.map +1 -0
  11. package/dist/chunk-5R2M36QB.js +195 -0
  12. package/dist/chunk-5R2M36QB.js.map +1 -0
  13. package/dist/chunk-5ZN622AO.js +73 -0
  14. package/dist/chunk-5ZN622AO.js.map +1 -0
  15. package/dist/chunk-77MSMAUQ.cjs +498 -0
  16. package/dist/chunk-77MSMAUQ.cjs.map +1 -0
  17. package/dist/chunk-ABPEQ6RK.cjs +596 -0
  18. package/dist/chunk-ABPEQ6RK.cjs.map +1 -0
  19. package/dist/chunk-BWYGJKRB.js +95 -0
  20. package/dist/chunk-BWYGJKRB.js.map +1 -0
  21. package/dist/chunk-BZHG5IZ4.js +73 -0
  22. package/dist/chunk-BZHG5IZ4.js.map +1 -0
  23. package/dist/chunk-G7VZBCD6.cjs +35 -0
  24. package/dist/chunk-G7VZBCD6.cjs.map +1 -0
  25. package/dist/chunk-GVLK7YUU.cjs +30 -0
  26. package/dist/chunk-GVLK7YUU.cjs.map +1 -0
  27. package/dist/chunk-HCCXC7XG.js +205 -0
  28. package/dist/chunk-HCCXC7XG.js.map +1 -0
  29. package/dist/chunk-HE6T6FIX.cjs +203 -0
  30. package/dist/chunk-HE6T6FIX.cjs.map +1 -0
  31. package/dist/chunk-KIXWPOCO.cjs +100 -0
  32. package/dist/chunk-KIXWPOCO.cjs.map +1 -0
  33. package/dist/chunk-KVGNW3FC.js +87 -0
  34. package/dist/chunk-KVGNW3FC.js.map +1 -0
  35. package/dist/chunk-LITNXTTT.js +3 -0
  36. package/dist/chunk-LITNXTTT.js.map +1 -0
  37. package/dist/chunk-M4ANN7RL.js +114 -0
  38. package/dist/chunk-M4ANN7RL.js.map +1 -0
  39. package/dist/chunk-NC52UBR2.cjs +32 -0
  40. package/dist/chunk-NC52UBR2.cjs.map +1 -0
  41. package/dist/chunk-NHCNRQD3.cjs +212 -0
  42. package/dist/chunk-NHCNRQD3.cjs.map +1 -0
  43. package/dist/chunk-NZ72VDNY.cjs +4 -0
  44. package/dist/chunk-NZ72VDNY.cjs.map +1 -0
  45. package/dist/chunk-P6JUDYNO.js +57 -0
  46. package/dist/chunk-P6JUDYNO.js.map +1 -0
  47. package/dist/chunk-RJYY7BWX.js +1349 -0
  48. package/dist/chunk-RJYY7BWX.js.map +1 -0
  49. package/dist/chunk-TRI4V5BF.cjs +126 -0
  50. package/dist/chunk-TRI4V5BF.cjs.map +1 -0
  51. package/dist/chunk-UL33I6IS.js +139 -0
  52. package/dist/chunk-UL33I6IS.js.map +1 -0
  53. package/dist/chunk-URRW6M2C.cjs +61 -0
  54. package/dist/chunk-URRW6M2C.cjs.map +1 -0
  55. package/dist/chunk-UY3UYPBZ.cjs +77 -0
  56. package/dist/chunk-UY3UYPBZ.cjs.map +1 -0
  57. package/dist/chunk-W3253FGB.cjs +277 -0
  58. package/dist/chunk-W3253FGB.cjs.map +1 -0
  59. package/dist/chunk-W7LHZVQF.js +26 -0
  60. package/dist/chunk-W7LHZVQF.js.map +1 -0
  61. package/dist/chunk-WBWNM6LB.cjs +1360 -0
  62. package/dist/chunk-WBWNM6LB.cjs.map +1 -0
  63. package/dist/chunk-WFJ7L2RV.js +494 -0
  64. package/dist/chunk-WFJ7L2RV.js.map +1 -0
  65. package/dist/chunk-X4RMFFMR.js +28 -0
  66. package/dist/chunk-X4RMFFMR.js.map +1 -0
  67. package/dist/chunk-Y4Y2S7BM.cjs +92 -0
  68. package/dist/chunk-Y4Y2S7BM.cjs.map +1 -0
  69. package/dist/chunk-YLPNXZFI.cjs +143 -0
  70. package/dist/chunk-YLPNXZFI.cjs.map +1 -0
  71. package/dist/chunk-YTXEZ4SD.cjs +77 -0
  72. package/dist/chunk-YTXEZ4SD.cjs.map +1 -0
  73. package/dist/chunk-Z6ZWNWWR.js +30 -0
  74. package/dist/chunk-Z6ZWNWWR.js.map +1 -0
  75. package/dist/config.cjs +26 -0
  76. package/dist/config.cjs.map +1 -0
  77. package/dist/config.d.cts +75 -0
  78. package/dist/config.d.ts +75 -0
  79. package/dist/config.js +5 -0
  80. package/dist/config.js.map +1 -0
  81. package/dist/db.cjs +233 -0
  82. package/dist/db.cjs.map +1 -0
  83. package/dist/db.d.cts +123 -0
  84. package/dist/db.d.ts +123 -0
  85. package/dist/db.js +228 -0
  86. package/dist/db.js.map +1 -0
  87. package/dist/decorators.cjs +67 -0
  88. package/dist/decorators.cjs.map +1 -0
  89. package/dist/decorators.d.cts +91 -0
  90. package/dist/decorators.d.ts +91 -0
  91. package/dist/decorators.js +65 -0
  92. package/dist/decorators.js.map +1 -0
  93. package/dist/event-subscriber.cjs +6 -0
  94. package/dist/event-subscriber.cjs.map +1 -0
  95. package/dist/event-subscriber.d.cts +116 -0
  96. package/dist/event-subscriber.d.ts +116 -0
  97. package/dist/event-subscriber.js +3 -0
  98. package/dist/event-subscriber.js.map +1 -0
  99. package/dist/event-testing.cjs +21 -0
  100. package/dist/event-testing.cjs.map +1 -0
  101. package/dist/event-testing.d.cts +110 -0
  102. package/dist/event-testing.d.ts +110 -0
  103. package/dist/event-testing.js +4 -0
  104. package/dist/event-testing.js.map +1 -0
  105. package/dist/event.cjs +30 -0
  106. package/dist/event.cjs.map +1 -0
  107. package/dist/event.d.cts +282 -0
  108. package/dist/event.d.ts +282 -0
  109. package/dist/event.js +13 -0
  110. package/dist/event.js.map +1 -0
  111. package/dist/exporters.cjs +17 -0
  112. package/dist/exporters.cjs.map +1 -0
  113. package/dist/exporters.d.cts +1 -0
  114. package/dist/exporters.d.ts +1 -0
  115. package/dist/exporters.js +4 -0
  116. package/dist/exporters.js.map +1 -0
  117. package/dist/functional.cjs +46 -0
  118. package/dist/functional.cjs.map +1 -0
  119. package/dist/functional.d.cts +478 -0
  120. package/dist/functional.d.ts +478 -0
  121. package/dist/functional.js +13 -0
  122. package/dist/functional.js.map +1 -0
  123. package/dist/http.cjs +189 -0
  124. package/dist/http.cjs.map +1 -0
  125. package/dist/http.d.cts +169 -0
  126. package/dist/http.d.ts +169 -0
  127. package/dist/http.js +184 -0
  128. package/dist/http.js.map +1 -0
  129. package/dist/index.cjs +333 -0
  130. package/dist/index.cjs.map +1 -0
  131. package/dist/index.d.cts +758 -0
  132. package/dist/index.d.ts +758 -0
  133. package/dist/index.js +143 -0
  134. package/dist/index.js.map +1 -0
  135. package/dist/instrumentation.cjs +182 -0
  136. package/dist/instrumentation.cjs.map +1 -0
  137. package/dist/instrumentation.d.cts +49 -0
  138. package/dist/instrumentation.d.ts +49 -0
  139. package/dist/instrumentation.js +179 -0
  140. package/dist/instrumentation.js.map +1 -0
  141. package/dist/logger.cjs +19 -0
  142. package/dist/logger.cjs.map +1 -0
  143. package/dist/logger.d.cts +146 -0
  144. package/dist/logger.d.ts +146 -0
  145. package/dist/logger.js +6 -0
  146. package/dist/logger.js.map +1 -0
  147. package/dist/metric-helpers.cjs +31 -0
  148. package/dist/metric-helpers.cjs.map +1 -0
  149. package/dist/metric-helpers.d.cts +13 -0
  150. package/dist/metric-helpers.d.ts +13 -0
  151. package/dist/metric-helpers.js +6 -0
  152. package/dist/metric-helpers.js.map +1 -0
  153. package/dist/metric-testing.cjs +21 -0
  154. package/dist/metric-testing.cjs.map +1 -0
  155. package/dist/metric-testing.d.cts +110 -0
  156. package/dist/metric-testing.d.ts +110 -0
  157. package/dist/metric-testing.js +4 -0
  158. package/dist/metric-testing.js.map +1 -0
  159. package/dist/metric.cjs +26 -0
  160. package/dist/metric.cjs.map +1 -0
  161. package/dist/metric.d.cts +240 -0
  162. package/dist/metric.d.ts +240 -0
  163. package/dist/metric.js +9 -0
  164. package/dist/metric.js.map +1 -0
  165. package/dist/processors.cjs +17 -0
  166. package/dist/processors.cjs.map +1 -0
  167. package/dist/processors.d.cts +1 -0
  168. package/dist/processors.d.ts +1 -0
  169. package/dist/processors.js +4 -0
  170. package/dist/processors.js.map +1 -0
  171. package/dist/sampling.cjs +40 -0
  172. package/dist/sampling.cjs.map +1 -0
  173. package/dist/sampling.d.cts +260 -0
  174. package/dist/sampling.d.ts +260 -0
  175. package/dist/sampling.js +7 -0
  176. package/dist/sampling.js.map +1 -0
  177. package/dist/semantic-helpers.cjs +35 -0
  178. package/dist/semantic-helpers.cjs.map +1 -0
  179. package/dist/semantic-helpers.d.cts +442 -0
  180. package/dist/semantic-helpers.d.ts +442 -0
  181. package/dist/semantic-helpers.js +14 -0
  182. package/dist/semantic-helpers.js.map +1 -0
  183. package/dist/tail-sampling-processor.cjs +13 -0
  184. package/dist/tail-sampling-processor.cjs.map +1 -0
  185. package/dist/tail-sampling-processor.d.cts +27 -0
  186. package/dist/tail-sampling-processor.d.ts +27 -0
  187. package/dist/tail-sampling-processor.js +4 -0
  188. package/dist/tail-sampling-processor.js.map +1 -0
  189. package/dist/testing.cjs +286 -0
  190. package/dist/testing.cjs.map +1 -0
  191. package/dist/testing.d.cts +291 -0
  192. package/dist/testing.d.ts +291 -0
  193. package/dist/testing.js +263 -0
  194. package/dist/testing.js.map +1 -0
  195. package/dist/trace-context-DRZdUvVY.d.cts +181 -0
  196. package/dist/trace-context-DRZdUvVY.d.ts +181 -0
  197. package/dist/trace-helpers.cjs +54 -0
  198. package/dist/trace-helpers.cjs.map +1 -0
  199. package/dist/trace-helpers.d.cts +524 -0
  200. package/dist/trace-helpers.d.ts +524 -0
  201. package/dist/trace-helpers.js +5 -0
  202. package/dist/trace-helpers.js.map +1 -0
  203. package/dist/tracer-provider.cjs +21 -0
  204. package/dist/tracer-provider.cjs.map +1 -0
  205. package/dist/tracer-provider.d.cts +169 -0
  206. package/dist/tracer-provider.d.ts +169 -0
  207. package/dist/tracer-provider.js +4 -0
  208. package/dist/tracer-provider.js.map +1 -0
  209. package/package.json +280 -0
  210. package/src/baggage-span-processor.test.ts +202 -0
  211. package/src/baggage-span-processor.ts +98 -0
  212. package/src/circuit-breaker.test.ts +341 -0
  213. package/src/circuit-breaker.ts +184 -0
  214. package/src/config.test.ts +94 -0
  215. package/src/config.ts +169 -0
  216. package/src/db.test.ts +252 -0
  217. package/src/db.ts +447 -0
  218. package/src/decorators.test.ts +203 -0
  219. package/src/decorators.ts +188 -0
  220. package/src/env-config.test.ts +246 -0
  221. package/src/env-config.ts +158 -0
  222. package/src/event-queue.test.ts +222 -0
  223. package/src/event-queue.ts +203 -0
  224. package/src/event-subscriber.ts +136 -0
  225. package/src/event-testing.ts +197 -0
  226. package/src/event.test.ts +718 -0
  227. package/src/event.ts +556 -0
  228. package/src/exporters.ts +96 -0
  229. package/src/functional.test.ts +1059 -0
  230. package/src/functional.ts +2295 -0
  231. package/src/http.test.ts +487 -0
  232. package/src/http.ts +424 -0
  233. package/src/index.ts +158 -0
  234. package/src/init.customization.test.ts +210 -0
  235. package/src/init.integrations.test.ts +366 -0
  236. package/src/init.openllmetry.test.ts +282 -0
  237. package/src/init.protocol.test.ts +215 -0
  238. package/src/init.ts +1426 -0
  239. package/src/instrumentation.test.ts +108 -0
  240. package/src/instrumentation.ts +308 -0
  241. package/src/logger.test.ts +117 -0
  242. package/src/logger.ts +246 -0
  243. package/src/metric-helpers.ts +47 -0
  244. package/src/metric-testing.ts +197 -0
  245. package/src/metric.ts +434 -0
  246. package/src/metrics.test.ts +205 -0
  247. package/src/operation-context.ts +93 -0
  248. package/src/processors.ts +106 -0
  249. package/src/rate-limiter.test.ts +199 -0
  250. package/src/rate-limiter.ts +98 -0
  251. package/src/sampling.test.ts +513 -0
  252. package/src/sampling.ts +428 -0
  253. package/src/semantic-helpers.test.ts +311 -0
  254. package/src/semantic-helpers.ts +584 -0
  255. package/src/shutdown.test.ts +311 -0
  256. package/src/shutdown.ts +222 -0
  257. package/src/stub.integration.test.ts +361 -0
  258. package/src/tail-sampling-processor.test.ts +226 -0
  259. package/src/tail-sampling-processor.ts +51 -0
  260. package/src/testing.ts +670 -0
  261. package/src/trace-context.ts +470 -0
  262. package/src/trace-helpers.new.test.ts +278 -0
  263. package/src/trace-helpers.test.ts +242 -0
  264. package/src/trace-helpers.ts +690 -0
  265. package/src/tracer-provider.test.ts +183 -0
  266. package/src/tracer-provider.ts +266 -0
  267. package/src/track.test.ts +153 -0
  268. package/src/track.ts +120 -0
  269. package/src/validation.test.ts +306 -0
  270. package/src/validation.ts +239 -0
  271. package/src/variable-name-inference.test.ts +178 -0
  272. package/src/variable-name-inference.ts +242 -0
package/src/track.ts ADDED
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Global track() function for business events
3
+ *
4
+ * Simple, no instantiation needed, auto-attaches trace context
5
+ */
6
+
7
+ import { trace } from '@opentelemetry/api';
8
+ import { EventQueue } from './event-queue';
9
+ import {
10
+ getConfig,
11
+ warnIfNotInitialized,
12
+ isInitialized,
13
+ getValidationConfig,
14
+ } from './init';
15
+ import { validateEvent } from './validation';
16
+
17
+ // Global events queue (initialized on first track call)
18
+ let eventsQueue: EventQueue | null = null;
19
+
20
+ /**
21
+ * Initialize events queue lazily
22
+ */
23
+ function getOrCreateQueue(): EventQueue | null {
24
+ if (!isInitialized()) {
25
+ warnIfNotInitialized('track()');
26
+ return null;
27
+ }
28
+
29
+ if (!eventsQueue) {
30
+ const config = getConfig();
31
+ if (!config?.subscribers || config.subscribers.length === 0) {
32
+ // No subscribers configured - no-op
33
+ return null;
34
+ }
35
+
36
+ eventsQueue = new EventQueue(config.subscribers);
37
+ }
38
+
39
+ return eventsQueue;
40
+ }
41
+
42
+ /**
43
+ * Track a business events event
44
+ *
45
+ * Features:
46
+ * - Auto-attaches traceId and spanId if in active span
47
+ * - Batched sending with retry
48
+ * - Type-safe with optional generic
49
+ * - No-op if init() not called or no subscribers configured
50
+ *
51
+ * @example Basic usage
52
+ * ```typescript
53
+ * track('user.signup', { userId: '123', plan: 'pro' })
54
+ * ```
55
+ *
56
+ * @example With type safety
57
+ * ```typescript
58
+ * interface EventDatas {
59
+ * 'user.signup': { userId: string; plan: string }
60
+ * 'plan.upgraded': { userId: string; revenue: number }
61
+ * }
62
+ *
63
+ * track<EventDatas>('user.signup', { userId: '123', plan: 'pro' })
64
+ * ```
65
+ *
66
+ * @example Trace correlation (automatic)
67
+ * ```typescript
68
+ * @Instrumented()
69
+ * class UserService {
70
+ * async createUser(data: CreateUserData) {
71
+ * // This track call automatically includes traceId + spanId
72
+ * track('user.signup', { userId: data.id })
73
+ * }
74
+ * }
75
+ * ```
76
+ */
77
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
78
+ export function track<Events extends Record<string, any> = Record<string, any>>(
79
+ event: keyof Events & string,
80
+ data?: Events[typeof event],
81
+ ): void {
82
+ const queue = getOrCreateQueue();
83
+ if (!queue) return; // No-op if not initialized or no subscribers
84
+
85
+ // Validate and sanitize input (with custom config if provided)
86
+ const validationConfig = getValidationConfig();
87
+ const validated = validateEvent(event, data, validationConfig || undefined);
88
+
89
+ // Auto-attach trace context if available (free win!)
90
+ const span = trace.getActiveSpan();
91
+ const enrichedData = span
92
+ ? {
93
+ ...validated.attributes,
94
+ traceId: span.spanContext().traceId,
95
+ spanId: span.spanContext().spanId,
96
+ }
97
+ : validated.attributes;
98
+
99
+ queue.enqueue({
100
+ name: validated.eventName,
101
+ attributes: enrichedData,
102
+ timestamp: Date.now(),
103
+ });
104
+ }
105
+
106
+ /**
107
+ * Get events queue (for flush/shutdown)
108
+ * @internal
109
+ */
110
+ export function getEventQueue(): EventQueue | null {
111
+ return eventsQueue;
112
+ }
113
+
114
+ /**
115
+ * Reset events queue (for shutdown/cleanup)
116
+ * @internal
117
+ */
118
+ export function resetEventQueue(): void {
119
+ eventsQueue = null;
120
+ }
@@ -0,0 +1,306 @@
1
+ /**
2
+ * Tests for input validation
3
+ */
4
+
5
+ import { describe, it, expect } from 'vitest';
6
+ import {
7
+ validateEventName,
8
+ validateAttributes,
9
+ validateEvent,
10
+ ValidationError,
11
+ getDefaultValidationConfig,
12
+ } from './validation';
13
+
14
+ describe('validateEventName()', () => {
15
+ it('should accept valid event names', () => {
16
+ expect(validateEventName('user.signup')).toBe('user.signup');
17
+ expect(validateEventName('order_completed')).toBe('order_completed');
18
+ expect(validateEventName('feature-used')).toBe('feature-used');
19
+ expect(validateEventName('app123.event456')).toBe('app123.event456');
20
+ });
21
+
22
+ it('should trim whitespace', () => {
23
+ expect(validateEventName(' user.signup ')).toBe('user.signup');
24
+ });
25
+
26
+ it('should reject empty event names', () => {
27
+ expect(() => validateEventName('')).toThrow(ValidationError);
28
+ expect(() => validateEventName(' ')).toThrow(ValidationError);
29
+ });
30
+
31
+ it('should reject non-string event names', () => {
32
+ expect(() => validateEventName(123 as any)).toThrow(ValidationError);
33
+
34
+ expect(() => validateEventName(null as any)).toThrow(ValidationError);
35
+
36
+ expect(() => validateEventName(undefined as any)).toThrow(ValidationError);
37
+ });
38
+
39
+ it('should reject event names that are too long', () => {
40
+ const longName = 'a'.repeat(101);
41
+ expect(() => validateEventName(longName)).toThrow(ValidationError);
42
+ });
43
+
44
+ it('should reject event names with invalid characters', () => {
45
+ expect(() => validateEventName('user signup')).toThrow(ValidationError);
46
+ expect(() => validateEventName('user@signup')).toThrow(ValidationError);
47
+ expect(() => validateEventName('user/signup')).toThrow(ValidationError);
48
+ expect(() => validateEventName(String.raw`user\signup`)).toThrow(
49
+ ValidationError,
50
+ );
51
+ });
52
+ });
53
+
54
+ describe('validateAttributes()', () => {
55
+ it('should accept valid attributes', () => {
56
+ const attrs = {
57
+ userId: '123',
58
+ plan: 'pro',
59
+ count: 5,
60
+ active: true,
61
+ };
62
+
63
+ expect(validateAttributes(attrs)).toEqual(attrs);
64
+ });
65
+
66
+ it('should handle undefined attributes', () => {
67
+ // eslint-disable-next-line unicorn/no-useless-undefined
68
+ expect(validateAttributes(undefined)).toBeUndefined();
69
+ });
70
+
71
+ it('should reject non-object attributes', () => {
72
+ expect(() => validateAttributes('string' as any)).toThrow(ValidationError);
73
+
74
+ expect(() => validateAttributes(123 as any)).toThrow(ValidationError);
75
+
76
+ expect(() => validateAttributes([] as any)).toThrow(ValidationError);
77
+ });
78
+
79
+ it('should reject too many attributes', () => {
80
+ const config = getDefaultValidationConfig();
81
+ const attrs: Record<string, unknown> = {};
82
+ for (let i = 0; i < config.maxAttributeCount + 1; i++) {
83
+ attrs[`key${i}`] = 'value';
84
+ }
85
+
86
+ expect(() =>
87
+ validateAttributes(attrs as Record<string, string | number | boolean>),
88
+ ).toThrow(ValidationError);
89
+ });
90
+
91
+ it('should reject attribute keys that are too long', () => {
92
+ const longKey = 'a'.repeat(101);
93
+ const attrs = { [longKey]: 'value' };
94
+
95
+ expect(() => validateAttributes(attrs)).toThrow(ValidationError);
96
+ });
97
+
98
+ it('should truncate long string values', () => {
99
+ const longValue = 'a'.repeat(1500);
100
+ const attrs = { field: longValue };
101
+
102
+ const result = validateAttributes(attrs);
103
+ expect(result?.field).toBe('a'.repeat(1000) + '...');
104
+ });
105
+
106
+ it('should redact sensitive fields', () => {
107
+ const attrs = {
108
+ email: 'user@example.com',
109
+ password: 'secret123',
110
+ apiKey: 'abc123',
111
+ normalField: 'value',
112
+ };
113
+
114
+ const result = validateAttributes(attrs);
115
+ expect(result?.email).toBe('user@example.com');
116
+ expect(result?.password).toBe('[REDACTED]');
117
+ expect(result?.apiKey).toBe('[REDACTED]');
118
+ expect(result?.normalField).toBe('value');
119
+ });
120
+
121
+ it('should handle nested objects within depth limit', () => {
122
+ const attrs = {
123
+ user: {
124
+ profile: {
125
+ name: 'John',
126
+ },
127
+ },
128
+ };
129
+
130
+ const result = validateAttributes(
131
+ attrs as unknown as Record<string, string | number | boolean>,
132
+ );
133
+ expect(result).toEqual(attrs);
134
+ });
135
+
136
+ it('should truncate deeply nested objects', () => {
137
+ const attrs = {
138
+ level1: {
139
+ level2: {
140
+ level3: {
141
+ level4: {
142
+ tooDeep: 'value',
143
+ },
144
+ },
145
+ },
146
+ },
147
+ };
148
+
149
+ const result = validateAttributes(attrs as any) as any;
150
+ expect(result.level1.level2.level3.level4).toBe('[MAX_DEPTH_EXCEEDED]');
151
+ });
152
+
153
+ it('should handle arrays', () => {
154
+ const attrs = {
155
+ tags: ['tag1', 'tag2', 'tag3'],
156
+ scores: [1, 2, 3],
157
+ };
158
+
159
+ const result = validateAttributes(attrs as any);
160
+ expect(result).toEqual(attrs);
161
+ });
162
+
163
+ it('should handle circular references', () => {
164
+ const circular: any = { name: 'test' };
165
+ circular.self = circular;
166
+
167
+ const attrs = { data: circular };
168
+
169
+ const result = validateAttributes(attrs) as any;
170
+ expect(result.data).toBe('[CIRCULAR]');
171
+ });
172
+
173
+ it('should handle null and undefined values', () => {
174
+ const attrs = {
175
+ nullable: null,
176
+ undefinedField: undefined,
177
+ normalField: 'value',
178
+ };
179
+
180
+ const result = validateAttributes(attrs as any);
181
+ expect(result?.nullable).toBeNull();
182
+ expect(result?.undefinedField).toBeUndefined();
183
+ expect(result?.normalField).toBe('value');
184
+ });
185
+
186
+ it('should handle unsupported types', () => {
187
+ const attrs = {
188
+ func: () => {},
189
+ symbol: Symbol('test'),
190
+ normalField: 'value',
191
+ };
192
+
193
+ const result = validateAttributes(attrs as any);
194
+ expect(result?.func).toBe('[function]');
195
+ expect(result?.symbol).toBe('[symbol]');
196
+ expect(result?.normalField).toBe('value');
197
+ });
198
+ });
199
+
200
+ describe('validateEvent()', () => {
201
+ it('should validate both event name and attributes', () => {
202
+ const result = validateEvent('user.signup', {
203
+ userId: '123',
204
+ password: 'secret',
205
+ });
206
+
207
+ expect(result.eventName).toBe('user.signup');
208
+ expect(result.attributes?.userId).toBe('123');
209
+ expect(result.attributes?.password).toBe('[REDACTED]');
210
+ });
211
+
212
+ it('should handle events without attributes', () => {
213
+ const result = validateEvent('page.viewed');
214
+
215
+ expect(result.eventName).toBe('page.viewed');
216
+ expect(result.attributes).toBeUndefined();
217
+ });
218
+
219
+ it('should allow custom validation config', () => {
220
+ const result = validateEvent(
221
+ 'test.event',
222
+ { field: 'value' },
223
+ { maxEventNameLength: 50 },
224
+ );
225
+
226
+ expect(result.eventName).toBe('test.event');
227
+ expect(result.attributes?.field).toBe('value');
228
+ });
229
+
230
+ it('should throw on invalid event name', () => {
231
+ expect(() => validateEvent('', { userId: '123' })).toThrow(ValidationError);
232
+ });
233
+
234
+ it('should throw on invalid attributes', () => {
235
+ expect(() => validateEvent('user.signup', 'invalid' as any)).toThrow(
236
+ ValidationError,
237
+ );
238
+ });
239
+ });
240
+
241
+ describe('Sensitive data patterns', () => {
242
+ it('should redact password fields', () => {
243
+ const attrs = {
244
+ password: 'secret',
245
+ userPassword: 'secret',
246
+ PASSWORD: 'secret',
247
+ };
248
+
249
+ const result = validateAttributes(attrs);
250
+ expect(result?.password).toBe('[REDACTED]');
251
+ expect(result?.userPassword).toBe('[REDACTED]');
252
+ expect(result?.PASSWORD).toBe('[REDACTED]');
253
+ });
254
+
255
+ it('should redact token fields', () => {
256
+ const attrs = {
257
+ token: 'abc123',
258
+ accessToken: 'abc123',
259
+ auth_token: 'abc123',
260
+ };
261
+
262
+ const result = validateAttributes(attrs);
263
+ expect(result?.token).toBe('[REDACTED]');
264
+ expect(result?.accessToken).toBe('[REDACTED]');
265
+ expect(result?.auth_token).toBe('[REDACTED]');
266
+ });
267
+
268
+ it('should redact API key fields', () => {
269
+ const attrs = {
270
+ apiKey: 'abc123',
271
+ api_key: 'abc123',
272
+ API_KEY: 'abc123',
273
+ };
274
+
275
+ const result = validateAttributes(attrs);
276
+ expect(result?.apiKey).toBe('[REDACTED]');
277
+ expect(result?.api_key).toBe('[REDACTED]');
278
+ expect(result?.API_KEY).toBe('[REDACTED]');
279
+ });
280
+
281
+ it('should redact auth fields', () => {
282
+ const attrs = {
283
+ auth: 'abc123',
284
+ authorization: 'Bearer token',
285
+ authenticated: true, // Contains "auth" but should still be redacted
286
+ };
287
+
288
+ const result = validateAttributes(attrs);
289
+ expect(result?.auth).toBe('[REDACTED]');
290
+ expect(result?.authorization).toBe('[REDACTED]');
291
+ expect(result?.authenticated).toBe('[REDACTED]');
292
+ });
293
+
294
+ it('should not redact non-sensitive fields with similar names', () => {
295
+ const attrs = {
296
+ email: 'user@example.com',
297
+ username: 'john',
298
+ userId: '123',
299
+ };
300
+
301
+ const result = validateAttributes(attrs);
302
+ expect(result?.email).toBe('user@example.com');
303
+ expect(result?.username).toBe('john');
304
+ expect(result?.userId).toBe('123');
305
+ });
306
+ });
@@ -0,0 +1,239 @@
1
+ /**
2
+ * Input validation for events events and attributes
3
+ *
4
+ * Prevents:
5
+ * - Invalid event names
6
+ * - Oversized payloads
7
+ * - Circular references
8
+ * - Sensitive data leaks
9
+ */
10
+
11
+ import type { EventAttributes } from './event-subscriber';
12
+
13
+ export interface ValidationConfig {
14
+ /** Max event name length (default: 100) */
15
+ maxEventNameLength: number;
16
+ /** Max attribute key length (default: 100) */
17
+ maxAttributeKeyLength: number;
18
+ /** Max attribute value length for strings (default: 1000) */
19
+ maxAttributeValueLength: number;
20
+ /** Max total attributes per event (default: 50) */
21
+ maxAttributeCount: number;
22
+ /** Max nesting depth for objects (default: 3) */
23
+ maxNestingDepth: number;
24
+ /** Sensitive field patterns to redact */
25
+ sensitivePatterns: RegExp[];
26
+ }
27
+
28
+ const DEFAULT_CONFIG: ValidationConfig = {
29
+ maxEventNameLength: 100,
30
+ maxAttributeKeyLength: 100,
31
+ maxAttributeValueLength: 1000,
32
+ maxAttributeCount: 50,
33
+ maxNestingDepth: 3,
34
+ sensitivePatterns: [
35
+ /password/i,
36
+ /secret/i,
37
+ /token/i,
38
+ /api[_-]?key/i,
39
+ /access[_-]?key/i,
40
+ /private[_-]?key/i,
41
+ /auth/i,
42
+ /credential/i,
43
+ /ssn/i,
44
+ /credit[_-]?card/i,
45
+ ],
46
+ };
47
+
48
+ export class ValidationError extends Error {
49
+ constructor(message: string) {
50
+ super(message);
51
+ this.name = 'ValidationError';
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Validate and sanitize event name
57
+ * Throws ValidationError if invalid
58
+ */
59
+ export function validateEventName(
60
+ eventName: string,
61
+ config: ValidationConfig = DEFAULT_CONFIG,
62
+ ): string {
63
+ // Check type
64
+ if (typeof eventName !== 'string') {
65
+ throw new ValidationError(
66
+ `Event name must be a string, got ${typeof eventName}`,
67
+ );
68
+ }
69
+
70
+ // Check non-empty
71
+ const trimmed = eventName.trim();
72
+ if (trimmed.length === 0) {
73
+ throw new ValidationError('Event name cannot be empty');
74
+ }
75
+
76
+ // Check length
77
+ if (trimmed.length > config.maxEventNameLength) {
78
+ throw new ValidationError(
79
+ `Event name too long (${trimmed.length} chars). ` +
80
+ `Max: ${config.maxEventNameLength}`,
81
+ );
82
+ }
83
+
84
+ // Check valid characters (alphanumeric, dots, underscores, hyphens)
85
+ if (!/^[a-zA-Z0-9._-]+$/.test(trimmed)) {
86
+ throw new ValidationError(
87
+ `Event name contains invalid characters: "${trimmed}". ` +
88
+ 'Use only letters, numbers, dots, underscores, and hyphens.',
89
+ );
90
+ }
91
+
92
+ return trimmed;
93
+ }
94
+
95
+ /**
96
+ * Validate and sanitize attributes
97
+ * Returns sanitized attributes (sensitive data redacted)
98
+ */
99
+ export function validateAttributes(
100
+ attributes: EventAttributes | undefined,
101
+ config: ValidationConfig = DEFAULT_CONFIG,
102
+ ): EventAttributes | undefined {
103
+ if (attributes === undefined || attributes === null) {
104
+ return undefined;
105
+ }
106
+
107
+ // Check type
108
+ if (typeof attributes !== 'object' || Array.isArray(attributes)) {
109
+ throw new ValidationError('Attributes must be an object');
110
+ }
111
+
112
+ // Count attributes
113
+ const keys = Object.keys(attributes);
114
+ if (keys.length > config.maxAttributeCount) {
115
+ throw new ValidationError(
116
+ `Too many attributes (${keys.length}). ` +
117
+ `Max: ${config.maxAttributeCount}`,
118
+ );
119
+ }
120
+
121
+ // Validate and sanitize each attribute
122
+ const sanitized: EventAttributes = {};
123
+
124
+ for (const key of keys) {
125
+ // Validate key
126
+ if (key.length > config.maxAttributeKeyLength) {
127
+ throw new ValidationError(
128
+ `Attribute key too long: "${key.slice(0, 20)}..." ` +
129
+ `(${key.length} chars). Max: ${config.maxAttributeKeyLength}`,
130
+ );
131
+ }
132
+
133
+ // Check for sensitive field
134
+ const isSensitive = config.sensitivePatterns.some((pattern) =>
135
+ pattern.test(key),
136
+ );
137
+
138
+ if (isSensitive) {
139
+ // Redact sensitive data
140
+ sanitized[key] = '[REDACTED]';
141
+ continue;
142
+ }
143
+
144
+ // Sanitize value
145
+ const value = attributes[key];
146
+ sanitized[key] = sanitizeValue(value, config, 1) as
147
+ | string
148
+ | number
149
+ | boolean;
150
+ }
151
+
152
+ return sanitized;
153
+ }
154
+
155
+ /**
156
+ * Sanitize attribute value (recursive)
157
+ */
158
+ function sanitizeValue(
159
+ value: unknown,
160
+ config: ValidationConfig,
161
+ depth: number,
162
+ ): unknown {
163
+ // Check nesting depth
164
+ if (depth > config.maxNestingDepth) {
165
+ return '[MAX_DEPTH_EXCEEDED]';
166
+ }
167
+
168
+ // Handle null/undefined
169
+ if (value === null || value === undefined) {
170
+ return value;
171
+ }
172
+
173
+ // Handle primitives
174
+ if (typeof value === 'string') {
175
+ if (value.length > config.maxAttributeValueLength) {
176
+ return value.slice(0, config.maxAttributeValueLength) + '...';
177
+ }
178
+ return value;
179
+ }
180
+
181
+ if (typeof value === 'number' || typeof value === 'boolean') {
182
+ return value;
183
+ }
184
+
185
+ // Handle arrays
186
+ if (Array.isArray(value)) {
187
+ return value.map((item) => sanitizeValue(item, config, depth + 1));
188
+ }
189
+
190
+ // Handle objects
191
+ if (typeof value === 'object') {
192
+ try {
193
+ // Check for circular references
194
+ JSON.stringify(value);
195
+
196
+ const sanitized: Record<string, unknown> = {};
197
+ for (const key in value) {
198
+ if (Object.prototype.hasOwnProperty.call(value, key)) {
199
+ sanitized[key] = sanitizeValue(
200
+ (value as Record<string, unknown>)[key],
201
+ config,
202
+ depth + 1,
203
+ );
204
+ }
205
+ }
206
+ return sanitized;
207
+ } catch {
208
+ // Circular reference detected
209
+ return '[CIRCULAR]';
210
+ }
211
+ }
212
+
213
+ // Unsupported type (function, symbol, etc.)
214
+ return `[${typeof value}]`;
215
+ }
216
+
217
+ /**
218
+ * Validate and sanitize an events event
219
+ * Returns { eventName, attributes } with sanitized values
220
+ */
221
+ export function validateEvent(
222
+ eventName: string,
223
+ attributes?: EventAttributes,
224
+ config?: Partial<ValidationConfig>,
225
+ ): { eventName: string; attributes?: EventAttributes } {
226
+ const fullConfig = { ...DEFAULT_CONFIG, ...config };
227
+
228
+ return {
229
+ eventName: validateEventName(eventName, fullConfig),
230
+ attributes: validateAttributes(attributes, fullConfig),
231
+ };
232
+ }
233
+
234
+ /**
235
+ * Get default validation config (for testing/customization)
236
+ */
237
+ export function getDefaultValidationConfig(): ValidationConfig {
238
+ return { ...DEFAULT_CONFIG };
239
+ }