@outlit/core 2.0.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/payload.ts","../src/types.ts","../src/utils.ts"],"sourcesContent":["// Types\n\n// Payload builders\nexport {\n batchEvents,\n buildCalendarEvent,\n buildCustomEvent,\n buildEngagementEvent,\n buildFormEvent,\n buildIdentifyEvent,\n buildIngestPayload,\n buildPageviewEvent,\n MAX_BATCH_SIZE,\n} from \"./payload\"\nexport type {\n BrowserIdentifyOptions,\n BrowserTrackOptions,\n CalendarEvent,\n CalendarProvider,\n CustomEvent,\n CustomerAttribution,\n CustomerTraits,\n EngagementEvent,\n EventType,\n FormEvent,\n IdentifyEvent,\n IdentifyTraits,\n IngestPayload,\n IngestResponse,\n PageviewEvent,\n PayloadCustomerIdentity,\n PayloadUserIdentity,\n ServerIdentifyOptions,\n ServerIdentity,\n ServerTrackOptions,\n SourceType,\n TrackerConfig,\n TrackerEvent,\n UtmParams,\n} from \"./types\"\n// Constants\nexport { DEFAULT_API_HOST, DEFAULT_DENIED_FORM_FIELDS } from \"./types\"\n\n// Auto-identify types\nexport type { ExtractedIdentity } from \"./utils\"\n// Utilities\nexport {\n extractIdentityFromForm,\n extractPathFromUrl,\n extractUtmParams,\n findEmailField,\n findNameFields,\n isFieldDenied,\n // Auto-identify utilities\n isValidEmail,\n sanitizeFormFields,\n validateServerIdentity,\n} from \"./utils\"\n","import { v7 as uuidv7 } from \"uuid\"\nimport type {\n CalendarEvent,\n CalendarProvider,\n CustomEvent,\n CustomerTraits,\n EngagementEvent,\n FormEvent,\n IdentifyEvent,\n IdentifyTraits,\n IngestPayload,\n PageviewEvent,\n PayloadCustomerIdentity,\n PayloadUserIdentity,\n SourceType,\n TrackerEvent,\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 uuid: uuidv7(),\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 uuid: uuidv7(),\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 fingerprint?: string\n traits?: IdentifyTraits\n customerId?: string\n customerTraits?: CustomerTraits\n },\n): IdentifyEvent {\n const {\n url,\n referrer,\n timestamp,\n email,\n userId,\n fingerprint,\n traits,\n customerId,\n customerTraits,\n } = params\n return {\n uuid: uuidv7(),\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 fingerprint,\n customerId,\n customerTraits,\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 email?: string\n userId?: string\n fingerprint?: string\n customerId?: string\n },\n): CustomEvent {\n const {\n url,\n referrer,\n timestamp,\n eventName,\n properties,\n email,\n userId,\n fingerprint,\n customerId,\n } = params\n return {\n uuid: uuidv7(),\n type: \"custom\",\n timestamp: timestamp ?? Date.now(),\n url,\n path: extractPathFromUrl(url),\n referrer,\n utm: extractUtmParams(url),\n eventName,\n email,\n userId,\n fingerprint,\n customerId,\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 uuid: uuidv7(),\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 * Build an engagement event.\n * Captures active time on page for session analytics.\n */\nexport function buildEngagementEvent(\n params: BaseEventParams & {\n activeTimeMs: number\n totalTimeMs: number\n sessionId: string\n },\n): EngagementEvent {\n const { url, referrer, timestamp, activeTimeMs, totalTimeMs, sessionId } = params\n return {\n uuid: uuidv7(),\n type: \"engagement\",\n timestamp: timestamp ?? Date.now(),\n url,\n path: extractPathFromUrl(url),\n referrer,\n utm: extractUtmParams(url),\n activeTimeMs,\n totalTimeMs,\n sessionId,\n }\n}\n\n// ============================================\n// PAYLOAD BUILDER\n// ============================================\n\n/**\n * Build an ingest payload from events.\n *\n * @param visitorId - The anonymous visitor ID from browser cookie/storage\n * @param source - The event source (client, server, integration)\n * @param events - Array of events to send\n * @param userIdentity - Optional user identity for immediate resolution (from setUser in SPA)\n * @param sessionId - Optional session ID for grouping events (browser SDK only)\n * @param fingerprint - Optional device identifier for server-side anonymous tracking\n * @param customerIdentity - Optional customer identity for batch attribution. `customerId`-only\n * batches are valid and remain provisional until a later identify(email, customerId) call\n * links that external account/workspace to a resolved customer.\n */\nexport function buildIngestPayload(\n visitorId: string,\n source: SourceType,\n events: TrackerEvent[],\n userIdentity?: PayloadUserIdentity,\n sessionId?: string,\n fingerprint?: string,\n customerIdentity?: PayloadCustomerIdentity,\n): IngestPayload {\n const payload: IngestPayload = {\n visitorId,\n source,\n events,\n }\n const legacyCustomerIdentity =\n customerIdentity === undefined &&\n userIdentity &&\n (userIdentity.customerId || userIdentity.customerTraits)\n ? {\n ...(userIdentity.customerId && { customerId: userIdentity.customerId }),\n ...(userIdentity.customerTraits && { customerTraits: userIdentity.customerTraits }),\n }\n : undefined\n const resolvedCustomerIdentity = customerIdentity ?? legacyCustomerIdentity\n\n // Only include fingerprint if provided (server SDK only)\n if (fingerprint) {\n payload.fingerprint = fingerprint\n }\n\n // Only include sessionId if provided (browser SDK only)\n if (sessionId) {\n payload.sessionId = sessionId\n }\n\n // Only include userIdentity if it has actual values\n if (userIdentity && (userIdentity.email || userIdentity.userId || userIdentity.traits)) {\n payload.userIdentity = {\n ...(userIdentity.email && { email: userIdentity.email }),\n ...(userIdentity.userId && { userId: userIdentity.userId }),\n ...(userIdentity.traits && { traits: userIdentity.traits }),\n }\n }\n\n if (\n resolvedCustomerIdentity &&\n (resolvedCustomerIdentity.customerId || resolvedCustomerIdentity.customerTraits)\n ) {\n payload.customerIdentity = {\n ...(resolvedCustomerIdentity.customerId && {\n customerId: resolvedCustomerIdentity.customerId,\n }),\n ...(resolvedCustomerIdentity.customerTraits && {\n customerTraits: resolvedCustomerIdentity.customerTraits,\n }),\n }\n }\n\n return payload\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","// ============================================\n// EVENT TYPES\n// ============================================\n\nexport type EventType = \"pageview\" | \"form\" | \"identify\" | \"custom\" | \"calendar\" | \"engagement\"\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 extends CustomerAttribution {\n email?: string\n /** Your system-owned user/contact ID. */\n userId?: string\n /** User/contact traits. */\n traits?: IdentifyTraits\n /** Customer/account-level traits. */\n customerTraits?: CustomerTraits\n}\n\n// ============================================\n// SERVER-SPECIFIC TYPES\n// Track calls can use user identity, customer attribution, or both.\n// Identify calls remain user-scoped and require email or userId.\n// ============================================\n\n/**\n * Base server-side user identity.\n * Track calls may use customerId without these fields.\n * Identify calls still require email or userId at runtime.\n * `customerId`-only track calls are valid immediately, but they stay provisional\n * until the same customer/account/workspace later appears on identify() with\n * the matching customerId and an email.\n *\n * - fingerprint: Device identifier for anonymous tracking (can be linked later)\n * - email: User's email address (definitive identity, resolves immediately)\n * - userId: Your system-owned user/contact ID\n */\nexport interface ServerIdentity {\n fingerprint?: string\n email?: string\n /** Your system-owned user/contact ID. */\n userId?: string\n}\n\nexport interface CustomerAttribution {\n /** Your system-owned customer/account/workspace ID. */\n customerId?: string\n}\n\n// ============================================\n// TRAITS\n// ============================================\n\nexport interface CustomerTraits {\n /** Customer's billing plan */\n plan?: string\n /** Allow additional custom properties */\n [key: string]: string | number | boolean | null | undefined\n}\n\n/**\n * Traits for identify calls.\n * These are user/contact traits, not customer/account traits.\n */\nexport interface IdentifyTraits {\n /** User-level traits */\n [key: string]: string | number | boolean | null | undefined\n}\n\nexport interface ServerTrackOptions extends ServerIdentity, CustomerAttribution {\n eventName: string\n properties?: Record<string, string | number | boolean | null>\n timestamp?: number\n}\n\nexport interface ServerIdentifyOptions extends ServerIdentity, CustomerAttribution {\n traits?: IdentifyTraits\n customerTraits?: CustomerTraits\n}\n\n// ============================================\n// INTERNAL EVENT TYPES\n// These are the full event objects sent to the API\n// ============================================\n\ninterface BaseEvent {\n uuid?: string\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 fingerprint?: string\n customerId?: string\n customerTraits?: CustomerTraits\n traits?: IdentifyTraits\n}\n\nexport interface CustomEvent extends BaseEvent {\n type: \"custom\"\n eventName: string\n email?: string\n userId?: string\n fingerprint?: string\n customerId?: 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 interface EngagementEvent extends BaseEvent {\n type: \"engagement\"\n /** Time in milliseconds the user was actively engaged (visible tab + user interactions) */\n activeTimeMs: number\n /** Total wall-clock time in milliseconds on the page */\n totalTimeMs: number\n /** Session ID for grouping engagement events. Resets after 30 min of inactivity or tab close. */\n sessionId: string\n}\n\nexport type TrackerEvent =\n | PageviewEvent\n | FormEvent\n | IdentifyEvent\n | CustomEvent\n | CalendarEvent\n | EngagementEvent\n\n// ============================================\n// INGEST PAYLOAD\n// This is what gets sent to the API\n// ============================================\n\n/**\n * User identity for payload-level resolution.\n * Used by browser SDK when user is logged in (via setUser).\n * Customer attribution is carried separately in `customerIdentity`.\n */\nexport interface PayloadUserIdentity {\n email?: string\n /** Your system-owned user/contact ID. */\n userId?: string\n /** User/contact traits. */\n traits?: IdentifyTraits\n /**\n * @deprecated Use payload-level `customerIdentity.customerId` instead.\n * Kept for one compatibility window while callers migrate.\n */\n customerId?: string\n /**\n * @deprecated Use payload-level `customerIdentity.customerTraits` instead.\n * Kept for one compatibility window while callers migrate.\n */\n customerTraits?: CustomerTraits\n}\n\n/**\n * Customer identity for payload-level attribution.\n * Used by browser SDK to attach account/workspace context to a batch.\n * `customerId`-only attribution is valid and remains provisional until a later\n * identify(email, customerId) call links that external account/workspace to a resolved customer.\n */\nexport interface PayloadCustomerIdentity extends CustomerAttribution {\n /** Customer/account traits. */\n customerTraits?: CustomerTraits\n}\n\nexport interface IngestPayload {\n visitorId?: string // Required for pixel, optional for server\n source: SourceType\n events: TrackerEvent[]\n /**\n * Device identifier for anonymous tracking.\n * Events with fingerprint can be linked to users later via identify.\n * Only present for server-side events.\n */\n fingerprint?: string\n /**\n * Session ID for grouping all events in this batch.\n * Only present for browser (client) source events.\n * Used to correlate pageviews, forms, custom events, and engagement\n * within the same browsing session.\n */\n sessionId?: string\n /**\n * User identity for this batch of events.\n * When present, the server can resolve directly to CustomerContact\n * instead of relying on anonymous visitor flow.\n *\n * This is set by the browser SDK when setUser() has been called,\n * allowing immediate identity resolution for SPA/React apps.\n */\n userIdentity?: PayloadUserIdentity\n /**\n * Customer/account identity for this batch of events.\n * Used to attribute browser batches to a customer/workspace even when\n * the customer fields are not present on every event.\n */\n customerIdentity?: PayloadCustomerIdentity\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\n// Re-export for convenience\nexport type { PayloadUserIdentity as UserIdentity }\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 (error) {\n console.warn(`[Outlit] Failed to parse URL for UTM extraction: \"${url}\"`, error)\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 let path = urlObj.pathname\n const hash = urlObj.hash\n\n // For file:// or custom protocol URLs with no real hostname (common in Electron),\n // the meaningful route is typically stored in the hash fragment.\n if (hash && hash.length > 1 && (!urlObj.hostname || urlObj.protocol === \"file:\")) {\n path = hash.replace(/^#/, \"\")\n }\n // For web hash-routing SPAs (e.g. \"/#/settings\"), prefer hash when the\n // pathname is an SPA entry point.\n else if (\n hash.startsWith(\"#/\") &&\n (urlObj.pathname === \"/\" || urlObj.pathname.endsWith(\"/index.html\"))\n ) {\n path = hash.slice(1)\n }\n\n if (!path) return \"/\"\n if (!path.startsWith(\"/\")) return `/${path}`\n\n return path\n } catch (error) {\n console.warn(\n `[Outlit] Failed to parse URL for path extraction: \"${url}\", defaulting to \"/\"`,\n error,\n )\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 *\n * Valid identities:\n * - fingerprint: Device identifier (for anonymous tracking, can be linked later)\n * - email: User's email (definitive identity)\n * - userId: Your system-owned user/contact ID\n * - customerId: Your system-owned customer/account/workspace ID\n */\nexport function validateServerIdentity(\n fingerprint?: string,\n email?: string,\n userId?: string,\n customerId?: string,\n): void {\n const hasFingerprint = fingerprint && fingerprint.trim().length > 0\n const hasEmail = email && email.trim().length > 0\n const hasUserId = userId && userId.trim().length > 0\n const hasCustomerId = customerId && customerId.trim().length > 0\n\n if (!hasFingerprint && !hasEmail && !hasUserId && !hasCustomerId) {\n throw new Error(\n \"Server SDK requires at least one of: fingerprint, email, userId, or customerId for track calls. \" +\n \"Use fingerprint/email/userId for user-scoped tracking or customerId for customer-only tracking.\",\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"],"mappings":"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","names":["uuidv7"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/payload.ts","../src/generated/ingest-contract.ts","../src/types.ts","../src/utils.ts","../src/transport.ts"],"sourcesContent":["// Types\n\n// Payload builders\nexport {\n batchEvents,\n buildCalendarEvent,\n buildCustomEvent,\n buildEngagementEvent,\n buildFormEvent,\n buildIdentifyEvent,\n buildIngestPayload,\n buildPageviewEvent,\n MAX_BATCH_SIZE,\n} from \"./payload\"\nexport { buildIngestUrl, INGEST_EVENT_TYPES, INGEST_METHOD } from \"./transport\"\nexport type {\n BrowserIdentifyOptions,\n BrowserTrackOptions,\n CalendarEvent,\n CalendarProvider,\n CustomEvent,\n CustomerAttribution,\n CustomerTraits,\n EngagementEvent,\n EventType,\n FormEvent,\n IdentifyEvent,\n IdentifyTraits,\n IngestPayload,\n IngestResponse,\n PageviewEvent,\n PayloadCustomerIdentity,\n PayloadCustomerIdentityInput,\n PayloadUserIdentity,\n PayloadUserIdentityInput,\n ServerIdentifyOptions,\n ServerIdentity,\n ServerTrackOptions,\n SourceType,\n TrackerConfig,\n TrackerEvent,\n UtmParams,\n} from \"./types\"\n// Constants\nexport { DEFAULT_API_HOST, DEFAULT_DENIED_FORM_FIELDS } from \"./types\"\n\n// Auto-identify types\nexport type { ExtractedIdentity } from \"./utils\"\n// Utilities\nexport {\n extractIdentityFromForm,\n extractPathFromUrl,\n extractUtmParams,\n findEmailField,\n findNameFields,\n isFieldDenied,\n // Auto-identify utilities\n isValidEmail,\n sanitizeFormFields,\n validateServerIdentity,\n} from \"./utils\"\n","import { v7 as uuidv7 } from \"uuid\"\nimport { ingestTransport } from \"./generated/ingest-contract\"\nimport type {\n CalendarEvent,\n CalendarProvider,\n CustomEvent,\n CustomerTraits,\n EngagementEvent,\n FormEvent,\n IdentifyEvent,\n IdentifyTraits,\n IngestPayload,\n PageviewEvent,\n PayloadCustomerIdentityInput,\n PayloadUserIdentityInput,\n SourceType,\n TrackerEvent,\n} from \"./types\"\nimport { extractPathFromUrl, extractUtmParams } from \"./utils\"\n\ntype WireTraitValue = string | number | boolean | null\n\nfunction normalizeTraits(\n traits: CustomerTraits | IdentifyTraits | undefined,\n): Record<string, WireTraitValue> | undefined {\n if (!traits) return undefined\n\n const normalized: Record<string, WireTraitValue> = {}\n for (const [key, value] of Object.entries(traits)) {\n if (value !== undefined) normalized[key] = value\n }\n return Object.keys(normalized).length > 0 ? normalized : undefined\n}\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 uuid: uuidv7(),\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 uuid: uuidv7(),\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 fingerprint?: string\n traits?: IdentifyTraits\n customerId?: string\n customerTraits?: CustomerTraits\n },\n): IdentifyEvent {\n const {\n url,\n referrer,\n timestamp,\n email,\n userId,\n fingerprint,\n traits,\n customerId,\n customerTraits,\n } = params\n return {\n uuid: uuidv7(),\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 fingerprint,\n customerId,\n customerTraits: normalizeTraits(customerTraits),\n traits: normalizeTraits(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 email?: string\n userId?: string\n fingerprint?: string\n customerId?: string\n },\n): CustomEvent {\n const {\n url,\n referrer,\n timestamp,\n eventName,\n properties,\n email,\n userId,\n fingerprint,\n customerId,\n } = params\n return {\n uuid: uuidv7(),\n type: \"custom\",\n timestamp: timestamp ?? Date.now(),\n url,\n path: extractPathFromUrl(url),\n referrer,\n utm: extractUtmParams(url),\n eventName,\n email,\n userId,\n fingerprint,\n customerId,\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 uuid: uuidv7(),\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 * Build an engagement event.\n * Captures active time on page for session analytics.\n */\nexport function buildEngagementEvent(\n params: BaseEventParams & {\n activeTimeMs: number\n totalTimeMs: number\n sessionId: string\n },\n): EngagementEvent {\n const { url, referrer, timestamp, activeTimeMs, totalTimeMs, sessionId } = params\n return {\n uuid: uuidv7(),\n type: \"engagement\",\n timestamp: timestamp ?? Date.now(),\n url,\n path: extractPathFromUrl(url),\n referrer,\n utm: extractUtmParams(url),\n activeTimeMs,\n totalTimeMs,\n sessionId,\n }\n}\n\n// ============================================\n// PAYLOAD BUILDER\n// ============================================\n\n/**\n * Build an ingest payload from events.\n *\n * @param visitorId - The anonymous visitor ID from browser cookie/storage\n * @param source - The event source (client, server, integration)\n * @param events - Array of events to send\n * @param userIdentity - Optional user identity for immediate resolution (from setUser in SPA)\n * @param sessionId - Optional session ID for grouping events (browser SDK only)\n * @param fingerprint - Optional device identifier for server-side anonymous tracking\n * @param customerIdentity - Optional customer identity for batch attribution. `customerId`-only\n * batches are valid and remain provisional until a later identify(email, customerId) call\n * links that external account/workspace to a resolved customer.\n */\nexport function buildIngestPayload(\n visitorId: string,\n source: SourceType,\n events: TrackerEvent[],\n userIdentity?: PayloadUserIdentityInput,\n sessionId?: string,\n fingerprint?: string,\n customerIdentity?: PayloadCustomerIdentityInput,\n): IngestPayload {\n const payload: IngestPayload = {\n visitorId,\n source,\n events,\n }\n const legacyCustomerIdentity =\n customerIdentity === undefined &&\n userIdentity &&\n (userIdentity.customerId || userIdentity.customerTraits)\n ? {\n ...(userIdentity.customerId && { customerId: userIdentity.customerId }),\n ...(userIdentity.customerTraits && { customerTraits: userIdentity.customerTraits }),\n }\n : undefined\n const resolvedCustomerIdentity = customerIdentity ?? legacyCustomerIdentity\n const userTraits = normalizeTraits(userIdentity?.traits)\n const customerTraits = normalizeTraits(resolvedCustomerIdentity?.customerTraits)\n\n // Only include fingerprint if provided (server SDK only)\n if (fingerprint) {\n payload.fingerprint = fingerprint\n }\n\n // Only include sessionId if provided (browser SDK only)\n if (sessionId) {\n payload.sessionId = sessionId\n }\n\n // Only include userIdentity if it has actual values\n if (\n userIdentity &&\n (userIdentity.email || userIdentity.userId || userIdentity.fingerprint || userTraits)\n ) {\n payload.userIdentity = {\n ...(userIdentity.email && { email: userIdentity.email }),\n ...(userIdentity.userId && { userId: userIdentity.userId }),\n ...(userIdentity.fingerprint && { fingerprint: userIdentity.fingerprint }),\n ...(userTraits && { traits: userTraits }),\n }\n }\n\n if (resolvedCustomerIdentity && (resolvedCustomerIdentity.customerId || customerTraits)) {\n payload.customerIdentity = {\n ...(resolvedCustomerIdentity.customerId && {\n customerId: resolvedCustomerIdentity.customerId,\n }),\n ...(customerTraits && { customerTraits }),\n }\n }\n\n return payload\n}\n\n// ============================================\n// BATCH HELPERS\n// ============================================\n\n/**\n * Maximum number of events in a single batch.\n */\nexport const MAX_BATCH_SIZE = ingestTransport.maxBatchSize\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","// Generated from packages/tools/src/generated/contracts.ts. Do not edit by hand.\n\nexport const ingestTransport = {\n method: \"POST\",\n pathTemplate: \"/api/i/v1/{publicKey}/events\",\n maxBatchSize: 100,\n eventTypes: [\"pageview\", \"form\", \"identify\", \"custom\", \"calendar\", \"engagement\"],\n} as const\n","import type { GeneratedIngestPayload, GeneratedIngestResponse } from \"./generated/ingest-wire-types\"\n\n// ============================================\n// CORE-OWNED INGEST WIRE TYPES\n// ============================================\n\nexport type IngestPayload = GeneratedIngestPayload\nexport type IngestResponse = GeneratedIngestResponse\nexport type TrackerEvent = IngestPayload[\"events\"][number]\nexport type EventType = TrackerEvent[\"type\"]\nexport type SourceType = NonNullable<IngestPayload[\"source\"]>\nexport type PageviewEvent = Extract<TrackerEvent, { type: \"pageview\" }>\nexport type FormEvent = Extract<TrackerEvent, { type: \"form\" }>\nexport type IdentifyEvent = Extract<TrackerEvent, { type: \"identify\" }>\nexport type CustomEvent = Extract<TrackerEvent, { type: \"custom\" }>\nexport type CalendarEvent = Extract<TrackerEvent, { type: \"calendar\" }>\nexport type EngagementEvent = Extract<TrackerEvent, { type: \"engagement\" }>\nexport type CalendarProvider = CalendarEvent[\"provider\"]\nexport type PayloadUserIdentity = NonNullable<IngestPayload[\"userIdentity\"]>\nexport type PayloadCustomerIdentity = NonNullable<IngestPayload[\"customerIdentity\"]>\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 ref?: 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 extends CustomerAttribution {\n email?: string\n /** Your system-owned user/contact ID. */\n userId?: string\n /** User/contact traits. */\n traits?: IdentifyTraits\n /** Customer/account-level traits. */\n customerTraits?: CustomerTraits\n}\n\n// ============================================\n// SERVER-SPECIFIC TYPES\n// Track calls can use user identity, customer attribution, or both.\n// Identify calls remain user-scoped and require email or userId.\n// ============================================\n\n/**\n * Base server-side user identity.\n * Track calls may use customerId without these fields.\n * Identify calls still require email or userId at runtime.\n * `customerId`-only track calls are valid immediately, but they stay provisional\n * until the same customer/account/workspace later appears on identify() with\n * the matching customerId and an email.\n *\n * - fingerprint: Device identifier for anonymous tracking (can be linked later)\n * - email: User's email address (definitive identity, resolves immediately)\n * - userId: Your system-owned user/contact ID\n */\nexport interface ServerIdentity {\n fingerprint?: string\n email?: string\n /** Your system-owned user/contact ID. */\n userId?: string\n}\n\nexport interface CustomerAttribution {\n /** Your system-owned customer/account/workspace ID. */\n customerId?: string\n}\n\n// ============================================\n// TRAITS\n// ============================================\n\nexport interface CustomerTraits {\n /** Customer's billing plan */\n plan?: string\n /** Allow additional custom properties */\n [key: string]: string | number | boolean | null | undefined\n}\n\n/**\n * Traits for identify calls.\n * These are user/contact traits, not customer/account traits.\n */\nexport interface IdentifyTraits {\n /** User-level traits */\n [key: string]: string | number | boolean | null | undefined\n}\n\n/**\n * Input accepted by the payload builder. Deprecated customer fields are lifted\n * into the wire-level `customerIdentity` object and are never sent in `userIdentity`.\n */\nexport type PayloadUserIdentityInput = Omit<PayloadUserIdentity, \"traits\"> & {\n /** User/contact traits. */\n traits?: IdentifyTraits\n /** @deprecated Pass `customerIdentity.customerId` to the payload builder instead. */\n customerId?: string\n /** @deprecated Pass `customerIdentity.customerTraits` to the payload builder instead. */\n customerTraits?: CustomerTraits\n}\n\nexport interface PayloadCustomerIdentityInput extends CustomerAttribution {\n /** Customer/account traits. */\n customerTraits?: CustomerTraits\n}\n\nexport interface ServerTrackOptions extends ServerIdentity, CustomerAttribution {\n eventName: string\n properties?: Record<string, string | number | boolean | null>\n timestamp?: number\n}\n\nexport interface ServerIdentifyOptions extends ServerIdentity, CustomerAttribution {\n traits?: IdentifyTraits\n customerTraits?: CustomerTraits\n}\n\n// ============================================\n// CONSTANTS\n// ============================================\n\nexport const DEFAULT_API_HOST = \"https://app.outlit.ai\"\n\n// Re-export for convenience\nexport type { PayloadUserIdentity as UserIdentity }\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 (error) {\n console.warn(`[Outlit] Failed to parse URL for UTM extraction: \"${url}\"`, error)\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 let path = urlObj.pathname\n const hash = urlObj.hash\n\n // For file:// or custom protocol URLs with no real hostname (common in Electron),\n // the meaningful route is typically stored in the hash fragment.\n if (hash && hash.length > 1 && (!urlObj.hostname || urlObj.protocol === \"file:\")) {\n path = hash.replace(/^#/, \"\")\n }\n // For web hash-routing SPAs (e.g. \"/#/settings\"), prefer hash when the\n // pathname is an SPA entry point.\n else if (\n hash.startsWith(\"#/\") &&\n (urlObj.pathname === \"/\" || urlObj.pathname.endsWith(\"/index.html\"))\n ) {\n path = hash.slice(1)\n }\n\n if (!path) return \"/\"\n if (!path.startsWith(\"/\")) return `/${path}`\n\n return path\n } catch (error) {\n console.warn(\n `[Outlit] Failed to parse URL for path extraction: \"${url}\", defaulting to \"/\"`,\n error,\n )\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 *\n * Valid identities:\n * - fingerprint: Device identifier (for anonymous tracking, can be linked later)\n * - email: User's email (definitive identity)\n * - userId: Your system-owned user/contact ID\n * - customerId: Your system-owned customer/account/workspace ID\n */\nexport function validateServerIdentity(\n fingerprint?: string,\n email?: string,\n userId?: string,\n customerId?: string,\n): void {\n const hasFingerprint = fingerprint && fingerprint.trim().length > 0\n const hasEmail = email && email.trim().length > 0\n const hasUserId = userId && userId.trim().length > 0\n const hasCustomerId = customerId && customerId.trim().length > 0\n\n if (!hasFingerprint && !hasEmail && !hasUserId && !hasCustomerId) {\n throw new Error(\n \"Server SDK requires at least one of: fingerprint, email, userId, or customerId for track calls. \" +\n \"Use fingerprint/email/userId for user-scoped tracking or customerId for customer-only tracking.\",\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 { ingestTransport } from \"./generated/ingest-contract\"\n\nexport const INGEST_METHOD = ingestTransport.method\nexport const INGEST_EVENT_TYPES = ingestTransport.eventTypes\n\nexport function buildIngestUrl(apiHost: string, publicKey: string): string {\n const path = ingestTransport.pathTemplate.replace(\"{publicKey}\", encodeURIComponent(publicKey))\n return `${apiHost.replace(/\\/$/, \"\")}${path}`\n}\n"],"mappings":"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","names":["uuidv7"]}
package/dist/index.mjs CHANGED
@@ -1,6 +1,14 @@
1
1
  // src/payload.ts
2
2
  import { v7 as uuidv7 } from "uuid";
3
3
 
4
+ // src/generated/ingest-contract.ts
5
+ var ingestTransport = {
6
+ method: "POST",
7
+ pathTemplate: "/api/i/v1/{publicKey}/events",
8
+ maxBatchSize: 100,
9
+ eventTypes: ["pageview", "form", "identify", "custom", "calendar", "engagement"]
10
+ };
11
+
4
12
  // src/types.ts
5
13
  var DEFAULT_API_HOST = "https://app.outlit.ai";
6
14
  var DEFAULT_DENIED_FORM_FIELDS = [
@@ -219,6 +227,14 @@ function extractIdentityFromForm(fields, inputTypes) {
219
227
  }
220
228
 
221
229
  // src/payload.ts
230
+ function normalizeTraits(traits) {
231
+ if (!traits) return void 0;
232
+ const normalized = {};
233
+ for (const [key, value] of Object.entries(traits)) {
234
+ if (value !== void 0) normalized[key] = value;
235
+ }
236
+ return Object.keys(normalized).length > 0 ? normalized : void 0;
237
+ }
222
238
  function buildPageviewEvent(params) {
223
239
  const { url, referrer, timestamp, title } = params;
224
240
  return {
@@ -270,8 +286,8 @@ function buildIdentifyEvent(params) {
270
286
  userId,
271
287
  fingerprint,
272
288
  customerId,
273
- customerTraits,
274
- traits
289
+ customerTraits: normalizeTraits(customerTraits),
290
+ traits: normalizeTraits(traits)
275
291
  };
276
292
  }
277
293
  function buildCustomEvent(params) {
@@ -360,32 +376,33 @@ function buildIngestPayload(visitorId, source, events, userIdentity, sessionId,
360
376
  ...userIdentity.customerTraits && { customerTraits: userIdentity.customerTraits }
361
377
  } : void 0;
362
378
  const resolvedCustomerIdentity = customerIdentity ?? legacyCustomerIdentity;
379
+ const userTraits = normalizeTraits(userIdentity?.traits);
380
+ const customerTraits = normalizeTraits(resolvedCustomerIdentity?.customerTraits);
363
381
  if (fingerprint) {
364
382
  payload.fingerprint = fingerprint;
365
383
  }
366
384
  if (sessionId) {
367
385
  payload.sessionId = sessionId;
368
386
  }
369
- if (userIdentity && (userIdentity.email || userIdentity.userId || userIdentity.traits)) {
387
+ if (userIdentity && (userIdentity.email || userIdentity.userId || userIdentity.fingerprint || userTraits)) {
370
388
  payload.userIdentity = {
371
389
  ...userIdentity.email && { email: userIdentity.email },
372
390
  ...userIdentity.userId && { userId: userIdentity.userId },
373
- ...userIdentity.traits && { traits: userIdentity.traits }
391
+ ...userIdentity.fingerprint && { fingerprint: userIdentity.fingerprint },
392
+ ...userTraits && { traits: userTraits }
374
393
  };
375
394
  }
376
- if (resolvedCustomerIdentity && (resolvedCustomerIdentity.customerId || resolvedCustomerIdentity.customerTraits)) {
395
+ if (resolvedCustomerIdentity && (resolvedCustomerIdentity.customerId || customerTraits)) {
377
396
  payload.customerIdentity = {
378
397
  ...resolvedCustomerIdentity.customerId && {
379
398
  customerId: resolvedCustomerIdentity.customerId
380
399
  },
381
- ...resolvedCustomerIdentity.customerTraits && {
382
- customerTraits: resolvedCustomerIdentity.customerTraits
383
- }
400
+ ...customerTraits && { customerTraits }
384
401
  };
385
402
  }
386
403
  return payload;
387
404
  }
388
- var MAX_BATCH_SIZE = 100;
405
+ var MAX_BATCH_SIZE = ingestTransport.maxBatchSize;
389
406
  function batchEvents(events) {
390
407
  const batches = [];
391
408
  for (let i = 0; i < events.length; i += MAX_BATCH_SIZE) {
@@ -393,9 +410,19 @@ function batchEvents(events) {
393
410
  }
394
411
  return batches;
395
412
  }
413
+
414
+ // src/transport.ts
415
+ var INGEST_METHOD = ingestTransport.method;
416
+ var INGEST_EVENT_TYPES = ingestTransport.eventTypes;
417
+ function buildIngestUrl(apiHost, publicKey) {
418
+ const path = ingestTransport.pathTemplate.replace("{publicKey}", encodeURIComponent(publicKey));
419
+ return `${apiHost.replace(/\/$/, "")}${path}`;
420
+ }
396
421
  export {
397
422
  DEFAULT_API_HOST,
398
423
  DEFAULT_DENIED_FORM_FIELDS,
424
+ INGEST_EVENT_TYPES,
425
+ INGEST_METHOD,
399
426
  MAX_BATCH_SIZE,
400
427
  batchEvents,
401
428
  buildCalendarEvent,
@@ -404,6 +431,7 @@ export {
404
431
  buildFormEvent,
405
432
  buildIdentifyEvent,
406
433
  buildIngestPayload,
434
+ buildIngestUrl,
407
435
  buildPageviewEvent,
408
436
  extractIdentityFromForm,
409
437
  extractPathFromUrl,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/payload.ts","../src/types.ts","../src/utils.ts"],"sourcesContent":["import { v7 as uuidv7 } from \"uuid\"\nimport type {\n CalendarEvent,\n CalendarProvider,\n CustomEvent,\n CustomerTraits,\n EngagementEvent,\n FormEvent,\n IdentifyEvent,\n IdentifyTraits,\n IngestPayload,\n PageviewEvent,\n PayloadCustomerIdentity,\n PayloadUserIdentity,\n SourceType,\n TrackerEvent,\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 uuid: uuidv7(),\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 uuid: uuidv7(),\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 fingerprint?: string\n traits?: IdentifyTraits\n customerId?: string\n customerTraits?: CustomerTraits\n },\n): IdentifyEvent {\n const {\n url,\n referrer,\n timestamp,\n email,\n userId,\n fingerprint,\n traits,\n customerId,\n customerTraits,\n } = params\n return {\n uuid: uuidv7(),\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 fingerprint,\n customerId,\n customerTraits,\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 email?: string\n userId?: string\n fingerprint?: string\n customerId?: string\n },\n): CustomEvent {\n const {\n url,\n referrer,\n timestamp,\n eventName,\n properties,\n email,\n userId,\n fingerprint,\n customerId,\n } = params\n return {\n uuid: uuidv7(),\n type: \"custom\",\n timestamp: timestamp ?? Date.now(),\n url,\n path: extractPathFromUrl(url),\n referrer,\n utm: extractUtmParams(url),\n eventName,\n email,\n userId,\n fingerprint,\n customerId,\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 uuid: uuidv7(),\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 * Build an engagement event.\n * Captures active time on page for session analytics.\n */\nexport function buildEngagementEvent(\n params: BaseEventParams & {\n activeTimeMs: number\n totalTimeMs: number\n sessionId: string\n },\n): EngagementEvent {\n const { url, referrer, timestamp, activeTimeMs, totalTimeMs, sessionId } = params\n return {\n uuid: uuidv7(),\n type: \"engagement\",\n timestamp: timestamp ?? Date.now(),\n url,\n path: extractPathFromUrl(url),\n referrer,\n utm: extractUtmParams(url),\n activeTimeMs,\n totalTimeMs,\n sessionId,\n }\n}\n\n// ============================================\n// PAYLOAD BUILDER\n// ============================================\n\n/**\n * Build an ingest payload from events.\n *\n * @param visitorId - The anonymous visitor ID from browser cookie/storage\n * @param source - The event source (client, server, integration)\n * @param events - Array of events to send\n * @param userIdentity - Optional user identity for immediate resolution (from setUser in SPA)\n * @param sessionId - Optional session ID for grouping events (browser SDK only)\n * @param fingerprint - Optional device identifier for server-side anonymous tracking\n * @param customerIdentity - Optional customer identity for batch attribution. `customerId`-only\n * batches are valid and remain provisional until a later identify(email, customerId) call\n * links that external account/workspace to a resolved customer.\n */\nexport function buildIngestPayload(\n visitorId: string,\n source: SourceType,\n events: TrackerEvent[],\n userIdentity?: PayloadUserIdentity,\n sessionId?: string,\n fingerprint?: string,\n customerIdentity?: PayloadCustomerIdentity,\n): IngestPayload {\n const payload: IngestPayload = {\n visitorId,\n source,\n events,\n }\n const legacyCustomerIdentity =\n customerIdentity === undefined &&\n userIdentity &&\n (userIdentity.customerId || userIdentity.customerTraits)\n ? {\n ...(userIdentity.customerId && { customerId: userIdentity.customerId }),\n ...(userIdentity.customerTraits && { customerTraits: userIdentity.customerTraits }),\n }\n : undefined\n const resolvedCustomerIdentity = customerIdentity ?? legacyCustomerIdentity\n\n // Only include fingerprint if provided (server SDK only)\n if (fingerprint) {\n payload.fingerprint = fingerprint\n }\n\n // Only include sessionId if provided (browser SDK only)\n if (sessionId) {\n payload.sessionId = sessionId\n }\n\n // Only include userIdentity if it has actual values\n if (userIdentity && (userIdentity.email || userIdentity.userId || userIdentity.traits)) {\n payload.userIdentity = {\n ...(userIdentity.email && { email: userIdentity.email }),\n ...(userIdentity.userId && { userId: userIdentity.userId }),\n ...(userIdentity.traits && { traits: userIdentity.traits }),\n }\n }\n\n if (\n resolvedCustomerIdentity &&\n (resolvedCustomerIdentity.customerId || resolvedCustomerIdentity.customerTraits)\n ) {\n payload.customerIdentity = {\n ...(resolvedCustomerIdentity.customerId && {\n customerId: resolvedCustomerIdentity.customerId,\n }),\n ...(resolvedCustomerIdentity.customerTraits && {\n customerTraits: resolvedCustomerIdentity.customerTraits,\n }),\n }\n }\n\n return payload\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","// ============================================\n// EVENT TYPES\n// ============================================\n\nexport type EventType = \"pageview\" | \"form\" | \"identify\" | \"custom\" | \"calendar\" | \"engagement\"\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 extends CustomerAttribution {\n email?: string\n /** Your system-owned user/contact ID. */\n userId?: string\n /** User/contact traits. */\n traits?: IdentifyTraits\n /** Customer/account-level traits. */\n customerTraits?: CustomerTraits\n}\n\n// ============================================\n// SERVER-SPECIFIC TYPES\n// Track calls can use user identity, customer attribution, or both.\n// Identify calls remain user-scoped and require email or userId.\n// ============================================\n\n/**\n * Base server-side user identity.\n * Track calls may use customerId without these fields.\n * Identify calls still require email or userId at runtime.\n * `customerId`-only track calls are valid immediately, but they stay provisional\n * until the same customer/account/workspace later appears on identify() with\n * the matching customerId and an email.\n *\n * - fingerprint: Device identifier for anonymous tracking (can be linked later)\n * - email: User's email address (definitive identity, resolves immediately)\n * - userId: Your system-owned user/contact ID\n */\nexport interface ServerIdentity {\n fingerprint?: string\n email?: string\n /** Your system-owned user/contact ID. */\n userId?: string\n}\n\nexport interface CustomerAttribution {\n /** Your system-owned customer/account/workspace ID. */\n customerId?: string\n}\n\n// ============================================\n// TRAITS\n// ============================================\n\nexport interface CustomerTraits {\n /** Customer's billing plan */\n plan?: string\n /** Allow additional custom properties */\n [key: string]: string | number | boolean | null | undefined\n}\n\n/**\n * Traits for identify calls.\n * These are user/contact traits, not customer/account traits.\n */\nexport interface IdentifyTraits {\n /** User-level traits */\n [key: string]: string | number | boolean | null | undefined\n}\n\nexport interface ServerTrackOptions extends ServerIdentity, CustomerAttribution {\n eventName: string\n properties?: Record<string, string | number | boolean | null>\n timestamp?: number\n}\n\nexport interface ServerIdentifyOptions extends ServerIdentity, CustomerAttribution {\n traits?: IdentifyTraits\n customerTraits?: CustomerTraits\n}\n\n// ============================================\n// INTERNAL EVENT TYPES\n// These are the full event objects sent to the API\n// ============================================\n\ninterface BaseEvent {\n uuid?: string\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 fingerprint?: string\n customerId?: string\n customerTraits?: CustomerTraits\n traits?: IdentifyTraits\n}\n\nexport interface CustomEvent extends BaseEvent {\n type: \"custom\"\n eventName: string\n email?: string\n userId?: string\n fingerprint?: string\n customerId?: 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 interface EngagementEvent extends BaseEvent {\n type: \"engagement\"\n /** Time in milliseconds the user was actively engaged (visible tab + user interactions) */\n activeTimeMs: number\n /** Total wall-clock time in milliseconds on the page */\n totalTimeMs: number\n /** Session ID for grouping engagement events. Resets after 30 min of inactivity or tab close. */\n sessionId: string\n}\n\nexport type TrackerEvent =\n | PageviewEvent\n | FormEvent\n | IdentifyEvent\n | CustomEvent\n | CalendarEvent\n | EngagementEvent\n\n// ============================================\n// INGEST PAYLOAD\n// This is what gets sent to the API\n// ============================================\n\n/**\n * User identity for payload-level resolution.\n * Used by browser SDK when user is logged in (via setUser).\n * Customer attribution is carried separately in `customerIdentity`.\n */\nexport interface PayloadUserIdentity {\n email?: string\n /** Your system-owned user/contact ID. */\n userId?: string\n /** User/contact traits. */\n traits?: IdentifyTraits\n /**\n * @deprecated Use payload-level `customerIdentity.customerId` instead.\n * Kept for one compatibility window while callers migrate.\n */\n customerId?: string\n /**\n * @deprecated Use payload-level `customerIdentity.customerTraits` instead.\n * Kept for one compatibility window while callers migrate.\n */\n customerTraits?: CustomerTraits\n}\n\n/**\n * Customer identity for payload-level attribution.\n * Used by browser SDK to attach account/workspace context to a batch.\n * `customerId`-only attribution is valid and remains provisional until a later\n * identify(email, customerId) call links that external account/workspace to a resolved customer.\n */\nexport interface PayloadCustomerIdentity extends CustomerAttribution {\n /** Customer/account traits. */\n customerTraits?: CustomerTraits\n}\n\nexport interface IngestPayload {\n visitorId?: string // Required for pixel, optional for server\n source: SourceType\n events: TrackerEvent[]\n /**\n * Device identifier for anonymous tracking.\n * Events with fingerprint can be linked to users later via identify.\n * Only present for server-side events.\n */\n fingerprint?: string\n /**\n * Session ID for grouping all events in this batch.\n * Only present for browser (client) source events.\n * Used to correlate pageviews, forms, custom events, and engagement\n * within the same browsing session.\n */\n sessionId?: string\n /**\n * User identity for this batch of events.\n * When present, the server can resolve directly to CustomerContact\n * instead of relying on anonymous visitor flow.\n *\n * This is set by the browser SDK when setUser() has been called,\n * allowing immediate identity resolution for SPA/React apps.\n */\n userIdentity?: PayloadUserIdentity\n /**\n * Customer/account identity for this batch of events.\n * Used to attribute browser batches to a customer/workspace even when\n * the customer fields are not present on every event.\n */\n customerIdentity?: PayloadCustomerIdentity\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\n// Re-export for convenience\nexport type { PayloadUserIdentity as UserIdentity }\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 (error) {\n console.warn(`[Outlit] Failed to parse URL for UTM extraction: \"${url}\"`, error)\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 let path = urlObj.pathname\n const hash = urlObj.hash\n\n // For file:// or custom protocol URLs with no real hostname (common in Electron),\n // the meaningful route is typically stored in the hash fragment.\n if (hash && hash.length > 1 && (!urlObj.hostname || urlObj.protocol === \"file:\")) {\n path = hash.replace(/^#/, \"\")\n }\n // For web hash-routing SPAs (e.g. \"/#/settings\"), prefer hash when the\n // pathname is an SPA entry point.\n else if (\n hash.startsWith(\"#/\") &&\n (urlObj.pathname === \"/\" || urlObj.pathname.endsWith(\"/index.html\"))\n ) {\n path = hash.slice(1)\n }\n\n if (!path) return \"/\"\n if (!path.startsWith(\"/\")) return `/${path}`\n\n return path\n } catch (error) {\n console.warn(\n `[Outlit] Failed to parse URL for path extraction: \"${url}\", defaulting to \"/\"`,\n error,\n )\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 *\n * Valid identities:\n * - fingerprint: Device identifier (for anonymous tracking, can be linked later)\n * - email: User's email (definitive identity)\n * - userId: Your system-owned user/contact ID\n * - customerId: Your system-owned customer/account/workspace ID\n */\nexport function validateServerIdentity(\n fingerprint?: string,\n email?: string,\n userId?: string,\n customerId?: string,\n): void {\n const hasFingerprint = fingerprint && fingerprint.trim().length > 0\n const hasEmail = email && email.trim().length > 0\n const hasUserId = userId && userId.trim().length > 0\n const hasCustomerId = customerId && customerId.trim().length > 0\n\n if (!hasFingerprint && !hasEmail && !hasUserId && !hasCustomerId) {\n throw new Error(\n \"Server SDK requires at least one of: fingerprint, email, userId, or customerId for track calls. \" +\n \"Use fingerprint/email/userId for user-scoped tracking or customerId for customer-only tracking.\",\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"],"mappings":";AAAA,SAAS,MAAM,cAAc;;;ACuRtB,IAAM,mBAAmB;AAKzB,IAAM,6BAA6B;AAAA,EACxC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;;;AC7SO,SAAS,iBAAiB,KAAoC;AACnE,MAAI;AACF,UAAM,SAAS,IAAI,IAAI,GAAG;AAC1B,UAAM,SAAS,OAAO;AAEtB,UAAM,MAAiB,CAAC;AAExB,QAAI,OAAO,IAAI,YAAY,EAAG,KAAI,SAAS,OAAO,IAAI,YAAY,KAAK;AACvE,QAAI,OAAO,IAAI,YAAY,EAAG,KAAI,SAAS,OAAO,IAAI,YAAY,KAAK;AACvE,QAAI,OAAO,IAAI,cAAc,EAAG,KAAI,WAAW,OAAO,IAAI,cAAc,KAAK;AAC7E,QAAI,OAAO,IAAI,UAAU,EAAG,KAAI,OAAO,OAAO,IAAI,UAAU,KAAK;AACjE,QAAI,OAAO,IAAI,aAAa,EAAG,KAAI,UAAU,OAAO,IAAI,aAAa,KAAK;AAE1E,WAAO,OAAO,KAAK,GAAG,EAAE,SAAS,IAAI,MAAM;AAAA,EAC7C,SAAS,OAAO;AACd,YAAQ,KAAK,qDAAqD,GAAG,KAAK,KAAK;AAC/E,WAAO;AAAA,EACT;AACF;AAKO,SAAS,mBAAmB,KAAqB;AACtD,MAAI;AACF,UAAM,SAAS,IAAI,IAAI,GAAG;AAC1B,QAAI,OAAO,OAAO;AAClB,UAAM,OAAO,OAAO;AAIpB,QAAI,QAAQ,KAAK,SAAS,MAAM,CAAC,OAAO,YAAY,OAAO,aAAa,UAAU;AAChF,aAAO,KAAK,QAAQ,MAAM,EAAE;AAAA,IAC9B,WAIE,KAAK,WAAW,IAAI,MACnB,OAAO,aAAa,OAAO,OAAO,SAAS,SAAS,aAAa,IAClE;AACA,aAAO,KAAK,MAAM,CAAC;AAAA,IACrB;AAEA,QAAI,CAAC,KAAM,QAAO;AAClB,QAAI,CAAC,KAAK,WAAW,GAAG,EAAG,QAAO,IAAI,IAAI;AAE1C,WAAO;AAAA,EACT,SAAS,OAAO;AACd,YAAQ;AAAA,MACN,sDAAsD,GAAG;AAAA,MACzD;AAAA,IACF;AACA,WAAO;AAAA,EACT;AACF;AASO,SAAS,cAAc,WAAmB,UAA6B;AAC5E,QAAM,iBAAiB,UAAU,YAAY,EAAE,QAAQ,WAAW,EAAE;AACpE,SAAO,SAAS,KAAK,CAAC,WAAW;AAC/B,UAAM,mBAAmB,OAAO,YAAY,EAAE,QAAQ,WAAW,EAAE;AACnE,WAAO,eAAe,SAAS,gBAAgB;AAAA,EACjD,CAAC;AACH;AAKA,SAAS,wBAAwB,OAAwB;AAEvD,QAAM,UAAU,MAAM,QAAQ,UAAU,EAAE;AAG1C,MAAI,cAAc,KAAK,OAAO,GAAG;AAC/B,WAAO;AAAA,EACT;AAGA,MAAI,UAAU,KAAK,OAAO,KAAK,sBAAsB,KAAK,KAAK,GAAG;AAChE,WAAO;AAAA,EACT;AAEA,SAAO;AACT;AAMO,SAAS,mBACd,QACA,gBACoC;AACpC,MAAI,CAAC,OAAQ,QAAO;AAEpB,QAAM,WAAW,kBAAkB;AACnC,QAAM,YAAoC,CAAC;AAE3C,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,MAAM,GAAG;AACjD,QAAI,CAAC,cAAc,KAAK,QAAQ,GAAG;AAEjC,UAAI,CAAC,wBAAwB,KAAK,GAAG;AACnC,kBAAU,GAAG,IAAI;AAAA,MACnB;AAAA,IACF;AAAA,EACF;AAEA,SAAO,OAAO,KAAK,SAAS,EAAE,SAAS,IAAI,YAAY;AACzD;AAuDO,SAAS,uBACd,aACA,OACA,QACA,YACM;AACN,QAAM,iBAAiB,eAAe,YAAY,KAAK,EAAE,SAAS;AAClE,QAAM,WAAW,SAAS,MAAM,KAAK,EAAE,SAAS;AAChD,QAAM,YAAY,UAAU,OAAO,KAAK,EAAE,SAAS;AACnD,QAAM,gBAAgB,cAAc,WAAW,KAAK,EAAE,SAAS;AAE/D,MAAI,CAAC,kBAAkB,CAAC,YAAY,CAAC,aAAa,CAAC,eAAe;AAChE,UAAM,IAAI;AAAA,MACR;AAAA,IAEF;AAAA,EACF;AACF;AASO,SAAS,aAAa,OAAwB;AACnD,MAAI,CAAC,SAAS,OAAO,UAAU,SAAU,QAAO;AAEhD,QAAM,aAAa;AACnB,SAAO,WAAW,KAAK,MAAM,KAAK,CAAC;AACrC;AAMA,IAAM,uBAAuB;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAKA,IAAM,qBAAqB;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAKA,IAAM,sBAAsB;AAAA,EAC1B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAKA,IAAM,qBAAqB;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAKA,SAAS,gBAAgB,WAAmB,UAA6B;AACvE,QAAM,aAAa,UAAU,KAAK;AAClC,SAAO,SAAS,KAAK,CAAC,YAAY,QAAQ,KAAK,UAAU,CAAC;AAC5D;AAcO,SAAS,eACd,QACA,YACoB;AAEpB,MAAI,YAAY;AACd,eAAW,CAAC,WAAW,SAAS,KAAK,WAAW,QAAQ,GAAG;AACzD,UAAI,cAAc,SAAS;AACzB,cAAM,QAAQ,OAAO,SAAS;AAC9B,YAAI,SAAS,aAAa,KAAK,GAAG;AAChC,iBAAO,MAAM,KAAK;AAAA,QACpB;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAGA,aAAW,CAAC,WAAW,KAAK,KAAK,OAAO,QAAQ,MAAM,GAAG;AACvD,QAAI,gBAAgB,WAAW,oBAAoB,KAAK,aAAa,KAAK,GAAG;AAC3E,aAAO,MAAM,KAAK;AAAA,IACpB;AAAA,EACF;AAGA,aAAW,SAAS,OAAO,OAAO,MAAM,GAAG;AACzC,QAAI,aAAa,KAAK,GAAG;AACvB,aAAO,MAAM,KAAK;AAAA,IACpB;AAAA,EACF;AAEA,SAAO;AACT;AAeO,SAAS,eAAe,QAI7B;AACA,MAAI;AACJ,MAAI;AACJ,MAAI;AAEJ,aAAW,CAAC,WAAW,KAAK,KAAK,OAAO,QAAQ,MAAM,GAAG;AACvD,UAAM,eAAe,OAAO,KAAK;AACjC,QAAI,CAAC,aAAc;AAGnB,QAAI,CAAC,YAAY,gBAAgB,WAAW,kBAAkB,GAAG;AAC/D,iBAAW;AAAA,IACb;AAGA,QAAI,CAAC,aAAa,gBAAgB,WAAW,mBAAmB,GAAG;AACjE,kBAAY;AAAA,IACd;AAGA,QAAI,CAAC,YAAY,gBAAgB,WAAW,kBAAkB,GAAG;AAC/D,iBAAW;AAAA,IACb;AAAA,EACF;AAEA,QAAM,SAAmE,CAAC;AAG1E,MAAI,UAAU;AACZ,WAAO,OAAO;AAAA,EAChB,WAES,aAAa,UAAU;AAC9B,WAAO,OAAO,GAAG,SAAS,IAAI,QAAQ;AACtC,WAAO,YAAY;AACnB,WAAO,WAAW;AAAA,EACpB,WAES,WAAW;AAClB,WAAO,YAAY;AAAA,EACrB,WAES,UAAU;AACjB,WAAO,WAAW;AAAA,EACpB;AAEA,SAAO;AACT;AAqBO,SAAS,wBACd,QACA,YAC+B;AAC/B,QAAM,QAAQ,eAAe,QAAQ,UAAU;AAG/C,MAAI,CAAC,OAAO;AACV,WAAO;AAAA,EACT;AAEA,QAAM,aAAa,eAAe,MAAM;AAExC,SAAO;AAAA,IACL;AAAA,IACA,GAAG;AAAA,EACL;AACF;;;AFhYO,SAAS,mBAAmB,QAA6D;AAC9F,QAAM,EAAE,KAAK,UAAU,WAAW,MAAM,IAAI;AAC5C,SAAO;AAAA,IACL,MAAM,OAAO;AAAA,IACb,MAAM;AAAA,IACN,WAAW,aAAa,KAAK,IAAI;AAAA,IACjC;AAAA,IACA,MAAM,mBAAmB,GAAG;AAAA,IAC5B;AAAA,IACA,KAAK,iBAAiB,GAAG;AAAA,IACzB;AAAA,EACF;AACF;AAKO,SAAS,eACd,QAIW;AACX,QAAM,EAAE,KAAK,UAAU,WAAW,QAAQ,WAAW,IAAI;AACzD,SAAO;AAAA,IACL,MAAM,OAAO;AAAA,IACb,MAAM;AAAA,IACN,WAAW,aAAa,KAAK,IAAI;AAAA,IACjC;AAAA,IACA,MAAM,mBAAmB,GAAG;AAAA,IAC5B;AAAA,IACA,KAAK,iBAAiB,GAAG;AAAA,IACzB;AAAA,IACA;AAAA,EACF;AACF;AAKO,SAAS,mBACd,QAQe;AACf,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,IAAI;AACJ,SAAO;AAAA,IACL,MAAM,OAAO;AAAA,IACb,MAAM;AAAA,IACN,WAAW,aAAa,KAAK,IAAI;AAAA,IACjC;AAAA,IACA,MAAM,mBAAmB,GAAG;AAAA,IAC5B;AAAA,IACA,KAAK,iBAAiB,GAAG;AAAA,IACzB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AAKO,SAAS,iBACd,QAQa;AACb,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,IAAI;AACJ,SAAO;AAAA,IACL,MAAM,OAAO;AAAA,IACb,MAAM;AAAA,IACN,WAAW,aAAa,KAAK,IAAI;AAAA,IACjC;AAAA,IACA,MAAM,mBAAmB,GAAG;AAAA,IAC5B;AAAA,IACA,KAAK,iBAAiB,GAAG;AAAA,IACzB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AAKO,SAAS,mBACd,QAUe;AACf,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,IAAI;AACJ,SAAO;AAAA,IACL,MAAM,OAAO;AAAA,IACb,MAAM;AAAA,IACN,WAAW,aAAa,KAAK,IAAI;AAAA,IACjC;AAAA,IACA,MAAM,mBAAmB,GAAG;AAAA,IAC5B;AAAA,IACA,KAAK,iBAAiB,GAAG;AAAA,IACzB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AAMO,SAAS,qBACd,QAKiB;AACjB,QAAM,EAAE,KAAK,UAAU,WAAW,cAAc,aAAa,UAAU,IAAI;AAC3E,SAAO;AAAA,IACL,MAAM,OAAO;AAAA,IACb,MAAM;AAAA,IACN,WAAW,aAAa,KAAK,IAAI;AAAA,IACjC;AAAA,IACA,MAAM,mBAAmB,GAAG;AAAA,IAC5B;AAAA,IACA,KAAK,iBAAiB,GAAG;AAAA,IACzB;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AAmBO,SAAS,mBACd,WACA,QACA,QACA,cACA,WACA,aACA,kBACe;AACf,QAAM,UAAyB;AAAA,IAC7B;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACA,QAAM,yBACJ,qBAAqB,UACrB,iBACC,aAAa,cAAc,aAAa,kBACrC;AAAA,IACE,GAAI,aAAa,cAAc,EAAE,YAAY,aAAa,WAAW;AAAA,IACrE,GAAI,aAAa,kBAAkB,EAAE,gBAAgB,aAAa,eAAe;AAAA,EACnF,IACA;AACN,QAAM,2BAA2B,oBAAoB;AAGrD,MAAI,aAAa;AACf,YAAQ,cAAc;AAAA,EACxB;AAGA,MAAI,WAAW;AACb,YAAQ,YAAY;AAAA,EACtB;AAGA,MAAI,iBAAiB,aAAa,SAAS,aAAa,UAAU,aAAa,SAAS;AACtF,YAAQ,eAAe;AAAA,MACrB,GAAI,aAAa,SAAS,EAAE,OAAO,aAAa,MAAM;AAAA,MACtD,GAAI,aAAa,UAAU,EAAE,QAAQ,aAAa,OAAO;AAAA,MACzD,GAAI,aAAa,UAAU,EAAE,QAAQ,aAAa,OAAO;AAAA,IAC3D;AAAA,EACF;AAEA,MACE,6BACC,yBAAyB,cAAc,yBAAyB,iBACjE;AACA,YAAQ,mBAAmB;AAAA,MACzB,GAAI,yBAAyB,cAAc;AAAA,QACzC,YAAY,yBAAyB;AAAA,MACvC;AAAA,MACA,GAAI,yBAAyB,kBAAkB;AAAA,QAC7C,gBAAgB,yBAAyB;AAAA,MAC3C;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;AASO,IAAM,iBAAiB;AAKvB,SAAS,YAAY,QAA0C;AACpE,QAAM,UAA4B,CAAC;AACnC,WAAS,IAAI,GAAG,IAAI,OAAO,QAAQ,KAAK,gBAAgB;AACtD,YAAQ,KAAK,OAAO,MAAM,GAAG,IAAI,cAAc,CAAC;AAAA,EAClD;AACA,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../src/payload.ts","../src/generated/ingest-contract.ts","../src/types.ts","../src/utils.ts","../src/transport.ts"],"sourcesContent":["import { v7 as uuidv7 } from \"uuid\"\nimport { ingestTransport } from \"./generated/ingest-contract\"\nimport type {\n CalendarEvent,\n CalendarProvider,\n CustomEvent,\n CustomerTraits,\n EngagementEvent,\n FormEvent,\n IdentifyEvent,\n IdentifyTraits,\n IngestPayload,\n PageviewEvent,\n PayloadCustomerIdentityInput,\n PayloadUserIdentityInput,\n SourceType,\n TrackerEvent,\n} from \"./types\"\nimport { extractPathFromUrl, extractUtmParams } from \"./utils\"\n\ntype WireTraitValue = string | number | boolean | null\n\nfunction normalizeTraits(\n traits: CustomerTraits | IdentifyTraits | undefined,\n): Record<string, WireTraitValue> | undefined {\n if (!traits) return undefined\n\n const normalized: Record<string, WireTraitValue> = {}\n for (const [key, value] of Object.entries(traits)) {\n if (value !== undefined) normalized[key] = value\n }\n return Object.keys(normalized).length > 0 ? normalized : undefined\n}\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 uuid: uuidv7(),\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 uuid: uuidv7(),\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 fingerprint?: string\n traits?: IdentifyTraits\n customerId?: string\n customerTraits?: CustomerTraits\n },\n): IdentifyEvent {\n const {\n url,\n referrer,\n timestamp,\n email,\n userId,\n fingerprint,\n traits,\n customerId,\n customerTraits,\n } = params\n return {\n uuid: uuidv7(),\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 fingerprint,\n customerId,\n customerTraits: normalizeTraits(customerTraits),\n traits: normalizeTraits(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 email?: string\n userId?: string\n fingerprint?: string\n customerId?: string\n },\n): CustomEvent {\n const {\n url,\n referrer,\n timestamp,\n eventName,\n properties,\n email,\n userId,\n fingerprint,\n customerId,\n } = params\n return {\n uuid: uuidv7(),\n type: \"custom\",\n timestamp: timestamp ?? Date.now(),\n url,\n path: extractPathFromUrl(url),\n referrer,\n utm: extractUtmParams(url),\n eventName,\n email,\n userId,\n fingerprint,\n customerId,\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 uuid: uuidv7(),\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 * Build an engagement event.\n * Captures active time on page for session analytics.\n */\nexport function buildEngagementEvent(\n params: BaseEventParams & {\n activeTimeMs: number\n totalTimeMs: number\n sessionId: string\n },\n): EngagementEvent {\n const { url, referrer, timestamp, activeTimeMs, totalTimeMs, sessionId } = params\n return {\n uuid: uuidv7(),\n type: \"engagement\",\n timestamp: timestamp ?? Date.now(),\n url,\n path: extractPathFromUrl(url),\n referrer,\n utm: extractUtmParams(url),\n activeTimeMs,\n totalTimeMs,\n sessionId,\n }\n}\n\n// ============================================\n// PAYLOAD BUILDER\n// ============================================\n\n/**\n * Build an ingest payload from events.\n *\n * @param visitorId - The anonymous visitor ID from browser cookie/storage\n * @param source - The event source (client, server, integration)\n * @param events - Array of events to send\n * @param userIdentity - Optional user identity for immediate resolution (from setUser in SPA)\n * @param sessionId - Optional session ID for grouping events (browser SDK only)\n * @param fingerprint - Optional device identifier for server-side anonymous tracking\n * @param customerIdentity - Optional customer identity for batch attribution. `customerId`-only\n * batches are valid and remain provisional until a later identify(email, customerId) call\n * links that external account/workspace to a resolved customer.\n */\nexport function buildIngestPayload(\n visitorId: string,\n source: SourceType,\n events: TrackerEvent[],\n userIdentity?: PayloadUserIdentityInput,\n sessionId?: string,\n fingerprint?: string,\n customerIdentity?: PayloadCustomerIdentityInput,\n): IngestPayload {\n const payload: IngestPayload = {\n visitorId,\n source,\n events,\n }\n const legacyCustomerIdentity =\n customerIdentity === undefined &&\n userIdentity &&\n (userIdentity.customerId || userIdentity.customerTraits)\n ? {\n ...(userIdentity.customerId && { customerId: userIdentity.customerId }),\n ...(userIdentity.customerTraits && { customerTraits: userIdentity.customerTraits }),\n }\n : undefined\n const resolvedCustomerIdentity = customerIdentity ?? legacyCustomerIdentity\n const userTraits = normalizeTraits(userIdentity?.traits)\n const customerTraits = normalizeTraits(resolvedCustomerIdentity?.customerTraits)\n\n // Only include fingerprint if provided (server SDK only)\n if (fingerprint) {\n payload.fingerprint = fingerprint\n }\n\n // Only include sessionId if provided (browser SDK only)\n if (sessionId) {\n payload.sessionId = sessionId\n }\n\n // Only include userIdentity if it has actual values\n if (\n userIdentity &&\n (userIdentity.email || userIdentity.userId || userIdentity.fingerprint || userTraits)\n ) {\n payload.userIdentity = {\n ...(userIdentity.email && { email: userIdentity.email }),\n ...(userIdentity.userId && { userId: userIdentity.userId }),\n ...(userIdentity.fingerprint && { fingerprint: userIdentity.fingerprint }),\n ...(userTraits && { traits: userTraits }),\n }\n }\n\n if (resolvedCustomerIdentity && (resolvedCustomerIdentity.customerId || customerTraits)) {\n payload.customerIdentity = {\n ...(resolvedCustomerIdentity.customerId && {\n customerId: resolvedCustomerIdentity.customerId,\n }),\n ...(customerTraits && { customerTraits }),\n }\n }\n\n return payload\n}\n\n// ============================================\n// BATCH HELPERS\n// ============================================\n\n/**\n * Maximum number of events in a single batch.\n */\nexport const MAX_BATCH_SIZE = ingestTransport.maxBatchSize\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","// Generated from packages/tools/src/generated/contracts.ts. Do not edit by hand.\n\nexport const ingestTransport = {\n method: \"POST\",\n pathTemplate: \"/api/i/v1/{publicKey}/events\",\n maxBatchSize: 100,\n eventTypes: [\"pageview\", \"form\", \"identify\", \"custom\", \"calendar\", \"engagement\"],\n} as const\n","import type { GeneratedIngestPayload, GeneratedIngestResponse } from \"./generated/ingest-wire-types\"\n\n// ============================================\n// CORE-OWNED INGEST WIRE TYPES\n// ============================================\n\nexport type IngestPayload = GeneratedIngestPayload\nexport type IngestResponse = GeneratedIngestResponse\nexport type TrackerEvent = IngestPayload[\"events\"][number]\nexport type EventType = TrackerEvent[\"type\"]\nexport type SourceType = NonNullable<IngestPayload[\"source\"]>\nexport type PageviewEvent = Extract<TrackerEvent, { type: \"pageview\" }>\nexport type FormEvent = Extract<TrackerEvent, { type: \"form\" }>\nexport type IdentifyEvent = Extract<TrackerEvent, { type: \"identify\" }>\nexport type CustomEvent = Extract<TrackerEvent, { type: \"custom\" }>\nexport type CalendarEvent = Extract<TrackerEvent, { type: \"calendar\" }>\nexport type EngagementEvent = Extract<TrackerEvent, { type: \"engagement\" }>\nexport type CalendarProvider = CalendarEvent[\"provider\"]\nexport type PayloadUserIdentity = NonNullable<IngestPayload[\"userIdentity\"]>\nexport type PayloadCustomerIdentity = NonNullable<IngestPayload[\"customerIdentity\"]>\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 ref?: 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 extends CustomerAttribution {\n email?: string\n /** Your system-owned user/contact ID. */\n userId?: string\n /** User/contact traits. */\n traits?: IdentifyTraits\n /** Customer/account-level traits. */\n customerTraits?: CustomerTraits\n}\n\n// ============================================\n// SERVER-SPECIFIC TYPES\n// Track calls can use user identity, customer attribution, or both.\n// Identify calls remain user-scoped and require email or userId.\n// ============================================\n\n/**\n * Base server-side user identity.\n * Track calls may use customerId without these fields.\n * Identify calls still require email or userId at runtime.\n * `customerId`-only track calls are valid immediately, but they stay provisional\n * until the same customer/account/workspace later appears on identify() with\n * the matching customerId and an email.\n *\n * - fingerprint: Device identifier for anonymous tracking (can be linked later)\n * - email: User's email address (definitive identity, resolves immediately)\n * - userId: Your system-owned user/contact ID\n */\nexport interface ServerIdentity {\n fingerprint?: string\n email?: string\n /** Your system-owned user/contact ID. */\n userId?: string\n}\n\nexport interface CustomerAttribution {\n /** Your system-owned customer/account/workspace ID. */\n customerId?: string\n}\n\n// ============================================\n// TRAITS\n// ============================================\n\nexport interface CustomerTraits {\n /** Customer's billing plan */\n plan?: string\n /** Allow additional custom properties */\n [key: string]: string | number | boolean | null | undefined\n}\n\n/**\n * Traits for identify calls.\n * These are user/contact traits, not customer/account traits.\n */\nexport interface IdentifyTraits {\n /** User-level traits */\n [key: string]: string | number | boolean | null | undefined\n}\n\n/**\n * Input accepted by the payload builder. Deprecated customer fields are lifted\n * into the wire-level `customerIdentity` object and are never sent in `userIdentity`.\n */\nexport type PayloadUserIdentityInput = Omit<PayloadUserIdentity, \"traits\"> & {\n /** User/contact traits. */\n traits?: IdentifyTraits\n /** @deprecated Pass `customerIdentity.customerId` to the payload builder instead. */\n customerId?: string\n /** @deprecated Pass `customerIdentity.customerTraits` to the payload builder instead. */\n customerTraits?: CustomerTraits\n}\n\nexport interface PayloadCustomerIdentityInput extends CustomerAttribution {\n /** Customer/account traits. */\n customerTraits?: CustomerTraits\n}\n\nexport interface ServerTrackOptions extends ServerIdentity, CustomerAttribution {\n eventName: string\n properties?: Record<string, string | number | boolean | null>\n timestamp?: number\n}\n\nexport interface ServerIdentifyOptions extends ServerIdentity, CustomerAttribution {\n traits?: IdentifyTraits\n customerTraits?: CustomerTraits\n}\n\n// ============================================\n// CONSTANTS\n// ============================================\n\nexport const DEFAULT_API_HOST = \"https://app.outlit.ai\"\n\n// Re-export for convenience\nexport type { PayloadUserIdentity as UserIdentity }\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 (error) {\n console.warn(`[Outlit] Failed to parse URL for UTM extraction: \"${url}\"`, error)\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 let path = urlObj.pathname\n const hash = urlObj.hash\n\n // For file:// or custom protocol URLs with no real hostname (common in Electron),\n // the meaningful route is typically stored in the hash fragment.\n if (hash && hash.length > 1 && (!urlObj.hostname || urlObj.protocol === \"file:\")) {\n path = hash.replace(/^#/, \"\")\n }\n // For web hash-routing SPAs (e.g. \"/#/settings\"), prefer hash when the\n // pathname is an SPA entry point.\n else if (\n hash.startsWith(\"#/\") &&\n (urlObj.pathname === \"/\" || urlObj.pathname.endsWith(\"/index.html\"))\n ) {\n path = hash.slice(1)\n }\n\n if (!path) return \"/\"\n if (!path.startsWith(\"/\")) return `/${path}`\n\n return path\n } catch (error) {\n console.warn(\n `[Outlit] Failed to parse URL for path extraction: \"${url}\", defaulting to \"/\"`,\n error,\n )\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 *\n * Valid identities:\n * - fingerprint: Device identifier (for anonymous tracking, can be linked later)\n * - email: User's email (definitive identity)\n * - userId: Your system-owned user/contact ID\n * - customerId: Your system-owned customer/account/workspace ID\n */\nexport function validateServerIdentity(\n fingerprint?: string,\n email?: string,\n userId?: string,\n customerId?: string,\n): void {\n const hasFingerprint = fingerprint && fingerprint.trim().length > 0\n const hasEmail = email && email.trim().length > 0\n const hasUserId = userId && userId.trim().length > 0\n const hasCustomerId = customerId && customerId.trim().length > 0\n\n if (!hasFingerprint && !hasEmail && !hasUserId && !hasCustomerId) {\n throw new Error(\n \"Server SDK requires at least one of: fingerprint, email, userId, or customerId for track calls. \" +\n \"Use fingerprint/email/userId for user-scoped tracking or customerId for customer-only tracking.\",\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 { ingestTransport } from \"./generated/ingest-contract\"\n\nexport const INGEST_METHOD = ingestTransport.method\nexport const INGEST_EVENT_TYPES = ingestTransport.eventTypes\n\nexport function buildIngestUrl(apiHost: string, publicKey: string): string {\n const path = ingestTransport.pathTemplate.replace(\"{publicKey}\", encodeURIComponent(publicKey))\n return `${apiHost.replace(/\\/$/, \"\")}${path}`\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": "2.0.0",
3
+ "version": "3.0.0",
4
4
  "description": "Shared types and utilities for Outlit browser and Node.js tracking SDKs",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Outlit AI",