@outlit/core 1.5.2 → 2.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/README.md ADDED
@@ -0,0 +1,16 @@
1
+ # @outlit/core
2
+
3
+ Shared types and utilities for the Outlit browser and Node.js tracking SDKs.
4
+
5
+ Most applications should install `@outlit/browser` or `@outlit/node` directly. Use this package when building lower-level integrations on top of Outlit's shared tracking types and client utilities.
6
+
7
+ Outlit is the real-time understanding of every customer, the infrastructure agents use to automate customer operations.
8
+
9
+ ## Canonical Docs
10
+
11
+ - Quick start: <https://docs.outlit.ai/tracking/quickstart>
12
+ - Tracking overview: <https://docs.outlit.ai/tracking/how-it-works>
13
+ - Browser SDK docs: <https://docs.outlit.ai/tracking/browser/npm>
14
+ - Node.js SDK docs: <https://docs.outlit.ai/tracking/server/nodejs>
15
+ - OpenAPI spec: <https://docs.outlit.ai/openapi.json>
16
+ - Agent-facing docs index: <https://docs.outlit.ai/llms.txt>
package/dist/index.d.mts CHANGED
@@ -1,6 +1,4 @@
1
- type EventType = "pageview" | "form" | "identify" | "custom" | "calendar" | "engagement" | "stage" | "billing";
2
- type ExplicitJourneyStage = "activated" | "engaged" | "inactive";
3
- type BillingStatus = "trialing" | "paid" | "churned";
1
+ type EventType = "pageview" | "form" | "identify" | "custom" | "calendar" | "engagement";
4
2
  type CalendarProvider = "cal.com" | "calendly" | "unknown";
5
3
  type SourceType = "client" | "server" | "integration";
6
4
  interface UtmParams {
@@ -72,17 +70,6 @@ interface ServerIdentifyOptions extends ServerIdentity, CustomerAttribution {
72
70
  traits?: IdentifyTraits;
73
71
  customerTraits?: CustomerTraits;
74
72
  }
75
- /**
76
- * Customer identity for SDK billing methods.
77
- * Public billing calls should use `customerId`.
78
- */
79
- interface CustomerIdentifier extends CustomerAttribution {
80
- /**
81
- * @deprecated Stripe customer identifier.
82
- * Billing attribution should use `customerId` publicly.
83
- */
84
- stripeCustomerId?: string;
85
- }
86
73
  interface BaseEvent {
87
74
  uuid?: string;
88
75
  type: EventType;
@@ -140,24 +127,7 @@ interface EngagementEvent extends BaseEvent {
140
127
  /** Session ID for grouping engagement events. Resets after 30 min of inactivity or tab close. */
141
128
  sessionId: string;
142
129
  }
143
- interface StageEvent extends BaseEvent {
144
- type: "stage";
145
- /** Stage event value. New SDK callers should only send "activated". */
146
- stage: ExplicitJourneyStage;
147
- /** Optional properties for context */
148
- properties?: Record<string, string | number | boolean | null>;
149
- }
150
- interface BillingEvent extends BaseEvent {
151
- type: "billing";
152
- /** The billing status to set for a customer */
153
- status: BillingStatus;
154
- /** Optional customer identifiers */
155
- customerId?: string;
156
- stripeCustomerId?: string;
157
- /** Optional properties for context */
158
- properties?: Record<string, string | number | boolean | null>;
159
- }
160
- type TrackerEvent = PageviewEvent | FormEvent | IdentifyEvent | CustomEvent | CalendarEvent | EngagementEvent | StageEvent | BillingEvent;
130
+ type TrackerEvent = PageviewEvent | FormEvent | IdentifyEvent | CustomEvent | CalendarEvent | EngagementEvent;
161
131
  /**
162
132
  * User identity for payload-level resolution.
163
133
  * Used by browser SDK when user is logged in (via setUser).
@@ -297,27 +267,6 @@ declare function buildEngagementEvent(params: BaseEventParams & {
297
267
  totalTimeMs: number;
298
268
  sessionId: string;
299
269
  }): EngagementEvent;
300
- /**
301
- * Build a stage event.
302
- * Used to explicitly send customer journey stage events.
303
- * New SDK callers should only send activated; engaged and inactive are derived
304
- * by Outlit from tracked activity but remain accepted for wire compatibility.
305
- * discovered/signed_up stages are inferred from identify calls.
306
- */
307
- declare function buildStageEvent(params: BaseEventParams & {
308
- stage: ExplicitJourneyStage;
309
- properties?: Record<string, string | number | boolean | null>;
310
- }): StageEvent;
311
- /**
312
- * Build a billing event.
313
- * Used to set customer billing status (trialing, paid, churned).
314
- */
315
- declare function buildBillingEvent(params: BaseEventParams & {
316
- status: BillingStatus;
317
- customerId?: string;
318
- stripeCustomerId?: string;
319
- properties?: Record<string, string | number | boolean | null>;
320
- }): BillingEvent;
321
270
  /**
322
271
  * Build an ingest payload from events.
323
272
  *
@@ -369,10 +318,6 @@ declare function sanitizeFormFields(fields: Record<string, string> | undefined,
369
318
  * - customerId: Your system-owned customer/account/workspace ID
370
319
  */
371
320
  declare function validateServerIdentity(fingerprint?: string, email?: string, userId?: string, customerId?: string): void;
372
- /**
373
- * Validate that at least one customer identifier is provided for billing calls.
374
- */
375
- declare function validateCustomerIdentity(customerId?: string, stripeCustomerId?: string): void;
376
321
  /**
377
322
  * Validate that a string looks like a valid email address.
378
323
  */
@@ -428,4 +373,4 @@ interface ExtractedIdentity {
428
373
  */
429
374
  declare function extractIdentityFromForm(fields: Record<string, string>, inputTypes?: Map<string, string>): ExtractedIdentity | undefined;
430
375
 
431
- export { type BillingEvent, type BillingStatus, type BrowserIdentifyOptions, type BrowserTrackOptions, type CalendarEvent, type CalendarProvider, type CustomEvent, type CustomerAttribution, type CustomerIdentifier, type CustomerTraits, DEFAULT_API_HOST, DEFAULT_DENIED_FORM_FIELDS, type EngagementEvent, type EventType, type ExplicitJourneyStage, type ExtractedIdentity, type FormEvent, type IdentifyEvent, type IdentifyTraits, type IngestPayload, type IngestResponse, MAX_BATCH_SIZE, type PageviewEvent, type PayloadCustomerIdentity, type PayloadUserIdentity, type ServerIdentifyOptions, type ServerIdentity, type ServerTrackOptions, type SourceType, type StageEvent, type TrackerConfig, type TrackerEvent, type UtmParams, batchEvents, buildBillingEvent, buildCalendarEvent, buildCustomEvent, buildEngagementEvent, buildFormEvent, buildIdentifyEvent, buildIngestPayload, buildPageviewEvent, buildStageEvent, extractIdentityFromForm, extractPathFromUrl, extractUtmParams, findEmailField, findNameFields, isFieldDenied, isValidEmail, sanitizeFormFields, validateCustomerIdentity, validateServerIdentity };
376
+ export { type BrowserIdentifyOptions, type BrowserTrackOptions, type CalendarEvent, type CalendarProvider, type CustomEvent, type CustomerAttribution, type CustomerTraits, DEFAULT_API_HOST, DEFAULT_DENIED_FORM_FIELDS, type EngagementEvent, type EventType, type ExtractedIdentity, type FormEvent, type IdentifyEvent, type IdentifyTraits, type IngestPayload, type IngestResponse, MAX_BATCH_SIZE, type PageviewEvent, type PayloadCustomerIdentity, type PayloadUserIdentity, type ServerIdentifyOptions, type ServerIdentity, type ServerTrackOptions, type SourceType, type TrackerConfig, type TrackerEvent, type UtmParams, batchEvents, buildCalendarEvent, buildCustomEvent, buildEngagementEvent, buildFormEvent, buildIdentifyEvent, buildIngestPayload, buildPageviewEvent, extractIdentityFromForm, extractPathFromUrl, extractUtmParams, findEmailField, findNameFields, isFieldDenied, isValidEmail, sanitizeFormFields, validateServerIdentity };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,4 @@
1
- type EventType = "pageview" | "form" | "identify" | "custom" | "calendar" | "engagement" | "stage" | "billing";
2
- type ExplicitJourneyStage = "activated" | "engaged" | "inactive";
3
- type BillingStatus = "trialing" | "paid" | "churned";
1
+ type EventType = "pageview" | "form" | "identify" | "custom" | "calendar" | "engagement";
4
2
  type CalendarProvider = "cal.com" | "calendly" | "unknown";
5
3
  type SourceType = "client" | "server" | "integration";
6
4
  interface UtmParams {
@@ -72,17 +70,6 @@ interface ServerIdentifyOptions extends ServerIdentity, CustomerAttribution {
72
70
  traits?: IdentifyTraits;
73
71
  customerTraits?: CustomerTraits;
74
72
  }
75
- /**
76
- * Customer identity for SDK billing methods.
77
- * Public billing calls should use `customerId`.
78
- */
79
- interface CustomerIdentifier extends CustomerAttribution {
80
- /**
81
- * @deprecated Stripe customer identifier.
82
- * Billing attribution should use `customerId` publicly.
83
- */
84
- stripeCustomerId?: string;
85
- }
86
73
  interface BaseEvent {
87
74
  uuid?: string;
88
75
  type: EventType;
@@ -140,24 +127,7 @@ interface EngagementEvent extends BaseEvent {
140
127
  /** Session ID for grouping engagement events. Resets after 30 min of inactivity or tab close. */
141
128
  sessionId: string;
142
129
  }
143
- interface StageEvent extends BaseEvent {
144
- type: "stage";
145
- /** Stage event value. New SDK callers should only send "activated". */
146
- stage: ExplicitJourneyStage;
147
- /** Optional properties for context */
148
- properties?: Record<string, string | number | boolean | null>;
149
- }
150
- interface BillingEvent extends BaseEvent {
151
- type: "billing";
152
- /** The billing status to set for a customer */
153
- status: BillingStatus;
154
- /** Optional customer identifiers */
155
- customerId?: string;
156
- stripeCustomerId?: string;
157
- /** Optional properties for context */
158
- properties?: Record<string, string | number | boolean | null>;
159
- }
160
- type TrackerEvent = PageviewEvent | FormEvent | IdentifyEvent | CustomEvent | CalendarEvent | EngagementEvent | StageEvent | BillingEvent;
130
+ type TrackerEvent = PageviewEvent | FormEvent | IdentifyEvent | CustomEvent | CalendarEvent | EngagementEvent;
161
131
  /**
162
132
  * User identity for payload-level resolution.
163
133
  * Used by browser SDK when user is logged in (via setUser).
@@ -297,27 +267,6 @@ declare function buildEngagementEvent(params: BaseEventParams & {
297
267
  totalTimeMs: number;
298
268
  sessionId: string;
299
269
  }): EngagementEvent;
300
- /**
301
- * Build a stage event.
302
- * Used to explicitly send customer journey stage events.
303
- * New SDK callers should only send activated; engaged and inactive are derived
304
- * by Outlit from tracked activity but remain accepted for wire compatibility.
305
- * discovered/signed_up stages are inferred from identify calls.
306
- */
307
- declare function buildStageEvent(params: BaseEventParams & {
308
- stage: ExplicitJourneyStage;
309
- properties?: Record<string, string | number | boolean | null>;
310
- }): StageEvent;
311
- /**
312
- * Build a billing event.
313
- * Used to set customer billing status (trialing, paid, churned).
314
- */
315
- declare function buildBillingEvent(params: BaseEventParams & {
316
- status: BillingStatus;
317
- customerId?: string;
318
- stripeCustomerId?: string;
319
- properties?: Record<string, string | number | boolean | null>;
320
- }): BillingEvent;
321
270
  /**
322
271
  * Build an ingest payload from events.
323
272
  *
@@ -369,10 +318,6 @@ declare function sanitizeFormFields(fields: Record<string, string> | undefined,
369
318
  * - customerId: Your system-owned customer/account/workspace ID
370
319
  */
371
320
  declare function validateServerIdentity(fingerprint?: string, email?: string, userId?: string, customerId?: string): void;
372
- /**
373
- * Validate that at least one customer identifier is provided for billing calls.
374
- */
375
- declare function validateCustomerIdentity(customerId?: string, stripeCustomerId?: string): void;
376
321
  /**
377
322
  * Validate that a string looks like a valid email address.
378
323
  */
@@ -428,4 +373,4 @@ interface ExtractedIdentity {
428
373
  */
429
374
  declare function extractIdentityFromForm(fields: Record<string, string>, inputTypes?: Map<string, string>): ExtractedIdentity | undefined;
430
375
 
431
- export { type BillingEvent, type BillingStatus, type BrowserIdentifyOptions, type BrowserTrackOptions, type CalendarEvent, type CalendarProvider, type CustomEvent, type CustomerAttribution, type CustomerIdentifier, type CustomerTraits, DEFAULT_API_HOST, DEFAULT_DENIED_FORM_FIELDS, type EngagementEvent, type EventType, type ExplicitJourneyStage, type ExtractedIdentity, type FormEvent, type IdentifyEvent, type IdentifyTraits, type IngestPayload, type IngestResponse, MAX_BATCH_SIZE, type PageviewEvent, type PayloadCustomerIdentity, type PayloadUserIdentity, type ServerIdentifyOptions, type ServerIdentity, type ServerTrackOptions, type SourceType, type StageEvent, type TrackerConfig, type TrackerEvent, type UtmParams, batchEvents, buildBillingEvent, buildCalendarEvent, buildCustomEvent, buildEngagementEvent, buildFormEvent, buildIdentifyEvent, buildIngestPayload, buildPageviewEvent, buildStageEvent, extractIdentityFromForm, extractPathFromUrl, extractUtmParams, findEmailField, findNameFields, isFieldDenied, isValidEmail, sanitizeFormFields, validateCustomerIdentity, validateServerIdentity };
376
+ export { type BrowserIdentifyOptions, type BrowserTrackOptions, type CalendarEvent, type CalendarProvider, type CustomEvent, type CustomerAttribution, type CustomerTraits, DEFAULT_API_HOST, DEFAULT_DENIED_FORM_FIELDS, type EngagementEvent, type EventType, type ExtractedIdentity, type FormEvent, type IdentifyEvent, type IdentifyTraits, type IngestPayload, type IngestResponse, MAX_BATCH_SIZE, type PageviewEvent, type PayloadCustomerIdentity, type PayloadUserIdentity, type ServerIdentifyOptions, type ServerIdentity, type ServerTrackOptions, type SourceType, type TrackerConfig, type TrackerEvent, type UtmParams, batchEvents, buildCalendarEvent, buildCustomEvent, buildEngagementEvent, buildFormEvent, buildIdentifyEvent, buildIngestPayload, buildPageviewEvent, extractIdentityFromForm, extractPathFromUrl, extractUtmParams, findEmailField, findNameFields, isFieldDenied, isValidEmail, sanitizeFormFields, validateServerIdentity };
package/dist/index.js CHANGED
@@ -24,7 +24,6 @@ __export(src_exports, {
24
24
  DEFAULT_DENIED_FORM_FIELDS: () => DEFAULT_DENIED_FORM_FIELDS,
25
25
  MAX_BATCH_SIZE: () => MAX_BATCH_SIZE,
26
26
  batchEvents: () => batchEvents,
27
- buildBillingEvent: () => buildBillingEvent,
28
27
  buildCalendarEvent: () => buildCalendarEvent,
29
28
  buildCustomEvent: () => buildCustomEvent,
30
29
  buildEngagementEvent: () => buildEngagementEvent,
@@ -32,7 +31,6 @@ __export(src_exports, {
32
31
  buildIdentifyEvent: () => buildIdentifyEvent,
33
32
  buildIngestPayload: () => buildIngestPayload,
34
33
  buildPageviewEvent: () => buildPageviewEvent,
35
- buildStageEvent: () => buildStageEvent,
36
34
  extractIdentityFromForm: () => extractIdentityFromForm,
37
35
  extractPathFromUrl: () => extractPathFromUrl,
38
36
  extractUtmParams: () => extractUtmParams,
@@ -41,7 +39,6 @@ __export(src_exports, {
41
39
  isFieldDenied: () => isFieldDenied,
42
40
  isValidEmail: () => isValidEmail,
43
41
  sanitizeFormFields: () => sanitizeFormFields,
44
- validateCustomerIdentity: () => validateCustomerIdentity,
45
42
  validateServerIdentity: () => validateServerIdentity
46
43
  });
47
44
  module.exports = __toCommonJS(src_exports);
@@ -158,15 +155,6 @@ function validateServerIdentity(fingerprint, email, userId, customerId) {
158
155
  );
159
156
  }
160
157
  }
161
- function validateCustomerIdentity(customerId, stripeCustomerId) {
162
- const hasCustomerId = customerId && customerId.trim().length > 0;
163
- const hasStripeCustomerId = stripeCustomerId && stripeCustomerId.trim().length > 0;
164
- if (!hasCustomerId && !hasStripeCustomerId) {
165
- throw new Error(
166
- "Billing methods require customerId or stripeCustomerId. Use customerId for your system-owned account ID."
167
- );
168
- }
169
- }
170
158
  function isValidEmail(value) {
171
159
  if (!value || typeof value !== "string") return false;
172
160
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
@@ -406,36 +394,6 @@ function buildEngagementEvent(params) {
406
394
  sessionId
407
395
  };
408
396
  }
409
- function buildStageEvent(params) {
410
- const { url, referrer, timestamp, stage, properties } = params;
411
- return {
412
- uuid: (0, import_uuid.v7)(),
413
- type: "stage",
414
- timestamp: timestamp ?? Date.now(),
415
- url,
416
- path: extractPathFromUrl(url),
417
- referrer,
418
- utm: extractUtmParams(url),
419
- stage,
420
- properties
421
- };
422
- }
423
- function buildBillingEvent(params) {
424
- const { url, referrer, timestamp, status, customerId, stripeCustomerId, properties } = params;
425
- return {
426
- uuid: (0, import_uuid.v7)(),
427
- type: "billing",
428
- timestamp: timestamp ?? Date.now(),
429
- url,
430
- path: extractPathFromUrl(url),
431
- referrer,
432
- utm: extractUtmParams(url),
433
- status,
434
- customerId,
435
- stripeCustomerId,
436
- properties
437
- };
438
- }
439
397
  function buildIngestPayload(visitorId, source, events, userIdentity, sessionId, fingerprint, customerIdentity) {
440
398
  const payload = {
441
399
  visitorId,
@@ -486,7 +444,6 @@ function batchEvents(events) {
486
444
  DEFAULT_DENIED_FORM_FIELDS,
487
445
  MAX_BATCH_SIZE,
488
446
  batchEvents,
489
- buildBillingEvent,
490
447
  buildCalendarEvent,
491
448
  buildCustomEvent,
492
449
  buildEngagementEvent,
@@ -494,7 +451,6 @@ function batchEvents(events) {
494
451
  buildIdentifyEvent,
495
452
  buildIngestPayload,
496
453
  buildPageviewEvent,
497
- buildStageEvent,
498
454
  extractIdentityFromForm,
499
455
  extractPathFromUrl,
500
456
  extractUtmParams,
@@ -503,7 +459,6 @@ function batchEvents(events) {
503
459
  isFieldDenied,
504
460
  isValidEmail,
505
461
  sanitizeFormFields,
506
- validateCustomerIdentity,
507
462
  validateServerIdentity
508
463
  });
509
464
  //# sourceMappingURL=index.js.map
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 buildBillingEvent,\n buildCalendarEvent,\n buildCustomEvent,\n buildEngagementEvent,\n buildFormEvent,\n buildIdentifyEvent,\n buildIngestPayload,\n buildPageviewEvent,\n buildStageEvent,\n MAX_BATCH_SIZE,\n} from \"./payload\"\nexport type {\n BillingEvent,\n BillingStatus,\n BrowserIdentifyOptions,\n BrowserTrackOptions,\n CalendarEvent,\n CalendarProvider,\n CustomEvent,\n CustomerAttribution,\n CustomerIdentifier,\n CustomerTraits,\n EngagementEvent,\n EventType,\n ExplicitJourneyStage,\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 StageEvent,\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 validateCustomerIdentity,\n validateServerIdentity,\n} from \"./utils\"\n","import { v7 as uuidv7 } from \"uuid\"\nimport type {\n BillingEvent,\n BillingStatus,\n CalendarEvent,\n CalendarProvider,\n CustomEvent,\n CustomerTraits,\n EngagementEvent,\n ExplicitJourneyStage,\n FormEvent,\n IdentifyEvent,\n IdentifyTraits,\n IngestPayload,\n PageviewEvent,\n PayloadCustomerIdentity,\n PayloadUserIdentity,\n SourceType,\n StageEvent,\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 * Build a stage event.\n * Used to explicitly send customer journey stage events.\n * New SDK callers should only send activated; engaged and inactive are derived\n * by Outlit from tracked activity but remain accepted for wire compatibility.\n * discovered/signed_up stages are inferred from identify calls.\n */\nexport function buildStageEvent(\n params: BaseEventParams & {\n stage: ExplicitJourneyStage\n properties?: Record<string, string | number | boolean | null>\n },\n): StageEvent {\n const { url, referrer, timestamp, stage, properties } = params\n return {\n uuid: uuidv7(),\n type: \"stage\",\n timestamp: timestamp ?? Date.now(),\n url,\n path: extractPathFromUrl(url),\n referrer,\n utm: extractUtmParams(url),\n stage,\n properties,\n }\n}\n\n/**\n * Build a billing event.\n * Used to set customer billing status (trialing, paid, churned).\n */\nexport function buildBillingEvent(\n params: BaseEventParams & {\n status: BillingStatus\n customerId?: string\n stripeCustomerId?: string\n properties?: Record<string, string | number | boolean | null>\n },\n): BillingEvent {\n const { url, referrer, timestamp, status, customerId, stripeCustomerId, properties } = params\n return {\n uuid: uuidv7(),\n type: \"billing\",\n timestamp: timestamp ?? Date.now(),\n url,\n path: extractPathFromUrl(url),\n referrer,\n utm: extractUtmParams(url),\n status,\n customerId,\n stripeCustomerId,\n properties,\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 =\n | \"pageview\"\n | \"form\"\n | \"identify\"\n | \"custom\"\n | \"calendar\"\n | \"engagement\"\n | \"stage\"\n | \"billing\"\n\n// Explicit stage event values accepted by ingest for wire compatibility.\n// New SDK callers should only send \"activated\"; \"engaged\" and \"inactive\"\n// are derived by Outlit from tracked activity.\nexport type ExplicitJourneyStage = \"activated\" | \"engaged\" | \"inactive\"\n\nexport type BillingStatus = \"trialing\" | \"paid\" | \"churned\"\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 * Customer identity for SDK billing methods.\n * Public billing calls should use `customerId`.\n */\nexport interface CustomerIdentifier extends CustomerAttribution {\n /**\n * @deprecated Stripe customer identifier.\n * Billing attribution should use `customerId` publicly.\n */\n stripeCustomerId?: string\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 interface StageEvent extends BaseEvent {\n type: \"stage\"\n /** Stage event value. New SDK callers should only send \"activated\". */\n stage: ExplicitJourneyStage\n /** Optional properties for context */\n properties?: Record<string, string | number | boolean | null>\n}\n\nexport interface BillingEvent extends BaseEvent {\n type: \"billing\"\n /** The billing status to set for a customer */\n status: BillingStatus\n /** Optional customer identifiers */\n customerId?: string\n stripeCustomerId?: string\n /** Optional properties for context */\n properties?: Record<string, string | number | boolean | null>\n}\n\nexport type TrackerEvent =\n | PageviewEvent\n | FormEvent\n | IdentifyEvent\n | CustomEvent\n | CalendarEvent\n | EngagementEvent\n | StageEvent\n | BillingEvent\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 * Validate that at least one customer identifier is provided for billing calls.\n */\nexport function validateCustomerIdentity(customerId?: string, stripeCustomerId?: string): void {\n const hasCustomerId = customerId && customerId.trim().length > 0\n const hasStripeCustomerId = stripeCustomerId && stripeCustomerId.trim().length > 0\n\n if (!hasCustomerId && !hasStripeCustomerId) {\n throw new Error(\n \"Billing methods require customerId or stripeCustomerId. \" +\n \"Use customerId for your system-owned account ID.\",\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;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,kBAA6B;;;ACuUtB,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;;;AC7VO,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;AAKO,SAAS,yBAAyB,YAAqB,kBAAiC;AAC7F,QAAM,gBAAgB,cAAc,WAAW,KAAK,EAAE,SAAS;AAC/D,QAAM,sBAAsB,oBAAoB,iBAAiB,KAAK,EAAE,SAAS;AAEjF,MAAI,CAAC,iBAAiB,CAAC,qBAAqB;AAC1C,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;;;AF3YO,SAAS,mBAAmB,QAA6D;AAC9F,QAAM,EAAE,KAAK,UAAU,WAAW,MAAM,IAAI;AAC5C,SAAO;AAAA,IACL,UAAM,YAAAA,IAAO;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,UAAM,YAAAA,IAAO;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,UAAM,YAAAA,IAAO;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,UAAM,YAAAA,IAAO;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,UAAM,YAAAA,IAAO;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,UAAM,YAAAA,IAAO;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;AASO,SAAS,gBACd,QAIY;AACZ,QAAM,EAAE,KAAK,UAAU,WAAW,OAAO,WAAW,IAAI;AACxD,SAAO;AAAA,IACL,UAAM,YAAAA,IAAO;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;AAMO,SAAS,kBACd,QAMc;AACd,QAAM,EAAE,KAAK,UAAU,WAAW,QAAQ,YAAY,kBAAkB,WAAW,IAAI;AACvF,SAAO;AAAA,IACL,UAAM,YAAAA,IAAO;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,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":["uuidv7"]}
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":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,kBAA6B;;;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,UAAM,YAAAA,IAAO;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,UAAM,YAAAA,IAAO;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,UAAM,YAAAA,IAAO;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,UAAM,YAAAA,IAAO;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,UAAM,YAAAA,IAAO;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,UAAM,YAAAA,IAAO;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":["uuidv7"]}
package/dist/index.mjs CHANGED
@@ -110,15 +110,6 @@ function validateServerIdentity(fingerprint, email, userId, customerId) {
110
110
  );
111
111
  }
112
112
  }
113
- function validateCustomerIdentity(customerId, stripeCustomerId) {
114
- const hasCustomerId = customerId && customerId.trim().length > 0;
115
- const hasStripeCustomerId = stripeCustomerId && stripeCustomerId.trim().length > 0;
116
- if (!hasCustomerId && !hasStripeCustomerId) {
117
- throw new Error(
118
- "Billing methods require customerId or stripeCustomerId. Use customerId for your system-owned account ID."
119
- );
120
- }
121
- }
122
113
  function isValidEmail(value) {
123
114
  if (!value || typeof value !== "string") return false;
124
115
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
@@ -358,36 +349,6 @@ function buildEngagementEvent(params) {
358
349
  sessionId
359
350
  };
360
351
  }
361
- function buildStageEvent(params) {
362
- const { url, referrer, timestamp, stage, properties } = params;
363
- return {
364
- uuid: uuidv7(),
365
- type: "stage",
366
- timestamp: timestamp ?? Date.now(),
367
- url,
368
- path: extractPathFromUrl(url),
369
- referrer,
370
- utm: extractUtmParams(url),
371
- stage,
372
- properties
373
- };
374
- }
375
- function buildBillingEvent(params) {
376
- const { url, referrer, timestamp, status, customerId, stripeCustomerId, properties } = params;
377
- return {
378
- uuid: uuidv7(),
379
- type: "billing",
380
- timestamp: timestamp ?? Date.now(),
381
- url,
382
- path: extractPathFromUrl(url),
383
- referrer,
384
- utm: extractUtmParams(url),
385
- status,
386
- customerId,
387
- stripeCustomerId,
388
- properties
389
- };
390
- }
391
352
  function buildIngestPayload(visitorId, source, events, userIdentity, sessionId, fingerprint, customerIdentity) {
392
353
  const payload = {
393
354
  visitorId,
@@ -437,7 +398,6 @@ export {
437
398
  DEFAULT_DENIED_FORM_FIELDS,
438
399
  MAX_BATCH_SIZE,
439
400
  batchEvents,
440
- buildBillingEvent,
441
401
  buildCalendarEvent,
442
402
  buildCustomEvent,
443
403
  buildEngagementEvent,
@@ -445,7 +405,6 @@ export {
445
405
  buildIdentifyEvent,
446
406
  buildIngestPayload,
447
407
  buildPageviewEvent,
448
- buildStageEvent,
449
408
  extractIdentityFromForm,
450
409
  extractPathFromUrl,
451
410
  extractUtmParams,
@@ -454,7 +413,6 @@ export {
454
413
  isFieldDenied,
455
414
  isValidEmail,
456
415
  sanitizeFormFields,
457
- validateCustomerIdentity,
458
416
  validateServerIdentity
459
417
  };
460
418
  //# sourceMappingURL=index.mjs.map
@@ -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 BillingEvent,\n BillingStatus,\n CalendarEvent,\n CalendarProvider,\n CustomEvent,\n CustomerTraits,\n EngagementEvent,\n ExplicitJourneyStage,\n FormEvent,\n IdentifyEvent,\n IdentifyTraits,\n IngestPayload,\n PageviewEvent,\n PayloadCustomerIdentity,\n PayloadUserIdentity,\n SourceType,\n StageEvent,\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 * Build a stage event.\n * Used to explicitly send customer journey stage events.\n * New SDK callers should only send activated; engaged and inactive are derived\n * by Outlit from tracked activity but remain accepted for wire compatibility.\n * discovered/signed_up stages are inferred from identify calls.\n */\nexport function buildStageEvent(\n params: BaseEventParams & {\n stage: ExplicitJourneyStage\n properties?: Record<string, string | number | boolean | null>\n },\n): StageEvent {\n const { url, referrer, timestamp, stage, properties } = params\n return {\n uuid: uuidv7(),\n type: \"stage\",\n timestamp: timestamp ?? Date.now(),\n url,\n path: extractPathFromUrl(url),\n referrer,\n utm: extractUtmParams(url),\n stage,\n properties,\n }\n}\n\n/**\n * Build a billing event.\n * Used to set customer billing status (trialing, paid, churned).\n */\nexport function buildBillingEvent(\n params: BaseEventParams & {\n status: BillingStatus\n customerId?: string\n stripeCustomerId?: string\n properties?: Record<string, string | number | boolean | null>\n },\n): BillingEvent {\n const { url, referrer, timestamp, status, customerId, stripeCustomerId, properties } = params\n return {\n uuid: uuidv7(),\n type: \"billing\",\n timestamp: timestamp ?? Date.now(),\n url,\n path: extractPathFromUrl(url),\n referrer,\n utm: extractUtmParams(url),\n status,\n customerId,\n stripeCustomerId,\n properties,\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 =\n | \"pageview\"\n | \"form\"\n | \"identify\"\n | \"custom\"\n | \"calendar\"\n | \"engagement\"\n | \"stage\"\n | \"billing\"\n\n// Explicit stage event values accepted by ingest for wire compatibility.\n// New SDK callers should only send \"activated\"; \"engaged\" and \"inactive\"\n// are derived by Outlit from tracked activity.\nexport type ExplicitJourneyStage = \"activated\" | \"engaged\" | \"inactive\"\n\nexport type BillingStatus = \"trialing\" | \"paid\" | \"churned\"\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 * Customer identity for SDK billing methods.\n * Public billing calls should use `customerId`.\n */\nexport interface CustomerIdentifier extends CustomerAttribution {\n /**\n * @deprecated Stripe customer identifier.\n * Billing attribution should use `customerId` publicly.\n */\n stripeCustomerId?: string\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 interface StageEvent extends BaseEvent {\n type: \"stage\"\n /** Stage event value. New SDK callers should only send \"activated\". */\n stage: ExplicitJourneyStage\n /** Optional properties for context */\n properties?: Record<string, string | number | boolean | null>\n}\n\nexport interface BillingEvent extends BaseEvent {\n type: \"billing\"\n /** The billing status to set for a customer */\n status: BillingStatus\n /** Optional customer identifiers */\n customerId?: string\n stripeCustomerId?: string\n /** Optional properties for context */\n properties?: Record<string, string | number | boolean | null>\n}\n\nexport type TrackerEvent =\n | PageviewEvent\n | FormEvent\n | IdentifyEvent\n | CustomEvent\n | CalendarEvent\n | EngagementEvent\n | StageEvent\n | BillingEvent\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 * Validate that at least one customer identifier is provided for billing calls.\n */\nexport function validateCustomerIdentity(customerId?: string, stripeCustomerId?: string): void {\n const hasCustomerId = customerId && customerId.trim().length > 0\n const hasStripeCustomerId = stripeCustomerId && stripeCustomerId.trim().length > 0\n\n if (!hasCustomerId && !hasStripeCustomerId) {\n throw new Error(\n \"Billing methods require customerId or stripeCustomerId. \" +\n \"Use customerId for your system-owned account ID.\",\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":[]}
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":[]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@outlit/core",
3
- "version": "1.5.2",
4
- "description": "Shared types and utilities for Outlit tracking SDKs",
3
+ "version": "2.0.0",
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",
7
7
  "repository": {
@@ -12,11 +12,13 @@
12
12
  "bugs": {
13
13
  "url": "https://github.com/OutlitAI/outlit-sdk/issues"
14
14
  },
15
- "homepage": "https://github.com/OutlitAI/outlit-sdk#readme",
15
+ "homepage": "https://docs.outlit.ai/tracking/how-it-works",
16
16
  "keywords": [
17
17
  "outlit",
18
18
  "analytics",
19
19
  "tracking",
20
+ "event-tracking",
21
+ "customer-context",
20
22
  "sdk"
21
23
  ],
22
24
  "publishConfig": {