@od-oneapp/analytics 2026.1.1301

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 (184) hide show
  1. package/README.md +509 -0
  2. package/dist/ai-YMnynb-t.mjs +3347 -0
  3. package/dist/ai-YMnynb-t.mjs.map +1 -0
  4. package/dist/chunk-DQk6qfdC.mjs +18 -0
  5. package/dist/client-CTzJVFU5.mjs +9 -0
  6. package/dist/client-CTzJVFU5.mjs.map +1 -0
  7. package/dist/client-CcFTauAh.mjs +54 -0
  8. package/dist/client-CcFTauAh.mjs.map +1 -0
  9. package/dist/client-CeOLjbac.mjs +281 -0
  10. package/dist/client-CeOLjbac.mjs.map +1 -0
  11. package/dist/client-D339NFJS.mjs +267 -0
  12. package/dist/client-D339NFJS.mjs.map +1 -0
  13. package/dist/client-next.d.mts +62 -0
  14. package/dist/client-next.d.mts.map +1 -0
  15. package/dist/client-next.mjs +525 -0
  16. package/dist/client-next.mjs.map +1 -0
  17. package/dist/client.d.mts +30 -0
  18. package/dist/client.d.mts.map +1 -0
  19. package/dist/client.mjs +186 -0
  20. package/dist/client.mjs.map +1 -0
  21. package/dist/config-DPS6bSYo.d.mts +34 -0
  22. package/dist/config-DPS6bSYo.d.mts.map +1 -0
  23. package/dist/config-P6P5adJg.mjs +287 -0
  24. package/dist/config-P6P5adJg.mjs.map +1 -0
  25. package/dist/console-8bND3mMU.mjs +128 -0
  26. package/dist/console-8bND3mMU.mjs.map +1 -0
  27. package/dist/ecommerce-Cgu4wlux.mjs +993 -0
  28. package/dist/ecommerce-Cgu4wlux.mjs.map +1 -0
  29. package/dist/emitters-6-nKo8i-.mjs +208 -0
  30. package/dist/emitters-6-nKo8i-.mjs.map +1 -0
  31. package/dist/emitters-DldkVSPp.d.mts +12 -0
  32. package/dist/emitters-DldkVSPp.d.mts.map +1 -0
  33. package/dist/index-BfNWgfa5.d.mts +1494 -0
  34. package/dist/index-BfNWgfa5.d.mts.map +1 -0
  35. package/dist/index-BkIWe--N.d.mts +953 -0
  36. package/dist/index-BkIWe--N.d.mts.map +1 -0
  37. package/dist/index-jPzXRn52.d.mts +184 -0
  38. package/dist/index-jPzXRn52.d.mts.map +1 -0
  39. package/dist/manager-DvRRjza6.d.mts +76 -0
  40. package/dist/manager-DvRRjza6.d.mts.map +1 -0
  41. package/dist/posthog-bootstrap-CYfIy_WS.mjs +1769 -0
  42. package/dist/posthog-bootstrap-CYfIy_WS.mjs.map +1 -0
  43. package/dist/posthog-bootstrap-DWxFrxlt.d.mts +81 -0
  44. package/dist/posthog-bootstrap-DWxFrxlt.d.mts.map +1 -0
  45. package/dist/providers-http-client.d.mts +37 -0
  46. package/dist/providers-http-client.d.mts.map +1 -0
  47. package/dist/providers-http-client.mjs +320 -0
  48. package/dist/providers-http-client.mjs.map +1 -0
  49. package/dist/providers-http-server.d.mts +31 -0
  50. package/dist/providers-http-server.d.mts.map +1 -0
  51. package/dist/providers-http-server.mjs +297 -0
  52. package/dist/providers-http-server.mjs.map +1 -0
  53. package/dist/providers-http.d.mts +46 -0
  54. package/dist/providers-http.d.mts.map +1 -0
  55. package/dist/providers-http.mjs +4 -0
  56. package/dist/server-edge.d.mts +9 -0
  57. package/dist/server-edge.d.mts.map +1 -0
  58. package/dist/server-edge.mjs +373 -0
  59. package/dist/server-edge.mjs.map +1 -0
  60. package/dist/server-next.d.mts +67 -0
  61. package/dist/server-next.d.mts.map +1 -0
  62. package/dist/server-next.mjs +193 -0
  63. package/dist/server-next.mjs.map +1 -0
  64. package/dist/server.d.mts +10 -0
  65. package/dist/server.mjs +7 -0
  66. package/dist/service-cYtBBL8x.mjs +945 -0
  67. package/dist/service-cYtBBL8x.mjs.map +1 -0
  68. package/dist/shared.d.mts +16 -0
  69. package/dist/shared.d.mts.map +1 -0
  70. package/dist/shared.mjs +93 -0
  71. package/dist/shared.mjs.map +1 -0
  72. package/dist/types-BxBnNQ0V.d.mts +354 -0
  73. package/dist/types-BxBnNQ0V.d.mts.map +1 -0
  74. package/dist/types-CBvxUEaF.d.mts +216 -0
  75. package/dist/types-CBvxUEaF.d.mts.map +1 -0
  76. package/dist/types.d.mts +4 -0
  77. package/dist/types.mjs +0 -0
  78. package/dist/vercel-types-lwakUfoI.d.mts +102 -0
  79. package/dist/vercel-types-lwakUfoI.d.mts.map +1 -0
  80. package/package.json +129 -0
  81. package/src/client/index.ts +164 -0
  82. package/src/client/manager.ts +71 -0
  83. package/src/client/next/components.tsx +270 -0
  84. package/src/client/next/hooks.ts +217 -0
  85. package/src/client/next/manager.ts +141 -0
  86. package/src/client/next.ts +144 -0
  87. package/src/client-next.ts +101 -0
  88. package/src/client.ts +89 -0
  89. package/src/examples/ai-sdk-patterns.ts +583 -0
  90. package/src/examples/emitter-patterns.ts +476 -0
  91. package/src/examples/nextjs-emitter-patterns.tsx +403 -0
  92. package/src/next/app-router.tsx +564 -0
  93. package/src/next/client.ts +419 -0
  94. package/src/next/index.ts +84 -0
  95. package/src/next/middleware.ts +429 -0
  96. package/src/next/rsc.tsx +300 -0
  97. package/src/next/server.ts +253 -0
  98. package/src/next/types.d.ts +220 -0
  99. package/src/providers/base-provider.ts +419 -0
  100. package/src/providers/console/client.ts +10 -0
  101. package/src/providers/console/index.ts +152 -0
  102. package/src/providers/console/server.ts +6 -0
  103. package/src/providers/console/types.ts +15 -0
  104. package/src/providers/http/client.ts +464 -0
  105. package/src/providers/http/index.ts +30 -0
  106. package/src/providers/http/server.ts +396 -0
  107. package/src/providers/http/types.ts +135 -0
  108. package/src/providers/posthog/client.ts +518 -0
  109. package/src/providers/posthog/index.ts +11 -0
  110. package/src/providers/posthog/server.ts +329 -0
  111. package/src/providers/posthog/types.ts +104 -0
  112. package/src/providers/segment/client.ts +113 -0
  113. package/src/providers/segment/index.ts +11 -0
  114. package/src/providers/segment/server.ts +115 -0
  115. package/src/providers/segment/types.ts +51 -0
  116. package/src/providers/vercel/client.ts +102 -0
  117. package/src/providers/vercel/index.ts +11 -0
  118. package/src/providers/vercel/server.ts +89 -0
  119. package/src/providers/vercel/types.ts +27 -0
  120. package/src/server/index.ts +103 -0
  121. package/src/server/manager.ts +62 -0
  122. package/src/server/next.ts +210 -0
  123. package/src/server-edge.ts +442 -0
  124. package/src/server-next.ts +39 -0
  125. package/src/server.ts +106 -0
  126. package/src/shared/emitters/ai/README.md +981 -0
  127. package/src/shared/emitters/ai/events/agent.ts +130 -0
  128. package/src/shared/emitters/ai/events/artifacts.ts +167 -0
  129. package/src/shared/emitters/ai/events/chat.ts +126 -0
  130. package/src/shared/emitters/ai/events/chatbot-ecommerce.ts +133 -0
  131. package/src/shared/emitters/ai/events/completion.ts +103 -0
  132. package/src/shared/emitters/ai/events/content-generation.ts +347 -0
  133. package/src/shared/emitters/ai/events/conversation.ts +332 -0
  134. package/src/shared/emitters/ai/events/product-features.ts +1402 -0
  135. package/src/shared/emitters/ai/events/streaming.ts +114 -0
  136. package/src/shared/emitters/ai/events/tool.ts +93 -0
  137. package/src/shared/emitters/ai/index.ts +69 -0
  138. package/src/shared/emitters/ai/track-ai-sdk.ts +74 -0
  139. package/src/shared/emitters/ai/track-ai.ts +50 -0
  140. package/src/shared/emitters/ai/types.ts +1041 -0
  141. package/src/shared/emitters/ai/utils.ts +468 -0
  142. package/src/shared/emitters/ecommerce/events/cart-checkout.ts +106 -0
  143. package/src/shared/emitters/ecommerce/events/coupon.ts +49 -0
  144. package/src/shared/emitters/ecommerce/events/engagement.ts +61 -0
  145. package/src/shared/emitters/ecommerce/events/marketplace.ts +119 -0
  146. package/src/shared/emitters/ecommerce/events/order.ts +199 -0
  147. package/src/shared/emitters/ecommerce/events/product.ts +205 -0
  148. package/src/shared/emitters/ecommerce/events/registry.ts +123 -0
  149. package/src/shared/emitters/ecommerce/events/wishlist-sharing.ts +140 -0
  150. package/src/shared/emitters/ecommerce/index.ts +46 -0
  151. package/src/shared/emitters/ecommerce/track-ecommerce.ts +53 -0
  152. package/src/shared/emitters/ecommerce/types.ts +314 -0
  153. package/src/shared/emitters/ecommerce/utils.ts +216 -0
  154. package/src/shared/emitters/emitter-types.ts +974 -0
  155. package/src/shared/emitters/emitters.ts +292 -0
  156. package/src/shared/emitters/helpers.ts +419 -0
  157. package/src/shared/emitters/index.ts +66 -0
  158. package/src/shared/index.ts +142 -0
  159. package/src/shared/ingestion/index.ts +66 -0
  160. package/src/shared/ingestion/schemas.ts +386 -0
  161. package/src/shared/ingestion/service.ts +628 -0
  162. package/src/shared/node22-features.ts +848 -0
  163. package/src/shared/providers/console-provider.ts +160 -0
  164. package/src/shared/types/base-types.ts +54 -0
  165. package/src/shared/types/console-types.ts +19 -0
  166. package/src/shared/types/posthog-types.ts +131 -0
  167. package/src/shared/types/segment-types.ts +15 -0
  168. package/src/shared/types/types.ts +397 -0
  169. package/src/shared/types/vercel-types.ts +19 -0
  170. package/src/shared/utils/config-client.ts +19 -0
  171. package/src/shared/utils/config.ts +250 -0
  172. package/src/shared/utils/emitter-adapter.ts +212 -0
  173. package/src/shared/utils/manager.test.ts +36 -0
  174. package/src/shared/utils/manager.ts +1322 -0
  175. package/src/shared/utils/posthog-bootstrap.ts +136 -0
  176. package/src/shared/utils/posthog-client-utils.ts +48 -0
  177. package/src/shared/utils/posthog-next-utils.ts +282 -0
  178. package/src/shared/utils/posthog-server-utils.ts +210 -0
  179. package/src/shared/utils/rate-limit.ts +289 -0
  180. package/src/shared/utils/security.ts +545 -0
  181. package/src/shared/utils/validation-client.ts +161 -0
  182. package/src/shared/utils/validation.ts +399 -0
  183. package/src/shared.ts +155 -0
  184. package/src/types/index.ts +62 -0
@@ -0,0 +1,545 @@
1
+ /**
2
+ * @fileoverview Security and sanitization utilities for analytics data
3
+ *
4
+ * This module provides comprehensive security utilities for analytics data:
5
+ *
6
+ * - **PII Detection**: Detects personally identifiable information (emails, phones, SSNs, credit cards, IPs)
7
+ * - **PII Redaction**: Redacts PII from strings before sending to analytics
8
+ * - **XSS Protection**: Strips HTML and script tags to prevent XSS attacks
9
+ * - **Property Validation**: Validates property keys to prevent prototype pollution
10
+ * - **Payload Sanitization**: Comprehensive sanitization with size limits and depth checks
11
+ * - **Size Limits**: Enforces maximum property and payload sizes
12
+ *
13
+ * **Security Features**:
14
+ * - Maximum property value size: 100KB
15
+ * - Maximum payload size: 1MB
16
+ * - Maximum nesting depth: 10 levels
17
+ * - Dangerous key filtering (prevents prototype pollution)
18
+ * - HTML/script tag removal
19
+ *
20
+ * @module @od-oneapp/analytics/shared/utils/security
21
+ */
22
+
23
+ import { logWarn } from '@repo/shared/logger';
24
+
25
+ /**
26
+ * Maximum allowed property value size (100KB).
27
+ *
28
+ * Prevents sending excessively large property values to analytics providers.
29
+ *
30
+ * @internal
31
+ */
32
+ const MAX_PROPERTY_VALUE_SIZE = 100 * 1024;
33
+
34
+ /**
35
+ * Maximum allowed total payload size (1MB).
36
+ *
37
+ * Prevents sending excessively large payloads to analytics providers.
38
+ *
39
+ * @internal
40
+ */
41
+ const MAX_PAYLOAD_SIZE = 1024 * 1024;
42
+
43
+ /**
44
+ * Maximum allowed object nesting depth.
45
+ *
46
+ * Prevents deeply nested objects that could cause performance issues or stack overflows.
47
+ *
48
+ * @internal
49
+ */
50
+ const MAX_DEPTH = 10;
51
+
52
+ /**
53
+ * PII patterns for detection and filtering
54
+ * Note: These regexes are intentionally complex for accurate PII detection.
55
+ * They are used only on bounded, sanitized input with size limits.
56
+ */
57
+ /* eslint-disable security/detect-unsafe-regex */
58
+ const PII_PATTERNS = {
59
+ email: /\b[\w%+.-]+@[\d.A-Za-z-]+\.[A-Za-z|]{2,}\b/g,
60
+ phone: /\b(?:\+?1[.-]?)?\(?(\d{3})\)?[.-]?(\d{3})[.-]?(\d{4})\b/g,
61
+ ssn: /\b\d{3}-?\d{2}-?\d{4}\b/g,
62
+ creditCard: /\b(?:\d{4}[ -]?){3}\d{4}\b/g,
63
+ ipv4: /\b(?:\d{1,3}\.){3}\d{1,3}\b/g,
64
+ ipv6: /\b(?:[\da-f]{1,4}:){7}[\da-f]{1,4}\b/gi,
65
+ };
66
+ /* eslint-enable security/detect-unsafe-regex */
67
+
68
+ /**
69
+ * Dangerous property keys that should never be allowed
70
+ */
71
+ const DANGEROUS_KEYS = new Set([
72
+ '__proto__',
73
+ 'constructor',
74
+ 'prototype',
75
+ '__defineGetter__',
76
+ '__defineSetter__',
77
+ '__lookupGetter__',
78
+ '__lookupSetter__',
79
+ ]);
80
+
81
+ /**
82
+ * HTML/Script patterns for XSS protection
83
+ * Note: These regexes are intentionally complex for accurate XSS detection.
84
+ * They are used only on bounded, sanitized input with size limits.
85
+ */
86
+ /* eslint-disable security/detect-unsafe-regex */
87
+ const XSS_PATTERNS = {
88
+ scriptTag: /<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,
89
+ onEvent: /\bon\w+\s*=\s*["'][^"']*["']/gi,
90
+ javascript: /javascript:/gi,
91
+ dataUri: /data:text\/html/gi,
92
+ };
93
+ /* eslint-enable security/detect-unsafe-regex */
94
+
95
+ /**
96
+ * Sanitization options.
97
+ *
98
+ * Configures how sanitization should be performed, including size limits,
99
+ * depth limits, and what content to strip.
100
+ */
101
+ export interface SanitizationOptions {
102
+ /** Maximum object nesting depth */
103
+ maxDepth?: number;
104
+ /** Maximum size per property value in bytes */
105
+ maxPropertySize?: number;
106
+ /** Maximum total payload size in bytes */
107
+ maxPayloadSize?: number;
108
+ /** Pattern for allowed property keys */
109
+ allowedKeyPattern?: RegExp;
110
+ /** Whether to strip PII */
111
+ stripPII?: boolean;
112
+ /** Whether to strip HTML/scripts */
113
+ stripHTML?: boolean;
114
+ /** Whether to allow prototype pollution keys */
115
+ allowDangerousKeys?: boolean;
116
+ }
117
+
118
+ /**
119
+ * Sanitization result.
120
+ *
121
+ * Contains the sanitized data, whether modifications were made, and any warnings
122
+ * generated during sanitization.
123
+ */
124
+ export interface SanitizationResult<T = unknown> {
125
+ /** Sanitized data */
126
+ data: T;
127
+ /** Whether sanitization made changes */
128
+ modified: boolean;
129
+ /** Warnings generated during sanitization */
130
+ warnings: string[];
131
+ }
132
+
133
+ /**
134
+ * Check if a value contains PII (Personally Identifiable Information).
135
+ *
136
+ * Detects common PII patterns including:
137
+ * - Email addresses
138
+ * - Phone numbers
139
+ * - Social Security Numbers
140
+ * - Credit card numbers
141
+ * - IPv4 and IPv6 addresses
142
+ *
143
+ * @param {string} value - String value to check for PII
144
+ * @returns {boolean} `true` if PII is detected, `false` otherwise
145
+ *
146
+ * @example
147
+ * ```typescript
148
+ * if (containsPII(userInput)) {
149
+ * const sanitized = redactPII(userInput);
150
+ * // Use sanitized value
151
+ * }
152
+ * ```
153
+ */
154
+ export function containsPII(value: string): boolean {
155
+ return Object.values(PII_PATTERNS).some(pattern => pattern.test(value));
156
+ }
157
+
158
+ /**
159
+ * Redact PII from a string.
160
+ *
161
+ * Replaces detected PII patterns with `[REDACTED_TYPE]` placeholders.
162
+ * Useful for logging or analytics where PII should not be stored.
163
+ *
164
+ * @param {string} value - String value to redact PII from
165
+ * @returns {string} String with PII redacted
166
+ *
167
+ * @example
168
+ * ```typescript
169
+ * const sanitized = redactPII('Contact john@example.com at 555-1234');
170
+ * // Returns: 'Contact [REDACTED_EMAIL] at [REDACTED_PHONE]'
171
+ * ```
172
+ */
173
+ export function redactPII(value: string): string {
174
+ let redacted = value;
175
+
176
+ for (const [type, pattern] of Object.entries(PII_PATTERNS)) {
177
+ redacted = redacted.replace(pattern, `[REDACTED_${type.toUpperCase()}]`);
178
+ }
179
+
180
+ return redacted;
181
+ }
182
+
183
+ /**
184
+ * Strip HTML and script tags from a string.
185
+ *
186
+ * Removes HTML tags, script tags, event handlers, and other potentially dangerous
187
+ * content to prevent XSS attacks. Also removes `data:text/html` URIs.
188
+ *
189
+ * @param {string} value - String value to strip HTML from
190
+ * @returns {string} String with HTML and scripts removed
191
+ *
192
+ * @example
193
+ * ```typescript
194
+ * const sanitized = stripHTML('<script>alert("xss")</script>Hello');
195
+ * // Returns: 'Hello'
196
+ * ```
197
+ */
198
+ export function stripHTML(value: string): string {
199
+ let stripped = value;
200
+
201
+ for (const pattern of Object.values(XSS_PATTERNS)) {
202
+ stripped = stripped.replace(pattern, '');
203
+ }
204
+
205
+ // Additional cleanup for remaining tags
206
+ stripped = stripped.replaceAll(/<[^>]*>/g, '');
207
+
208
+ return stripped;
209
+ }
210
+
211
+ /**
212
+ * Validate that a property key is safe.
213
+ *
214
+ * Checks for dangerous keys that could cause prototype pollution or other
215
+ * security issues. Also validates against allowed key patterns if provided.
216
+ *
217
+ * @param {string} key - Property key to validate
218
+ * @param {SanitizationOptions} [options] - Sanitization options
219
+ * @returns {{ valid: boolean; reason?: string }} Validation result with reason if invalid
220
+ *
221
+ * @example
222
+ * ```typescript
223
+ * const result = isValidPropertyKey('__proto__');
224
+ * if (!result.valid) {
225
+ * console.warn('Unsafe key:', result.reason);
226
+ * }
227
+ * ```
228
+ */
229
+ export function isValidPropertyKey(
230
+ key: string,
231
+ options: SanitizationOptions = {},
232
+ ): { valid: boolean; reason?: string } {
233
+ // Check for dangerous keys
234
+ if (!options.allowDangerousKeys && DANGEROUS_KEYS.has(key)) {
235
+ return { valid: false, reason: 'Dangerous key that could cause prototype pollution' };
236
+ }
237
+
238
+ // Check against allowed pattern
239
+ if (options.allowedKeyPattern && !options.allowedKeyPattern.test(key)) {
240
+ return { valid: false, reason: 'Key does not match allowed pattern' };
241
+ }
242
+
243
+ // Check for special characters that could cause issues
244
+ if (/[<>[\\\]{}]/.test(key)) {
245
+ return { valid: false, reason: 'Key contains potentially dangerous characters' };
246
+ }
247
+
248
+ return { valid: true };
249
+ }
250
+
251
+ /**
252
+ * Get the size of a value in bytes
253
+ */
254
+ function getValueSize(value: unknown): number {
255
+ if (typeof value === 'string') {
256
+ return new Blob([value]).size;
257
+ }
258
+
259
+ try {
260
+ return new Blob([JSON.stringify(value)]).size;
261
+ } catch {
262
+ return 0;
263
+ }
264
+ }
265
+
266
+ /**
267
+ * Sanitize a single property value
268
+ */
269
+ function sanitizeValue(
270
+ value: unknown,
271
+ options: SanitizationOptions,
272
+ warnings: string[],
273
+ ): { value: unknown; modified: boolean } {
274
+ if (value === null || value === undefined) {
275
+ return { value, modified: false };
276
+ }
277
+
278
+ let modified = false;
279
+
280
+ // Handle strings
281
+ if (typeof value === 'string') {
282
+ let sanitized = value;
283
+
284
+ // Check size
285
+ const size = getValueSize(value);
286
+ const maxSize = options.maxPropertySize ?? MAX_PROPERTY_VALUE_SIZE;
287
+
288
+ if (size > maxSize) {
289
+ sanitized = value.slice(0, Math.max(0, maxSize));
290
+ warnings.push(`String value truncated from ${size} to ${maxSize} bytes`);
291
+ modified = true;
292
+ }
293
+
294
+ // Strip PII if requested
295
+ if (options.stripPII && containsPII(sanitized)) {
296
+ sanitized = redactPII(sanitized);
297
+ warnings.push('PII detected and redacted from value');
298
+ modified = true;
299
+ }
300
+
301
+ // Strip HTML if requested
302
+ if (options.stripHTML) {
303
+ const stripped = stripHTML(sanitized);
304
+ if (stripped !== sanitized) {
305
+ sanitized = stripped;
306
+ warnings.push('HTML/scripts stripped from value');
307
+ modified = true;
308
+ }
309
+ }
310
+
311
+ return { value: sanitized, modified };
312
+ }
313
+
314
+ // Handle numbers, booleans, etc.
315
+ if (typeof value !== 'object') {
316
+ return { value, modified: false };
317
+ }
318
+
319
+ // Arrays and objects handled by sanitizeProperties
320
+ return { value, modified: false };
321
+ }
322
+
323
+ /**
324
+ * Sanitize analytics properties recursively.
325
+ *
326
+ * Comprehensive sanitization of analytics properties including:
327
+ * - Size limit enforcement
328
+ * - Depth limit enforcement
329
+ * - PII detection and redaction
330
+ * - HTML/script tag removal
331
+ * - Dangerous key filtering
332
+ * - Circular reference detection
333
+ *
334
+ * @param {T} properties - Properties to sanitize
335
+ * @param {SanitizationOptions} [options] - Sanitization options
336
+ * @returns {SanitizationResult<T>} Sanitization result with sanitized data and warnings
337
+ *
338
+ * @throws {Error} If properties contain circular references or exceed payload size limit
339
+ *
340
+ * @example
341
+ * ```typescript
342
+ * const result = sanitizeProperties({
343
+ * user_email: 'test@example.com',
344
+ * phone: '555-1234',
345
+ * description: '<script>alert("xss")</script>Hello',
346
+ * }, {
347
+ * stripPII: true,
348
+ * stripHTML: true,
349
+ * });
350
+ *
351
+ * console.log(result.data);
352
+ * // {
353
+ * // user_email: '[REDACTED_EMAIL]',
354
+ * // phone: '[REDACTED_PHONE]',
355
+ * // description: 'Hello'
356
+ * // }
357
+ * ```
358
+ */
359
+ export function sanitizeProperties<T extends Record<string, unknown>>(
360
+ properties: T,
361
+ options: SanitizationOptions = {},
362
+ ): SanitizationResult<T> {
363
+ const warnings: string[] = [];
364
+ let modified = false;
365
+
366
+ // Check for circular references
367
+ try {
368
+ JSON.stringify(properties);
369
+ } catch {
370
+ throw new Error('Properties contain circular references');
371
+ }
372
+
373
+ // Check total payload size
374
+ const totalSize = getValueSize(properties);
375
+ const maxPayloadSize = options.maxPayloadSize ?? MAX_PAYLOAD_SIZE;
376
+
377
+ if (totalSize > maxPayloadSize) {
378
+ throw new Error(`Payload size ${totalSize} bytes exceeds maximum ${maxPayloadSize} bytes`);
379
+ }
380
+
381
+ /**
382
+ * Recursive sanitization helper
383
+ */
384
+ function sanitizeRecursive(
385
+ obj: Record<string, unknown>,
386
+ depth: number = 0,
387
+ ): Record<string, unknown> {
388
+ const maxDepth = options.maxDepth ?? MAX_DEPTH;
389
+
390
+ if (depth > maxDepth) {
391
+ warnings.push(`Maximum nesting depth ${maxDepth} exceeded, truncating`);
392
+ modified = true;
393
+ return {};
394
+ }
395
+
396
+ const sanitized: Record<string, unknown> = {};
397
+
398
+ for (const [key, value] of Object.entries(obj)) {
399
+ // Validate key
400
+ const keyValidation = isValidPropertyKey(key, options);
401
+ if (!keyValidation.valid) {
402
+ warnings.push(`Skipping dangerous key "${key}": ${keyValidation.reason}`);
403
+ modified = true;
404
+ continue;
405
+ }
406
+
407
+ // Handle null/undefined
408
+ if (value === null || value === undefined) {
409
+ sanitized[key] = value;
410
+ continue;
411
+ }
412
+
413
+ // Handle arrays
414
+ if (Array.isArray(value)) {
415
+ sanitized[key] = value.map(item => {
416
+ if (typeof item === 'object' && item !== null) {
417
+ return sanitizeRecursive(item as Record<string, unknown>, depth + 1);
418
+ }
419
+
420
+ const result = sanitizeValue(item, options, warnings);
421
+ if (result.modified) modified = true;
422
+ return result.value;
423
+ });
424
+ continue;
425
+ }
426
+
427
+ // Handle objects
428
+ if (typeof value === 'object') {
429
+ sanitized[key] = sanitizeRecursive(value as Record<string, unknown>, depth + 1);
430
+ continue;
431
+ }
432
+
433
+ // Handle primitives
434
+ const result = sanitizeValue(value, options, warnings);
435
+ if (result.modified) modified = true;
436
+ sanitized[key] = result.value;
437
+ }
438
+
439
+ return sanitized;
440
+ }
441
+
442
+ const sanitized = sanitizeRecursive(properties) as T;
443
+
444
+ // Log warnings in development
445
+ if (warnings.length > 0 && process.env.NODE_ENV === 'development') {
446
+ logWarn('Analytics properties sanitization warnings', { warnings });
447
+ }
448
+
449
+ return {
450
+ data: sanitized,
451
+ modified,
452
+ warnings,
453
+ };
454
+ }
455
+
456
+ /**
457
+ * Validate event name.
458
+ *
459
+ * Checks that an event name is safe and follows best practices:
460
+ * - Non-empty string
461
+ * - Not exceeding 255 characters
462
+ * - No XSS patterns
463
+ *
464
+ * @param {string} event - Event name to validate
465
+ * @returns {{ valid: boolean; reason?: string }} Validation result with reason if invalid
466
+ *
467
+ * @example
468
+ * ```typescript
469
+ * const result = validateEventName('Button Clicked');
470
+ * if (!result.valid) {
471
+ * console.error('Invalid event name:', result.reason);
472
+ * }
473
+ * ```
474
+ */
475
+ export function validateEventName(event: string): {
476
+ valid: boolean;
477
+ reason?: string;
478
+ } {
479
+ if (!event || typeof event !== 'string') {
480
+ return { valid: false, reason: 'Event name must be a non-empty string' };
481
+ }
482
+
483
+ if (event.trim() === '') {
484
+ return { valid: false, reason: 'Event name cannot be empty or whitespace only' };
485
+ }
486
+
487
+ if (event.length > 255) {
488
+ return { valid: false, reason: 'Event name cannot exceed 255 characters' };
489
+ }
490
+
491
+ // Check for XSS patterns
492
+ if (Object.values(XSS_PATTERNS).some(pattern => pattern.test(event))) {
493
+ return { valid: false, reason: 'Event name contains potentially malicious content' };
494
+ }
495
+
496
+ return { valid: true };
497
+ }
498
+
499
+ /**
500
+ * Safe property value types that can be tracked.
501
+ *
502
+ * Only includes primitive types that are safe to send to analytics providers.
503
+ */
504
+ export type SafePropertyValue = string | number | boolean | null | undefined;
505
+
506
+ /**
507
+ * Safe properties object type.
508
+ *
509
+ * Properties object with only safe value types.
510
+ */
511
+ export type SafeProperties = Record<string, SafePropertyValue | SafePropertyValue[]>;
512
+
513
+ /**
514
+ * Create a type-safe analytics properties object.
515
+ *
516
+ * Sanitizes raw properties and returns a type-safe properties object.
517
+ * Automatically strips PII in production and HTML/scripts always.
518
+ *
519
+ * @param {Record<string, unknown>} properties - Raw properties to make safe
520
+ * @returns {SafeProperties} Type-safe properties object
521
+ *
522
+ * @example
523
+ * ```typescript
524
+ * const safeProps = createSafeProperties({
525
+ * userId: 'user-123',
526
+ * email: 'user@example.com', // Will be redacted in production
527
+ * html: '<script>alert("xss")</script>' // Will be stripped
528
+ * });
529
+ * ```
530
+ */
531
+ export function createSafeProperties(properties: Record<string, unknown>): SafeProperties {
532
+ const result = sanitizeProperties(properties, {
533
+ stripPII: process.env.NODE_ENV === 'production',
534
+ stripHTML: true,
535
+ allowDangerousKeys: false,
536
+ });
537
+
538
+ if (result.warnings.length > 0 && process.env.NODE_ENV === 'development') {
539
+ logWarn('Created safe properties with sanitization', {
540
+ warnings: result.warnings,
541
+ });
542
+ }
543
+
544
+ return result.data as SafeProperties;
545
+ }
@@ -0,0 +1,161 @@
1
+ /**
2
+ * @fileoverview Client-safe validation utilities for analytics configuration
3
+ * Client-safe validation utilities for analytics configuration
4
+ * This file contains only validation functions that are safe to use in browser environments
5
+ */
6
+
7
+ import { PROVIDER_REQUIREMENTS } from './config';
8
+
9
+ import type { AnalyticsConfig, ProviderConfig } from '../types/types';
10
+
11
+ interface ValidationError {
12
+ field: string;
13
+ message: string;
14
+ provider: string;
15
+ }
16
+
17
+ interface ValidationResult {
18
+ errors: ValidationError[];
19
+ isValid: boolean;
20
+ warnings: string[];
21
+ }
22
+
23
+ /**
24
+ * Comprehensive configuration validation (client-safe version)
25
+ * Accepts unknown input for defensive validation of potentially malformed configs
26
+ */
27
+ export function validateAnalyticsConfig(config: unknown): ValidationResult {
28
+ const errors: ValidationError[] = [];
29
+ const warnings: string[] = [];
30
+
31
+ // Check if config exists and is an object
32
+ if (!config || typeof config !== 'object') {
33
+ errors.push({
34
+ provider: 'global',
35
+ field: 'config',
36
+ message: 'Analytics configuration is required and must be an object',
37
+ });
38
+ return { isValid: false, errors, warnings };
39
+ }
40
+
41
+ const typedConfig = config as AnalyticsConfig;
42
+
43
+ // Check if providers object exists
44
+ if (!typedConfig.providers || typeof typedConfig.providers !== 'object') {
45
+ errors.push({
46
+ provider: 'global',
47
+ field: 'providers',
48
+ message: 'Providers configuration is required and must be an object',
49
+ });
50
+ return { isValid: false, errors, warnings };
51
+ }
52
+
53
+ // Check if at least one provider is configured
54
+ const providerCount = Object.keys(typedConfig.providers).length;
55
+ if (providerCount === 0) {
56
+ warnings.push('No providers configured. Analytics will not track any events.');
57
+ }
58
+
59
+ // Validate each provider
60
+ for (const [providerName, providerConfig] of Object.entries(typedConfig.providers)) {
61
+ const providerErrors = validateProvider(providerName, providerConfig);
62
+ errors.push(...providerErrors);
63
+ }
64
+
65
+ // Environment-specific warnings
66
+ if (typedConfig.providers.mixpanel) {
67
+ warnings.push(
68
+ 'Mixpanel provider configured on client-side. Consider using server-side for better performance.',
69
+ );
70
+ }
71
+
72
+ return {
73
+ isValid: errors.length === 0,
74
+ errors,
75
+ warnings,
76
+ };
77
+ }
78
+
79
+ /**
80
+ * Validate a single provider configuration
81
+ */
82
+ export function validateProvider(providerName: string, config: ProviderConfig): ValidationError[] {
83
+ const errors: ValidationError[] = [];
84
+
85
+ // Check if provider is known
86
+ const knownProviders = ['segment', 'posthog', 'vercel', 'console', 'mixpanel'];
87
+ if (!knownProviders.includes(providerName)) {
88
+ errors.push({
89
+ provider: providerName,
90
+ field: 'name',
91
+ message: `Unknown provider '${providerName}'. Known providers: ${knownProviders.join(', ')}`,
92
+ });
93
+ return errors;
94
+ }
95
+
96
+ // Check required fields
97
+ const requiredFields = PROVIDER_REQUIREMENTS[providerName] ?? [];
98
+ for (const field of requiredFields) {
99
+ const value = config[field as keyof ProviderConfig];
100
+
101
+ if (!value) {
102
+ errors.push({
103
+ provider: providerName,
104
+ field,
105
+ message: `Required field '${field}' is missing for provider '${providerName}'`,
106
+ });
107
+ } else if (typeof value === 'string' && value.trim() === '') {
108
+ errors.push({
109
+ provider: providerName,
110
+ field,
111
+ message: `Required field '${field}' cannot be empty for provider '${providerName}'`,
112
+ });
113
+ }
114
+ }
115
+
116
+ // Provider-specific validation
117
+ switch (providerName) {
118
+ case 'segment':
119
+ if (config.writeKey && !isValidSegmentWriteKey(config.writeKey)) {
120
+ errors.push({
121
+ provider: providerName,
122
+ field: 'writeKey',
123
+ message: 'Segment writeKey appears to be invalid format',
124
+ });
125
+ }
126
+ break;
127
+
128
+ case 'posthog':
129
+ if (config.apiKey && !isValidPostHogApiKey(config.apiKey)) {
130
+ errors.push({
131
+ provider: providerName,
132
+ field: 'apiKey',
133
+ message: 'PostHog apiKey appears to be invalid format',
134
+ });
135
+ }
136
+ break;
137
+ }
138
+
139
+ return errors;
140
+ }
141
+
142
+ /**
143
+ * Helper functions for format validation
144
+ */
145
+ function isValidSegmentWriteKey(writeKey: string): boolean {
146
+ // Segment write keys are typically 32 characters, alphanumeric
147
+ return /^[\dA-Za-z]{20,40}$/.test(writeKey);
148
+ }
149
+
150
+ function isValidPostHogApiKey(apiKey: string): boolean {
151
+ // PostHog API keys start with 'phc_' followed by alphanumeric characters
152
+ return /^phc_[\dA-Za-z]{43}$/.test(apiKey);
153
+ }
154
+
155
+ /**
156
+ * Utility to validate configuration (client-safe version without throwing)
157
+ * Returns validation result instead of throwing errors
158
+ */
159
+ export function validateConfig(config: AnalyticsConfig): ValidationResult {
160
+ return validateAnalyticsConfig(config);
161
+ }