@outlit/core 0.0.0-canary-202512100315-e4db4b4-20251210031539 → 0.0.0-canary-202512110053-50cdbec-20251211005400

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/dist/index.d.mts CHANGED
@@ -1,5 +1,6 @@
1
- type EventType = "pageview" | "form" | "identify" | "custom";
2
- type SourceType = "pixel" | "server" | "integration";
1
+ type EventType = "pageview" | "form" | "identify" | "custom" | "calendar";
2
+ type CalendarProvider = "cal.com" | "calendly" | "unknown";
3
+ type SourceType = "client" | "server" | "integration";
3
4
  interface UtmParams {
4
5
  source?: string;
5
6
  medium?: string;
@@ -60,7 +61,19 @@ interface CustomEvent extends BaseEvent {
60
61
  eventName: string;
61
62
  properties?: Record<string, string | number | boolean | null>;
62
63
  }
63
- type TrackerEvent = PageviewEvent | FormEvent | IdentifyEvent | CustomEvent;
64
+ interface CalendarEvent extends BaseEvent {
65
+ type: "calendar";
66
+ provider: CalendarProvider;
67
+ eventType?: string;
68
+ startTime?: string;
69
+ endTime?: string;
70
+ duration?: number;
71
+ isRecurring?: boolean;
72
+ /** Available when identity is passed via webhooks or manual integration */
73
+ inviteeEmail?: string;
74
+ inviteeName?: string;
75
+ }
76
+ type TrackerEvent = PageviewEvent | FormEvent | IdentifyEvent | CustomEvent | CalendarEvent;
64
77
  interface IngestPayload {
65
78
  visitorId?: string;
66
79
  source: SourceType;
@@ -187,6 +200,19 @@ declare function buildCustomEvent(params: BaseEventParams & {
187
200
  eventName: string;
188
201
  properties?: Record<string, string | number | boolean | null>;
189
202
  }): CustomEvent;
203
+ /**
204
+ * Build a calendar booking event.
205
+ */
206
+ declare function buildCalendarEvent(params: BaseEventParams & {
207
+ provider: CalendarProvider;
208
+ eventType?: string;
209
+ startTime?: string;
210
+ endTime?: string;
211
+ duration?: number;
212
+ isRecurring?: boolean;
213
+ inviteeEmail?: string;
214
+ inviteeName?: string;
215
+ }): CalendarEvent;
190
216
  /**
191
217
  * Build an ingest payload from events.
192
218
  */
@@ -200,4 +226,4 @@ declare const MAX_BATCH_SIZE = 100;
200
226
  */
201
227
  declare function batchEvents(events: TrackerEvent[]): TrackerEvent[][];
202
228
 
203
- export { type BrowserIdentifyOptions, type BrowserTrackOptions, type CustomEvent, DEFAULT_API_HOST, DEFAULT_DENIED_FORM_FIELDS, type EventType, type ExtractedIdentity, type FormEvent, type IdentifyEvent, type IngestPayload, type IngestResponse, MAX_BATCH_SIZE, type PageviewEvent, type ServerIdentifyOptions, type ServerTrackOptions, type SourceType, type TrackerConfig, type TrackerEvent, type UtmParams, batchEvents, buildCustomEvent, buildFormEvent, buildIdentifyEvent, buildIngestPayload, buildPageviewEvent, extractIdentityFromForm, extractPathFromUrl, extractUtmParams, findEmailField, findNameFields, isFieldDenied, isValidEmail, sanitizeFormFields, validateServerIdentity };
229
+ export { type BrowserIdentifyOptions, type BrowserTrackOptions, type CalendarEvent, type CalendarProvider, type CustomEvent, DEFAULT_API_HOST, DEFAULT_DENIED_FORM_FIELDS, type EventType, type ExtractedIdentity, type FormEvent, type IdentifyEvent, type IngestPayload, type IngestResponse, MAX_BATCH_SIZE, type PageviewEvent, type ServerIdentifyOptions, type ServerTrackOptions, type SourceType, type TrackerConfig, type TrackerEvent, type UtmParams, batchEvents, buildCalendarEvent, buildCustomEvent, buildFormEvent, buildIdentifyEvent, buildIngestPayload, buildPageviewEvent, extractIdentityFromForm, extractPathFromUrl, extractUtmParams, findEmailField, findNameFields, isFieldDenied, isValidEmail, sanitizeFormFields, validateServerIdentity };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
- type EventType = "pageview" | "form" | "identify" | "custom";
2
- type SourceType = "pixel" | "server" | "integration";
1
+ type EventType = "pageview" | "form" | "identify" | "custom" | "calendar";
2
+ type CalendarProvider = "cal.com" | "calendly" | "unknown";
3
+ type SourceType = "client" | "server" | "integration";
3
4
  interface UtmParams {
4
5
  source?: string;
5
6
  medium?: string;
@@ -60,7 +61,19 @@ interface CustomEvent extends BaseEvent {
60
61
  eventName: string;
61
62
  properties?: Record<string, string | number | boolean | null>;
62
63
  }
63
- type TrackerEvent = PageviewEvent | FormEvent | IdentifyEvent | CustomEvent;
64
+ interface CalendarEvent extends BaseEvent {
65
+ type: "calendar";
66
+ provider: CalendarProvider;
67
+ eventType?: string;
68
+ startTime?: string;
69
+ endTime?: string;
70
+ duration?: number;
71
+ isRecurring?: boolean;
72
+ /** Available when identity is passed via webhooks or manual integration */
73
+ inviteeEmail?: string;
74
+ inviteeName?: string;
75
+ }
76
+ type TrackerEvent = PageviewEvent | FormEvent | IdentifyEvent | CustomEvent | CalendarEvent;
64
77
  interface IngestPayload {
65
78
  visitorId?: string;
66
79
  source: SourceType;
@@ -187,6 +200,19 @@ declare function buildCustomEvent(params: BaseEventParams & {
187
200
  eventName: string;
188
201
  properties?: Record<string, string | number | boolean | null>;
189
202
  }): CustomEvent;
203
+ /**
204
+ * Build a calendar booking event.
205
+ */
206
+ declare function buildCalendarEvent(params: BaseEventParams & {
207
+ provider: CalendarProvider;
208
+ eventType?: string;
209
+ startTime?: string;
210
+ endTime?: string;
211
+ duration?: number;
212
+ isRecurring?: boolean;
213
+ inviteeEmail?: string;
214
+ inviteeName?: string;
215
+ }): CalendarEvent;
190
216
  /**
191
217
  * Build an ingest payload from events.
192
218
  */
@@ -200,4 +226,4 @@ declare const MAX_BATCH_SIZE = 100;
200
226
  */
201
227
  declare function batchEvents(events: TrackerEvent[]): TrackerEvent[][];
202
228
 
203
- export { type BrowserIdentifyOptions, type BrowserTrackOptions, type CustomEvent, DEFAULT_API_HOST, DEFAULT_DENIED_FORM_FIELDS, type EventType, type ExtractedIdentity, type FormEvent, type IdentifyEvent, type IngestPayload, type IngestResponse, MAX_BATCH_SIZE, type PageviewEvent, type ServerIdentifyOptions, type ServerTrackOptions, type SourceType, type TrackerConfig, type TrackerEvent, type UtmParams, batchEvents, buildCustomEvent, buildFormEvent, buildIdentifyEvent, buildIngestPayload, buildPageviewEvent, extractIdentityFromForm, extractPathFromUrl, extractUtmParams, findEmailField, findNameFields, isFieldDenied, isValidEmail, sanitizeFormFields, validateServerIdentity };
229
+ export { type BrowserIdentifyOptions, type BrowserTrackOptions, type CalendarEvent, type CalendarProvider, type CustomEvent, DEFAULT_API_HOST, DEFAULT_DENIED_FORM_FIELDS, type EventType, type ExtractedIdentity, type FormEvent, type IdentifyEvent, type IngestPayload, type IngestResponse, MAX_BATCH_SIZE, type PageviewEvent, type ServerIdentifyOptions, type ServerTrackOptions, type SourceType, type TrackerConfig, type TrackerEvent, type UtmParams, batchEvents, buildCalendarEvent, buildCustomEvent, buildFormEvent, buildIdentifyEvent, buildIngestPayload, buildPageviewEvent, extractIdentityFromForm, extractPathFromUrl, extractUtmParams, findEmailField, findNameFields, isFieldDenied, isValidEmail, sanitizeFormFields, validateServerIdentity };
package/dist/index.js CHANGED
@@ -24,6 +24,7 @@ __export(src_exports, {
24
24
  DEFAULT_DENIED_FORM_FIELDS: () => DEFAULT_DENIED_FORM_FIELDS,
25
25
  MAX_BATCH_SIZE: () => MAX_BATCH_SIZE,
26
26
  batchEvents: () => batchEvents,
27
+ buildCalendarEvent: () => buildCalendarEvent,
27
28
  buildCustomEvent: () => buildCustomEvent,
28
29
  buildFormEvent: () => buildFormEvent,
29
30
  buildIdentifyEvent: () => buildIdentifyEvent,
@@ -293,6 +294,37 @@ function buildCustomEvent(params) {
293
294
  properties
294
295
  };
295
296
  }
297
+ function buildCalendarEvent(params) {
298
+ const {
299
+ url,
300
+ referrer,
301
+ timestamp,
302
+ provider,
303
+ eventType,
304
+ startTime,
305
+ endTime,
306
+ duration,
307
+ isRecurring,
308
+ inviteeEmail,
309
+ inviteeName
310
+ } = params;
311
+ return {
312
+ type: "calendar",
313
+ timestamp: timestamp ?? Date.now(),
314
+ url,
315
+ path: extractPathFromUrl(url),
316
+ referrer,
317
+ utm: extractUtmParams(url),
318
+ provider,
319
+ eventType,
320
+ startTime,
321
+ endTime,
322
+ duration,
323
+ isRecurring,
324
+ inviteeEmail,
325
+ inviteeName
326
+ };
327
+ }
296
328
  function buildIngestPayload(visitorId, source, events) {
297
329
  return {
298
330
  visitorId,
@@ -314,6 +346,7 @@ function batchEvents(events) {
314
346
  DEFAULT_DENIED_FORM_FIELDS,
315
347
  MAX_BATCH_SIZE,
316
348
  batchEvents,
349
+ buildCalendarEvent,
317
350
  buildCustomEvent,
318
351
  buildFormEvent,
319
352
  buildIdentifyEvent,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/types.ts","../src/utils.ts","../src/payload.ts"],"sourcesContent":["// Types\nexport type {\n EventType,\n SourceType,\n UtmParams,\n TrackerConfig,\n BrowserTrackOptions,\n BrowserIdentifyOptions,\n ServerTrackOptions,\n ServerIdentifyOptions,\n PageviewEvent,\n FormEvent,\n IdentifyEvent,\n CustomEvent,\n TrackerEvent,\n IngestPayload,\n IngestResponse,\n} from \"./types\"\n\n// Constants\nexport { DEFAULT_API_HOST, DEFAULT_DENIED_FORM_FIELDS } from \"./types\"\n\n// Utilities\nexport {\n extractUtmParams,\n extractPathFromUrl,\n isFieldDenied,\n sanitizeFormFields,\n validateServerIdentity,\n // Auto-identify utilities\n isValidEmail,\n findEmailField,\n findNameFields,\n extractIdentityFromForm,\n} from \"./utils\"\n\n// Auto-identify types\nexport type { ExtractedIdentity } from \"./utils\"\n\n// Payload builders\nexport {\n buildPageviewEvent,\n buildFormEvent,\n buildIdentifyEvent,\n buildCustomEvent,\n buildIngestPayload,\n batchEvents,\n MAX_BATCH_SIZE,\n} from \"./payload\"\n","// ============================================\n// EVENT TYPES\n// ============================================\n\nexport type EventType = \"pageview\" | \"form\" | \"identify\" | \"custom\"\n\nexport type SourceType = \"pixel\" | \"server\" | \"integration\"\n\n// ============================================\n// UTM PARAMETERS\n// ============================================\n\nexport interface UtmParams {\n source?: string\n medium?: string\n campaign?: string\n term?: string\n content?: string\n}\n\n// ============================================\n// TRACKER CONFIGURATION\n// ============================================\n\nexport interface TrackerConfig {\n publicKey: string\n apiHost?: string // default: 'https://app.outlit.ai'\n}\n\n// ============================================\n// BROWSER-SPECIFIC TYPES (anonymous allowed)\n// visitorId is auto-managed by the browser SDK\n// ============================================\n\nexport interface BrowserTrackOptions {\n eventName: string\n properties?: Record<string, string | number | boolean | null>\n}\n\nexport interface BrowserIdentifyOptions {\n email?: string\n userId?: string\n traits?: Record<string, string | number | boolean | null>\n}\n\n// ============================================\n// SERVER-SPECIFIC TYPES (identity required)\n// No anonymous tracking - must identify the user\n// ============================================\n\nexport interface ServerTrackOptions {\n email?: string // At least one of email/userId required\n userId?: string // At least one of email/userId required\n eventName: string\n properties?: Record<string, string | number | boolean | null>\n timestamp?: number\n}\n\nexport interface ServerIdentifyOptions {\n email?: string // At least one of email/userId required\n userId?: string // At least one of email/userId required\n traits?: Record<string, string | number | boolean | null>\n}\n\n// ============================================\n// INTERNAL EVENT TYPES\n// These are the full event objects sent to the API\n// ============================================\n\ninterface BaseEvent {\n type: EventType\n timestamp: number // Unix timestamp in milliseconds\n url: string\n path: string\n referrer?: string\n utm?: UtmParams\n}\n\nexport interface PageviewEvent extends BaseEvent {\n type: \"pageview\"\n title?: string\n}\n\nexport interface FormEvent extends BaseEvent {\n type: \"form\"\n formId?: string\n formFields?: Record<string, string>\n}\n\nexport interface IdentifyEvent extends BaseEvent {\n type: \"identify\"\n email?: string\n userId?: string\n traits?: Record<string, string | number | boolean | null>\n}\n\nexport interface CustomEvent extends BaseEvent {\n type: \"custom\"\n eventName: string\n properties?: Record<string, string | number | boolean | null>\n}\n\nexport type TrackerEvent = PageviewEvent | FormEvent | IdentifyEvent | CustomEvent\n\n// ============================================\n// INGEST PAYLOAD\n// This is what gets sent to the API\n// ============================================\n\nexport interface IngestPayload {\n visitorId?: string // Required for pixel, optional for server\n source: SourceType\n events: TrackerEvent[]\n}\n\n// ============================================\n// API RESPONSE\n// ============================================\n\nexport interface IngestResponse {\n success: boolean\n processed: number\n errors?: Array<{\n index: number\n message: string\n }>\n}\n\n// ============================================\n// CONSTANTS\n// ============================================\n\nexport const DEFAULT_API_HOST = \"https://app.outlit.ai\"\n\nexport const DEFAULT_DENIED_FORM_FIELDS = [\n \"password\",\n \"passwd\",\n \"pass\",\n \"pwd\",\n \"token\",\n \"secret\",\n \"api_key\",\n \"apikey\",\n \"api-key\",\n \"credit_card\",\n \"creditcard\",\n \"credit-card\",\n \"cc_number\",\n \"ccnumber\",\n \"card_number\",\n \"cardnumber\",\n \"cvv\",\n \"cvc\",\n \"ssn\",\n \"social_security\",\n \"socialsecurity\",\n \"bank_account\",\n \"bankaccount\",\n \"routing_number\",\n \"routingnumber\",\n]\n","import { DEFAULT_DENIED_FORM_FIELDS, type UtmParams } from \"./types\"\n\n// ============================================\n// UTM EXTRACTION\n// ============================================\n\n/**\n * Extract UTM parameters from a URL.\n */\nexport function extractUtmParams(url: string): UtmParams | undefined {\n try {\n const urlObj = new URL(url)\n const params = urlObj.searchParams\n\n const utm: UtmParams = {}\n\n if (params.has(\"utm_source\")) utm.source = params.get(\"utm_source\") ?? undefined\n if (params.has(\"utm_medium\")) utm.medium = params.get(\"utm_medium\") ?? undefined\n if (params.has(\"utm_campaign\")) utm.campaign = params.get(\"utm_campaign\") ?? undefined\n if (params.has(\"utm_term\")) utm.term = params.get(\"utm_term\") ?? undefined\n if (params.has(\"utm_content\")) utm.content = params.get(\"utm_content\") ?? undefined\n\n return Object.keys(utm).length > 0 ? utm : undefined\n } catch {\n return undefined\n }\n}\n\n/**\n * Extract path from a URL.\n */\nexport function extractPathFromUrl(url: string): string {\n try {\n const urlObj = new URL(url)\n return urlObj.pathname\n } catch {\n return \"/\"\n }\n}\n\n// ============================================\n// FORM FIELD SANITIZATION\n// ============================================\n\n/**\n * Check if a field name should be denied (case-insensitive).\n */\nexport function isFieldDenied(fieldName: string, denylist: string[]): boolean {\n const normalizedName = fieldName.toLowerCase().replace(/[-_\\s]/g, \"\")\n return denylist.some((denied) => {\n const normalizedDenied = denied.toLowerCase().replace(/[-_\\s]/g, \"\")\n return normalizedName.includes(normalizedDenied)\n })\n}\n\n/**\n * Check if a value looks like sensitive data (e.g., credit card number).\n */\nfunction looksLikeSensitiveValue(value: string): boolean {\n // Remove spaces and dashes\n const cleaned = value.replace(/[\\s-]/g, \"\")\n\n // Check for credit card patterns (13-19 digits)\n if (/^\\d{13,19}$/.test(cleaned)) {\n return true\n }\n\n // Check for SSN pattern (9 digits)\n if (/^\\d{9}$/.test(cleaned) || /^\\d{3}-\\d{2}-\\d{4}$/.test(value)) {\n return true\n }\n\n return false\n}\n\n/**\n * Sanitize form fields by removing sensitive data.\n * Returns a new object with denied fields removed.\n */\nexport function sanitizeFormFields(\n fields: Record<string, string> | undefined,\n customDenylist?: string[],\n): Record<string, string> | undefined {\n if (!fields) return undefined\n\n const denylist = customDenylist ?? DEFAULT_DENIED_FORM_FIELDS\n const sanitized: Record<string, string> = {}\n\n for (const [key, value] of Object.entries(fields)) {\n if (!isFieldDenied(key, denylist)) {\n // Also check for credit card patterns in values\n if (!looksLikeSensitiveValue(value)) {\n sanitized[key] = value\n }\n }\n }\n\n return Object.keys(sanitized).length > 0 ? sanitized : undefined\n}\n\n// ============================================\n// VISITOR ID DERIVATION (for server SDK)\n// ============================================\n\n/**\n * Derive a deterministic visitor ID from email and/or userId.\n * This is used by the server SDK to create consistent IDs for API compatibility.\n *\n * Uses a simple hash to create a UUID-like string that will be consistent\n * for the same email/userId combination.\n */\nexport function deriveVisitorIdFromIdentity(email?: string, userId?: string): string {\n const identity = [email?.toLowerCase(), userId].filter(Boolean).join(\"|\")\n if (!identity) {\n throw new Error(\"Either email or userId must be provided\")\n }\n\n // Simple hash function to create a deterministic UUID-like string\n let hash = 0\n for (let i = 0; i < identity.length; i++) {\n const char = identity.charCodeAt(i)\n hash = (hash << 5) - hash + char\n hash = hash & hash // Convert to 32-bit integer\n }\n\n // Convert to hex and format as UUID-like string\n const hex = Math.abs(hash).toString(16).padStart(8, \"0\")\n const part1 = hex.slice(0, 8)\n const part2 = identity.length.toString(16).padStart(4, \"0\")\n const part3 = \"4000\" // Version 4 UUID marker\n const part4 = (((hash >>> 16) & 0x0fff) | 0x8000).toString(16)\n const part5 = Math.abs(hash * 31)\n .toString(16)\n .padStart(12, \"0\")\n .slice(0, 12)\n\n return `${part1}-${part2}-${part3}-${part4}-${part5}`\n}\n\n// ============================================\n// VALIDATION\n// ============================================\n\n/**\n * Validate that at least one identity field is provided.\n * Used by the server SDK to enforce identity requirements.\n */\nexport function validateServerIdentity(email?: string, userId?: string): void {\n if (!email && !userId) {\n throw new Error(\n \"Server SDK requires either email or userId for all track/identify calls. \" +\n \"Anonymous tracking is only supported in the browser SDK.\",\n )\n }\n}\n\n// ============================================\n// AUTO-IDENTIFY: EMAIL & NAME EXTRACTION\n// ============================================\n\n/**\n * Validate that a string looks like a valid email address.\n */\nexport function isValidEmail(value: string): boolean {\n if (!value || typeof value !== \"string\") return false\n // Basic email regex - intentionally permissive to avoid false negatives\n const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n return emailRegex.test(value.trim())\n}\n\n/**\n * Email field name patterns (case-insensitive, normalized).\n * Order matters - more specific patterns first.\n */\nconst EMAIL_FIELD_PATTERNS = [\n /^e?-?mail$/i,\n /^email[_-]?address$/i,\n /^user[_-]?email$/i,\n /^work[_-]?email$/i,\n /^contact[_-]?email$/i,\n /^primary[_-]?email$/i,\n /^business[_-]?email$/i,\n]\n\n/**\n * Full name field patterns.\n */\nconst FULL_NAME_PATTERNS = [\n /^name$/i,\n /^full[_-]?name$/i,\n /^your[_-]?name$/i,\n /^customer[_-]?name$/i,\n /^contact[_-]?name$/i,\n /^display[_-]?name$/i,\n]\n\n/**\n * First name field patterns.\n */\nconst FIRST_NAME_PATTERNS = [\n /^first[_-]?name$/i,\n /^firstname$/i,\n /^first$/i,\n /^fname$/i,\n /^given[_-]?name$/i,\n /^forename$/i,\n]\n\n/**\n * Last name field patterns.\n */\nconst LAST_NAME_PATTERNS = [\n /^last[_-]?name$/i,\n /^lastname$/i,\n /^last$/i,\n /^lname$/i,\n /^surname$/i,\n /^family[_-]?name$/i,\n]\n\n/**\n * Check if a field name matches any of the given patterns.\n */\nfunction matchesPatterns(fieldName: string, patterns: RegExp[]): boolean {\n const normalized = fieldName.trim()\n return patterns.some((pattern) => pattern.test(normalized))\n}\n\n/**\n * Find an email value from form fields.\n *\n * Priority:\n * 1. Fields with input type=\"email\" (if inputTypes map provided)\n * 2. Field names matching email patterns\n * 3. Any field with a value that looks like an email\n *\n * @param fields - Form field key-value pairs\n * @param inputTypes - Optional map of field names to input types\n * @returns The email value if found, undefined otherwise\n */\nexport function findEmailField(\n fields: Record<string, string>,\n inputTypes?: Map<string, string>,\n): string | undefined {\n // Priority 1: Check fields with type=\"email\"\n if (inputTypes) {\n for (const [fieldName, inputType] of inputTypes.entries()) {\n if (inputType === \"email\") {\n const value = fields[fieldName]\n if (value && isValidEmail(value)) {\n return value.trim()\n }\n }\n }\n }\n\n // Priority 2: Check field names matching email patterns\n for (const [fieldName, value] of Object.entries(fields)) {\n if (matchesPatterns(fieldName, EMAIL_FIELD_PATTERNS) && isValidEmail(value)) {\n return value.trim()\n }\n }\n\n // Priority 3: Any field with email-like value (fallback)\n for (const value of Object.values(fields)) {\n if (isValidEmail(value)) {\n return value.trim()\n }\n }\n\n return undefined\n}\n\n/**\n * Extract name fields from form data.\n *\n * Looks for:\n * - Full name fields (name, full_name, etc.)\n * - First name fields (first_name, fname, etc.)\n * - Last name fields (last_name, lname, etc.)\n *\n * If only first/last names are found, combines them into a full name.\n *\n * @param fields - Form field key-value pairs\n * @returns Object with name, firstName, and/or lastName if found\n */\nexport function findNameFields(fields: Record<string, string>): {\n name?: string\n firstName?: string\n lastName?: string\n} {\n let fullName: string | undefined\n let firstName: string | undefined\n let lastName: string | undefined\n\n for (const [fieldName, value] of Object.entries(fields)) {\n const trimmedValue = value?.trim()\n if (!trimmedValue) continue\n\n // Check for full name\n if (!fullName && matchesPatterns(fieldName, FULL_NAME_PATTERNS)) {\n fullName = trimmedValue\n }\n\n // Check for first name\n if (!firstName && matchesPatterns(fieldName, FIRST_NAME_PATTERNS)) {\n firstName = trimmedValue\n }\n\n // Check for last name\n if (!lastName && matchesPatterns(fieldName, LAST_NAME_PATTERNS)) {\n lastName = trimmedValue\n }\n }\n\n const result: { name?: string; firstName?: string; lastName?: string } = {}\n\n // If we have a full name, use it\n if (fullName) {\n result.name = fullName\n }\n // If we have first and last, combine them\n else if (firstName && lastName) {\n result.name = `${firstName} ${lastName}`\n result.firstName = firstName\n result.lastName = lastName\n }\n // If we only have first name\n else if (firstName) {\n result.firstName = firstName\n }\n // If we only have last name\n else if (lastName) {\n result.lastName = lastName\n }\n\n return result\n}\n\n/**\n * Identity extracted from a form submission.\n */\nexport interface ExtractedIdentity {\n email: string\n name?: string\n firstName?: string\n lastName?: string\n}\n\n/**\n * Extract identity information (email + name) from form fields.\n *\n * Returns undefined if no valid email is found (email is required for identification).\n *\n * @param fields - Form field key-value pairs\n * @param inputTypes - Optional map of field names to input types\n * @returns Extracted identity with email and optional name fields, or undefined\n */\nexport function extractIdentityFromForm(\n fields: Record<string, string>,\n inputTypes?: Map<string, string>,\n): ExtractedIdentity | undefined {\n const email = findEmailField(fields, inputTypes)\n\n // Email is required for identification\n if (!email) {\n return undefined\n }\n\n const nameFields = findNameFields(fields)\n\n return {\n email,\n ...nameFields,\n }\n}\n","import type {\n CustomEvent,\n FormEvent,\n IdentifyEvent,\n IngestPayload,\n PageviewEvent,\n SourceType,\n TrackerEvent,\n UtmParams,\n} from \"./types\"\nimport { extractPathFromUrl, extractUtmParams } from \"./utils\"\n\n// ============================================\n// EVENT BUILDERS\n// ============================================\n\ninterface BaseEventParams {\n url: string\n referrer?: string\n timestamp?: number\n}\n\n/**\n * Build a pageview event.\n */\nexport function buildPageviewEvent(params: BaseEventParams & { title?: string }): PageviewEvent {\n const { url, referrer, timestamp, title } = params\n return {\n type: \"pageview\",\n timestamp: timestamp ?? Date.now(),\n url,\n path: extractPathFromUrl(url),\n referrer,\n utm: extractUtmParams(url),\n title,\n }\n}\n\n/**\n * Build a form event.\n */\nexport function buildFormEvent(\n params: BaseEventParams & {\n formId?: string\n formFields?: Record<string, string>\n },\n): FormEvent {\n const { url, referrer, timestamp, formId, formFields } = params\n return {\n type: \"form\",\n timestamp: timestamp ?? Date.now(),\n url,\n path: extractPathFromUrl(url),\n referrer,\n utm: extractUtmParams(url),\n formId,\n formFields,\n }\n}\n\n/**\n * Build an identify event.\n */\nexport function buildIdentifyEvent(\n params: BaseEventParams & {\n email?: string\n userId?: string\n traits?: Record<string, string | number | boolean | null>\n },\n): IdentifyEvent {\n const { url, referrer, timestamp, email, userId, traits } = params\n return {\n type: \"identify\",\n timestamp: timestamp ?? Date.now(),\n url,\n path: extractPathFromUrl(url),\n referrer,\n utm: extractUtmParams(url),\n email,\n userId,\n traits,\n }\n}\n\n/**\n * Build a custom event.\n */\nexport function buildCustomEvent(\n params: BaseEventParams & {\n eventName: string\n properties?: Record<string, string | number | boolean | null>\n },\n): CustomEvent {\n const { url, referrer, timestamp, eventName, properties } = params\n return {\n type: \"custom\",\n timestamp: timestamp ?? Date.now(),\n url,\n path: extractPathFromUrl(url),\n referrer,\n utm: extractUtmParams(url),\n eventName,\n properties,\n }\n}\n\n// ============================================\n// PAYLOAD BUILDER\n// ============================================\n\n/**\n * Build an ingest payload from events.\n */\nexport function buildIngestPayload(\n visitorId: string,\n source: SourceType,\n events: TrackerEvent[],\n): IngestPayload {\n return {\n visitorId,\n source,\n events,\n }\n}\n\n// ============================================\n// BATCH HELPERS\n// ============================================\n\n/**\n * Maximum number of events in a single batch.\n */\nexport const MAX_BATCH_SIZE = 100\n\n/**\n * Split events into batches of MAX_BATCH_SIZE.\n */\nexport function batchEvents(events: TrackerEvent[]): TrackerEvent[][] {\n const batches: TrackerEvent[][] = []\n for (let i = 0; i < events.length; i += MAX_BATCH_SIZE) {\n batches.push(events.slice(i, i + MAX_BATCH_SIZE))\n }\n return batches\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/types.ts","../src/utils.ts","../src/payload.ts"],"sourcesContent":["// Types\nexport type {\n EventType,\n SourceType,\n CalendarProvider,\n UtmParams,\n TrackerConfig,\n BrowserTrackOptions,\n BrowserIdentifyOptions,\n ServerTrackOptions,\n ServerIdentifyOptions,\n PageviewEvent,\n FormEvent,\n IdentifyEvent,\n CustomEvent,\n CalendarEvent,\n TrackerEvent,\n IngestPayload,\n IngestResponse,\n} from \"./types\"\n\n// Constants\nexport { DEFAULT_API_HOST, DEFAULT_DENIED_FORM_FIELDS } from \"./types\"\n\n// Utilities\nexport {\n extractUtmParams,\n extractPathFromUrl,\n isFieldDenied,\n sanitizeFormFields,\n validateServerIdentity,\n // Auto-identify utilities\n isValidEmail,\n findEmailField,\n findNameFields,\n extractIdentityFromForm,\n} from \"./utils\"\n\n// Auto-identify types\nexport type { ExtractedIdentity } from \"./utils\"\n\n// Payload builders\nexport {\n buildPageviewEvent,\n buildFormEvent,\n buildIdentifyEvent,\n buildCustomEvent,\n buildCalendarEvent,\n buildIngestPayload,\n batchEvents,\n MAX_BATCH_SIZE,\n} from \"./payload\"\n","// ============================================\n// EVENT TYPES\n// ============================================\n\nexport type EventType = \"pageview\" | \"form\" | \"identify\" | \"custom\" | \"calendar\"\n\nexport type CalendarProvider = \"cal.com\" | \"calendly\" | \"unknown\"\n\nexport type SourceType = \"client\" | \"server\" | \"integration\"\n\n// ============================================\n// UTM PARAMETERS\n// ============================================\n\nexport interface UtmParams {\n source?: string\n medium?: string\n campaign?: string\n term?: string\n content?: string\n}\n\n// ============================================\n// TRACKER CONFIGURATION\n// ============================================\n\nexport interface TrackerConfig {\n publicKey: string\n apiHost?: string // default: 'https://app.outlit.ai'\n}\n\n// ============================================\n// BROWSER-SPECIFIC TYPES (anonymous allowed)\n// visitorId is auto-managed by the browser SDK\n// ============================================\n\nexport interface BrowserTrackOptions {\n eventName: string\n properties?: Record<string, string | number | boolean | null>\n}\n\nexport interface BrowserIdentifyOptions {\n email?: string\n userId?: string\n traits?: Record<string, string | number | boolean | null>\n}\n\n// ============================================\n// SERVER-SPECIFIC TYPES (identity required)\n// No anonymous tracking - must identify the user\n// ============================================\n\nexport interface ServerTrackOptions {\n email?: string // At least one of email/userId required\n userId?: string // At least one of email/userId required\n eventName: string\n properties?: Record<string, string | number | boolean | null>\n timestamp?: number\n}\n\nexport interface ServerIdentifyOptions {\n email?: string // At least one of email/userId required\n userId?: string // At least one of email/userId required\n traits?: Record<string, string | number | boolean | null>\n}\n\n// ============================================\n// INTERNAL EVENT TYPES\n// These are the full event objects sent to the API\n// ============================================\n\ninterface BaseEvent {\n type: EventType\n timestamp: number // Unix timestamp in milliseconds\n url: string\n path: string\n referrer?: string\n utm?: UtmParams\n}\n\nexport interface PageviewEvent extends BaseEvent {\n type: \"pageview\"\n title?: string\n}\n\nexport interface FormEvent extends BaseEvent {\n type: \"form\"\n formId?: string\n formFields?: Record<string, string>\n}\n\nexport interface IdentifyEvent extends BaseEvent {\n type: \"identify\"\n email?: string\n userId?: string\n traits?: Record<string, string | number | boolean | null>\n}\n\nexport interface CustomEvent extends BaseEvent {\n type: \"custom\"\n eventName: string\n properties?: Record<string, string | number | boolean | null>\n}\n\nexport interface CalendarEvent extends BaseEvent {\n type: \"calendar\"\n provider: CalendarProvider\n eventType?: string // e.g., \"30 Minute Meeting\"\n startTime?: string // ISO timestamp\n endTime?: string // ISO timestamp\n duration?: number // Duration in minutes\n isRecurring?: boolean\n /** Available when identity is passed via webhooks or manual integration */\n inviteeEmail?: string\n inviteeName?: string\n}\n\nexport type TrackerEvent = PageviewEvent | FormEvent | IdentifyEvent | CustomEvent | CalendarEvent\n\n// ============================================\n// INGEST PAYLOAD\n// This is what gets sent to the API\n// ============================================\n\nexport interface IngestPayload {\n visitorId?: string // Required for pixel, optional for server\n source: SourceType\n events: TrackerEvent[]\n}\n\n// ============================================\n// API RESPONSE\n// ============================================\n\nexport interface IngestResponse {\n success: boolean\n processed: number\n errors?: Array<{\n index: number\n message: string\n }>\n}\n\n// ============================================\n// CONSTANTS\n// ============================================\n\nexport const DEFAULT_API_HOST = \"https://app.outlit.ai\"\n\nexport const DEFAULT_DENIED_FORM_FIELDS = [\n \"password\",\n \"passwd\",\n \"pass\",\n \"pwd\",\n \"token\",\n \"secret\",\n \"api_key\",\n \"apikey\",\n \"api-key\",\n \"credit_card\",\n \"creditcard\",\n \"credit-card\",\n \"cc_number\",\n \"ccnumber\",\n \"card_number\",\n \"cardnumber\",\n \"cvv\",\n \"cvc\",\n \"ssn\",\n \"social_security\",\n \"socialsecurity\",\n \"bank_account\",\n \"bankaccount\",\n \"routing_number\",\n \"routingnumber\",\n]\n","import { DEFAULT_DENIED_FORM_FIELDS, type UtmParams } from \"./types\"\n\n// ============================================\n// UTM EXTRACTION\n// ============================================\n\n/**\n * Extract UTM parameters from a URL.\n */\nexport function extractUtmParams(url: string): UtmParams | undefined {\n try {\n const urlObj = new URL(url)\n const params = urlObj.searchParams\n\n const utm: UtmParams = {}\n\n if (params.has(\"utm_source\")) utm.source = params.get(\"utm_source\") ?? undefined\n if (params.has(\"utm_medium\")) utm.medium = params.get(\"utm_medium\") ?? undefined\n if (params.has(\"utm_campaign\")) utm.campaign = params.get(\"utm_campaign\") ?? undefined\n if (params.has(\"utm_term\")) utm.term = params.get(\"utm_term\") ?? undefined\n if (params.has(\"utm_content\")) utm.content = params.get(\"utm_content\") ?? undefined\n\n return Object.keys(utm).length > 0 ? utm : undefined\n } catch {\n return undefined\n }\n}\n\n/**\n * Extract path from a URL.\n */\nexport function extractPathFromUrl(url: string): string {\n try {\n const urlObj = new URL(url)\n return urlObj.pathname\n } catch {\n return \"/\"\n }\n}\n\n// ============================================\n// FORM FIELD SANITIZATION\n// ============================================\n\n/**\n * Check if a field name should be denied (case-insensitive).\n */\nexport function isFieldDenied(fieldName: string, denylist: string[]): boolean {\n const normalizedName = fieldName.toLowerCase().replace(/[-_\\s]/g, \"\")\n return denylist.some((denied) => {\n const normalizedDenied = denied.toLowerCase().replace(/[-_\\s]/g, \"\")\n return normalizedName.includes(normalizedDenied)\n })\n}\n\n/**\n * Check if a value looks like sensitive data (e.g., credit card number).\n */\nfunction looksLikeSensitiveValue(value: string): boolean {\n // Remove spaces and dashes\n const cleaned = value.replace(/[\\s-]/g, \"\")\n\n // Check for credit card patterns (13-19 digits)\n if (/^\\d{13,19}$/.test(cleaned)) {\n return true\n }\n\n // Check for SSN pattern (9 digits)\n if (/^\\d{9}$/.test(cleaned) || /^\\d{3}-\\d{2}-\\d{4}$/.test(value)) {\n return true\n }\n\n return false\n}\n\n/**\n * Sanitize form fields by removing sensitive data.\n * Returns a new object with denied fields removed.\n */\nexport function sanitizeFormFields(\n fields: Record<string, string> | undefined,\n customDenylist?: string[],\n): Record<string, string> | undefined {\n if (!fields) return undefined\n\n const denylist = customDenylist ?? DEFAULT_DENIED_FORM_FIELDS\n const sanitized: Record<string, string> = {}\n\n for (const [key, value] of Object.entries(fields)) {\n if (!isFieldDenied(key, denylist)) {\n // Also check for credit card patterns in values\n if (!looksLikeSensitiveValue(value)) {\n sanitized[key] = value\n }\n }\n }\n\n return Object.keys(sanitized).length > 0 ? sanitized : undefined\n}\n\n// ============================================\n// VISITOR ID DERIVATION (for server SDK)\n// ============================================\n\n/**\n * Derive a deterministic visitor ID from email and/or userId.\n * This is used by the server SDK to create consistent IDs for API compatibility.\n *\n * Uses a simple hash to create a UUID-like string that will be consistent\n * for the same email/userId combination.\n */\nexport function deriveVisitorIdFromIdentity(email?: string, userId?: string): string {\n const identity = [email?.toLowerCase(), userId].filter(Boolean).join(\"|\")\n if (!identity) {\n throw new Error(\"Either email or userId must be provided\")\n }\n\n // Simple hash function to create a deterministic UUID-like string\n let hash = 0\n for (let i = 0; i < identity.length; i++) {\n const char = identity.charCodeAt(i)\n hash = (hash << 5) - hash + char\n hash = hash & hash // Convert to 32-bit integer\n }\n\n // Convert to hex and format as UUID-like string\n const hex = Math.abs(hash).toString(16).padStart(8, \"0\")\n const part1 = hex.slice(0, 8)\n const part2 = identity.length.toString(16).padStart(4, \"0\")\n const part3 = \"4000\" // Version 4 UUID marker\n const part4 = (((hash >>> 16) & 0x0fff) | 0x8000).toString(16)\n const part5 = Math.abs(hash * 31)\n .toString(16)\n .padStart(12, \"0\")\n .slice(0, 12)\n\n return `${part1}-${part2}-${part3}-${part4}-${part5}`\n}\n\n// ============================================\n// VALIDATION\n// ============================================\n\n/**\n * Validate that at least one identity field is provided.\n * Used by the server SDK to enforce identity requirements.\n */\nexport function validateServerIdentity(email?: string, userId?: string): void {\n if (!email && !userId) {\n throw new Error(\n \"Server SDK requires either email or userId for all track/identify calls. \" +\n \"Anonymous tracking is only supported in the browser SDK.\",\n )\n }\n}\n\n// ============================================\n// AUTO-IDENTIFY: EMAIL & NAME EXTRACTION\n// ============================================\n\n/**\n * Validate that a string looks like a valid email address.\n */\nexport function isValidEmail(value: string): boolean {\n if (!value || typeof value !== \"string\") return false\n // Basic email regex - intentionally permissive to avoid false negatives\n const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n return emailRegex.test(value.trim())\n}\n\n/**\n * Email field name patterns (case-insensitive, normalized).\n * Order matters - more specific patterns first.\n */\nconst EMAIL_FIELD_PATTERNS = [\n /^e?-?mail$/i,\n /^email[_-]?address$/i,\n /^user[_-]?email$/i,\n /^work[_-]?email$/i,\n /^contact[_-]?email$/i,\n /^primary[_-]?email$/i,\n /^business[_-]?email$/i,\n]\n\n/**\n * Full name field patterns.\n */\nconst FULL_NAME_PATTERNS = [\n /^name$/i,\n /^full[_-]?name$/i,\n /^your[_-]?name$/i,\n /^customer[_-]?name$/i,\n /^contact[_-]?name$/i,\n /^display[_-]?name$/i,\n]\n\n/**\n * First name field patterns.\n */\nconst FIRST_NAME_PATTERNS = [\n /^first[_-]?name$/i,\n /^firstname$/i,\n /^first$/i,\n /^fname$/i,\n /^given[_-]?name$/i,\n /^forename$/i,\n]\n\n/**\n * Last name field patterns.\n */\nconst LAST_NAME_PATTERNS = [\n /^last[_-]?name$/i,\n /^lastname$/i,\n /^last$/i,\n /^lname$/i,\n /^surname$/i,\n /^family[_-]?name$/i,\n]\n\n/**\n * Check if a field name matches any of the given patterns.\n */\nfunction matchesPatterns(fieldName: string, patterns: RegExp[]): boolean {\n const normalized = fieldName.trim()\n return patterns.some((pattern) => pattern.test(normalized))\n}\n\n/**\n * Find an email value from form fields.\n *\n * Priority:\n * 1. Fields with input type=\"email\" (if inputTypes map provided)\n * 2. Field names matching email patterns\n * 3. Any field with a value that looks like an email\n *\n * @param fields - Form field key-value pairs\n * @param inputTypes - Optional map of field names to input types\n * @returns The email value if found, undefined otherwise\n */\nexport function findEmailField(\n fields: Record<string, string>,\n inputTypes?: Map<string, string>,\n): string | undefined {\n // Priority 1: Check fields with type=\"email\"\n if (inputTypes) {\n for (const [fieldName, inputType] of inputTypes.entries()) {\n if (inputType === \"email\") {\n const value = fields[fieldName]\n if (value && isValidEmail(value)) {\n return value.trim()\n }\n }\n }\n }\n\n // Priority 2: Check field names matching email patterns\n for (const [fieldName, value] of Object.entries(fields)) {\n if (matchesPatterns(fieldName, EMAIL_FIELD_PATTERNS) && isValidEmail(value)) {\n return value.trim()\n }\n }\n\n // Priority 3: Any field with email-like value (fallback)\n for (const value of Object.values(fields)) {\n if (isValidEmail(value)) {\n return value.trim()\n }\n }\n\n return undefined\n}\n\n/**\n * Extract name fields from form data.\n *\n * Looks for:\n * - Full name fields (name, full_name, etc.)\n * - First name fields (first_name, fname, etc.)\n * - Last name fields (last_name, lname, etc.)\n *\n * If only first/last names are found, combines them into a full name.\n *\n * @param fields - Form field key-value pairs\n * @returns Object with name, firstName, and/or lastName if found\n */\nexport function findNameFields(fields: Record<string, string>): {\n name?: string\n firstName?: string\n lastName?: string\n} {\n let fullName: string | undefined\n let firstName: string | undefined\n let lastName: string | undefined\n\n for (const [fieldName, value] of Object.entries(fields)) {\n const trimmedValue = value?.trim()\n if (!trimmedValue) continue\n\n // Check for full name\n if (!fullName && matchesPatterns(fieldName, FULL_NAME_PATTERNS)) {\n fullName = trimmedValue\n }\n\n // Check for first name\n if (!firstName && matchesPatterns(fieldName, FIRST_NAME_PATTERNS)) {\n firstName = trimmedValue\n }\n\n // Check for last name\n if (!lastName && matchesPatterns(fieldName, LAST_NAME_PATTERNS)) {\n lastName = trimmedValue\n }\n }\n\n const result: { name?: string; firstName?: string; lastName?: string } = {}\n\n // If we have a full name, use it\n if (fullName) {\n result.name = fullName\n }\n // If we have first and last, combine them\n else if (firstName && lastName) {\n result.name = `${firstName} ${lastName}`\n result.firstName = firstName\n result.lastName = lastName\n }\n // If we only have first name\n else if (firstName) {\n result.firstName = firstName\n }\n // If we only have last name\n else if (lastName) {\n result.lastName = lastName\n }\n\n return result\n}\n\n/**\n * Identity extracted from a form submission.\n */\nexport interface ExtractedIdentity {\n email: string\n name?: string\n firstName?: string\n lastName?: string\n}\n\n/**\n * Extract identity information (email + name) from form fields.\n *\n * Returns undefined if no valid email is found (email is required for identification).\n *\n * @param fields - Form field key-value pairs\n * @param inputTypes - Optional map of field names to input types\n * @returns Extracted identity with email and optional name fields, or undefined\n */\nexport function extractIdentityFromForm(\n fields: Record<string, string>,\n inputTypes?: Map<string, string>,\n): ExtractedIdentity | undefined {\n const email = findEmailField(fields, inputTypes)\n\n // Email is required for identification\n if (!email) {\n return undefined\n }\n\n const nameFields = findNameFields(fields)\n\n return {\n email,\n ...nameFields,\n }\n}\n","import type {\n CalendarEvent,\n CalendarProvider,\n CustomEvent,\n FormEvent,\n IdentifyEvent,\n IngestPayload,\n PageviewEvent,\n SourceType,\n TrackerEvent,\n UtmParams,\n} from \"./types\"\nimport { extractPathFromUrl, extractUtmParams } from \"./utils\"\n\n// ============================================\n// EVENT BUILDERS\n// ============================================\n\ninterface BaseEventParams {\n url: string\n referrer?: string\n timestamp?: number\n}\n\n/**\n * Build a pageview event.\n */\nexport function buildPageviewEvent(params: BaseEventParams & { title?: string }): PageviewEvent {\n const { url, referrer, timestamp, title } = params\n return {\n type: \"pageview\",\n timestamp: timestamp ?? Date.now(),\n url,\n path: extractPathFromUrl(url),\n referrer,\n utm: extractUtmParams(url),\n title,\n }\n}\n\n/**\n * Build a form event.\n */\nexport function buildFormEvent(\n params: BaseEventParams & {\n formId?: string\n formFields?: Record<string, string>\n },\n): FormEvent {\n const { url, referrer, timestamp, formId, formFields } = params\n return {\n type: \"form\",\n timestamp: timestamp ?? Date.now(),\n url,\n path: extractPathFromUrl(url),\n referrer,\n utm: extractUtmParams(url),\n formId,\n formFields,\n }\n}\n\n/**\n * Build an identify event.\n */\nexport function buildIdentifyEvent(\n params: BaseEventParams & {\n email?: string\n userId?: string\n traits?: Record<string, string | number | boolean | null>\n },\n): IdentifyEvent {\n const { url, referrer, timestamp, email, userId, traits } = params\n return {\n type: \"identify\",\n timestamp: timestamp ?? Date.now(),\n url,\n path: extractPathFromUrl(url),\n referrer,\n utm: extractUtmParams(url),\n email,\n userId,\n traits,\n }\n}\n\n/**\n * Build a custom event.\n */\nexport function buildCustomEvent(\n params: BaseEventParams & {\n eventName: string\n properties?: Record<string, string | number | boolean | null>\n },\n): CustomEvent {\n const { url, referrer, timestamp, eventName, properties } = params\n return {\n type: \"custom\",\n timestamp: timestamp ?? Date.now(),\n url,\n path: extractPathFromUrl(url),\n referrer,\n utm: extractUtmParams(url),\n eventName,\n properties,\n }\n}\n\n/**\n * Build a calendar booking event.\n */\nexport function buildCalendarEvent(\n params: BaseEventParams & {\n provider: CalendarProvider\n eventType?: string\n startTime?: string\n endTime?: string\n duration?: number\n isRecurring?: boolean\n inviteeEmail?: string\n inviteeName?: string\n },\n): CalendarEvent {\n const {\n url,\n referrer,\n timestamp,\n provider,\n eventType,\n startTime,\n endTime,\n duration,\n isRecurring,\n inviteeEmail,\n inviteeName,\n } = params\n return {\n type: \"calendar\",\n timestamp: timestamp ?? Date.now(),\n url,\n path: extractPathFromUrl(url),\n referrer,\n utm: extractUtmParams(url),\n provider,\n eventType,\n startTime,\n endTime,\n duration,\n isRecurring,\n inviteeEmail,\n inviteeName,\n }\n}\n\n// ============================================\n// PAYLOAD BUILDER\n// ============================================\n\n/**\n * Build an ingest payload from events.\n */\nexport function buildIngestPayload(\n visitorId: string,\n source: SourceType,\n events: TrackerEvent[],\n): IngestPayload {\n return {\n visitorId,\n source,\n events,\n }\n}\n\n// ============================================\n// BATCH HELPERS\n// ============================================\n\n/**\n * Maximum number of events in a single batch.\n */\nexport const MAX_BATCH_SIZE = 100\n\n/**\n * Split events into batches of MAX_BATCH_SIZE.\n */\nexport function batchEvents(events: TrackerEvent[]): TrackerEvent[][] {\n const batches: TrackerEvent[][] = []\n for (let i = 0; i < events.length; i += MAX_BATCH_SIZE) {\n batches.push(events.slice(i, i + MAX_BATCH_SIZE))\n }\n return batches\n}\n"],"mappings":"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","names":[]}
package/dist/index.mjs CHANGED
@@ -250,6 +250,37 @@ function buildCustomEvent(params) {
250
250
  properties
251
251
  };
252
252
  }
253
+ function buildCalendarEvent(params) {
254
+ const {
255
+ url,
256
+ referrer,
257
+ timestamp,
258
+ provider,
259
+ eventType,
260
+ startTime,
261
+ endTime,
262
+ duration,
263
+ isRecurring,
264
+ inviteeEmail,
265
+ inviteeName
266
+ } = params;
267
+ return {
268
+ type: "calendar",
269
+ timestamp: timestamp ?? Date.now(),
270
+ url,
271
+ path: extractPathFromUrl(url),
272
+ referrer,
273
+ utm: extractUtmParams(url),
274
+ provider,
275
+ eventType,
276
+ startTime,
277
+ endTime,
278
+ duration,
279
+ isRecurring,
280
+ inviteeEmail,
281
+ inviteeName
282
+ };
283
+ }
253
284
  function buildIngestPayload(visitorId, source, events) {
254
285
  return {
255
286
  visitorId,
@@ -270,6 +301,7 @@ export {
270
301
  DEFAULT_DENIED_FORM_FIELDS,
271
302
  MAX_BATCH_SIZE,
272
303
  batchEvents,
304
+ buildCalendarEvent,
273
305
  buildCustomEvent,
274
306
  buildFormEvent,
275
307
  buildIdentifyEvent,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/types.ts","../src/utils.ts","../src/payload.ts"],"sourcesContent":["// ============================================\n// EVENT TYPES\n// ============================================\n\nexport type EventType = \"pageview\" | \"form\" | \"identify\" | \"custom\"\n\nexport type SourceType = \"pixel\" | \"server\" | \"integration\"\n\n// ============================================\n// UTM PARAMETERS\n// ============================================\n\nexport interface UtmParams {\n source?: string\n medium?: string\n campaign?: string\n term?: string\n content?: string\n}\n\n// ============================================\n// TRACKER CONFIGURATION\n// ============================================\n\nexport interface TrackerConfig {\n publicKey: string\n apiHost?: string // default: 'https://app.outlit.ai'\n}\n\n// ============================================\n// BROWSER-SPECIFIC TYPES (anonymous allowed)\n// visitorId is auto-managed by the browser SDK\n// ============================================\n\nexport interface BrowserTrackOptions {\n eventName: string\n properties?: Record<string, string | number | boolean | null>\n}\n\nexport interface BrowserIdentifyOptions {\n email?: string\n userId?: string\n traits?: Record<string, string | number | boolean | null>\n}\n\n// ============================================\n// SERVER-SPECIFIC TYPES (identity required)\n// No anonymous tracking - must identify the user\n// ============================================\n\nexport interface ServerTrackOptions {\n email?: string // At least one of email/userId required\n userId?: string // At least one of email/userId required\n eventName: string\n properties?: Record<string, string | number | boolean | null>\n timestamp?: number\n}\n\nexport interface ServerIdentifyOptions {\n email?: string // At least one of email/userId required\n userId?: string // At least one of email/userId required\n traits?: Record<string, string | number | boolean | null>\n}\n\n// ============================================\n// INTERNAL EVENT TYPES\n// These are the full event objects sent to the API\n// ============================================\n\ninterface BaseEvent {\n type: EventType\n timestamp: number // Unix timestamp in milliseconds\n url: string\n path: string\n referrer?: string\n utm?: UtmParams\n}\n\nexport interface PageviewEvent extends BaseEvent {\n type: \"pageview\"\n title?: string\n}\n\nexport interface FormEvent extends BaseEvent {\n type: \"form\"\n formId?: string\n formFields?: Record<string, string>\n}\n\nexport interface IdentifyEvent extends BaseEvent {\n type: \"identify\"\n email?: string\n userId?: string\n traits?: Record<string, string | number | boolean | null>\n}\n\nexport interface CustomEvent extends BaseEvent {\n type: \"custom\"\n eventName: string\n properties?: Record<string, string | number | boolean | null>\n}\n\nexport type TrackerEvent = PageviewEvent | FormEvent | IdentifyEvent | CustomEvent\n\n// ============================================\n// INGEST PAYLOAD\n// This is what gets sent to the API\n// ============================================\n\nexport interface IngestPayload {\n visitorId?: string // Required for pixel, optional for server\n source: SourceType\n events: TrackerEvent[]\n}\n\n// ============================================\n// API RESPONSE\n// ============================================\n\nexport interface IngestResponse {\n success: boolean\n processed: number\n errors?: Array<{\n index: number\n message: string\n }>\n}\n\n// ============================================\n// CONSTANTS\n// ============================================\n\nexport const DEFAULT_API_HOST = \"https://app.outlit.ai\"\n\nexport const DEFAULT_DENIED_FORM_FIELDS = [\n \"password\",\n \"passwd\",\n \"pass\",\n \"pwd\",\n \"token\",\n \"secret\",\n \"api_key\",\n \"apikey\",\n \"api-key\",\n \"credit_card\",\n \"creditcard\",\n \"credit-card\",\n \"cc_number\",\n \"ccnumber\",\n \"card_number\",\n \"cardnumber\",\n \"cvv\",\n \"cvc\",\n \"ssn\",\n \"social_security\",\n \"socialsecurity\",\n \"bank_account\",\n \"bankaccount\",\n \"routing_number\",\n \"routingnumber\",\n]\n","import { DEFAULT_DENIED_FORM_FIELDS, type UtmParams } from \"./types\"\n\n// ============================================\n// UTM EXTRACTION\n// ============================================\n\n/**\n * Extract UTM parameters from a URL.\n */\nexport function extractUtmParams(url: string): UtmParams | undefined {\n try {\n const urlObj = new URL(url)\n const params = urlObj.searchParams\n\n const utm: UtmParams = {}\n\n if (params.has(\"utm_source\")) utm.source = params.get(\"utm_source\") ?? undefined\n if (params.has(\"utm_medium\")) utm.medium = params.get(\"utm_medium\") ?? undefined\n if (params.has(\"utm_campaign\")) utm.campaign = params.get(\"utm_campaign\") ?? undefined\n if (params.has(\"utm_term\")) utm.term = params.get(\"utm_term\") ?? undefined\n if (params.has(\"utm_content\")) utm.content = params.get(\"utm_content\") ?? undefined\n\n return Object.keys(utm).length > 0 ? utm : undefined\n } catch {\n return undefined\n }\n}\n\n/**\n * Extract path from a URL.\n */\nexport function extractPathFromUrl(url: string): string {\n try {\n const urlObj = new URL(url)\n return urlObj.pathname\n } catch {\n return \"/\"\n }\n}\n\n// ============================================\n// FORM FIELD SANITIZATION\n// ============================================\n\n/**\n * Check if a field name should be denied (case-insensitive).\n */\nexport function isFieldDenied(fieldName: string, denylist: string[]): boolean {\n const normalizedName = fieldName.toLowerCase().replace(/[-_\\s]/g, \"\")\n return denylist.some((denied) => {\n const normalizedDenied = denied.toLowerCase().replace(/[-_\\s]/g, \"\")\n return normalizedName.includes(normalizedDenied)\n })\n}\n\n/**\n * Check if a value looks like sensitive data (e.g., credit card number).\n */\nfunction looksLikeSensitiveValue(value: string): boolean {\n // Remove spaces and dashes\n const cleaned = value.replace(/[\\s-]/g, \"\")\n\n // Check for credit card patterns (13-19 digits)\n if (/^\\d{13,19}$/.test(cleaned)) {\n return true\n }\n\n // Check for SSN pattern (9 digits)\n if (/^\\d{9}$/.test(cleaned) || /^\\d{3}-\\d{2}-\\d{4}$/.test(value)) {\n return true\n }\n\n return false\n}\n\n/**\n * Sanitize form fields by removing sensitive data.\n * Returns a new object with denied fields removed.\n */\nexport function sanitizeFormFields(\n fields: Record<string, string> | undefined,\n customDenylist?: string[],\n): Record<string, string> | undefined {\n if (!fields) return undefined\n\n const denylist = customDenylist ?? DEFAULT_DENIED_FORM_FIELDS\n const sanitized: Record<string, string> = {}\n\n for (const [key, value] of Object.entries(fields)) {\n if (!isFieldDenied(key, denylist)) {\n // Also check for credit card patterns in values\n if (!looksLikeSensitiveValue(value)) {\n sanitized[key] = value\n }\n }\n }\n\n return Object.keys(sanitized).length > 0 ? sanitized : undefined\n}\n\n// ============================================\n// VISITOR ID DERIVATION (for server SDK)\n// ============================================\n\n/**\n * Derive a deterministic visitor ID from email and/or userId.\n * This is used by the server SDK to create consistent IDs for API compatibility.\n *\n * Uses a simple hash to create a UUID-like string that will be consistent\n * for the same email/userId combination.\n */\nexport function deriveVisitorIdFromIdentity(email?: string, userId?: string): string {\n const identity = [email?.toLowerCase(), userId].filter(Boolean).join(\"|\")\n if (!identity) {\n throw new Error(\"Either email or userId must be provided\")\n }\n\n // Simple hash function to create a deterministic UUID-like string\n let hash = 0\n for (let i = 0; i < identity.length; i++) {\n const char = identity.charCodeAt(i)\n hash = (hash << 5) - hash + char\n hash = hash & hash // Convert to 32-bit integer\n }\n\n // Convert to hex and format as UUID-like string\n const hex = Math.abs(hash).toString(16).padStart(8, \"0\")\n const part1 = hex.slice(0, 8)\n const part2 = identity.length.toString(16).padStart(4, \"0\")\n const part3 = \"4000\" // Version 4 UUID marker\n const part4 = (((hash >>> 16) & 0x0fff) | 0x8000).toString(16)\n const part5 = Math.abs(hash * 31)\n .toString(16)\n .padStart(12, \"0\")\n .slice(0, 12)\n\n return `${part1}-${part2}-${part3}-${part4}-${part5}`\n}\n\n// ============================================\n// VALIDATION\n// ============================================\n\n/**\n * Validate that at least one identity field is provided.\n * Used by the server SDK to enforce identity requirements.\n */\nexport function validateServerIdentity(email?: string, userId?: string): void {\n if (!email && !userId) {\n throw new Error(\n \"Server SDK requires either email or userId for all track/identify calls. \" +\n \"Anonymous tracking is only supported in the browser SDK.\",\n )\n }\n}\n\n// ============================================\n// AUTO-IDENTIFY: EMAIL & NAME EXTRACTION\n// ============================================\n\n/**\n * Validate that a string looks like a valid email address.\n */\nexport function isValidEmail(value: string): boolean {\n if (!value || typeof value !== \"string\") return false\n // Basic email regex - intentionally permissive to avoid false negatives\n const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n return emailRegex.test(value.trim())\n}\n\n/**\n * Email field name patterns (case-insensitive, normalized).\n * Order matters - more specific patterns first.\n */\nconst EMAIL_FIELD_PATTERNS = [\n /^e?-?mail$/i,\n /^email[_-]?address$/i,\n /^user[_-]?email$/i,\n /^work[_-]?email$/i,\n /^contact[_-]?email$/i,\n /^primary[_-]?email$/i,\n /^business[_-]?email$/i,\n]\n\n/**\n * Full name field patterns.\n */\nconst FULL_NAME_PATTERNS = [\n /^name$/i,\n /^full[_-]?name$/i,\n /^your[_-]?name$/i,\n /^customer[_-]?name$/i,\n /^contact[_-]?name$/i,\n /^display[_-]?name$/i,\n]\n\n/**\n * First name field patterns.\n */\nconst FIRST_NAME_PATTERNS = [\n /^first[_-]?name$/i,\n /^firstname$/i,\n /^first$/i,\n /^fname$/i,\n /^given[_-]?name$/i,\n /^forename$/i,\n]\n\n/**\n * Last name field patterns.\n */\nconst LAST_NAME_PATTERNS = [\n /^last[_-]?name$/i,\n /^lastname$/i,\n /^last$/i,\n /^lname$/i,\n /^surname$/i,\n /^family[_-]?name$/i,\n]\n\n/**\n * Check if a field name matches any of the given patterns.\n */\nfunction matchesPatterns(fieldName: string, patterns: RegExp[]): boolean {\n const normalized = fieldName.trim()\n return patterns.some((pattern) => pattern.test(normalized))\n}\n\n/**\n * Find an email value from form fields.\n *\n * Priority:\n * 1. Fields with input type=\"email\" (if inputTypes map provided)\n * 2. Field names matching email patterns\n * 3. Any field with a value that looks like an email\n *\n * @param fields - Form field key-value pairs\n * @param inputTypes - Optional map of field names to input types\n * @returns The email value if found, undefined otherwise\n */\nexport function findEmailField(\n fields: Record<string, string>,\n inputTypes?: Map<string, string>,\n): string | undefined {\n // Priority 1: Check fields with type=\"email\"\n if (inputTypes) {\n for (const [fieldName, inputType] of inputTypes.entries()) {\n if (inputType === \"email\") {\n const value = fields[fieldName]\n if (value && isValidEmail(value)) {\n return value.trim()\n }\n }\n }\n }\n\n // Priority 2: Check field names matching email patterns\n for (const [fieldName, value] of Object.entries(fields)) {\n if (matchesPatterns(fieldName, EMAIL_FIELD_PATTERNS) && isValidEmail(value)) {\n return value.trim()\n }\n }\n\n // Priority 3: Any field with email-like value (fallback)\n for (const value of Object.values(fields)) {\n if (isValidEmail(value)) {\n return value.trim()\n }\n }\n\n return undefined\n}\n\n/**\n * Extract name fields from form data.\n *\n * Looks for:\n * - Full name fields (name, full_name, etc.)\n * - First name fields (first_name, fname, etc.)\n * - Last name fields (last_name, lname, etc.)\n *\n * If only first/last names are found, combines them into a full name.\n *\n * @param fields - Form field key-value pairs\n * @returns Object with name, firstName, and/or lastName if found\n */\nexport function findNameFields(fields: Record<string, string>): {\n name?: string\n firstName?: string\n lastName?: string\n} {\n let fullName: string | undefined\n let firstName: string | undefined\n let lastName: string | undefined\n\n for (const [fieldName, value] of Object.entries(fields)) {\n const trimmedValue = value?.trim()\n if (!trimmedValue) continue\n\n // Check for full name\n if (!fullName && matchesPatterns(fieldName, FULL_NAME_PATTERNS)) {\n fullName = trimmedValue\n }\n\n // Check for first name\n if (!firstName && matchesPatterns(fieldName, FIRST_NAME_PATTERNS)) {\n firstName = trimmedValue\n }\n\n // Check for last name\n if (!lastName && matchesPatterns(fieldName, LAST_NAME_PATTERNS)) {\n lastName = trimmedValue\n }\n }\n\n const result: { name?: string; firstName?: string; lastName?: string } = {}\n\n // If we have a full name, use it\n if (fullName) {\n result.name = fullName\n }\n // If we have first and last, combine them\n else if (firstName && lastName) {\n result.name = `${firstName} ${lastName}`\n result.firstName = firstName\n result.lastName = lastName\n }\n // If we only have first name\n else if (firstName) {\n result.firstName = firstName\n }\n // If we only have last name\n else if (lastName) {\n result.lastName = lastName\n }\n\n return result\n}\n\n/**\n * Identity extracted from a form submission.\n */\nexport interface ExtractedIdentity {\n email: string\n name?: string\n firstName?: string\n lastName?: string\n}\n\n/**\n * Extract identity information (email + name) from form fields.\n *\n * Returns undefined if no valid email is found (email is required for identification).\n *\n * @param fields - Form field key-value pairs\n * @param inputTypes - Optional map of field names to input types\n * @returns Extracted identity with email and optional name fields, or undefined\n */\nexport function extractIdentityFromForm(\n fields: Record<string, string>,\n inputTypes?: Map<string, string>,\n): ExtractedIdentity | undefined {\n const email = findEmailField(fields, inputTypes)\n\n // Email is required for identification\n if (!email) {\n return undefined\n }\n\n const nameFields = findNameFields(fields)\n\n return {\n email,\n ...nameFields,\n }\n}\n","import type {\n CustomEvent,\n FormEvent,\n IdentifyEvent,\n IngestPayload,\n PageviewEvent,\n SourceType,\n TrackerEvent,\n UtmParams,\n} from \"./types\"\nimport { extractPathFromUrl, extractUtmParams } from \"./utils\"\n\n// ============================================\n// EVENT BUILDERS\n// ============================================\n\ninterface BaseEventParams {\n url: string\n referrer?: string\n timestamp?: number\n}\n\n/**\n * Build a pageview event.\n */\nexport function buildPageviewEvent(params: BaseEventParams & { title?: string }): PageviewEvent {\n const { url, referrer, timestamp, title } = params\n return {\n type: \"pageview\",\n timestamp: timestamp ?? Date.now(),\n url,\n path: extractPathFromUrl(url),\n referrer,\n utm: extractUtmParams(url),\n title,\n }\n}\n\n/**\n * Build a form event.\n */\nexport function buildFormEvent(\n params: BaseEventParams & {\n formId?: string\n formFields?: Record<string, string>\n },\n): FormEvent {\n const { url, referrer, timestamp, formId, formFields } = params\n return {\n type: \"form\",\n timestamp: timestamp ?? Date.now(),\n url,\n path: extractPathFromUrl(url),\n referrer,\n utm: extractUtmParams(url),\n formId,\n formFields,\n }\n}\n\n/**\n * Build an identify event.\n */\nexport function buildIdentifyEvent(\n params: BaseEventParams & {\n email?: string\n userId?: string\n traits?: Record<string, string | number | boolean | null>\n },\n): IdentifyEvent {\n const { url, referrer, timestamp, email, userId, traits } = params\n return {\n type: \"identify\",\n timestamp: timestamp ?? Date.now(),\n url,\n path: extractPathFromUrl(url),\n referrer,\n utm: extractUtmParams(url),\n email,\n userId,\n traits,\n }\n}\n\n/**\n * Build a custom event.\n */\nexport function buildCustomEvent(\n params: BaseEventParams & {\n eventName: string\n properties?: Record<string, string | number | boolean | null>\n },\n): CustomEvent {\n const { url, referrer, timestamp, eventName, properties } = params\n return {\n type: \"custom\",\n timestamp: timestamp ?? Date.now(),\n url,\n path: extractPathFromUrl(url),\n referrer,\n utm: extractUtmParams(url),\n eventName,\n properties,\n }\n}\n\n// ============================================\n// PAYLOAD BUILDER\n// ============================================\n\n/**\n * Build an ingest payload from events.\n */\nexport function buildIngestPayload(\n visitorId: string,\n source: SourceType,\n events: TrackerEvent[],\n): IngestPayload {\n return {\n visitorId,\n source,\n events,\n }\n}\n\n// ============================================\n// BATCH HELPERS\n// ============================================\n\n/**\n * Maximum number of events in a single batch.\n */\nexport const MAX_BATCH_SIZE = 100\n\n/**\n * Split events into batches of MAX_BATCH_SIZE.\n */\nexport function batchEvents(events: TrackerEvent[]): TrackerEvent[][] {\n const batches: TrackerEvent[][] = []\n for (let i = 0; i < events.length; i += MAX_BATCH_SIZE) {\n batches.push(events.slice(i, i + MAX_BATCH_SIZE))\n }\n return batches\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/types.ts","../src/utils.ts","../src/payload.ts"],"sourcesContent":["// ============================================\n// EVENT TYPES\n// ============================================\n\nexport type EventType = \"pageview\" | \"form\" | \"identify\" | \"custom\" | \"calendar\"\n\nexport type CalendarProvider = \"cal.com\" | \"calendly\" | \"unknown\"\n\nexport type SourceType = \"client\" | \"server\" | \"integration\"\n\n// ============================================\n// UTM PARAMETERS\n// ============================================\n\nexport interface UtmParams {\n source?: string\n medium?: string\n campaign?: string\n term?: string\n content?: string\n}\n\n// ============================================\n// TRACKER CONFIGURATION\n// ============================================\n\nexport interface TrackerConfig {\n publicKey: string\n apiHost?: string // default: 'https://app.outlit.ai'\n}\n\n// ============================================\n// BROWSER-SPECIFIC TYPES (anonymous allowed)\n// visitorId is auto-managed by the browser SDK\n// ============================================\n\nexport interface BrowserTrackOptions {\n eventName: string\n properties?: Record<string, string | number | boolean | null>\n}\n\nexport interface BrowserIdentifyOptions {\n email?: string\n userId?: string\n traits?: Record<string, string | number | boolean | null>\n}\n\n// ============================================\n// SERVER-SPECIFIC TYPES (identity required)\n// No anonymous tracking - must identify the user\n// ============================================\n\nexport interface ServerTrackOptions {\n email?: string // At least one of email/userId required\n userId?: string // At least one of email/userId required\n eventName: string\n properties?: Record<string, string | number | boolean | null>\n timestamp?: number\n}\n\nexport interface ServerIdentifyOptions {\n email?: string // At least one of email/userId required\n userId?: string // At least one of email/userId required\n traits?: Record<string, string | number | boolean | null>\n}\n\n// ============================================\n// INTERNAL EVENT TYPES\n// These are the full event objects sent to the API\n// ============================================\n\ninterface BaseEvent {\n type: EventType\n timestamp: number // Unix timestamp in milliseconds\n url: string\n path: string\n referrer?: string\n utm?: UtmParams\n}\n\nexport interface PageviewEvent extends BaseEvent {\n type: \"pageview\"\n title?: string\n}\n\nexport interface FormEvent extends BaseEvent {\n type: \"form\"\n formId?: string\n formFields?: Record<string, string>\n}\n\nexport interface IdentifyEvent extends BaseEvent {\n type: \"identify\"\n email?: string\n userId?: string\n traits?: Record<string, string | number | boolean | null>\n}\n\nexport interface CustomEvent extends BaseEvent {\n type: \"custom\"\n eventName: string\n properties?: Record<string, string | number | boolean | null>\n}\n\nexport interface CalendarEvent extends BaseEvent {\n type: \"calendar\"\n provider: CalendarProvider\n eventType?: string // e.g., \"30 Minute Meeting\"\n startTime?: string // ISO timestamp\n endTime?: string // ISO timestamp\n duration?: number // Duration in minutes\n isRecurring?: boolean\n /** Available when identity is passed via webhooks or manual integration */\n inviteeEmail?: string\n inviteeName?: string\n}\n\nexport type TrackerEvent = PageviewEvent | FormEvent | IdentifyEvent | CustomEvent | CalendarEvent\n\n// ============================================\n// INGEST PAYLOAD\n// This is what gets sent to the API\n// ============================================\n\nexport interface IngestPayload {\n visitorId?: string // Required for pixel, optional for server\n source: SourceType\n events: TrackerEvent[]\n}\n\n// ============================================\n// API RESPONSE\n// ============================================\n\nexport interface IngestResponse {\n success: boolean\n processed: number\n errors?: Array<{\n index: number\n message: string\n }>\n}\n\n// ============================================\n// CONSTANTS\n// ============================================\n\nexport const DEFAULT_API_HOST = \"https://app.outlit.ai\"\n\nexport const DEFAULT_DENIED_FORM_FIELDS = [\n \"password\",\n \"passwd\",\n \"pass\",\n \"pwd\",\n \"token\",\n \"secret\",\n \"api_key\",\n \"apikey\",\n \"api-key\",\n \"credit_card\",\n \"creditcard\",\n \"credit-card\",\n \"cc_number\",\n \"ccnumber\",\n \"card_number\",\n \"cardnumber\",\n \"cvv\",\n \"cvc\",\n \"ssn\",\n \"social_security\",\n \"socialsecurity\",\n \"bank_account\",\n \"bankaccount\",\n \"routing_number\",\n \"routingnumber\",\n]\n","import { DEFAULT_DENIED_FORM_FIELDS, type UtmParams } from \"./types\"\n\n// ============================================\n// UTM EXTRACTION\n// ============================================\n\n/**\n * Extract UTM parameters from a URL.\n */\nexport function extractUtmParams(url: string): UtmParams | undefined {\n try {\n const urlObj = new URL(url)\n const params = urlObj.searchParams\n\n const utm: UtmParams = {}\n\n if (params.has(\"utm_source\")) utm.source = params.get(\"utm_source\") ?? undefined\n if (params.has(\"utm_medium\")) utm.medium = params.get(\"utm_medium\") ?? undefined\n if (params.has(\"utm_campaign\")) utm.campaign = params.get(\"utm_campaign\") ?? undefined\n if (params.has(\"utm_term\")) utm.term = params.get(\"utm_term\") ?? undefined\n if (params.has(\"utm_content\")) utm.content = params.get(\"utm_content\") ?? undefined\n\n return Object.keys(utm).length > 0 ? utm : undefined\n } catch {\n return undefined\n }\n}\n\n/**\n * Extract path from a URL.\n */\nexport function extractPathFromUrl(url: string): string {\n try {\n const urlObj = new URL(url)\n return urlObj.pathname\n } catch {\n return \"/\"\n }\n}\n\n// ============================================\n// FORM FIELD SANITIZATION\n// ============================================\n\n/**\n * Check if a field name should be denied (case-insensitive).\n */\nexport function isFieldDenied(fieldName: string, denylist: string[]): boolean {\n const normalizedName = fieldName.toLowerCase().replace(/[-_\\s]/g, \"\")\n return denylist.some((denied) => {\n const normalizedDenied = denied.toLowerCase().replace(/[-_\\s]/g, \"\")\n return normalizedName.includes(normalizedDenied)\n })\n}\n\n/**\n * Check if a value looks like sensitive data (e.g., credit card number).\n */\nfunction looksLikeSensitiveValue(value: string): boolean {\n // Remove spaces and dashes\n const cleaned = value.replace(/[\\s-]/g, \"\")\n\n // Check for credit card patterns (13-19 digits)\n if (/^\\d{13,19}$/.test(cleaned)) {\n return true\n }\n\n // Check for SSN pattern (9 digits)\n if (/^\\d{9}$/.test(cleaned) || /^\\d{3}-\\d{2}-\\d{4}$/.test(value)) {\n return true\n }\n\n return false\n}\n\n/**\n * Sanitize form fields by removing sensitive data.\n * Returns a new object with denied fields removed.\n */\nexport function sanitizeFormFields(\n fields: Record<string, string> | undefined,\n customDenylist?: string[],\n): Record<string, string> | undefined {\n if (!fields) return undefined\n\n const denylist = customDenylist ?? DEFAULT_DENIED_FORM_FIELDS\n const sanitized: Record<string, string> = {}\n\n for (const [key, value] of Object.entries(fields)) {\n if (!isFieldDenied(key, denylist)) {\n // Also check for credit card patterns in values\n if (!looksLikeSensitiveValue(value)) {\n sanitized[key] = value\n }\n }\n }\n\n return Object.keys(sanitized).length > 0 ? sanitized : undefined\n}\n\n// ============================================\n// VISITOR ID DERIVATION (for server SDK)\n// ============================================\n\n/**\n * Derive a deterministic visitor ID from email and/or userId.\n * This is used by the server SDK to create consistent IDs for API compatibility.\n *\n * Uses a simple hash to create a UUID-like string that will be consistent\n * for the same email/userId combination.\n */\nexport function deriveVisitorIdFromIdentity(email?: string, userId?: string): string {\n const identity = [email?.toLowerCase(), userId].filter(Boolean).join(\"|\")\n if (!identity) {\n throw new Error(\"Either email or userId must be provided\")\n }\n\n // Simple hash function to create a deterministic UUID-like string\n let hash = 0\n for (let i = 0; i < identity.length; i++) {\n const char = identity.charCodeAt(i)\n hash = (hash << 5) - hash + char\n hash = hash & hash // Convert to 32-bit integer\n }\n\n // Convert to hex and format as UUID-like string\n const hex = Math.abs(hash).toString(16).padStart(8, \"0\")\n const part1 = hex.slice(0, 8)\n const part2 = identity.length.toString(16).padStart(4, \"0\")\n const part3 = \"4000\" // Version 4 UUID marker\n const part4 = (((hash >>> 16) & 0x0fff) | 0x8000).toString(16)\n const part5 = Math.abs(hash * 31)\n .toString(16)\n .padStart(12, \"0\")\n .slice(0, 12)\n\n return `${part1}-${part2}-${part3}-${part4}-${part5}`\n}\n\n// ============================================\n// VALIDATION\n// ============================================\n\n/**\n * Validate that at least one identity field is provided.\n * Used by the server SDK to enforce identity requirements.\n */\nexport function validateServerIdentity(email?: string, userId?: string): void {\n if (!email && !userId) {\n throw new Error(\n \"Server SDK requires either email or userId for all track/identify calls. \" +\n \"Anonymous tracking is only supported in the browser SDK.\",\n )\n }\n}\n\n// ============================================\n// AUTO-IDENTIFY: EMAIL & NAME EXTRACTION\n// ============================================\n\n/**\n * Validate that a string looks like a valid email address.\n */\nexport function isValidEmail(value: string): boolean {\n if (!value || typeof value !== \"string\") return false\n // Basic email regex - intentionally permissive to avoid false negatives\n const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n return emailRegex.test(value.trim())\n}\n\n/**\n * Email field name patterns (case-insensitive, normalized).\n * Order matters - more specific patterns first.\n */\nconst EMAIL_FIELD_PATTERNS = [\n /^e?-?mail$/i,\n /^email[_-]?address$/i,\n /^user[_-]?email$/i,\n /^work[_-]?email$/i,\n /^contact[_-]?email$/i,\n /^primary[_-]?email$/i,\n /^business[_-]?email$/i,\n]\n\n/**\n * Full name field patterns.\n */\nconst FULL_NAME_PATTERNS = [\n /^name$/i,\n /^full[_-]?name$/i,\n /^your[_-]?name$/i,\n /^customer[_-]?name$/i,\n /^contact[_-]?name$/i,\n /^display[_-]?name$/i,\n]\n\n/**\n * First name field patterns.\n */\nconst FIRST_NAME_PATTERNS = [\n /^first[_-]?name$/i,\n /^firstname$/i,\n /^first$/i,\n /^fname$/i,\n /^given[_-]?name$/i,\n /^forename$/i,\n]\n\n/**\n * Last name field patterns.\n */\nconst LAST_NAME_PATTERNS = [\n /^last[_-]?name$/i,\n /^lastname$/i,\n /^last$/i,\n /^lname$/i,\n /^surname$/i,\n /^family[_-]?name$/i,\n]\n\n/**\n * Check if a field name matches any of the given patterns.\n */\nfunction matchesPatterns(fieldName: string, patterns: RegExp[]): boolean {\n const normalized = fieldName.trim()\n return patterns.some((pattern) => pattern.test(normalized))\n}\n\n/**\n * Find an email value from form fields.\n *\n * Priority:\n * 1. Fields with input type=\"email\" (if inputTypes map provided)\n * 2. Field names matching email patterns\n * 3. Any field with a value that looks like an email\n *\n * @param fields - Form field key-value pairs\n * @param inputTypes - Optional map of field names to input types\n * @returns The email value if found, undefined otherwise\n */\nexport function findEmailField(\n fields: Record<string, string>,\n inputTypes?: Map<string, string>,\n): string | undefined {\n // Priority 1: Check fields with type=\"email\"\n if (inputTypes) {\n for (const [fieldName, inputType] of inputTypes.entries()) {\n if (inputType === \"email\") {\n const value = fields[fieldName]\n if (value && isValidEmail(value)) {\n return value.trim()\n }\n }\n }\n }\n\n // Priority 2: Check field names matching email patterns\n for (const [fieldName, value] of Object.entries(fields)) {\n if (matchesPatterns(fieldName, EMAIL_FIELD_PATTERNS) && isValidEmail(value)) {\n return value.trim()\n }\n }\n\n // Priority 3: Any field with email-like value (fallback)\n for (const value of Object.values(fields)) {\n if (isValidEmail(value)) {\n return value.trim()\n }\n }\n\n return undefined\n}\n\n/**\n * Extract name fields from form data.\n *\n * Looks for:\n * - Full name fields (name, full_name, etc.)\n * - First name fields (first_name, fname, etc.)\n * - Last name fields (last_name, lname, etc.)\n *\n * If only first/last names are found, combines them into a full name.\n *\n * @param fields - Form field key-value pairs\n * @returns Object with name, firstName, and/or lastName if found\n */\nexport function findNameFields(fields: Record<string, string>): {\n name?: string\n firstName?: string\n lastName?: string\n} {\n let fullName: string | undefined\n let firstName: string | undefined\n let lastName: string | undefined\n\n for (const [fieldName, value] of Object.entries(fields)) {\n const trimmedValue = value?.trim()\n if (!trimmedValue) continue\n\n // Check for full name\n if (!fullName && matchesPatterns(fieldName, FULL_NAME_PATTERNS)) {\n fullName = trimmedValue\n }\n\n // Check for first name\n if (!firstName && matchesPatterns(fieldName, FIRST_NAME_PATTERNS)) {\n firstName = trimmedValue\n }\n\n // Check for last name\n if (!lastName && matchesPatterns(fieldName, LAST_NAME_PATTERNS)) {\n lastName = trimmedValue\n }\n }\n\n const result: { name?: string; firstName?: string; lastName?: string } = {}\n\n // If we have a full name, use it\n if (fullName) {\n result.name = fullName\n }\n // If we have first and last, combine them\n else if (firstName && lastName) {\n result.name = `${firstName} ${lastName}`\n result.firstName = firstName\n result.lastName = lastName\n }\n // If we only have first name\n else if (firstName) {\n result.firstName = firstName\n }\n // If we only have last name\n else if (lastName) {\n result.lastName = lastName\n }\n\n return result\n}\n\n/**\n * Identity extracted from a form submission.\n */\nexport interface ExtractedIdentity {\n email: string\n name?: string\n firstName?: string\n lastName?: string\n}\n\n/**\n * Extract identity information (email + name) from form fields.\n *\n * Returns undefined if no valid email is found (email is required for identification).\n *\n * @param fields - Form field key-value pairs\n * @param inputTypes - Optional map of field names to input types\n * @returns Extracted identity with email and optional name fields, or undefined\n */\nexport function extractIdentityFromForm(\n fields: Record<string, string>,\n inputTypes?: Map<string, string>,\n): ExtractedIdentity | undefined {\n const email = findEmailField(fields, inputTypes)\n\n // Email is required for identification\n if (!email) {\n return undefined\n }\n\n const nameFields = findNameFields(fields)\n\n return {\n email,\n ...nameFields,\n }\n}\n","import type {\n CalendarEvent,\n CalendarProvider,\n CustomEvent,\n FormEvent,\n IdentifyEvent,\n IngestPayload,\n PageviewEvent,\n SourceType,\n TrackerEvent,\n UtmParams,\n} from \"./types\"\nimport { extractPathFromUrl, extractUtmParams } from \"./utils\"\n\n// ============================================\n// EVENT BUILDERS\n// ============================================\n\ninterface BaseEventParams {\n url: string\n referrer?: string\n timestamp?: number\n}\n\n/**\n * Build a pageview event.\n */\nexport function buildPageviewEvent(params: BaseEventParams & { title?: string }): PageviewEvent {\n const { url, referrer, timestamp, title } = params\n return {\n type: \"pageview\",\n timestamp: timestamp ?? Date.now(),\n url,\n path: extractPathFromUrl(url),\n referrer,\n utm: extractUtmParams(url),\n title,\n }\n}\n\n/**\n * Build a form event.\n */\nexport function buildFormEvent(\n params: BaseEventParams & {\n formId?: string\n formFields?: Record<string, string>\n },\n): FormEvent {\n const { url, referrer, timestamp, formId, formFields } = params\n return {\n type: \"form\",\n timestamp: timestamp ?? Date.now(),\n url,\n path: extractPathFromUrl(url),\n referrer,\n utm: extractUtmParams(url),\n formId,\n formFields,\n }\n}\n\n/**\n * Build an identify event.\n */\nexport function buildIdentifyEvent(\n params: BaseEventParams & {\n email?: string\n userId?: string\n traits?: Record<string, string | number | boolean | null>\n },\n): IdentifyEvent {\n const { url, referrer, timestamp, email, userId, traits } = params\n return {\n type: \"identify\",\n timestamp: timestamp ?? Date.now(),\n url,\n path: extractPathFromUrl(url),\n referrer,\n utm: extractUtmParams(url),\n email,\n userId,\n traits,\n }\n}\n\n/**\n * Build a custom event.\n */\nexport function buildCustomEvent(\n params: BaseEventParams & {\n eventName: string\n properties?: Record<string, string | number | boolean | null>\n },\n): CustomEvent {\n const { url, referrer, timestamp, eventName, properties } = params\n return {\n type: \"custom\",\n timestamp: timestamp ?? Date.now(),\n url,\n path: extractPathFromUrl(url),\n referrer,\n utm: extractUtmParams(url),\n eventName,\n properties,\n }\n}\n\n/**\n * Build a calendar booking event.\n */\nexport function buildCalendarEvent(\n params: BaseEventParams & {\n provider: CalendarProvider\n eventType?: string\n startTime?: string\n endTime?: string\n duration?: number\n isRecurring?: boolean\n inviteeEmail?: string\n inviteeName?: string\n },\n): CalendarEvent {\n const {\n url,\n referrer,\n timestamp,\n provider,\n eventType,\n startTime,\n endTime,\n duration,\n isRecurring,\n inviteeEmail,\n inviteeName,\n } = params\n return {\n type: \"calendar\",\n timestamp: timestamp ?? Date.now(),\n url,\n path: extractPathFromUrl(url),\n referrer,\n utm: extractUtmParams(url),\n provider,\n eventType,\n startTime,\n endTime,\n duration,\n isRecurring,\n inviteeEmail,\n inviteeName,\n }\n}\n\n// ============================================\n// PAYLOAD BUILDER\n// ============================================\n\n/**\n * Build an ingest payload from events.\n */\nexport function buildIngestPayload(\n visitorId: string,\n source: SourceType,\n events: TrackerEvent[],\n): IngestPayload {\n return {\n visitorId,\n source,\n events,\n }\n}\n\n// ============================================\n// BATCH HELPERS\n// ============================================\n\n/**\n * Maximum number of events in a single batch.\n */\nexport const MAX_BATCH_SIZE = 100\n\n/**\n * Split events into batches of MAX_BATCH_SIZE.\n */\nexport function batchEvents(events: TrackerEvent[]): TrackerEvent[][] {\n const batches: TrackerEvent[][] = []\n for (let i = 0; i < events.length; i += MAX_BATCH_SIZE) {\n batches.push(events.slice(i, i + MAX_BATCH_SIZE))\n }\n return batches\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@outlit/core",
3
- "version": "0.0.0-canary-202512100315-e4db4b4-20251210031539",
3
+ "version": "0.0.0-canary-202512110053-50cdbec-20251211005400",
4
4
  "description": "Shared types and utilities for Outlit tracking SDKs",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Outlit AI",