@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.
- package/README.md +509 -0
- package/dist/ai-YMnynb-t.mjs +3347 -0
- package/dist/ai-YMnynb-t.mjs.map +1 -0
- package/dist/chunk-DQk6qfdC.mjs +18 -0
- package/dist/client-CTzJVFU5.mjs +9 -0
- package/dist/client-CTzJVFU5.mjs.map +1 -0
- package/dist/client-CcFTauAh.mjs +54 -0
- package/dist/client-CcFTauAh.mjs.map +1 -0
- package/dist/client-CeOLjbac.mjs +281 -0
- package/dist/client-CeOLjbac.mjs.map +1 -0
- package/dist/client-D339NFJS.mjs +267 -0
- package/dist/client-D339NFJS.mjs.map +1 -0
- package/dist/client-next.d.mts +62 -0
- package/dist/client-next.d.mts.map +1 -0
- package/dist/client-next.mjs +525 -0
- package/dist/client-next.mjs.map +1 -0
- package/dist/client.d.mts +30 -0
- package/dist/client.d.mts.map +1 -0
- package/dist/client.mjs +186 -0
- package/dist/client.mjs.map +1 -0
- package/dist/config-DPS6bSYo.d.mts +34 -0
- package/dist/config-DPS6bSYo.d.mts.map +1 -0
- package/dist/config-P6P5adJg.mjs +287 -0
- package/dist/config-P6P5adJg.mjs.map +1 -0
- package/dist/console-8bND3mMU.mjs +128 -0
- package/dist/console-8bND3mMU.mjs.map +1 -0
- package/dist/ecommerce-Cgu4wlux.mjs +993 -0
- package/dist/ecommerce-Cgu4wlux.mjs.map +1 -0
- package/dist/emitters-6-nKo8i-.mjs +208 -0
- package/dist/emitters-6-nKo8i-.mjs.map +1 -0
- package/dist/emitters-DldkVSPp.d.mts +12 -0
- package/dist/emitters-DldkVSPp.d.mts.map +1 -0
- package/dist/index-BfNWgfa5.d.mts +1494 -0
- package/dist/index-BfNWgfa5.d.mts.map +1 -0
- package/dist/index-BkIWe--N.d.mts +953 -0
- package/dist/index-BkIWe--N.d.mts.map +1 -0
- package/dist/index-jPzXRn52.d.mts +184 -0
- package/dist/index-jPzXRn52.d.mts.map +1 -0
- package/dist/manager-DvRRjza6.d.mts +76 -0
- package/dist/manager-DvRRjza6.d.mts.map +1 -0
- package/dist/posthog-bootstrap-CYfIy_WS.mjs +1769 -0
- package/dist/posthog-bootstrap-CYfIy_WS.mjs.map +1 -0
- package/dist/posthog-bootstrap-DWxFrxlt.d.mts +81 -0
- package/dist/posthog-bootstrap-DWxFrxlt.d.mts.map +1 -0
- package/dist/providers-http-client.d.mts +37 -0
- package/dist/providers-http-client.d.mts.map +1 -0
- package/dist/providers-http-client.mjs +320 -0
- package/dist/providers-http-client.mjs.map +1 -0
- package/dist/providers-http-server.d.mts +31 -0
- package/dist/providers-http-server.d.mts.map +1 -0
- package/dist/providers-http-server.mjs +297 -0
- package/dist/providers-http-server.mjs.map +1 -0
- package/dist/providers-http.d.mts +46 -0
- package/dist/providers-http.d.mts.map +1 -0
- package/dist/providers-http.mjs +4 -0
- package/dist/server-edge.d.mts +9 -0
- package/dist/server-edge.d.mts.map +1 -0
- package/dist/server-edge.mjs +373 -0
- package/dist/server-edge.mjs.map +1 -0
- package/dist/server-next.d.mts +67 -0
- package/dist/server-next.d.mts.map +1 -0
- package/dist/server-next.mjs +193 -0
- package/dist/server-next.mjs.map +1 -0
- package/dist/server.d.mts +10 -0
- package/dist/server.mjs +7 -0
- package/dist/service-cYtBBL8x.mjs +945 -0
- package/dist/service-cYtBBL8x.mjs.map +1 -0
- package/dist/shared.d.mts +16 -0
- package/dist/shared.d.mts.map +1 -0
- package/dist/shared.mjs +93 -0
- package/dist/shared.mjs.map +1 -0
- package/dist/types-BxBnNQ0V.d.mts +354 -0
- package/dist/types-BxBnNQ0V.d.mts.map +1 -0
- package/dist/types-CBvxUEaF.d.mts +216 -0
- package/dist/types-CBvxUEaF.d.mts.map +1 -0
- package/dist/types.d.mts +4 -0
- package/dist/types.mjs +0 -0
- package/dist/vercel-types-lwakUfoI.d.mts +102 -0
- package/dist/vercel-types-lwakUfoI.d.mts.map +1 -0
- package/package.json +129 -0
- package/src/client/index.ts +164 -0
- package/src/client/manager.ts +71 -0
- package/src/client/next/components.tsx +270 -0
- package/src/client/next/hooks.ts +217 -0
- package/src/client/next/manager.ts +141 -0
- package/src/client/next.ts +144 -0
- package/src/client-next.ts +101 -0
- package/src/client.ts +89 -0
- package/src/examples/ai-sdk-patterns.ts +583 -0
- package/src/examples/emitter-patterns.ts +476 -0
- package/src/examples/nextjs-emitter-patterns.tsx +403 -0
- package/src/next/app-router.tsx +564 -0
- package/src/next/client.ts +419 -0
- package/src/next/index.ts +84 -0
- package/src/next/middleware.ts +429 -0
- package/src/next/rsc.tsx +300 -0
- package/src/next/server.ts +253 -0
- package/src/next/types.d.ts +220 -0
- package/src/providers/base-provider.ts +419 -0
- package/src/providers/console/client.ts +10 -0
- package/src/providers/console/index.ts +152 -0
- package/src/providers/console/server.ts +6 -0
- package/src/providers/console/types.ts +15 -0
- package/src/providers/http/client.ts +464 -0
- package/src/providers/http/index.ts +30 -0
- package/src/providers/http/server.ts +396 -0
- package/src/providers/http/types.ts +135 -0
- package/src/providers/posthog/client.ts +518 -0
- package/src/providers/posthog/index.ts +11 -0
- package/src/providers/posthog/server.ts +329 -0
- package/src/providers/posthog/types.ts +104 -0
- package/src/providers/segment/client.ts +113 -0
- package/src/providers/segment/index.ts +11 -0
- package/src/providers/segment/server.ts +115 -0
- package/src/providers/segment/types.ts +51 -0
- package/src/providers/vercel/client.ts +102 -0
- package/src/providers/vercel/index.ts +11 -0
- package/src/providers/vercel/server.ts +89 -0
- package/src/providers/vercel/types.ts +27 -0
- package/src/server/index.ts +103 -0
- package/src/server/manager.ts +62 -0
- package/src/server/next.ts +210 -0
- package/src/server-edge.ts +442 -0
- package/src/server-next.ts +39 -0
- package/src/server.ts +106 -0
- package/src/shared/emitters/ai/README.md +981 -0
- package/src/shared/emitters/ai/events/agent.ts +130 -0
- package/src/shared/emitters/ai/events/artifacts.ts +167 -0
- package/src/shared/emitters/ai/events/chat.ts +126 -0
- package/src/shared/emitters/ai/events/chatbot-ecommerce.ts +133 -0
- package/src/shared/emitters/ai/events/completion.ts +103 -0
- package/src/shared/emitters/ai/events/content-generation.ts +347 -0
- package/src/shared/emitters/ai/events/conversation.ts +332 -0
- package/src/shared/emitters/ai/events/product-features.ts +1402 -0
- package/src/shared/emitters/ai/events/streaming.ts +114 -0
- package/src/shared/emitters/ai/events/tool.ts +93 -0
- package/src/shared/emitters/ai/index.ts +69 -0
- package/src/shared/emitters/ai/track-ai-sdk.ts +74 -0
- package/src/shared/emitters/ai/track-ai.ts +50 -0
- package/src/shared/emitters/ai/types.ts +1041 -0
- package/src/shared/emitters/ai/utils.ts +468 -0
- package/src/shared/emitters/ecommerce/events/cart-checkout.ts +106 -0
- package/src/shared/emitters/ecommerce/events/coupon.ts +49 -0
- package/src/shared/emitters/ecommerce/events/engagement.ts +61 -0
- package/src/shared/emitters/ecommerce/events/marketplace.ts +119 -0
- package/src/shared/emitters/ecommerce/events/order.ts +199 -0
- package/src/shared/emitters/ecommerce/events/product.ts +205 -0
- package/src/shared/emitters/ecommerce/events/registry.ts +123 -0
- package/src/shared/emitters/ecommerce/events/wishlist-sharing.ts +140 -0
- package/src/shared/emitters/ecommerce/index.ts +46 -0
- package/src/shared/emitters/ecommerce/track-ecommerce.ts +53 -0
- package/src/shared/emitters/ecommerce/types.ts +314 -0
- package/src/shared/emitters/ecommerce/utils.ts +216 -0
- package/src/shared/emitters/emitter-types.ts +974 -0
- package/src/shared/emitters/emitters.ts +292 -0
- package/src/shared/emitters/helpers.ts +419 -0
- package/src/shared/emitters/index.ts +66 -0
- package/src/shared/index.ts +142 -0
- package/src/shared/ingestion/index.ts +66 -0
- package/src/shared/ingestion/schemas.ts +386 -0
- package/src/shared/ingestion/service.ts +628 -0
- package/src/shared/node22-features.ts +848 -0
- package/src/shared/providers/console-provider.ts +160 -0
- package/src/shared/types/base-types.ts +54 -0
- package/src/shared/types/console-types.ts +19 -0
- package/src/shared/types/posthog-types.ts +131 -0
- package/src/shared/types/segment-types.ts +15 -0
- package/src/shared/types/types.ts +397 -0
- package/src/shared/types/vercel-types.ts +19 -0
- package/src/shared/utils/config-client.ts +19 -0
- package/src/shared/utils/config.ts +250 -0
- package/src/shared/utils/emitter-adapter.ts +212 -0
- package/src/shared/utils/manager.test.ts +36 -0
- package/src/shared/utils/manager.ts +1322 -0
- package/src/shared/utils/posthog-bootstrap.ts +136 -0
- package/src/shared/utils/posthog-client-utils.ts +48 -0
- package/src/shared/utils/posthog-next-utils.ts +282 -0
- package/src/shared/utils/posthog-server-utils.ts +210 -0
- package/src/shared/utils/rate-limit.ts +289 -0
- package/src/shared/utils/security.ts +545 -0
- package/src/shared/utils/validation-client.ts +161 -0
- package/src/shared/utils/validation.ts +399 -0
- package/src/shared.ts +155 -0
- 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
|
+
}
|