@aranova/tracking-next 1.0.0 → 1.2.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.
Files changed (48) hide show
  1. package/README.md +48 -1
  2. package/dist/blog-server.js.map +1 -1
  3. package/dist/blog-server.mjs.map +1 -1
  4. package/dist/blog.js.map +1 -1
  5. package/dist/blog.mjs.map +1 -1
  6. package/dist/calendar-server.d.mts +1 -1
  7. package/dist/calendar-server.d.ts +1 -1
  8. package/dist/calendar-server.js.map +1 -1
  9. package/dist/calendar-server.mjs.map +1 -1
  10. package/dist/calendar.d.mts +14 -3
  11. package/dist/calendar.d.ts +14 -3
  12. package/dist/calendar.js +212 -18
  13. package/dist/calendar.js.map +1 -1
  14. package/dist/calendar.mjs +211 -18
  15. package/dist/calendar.mjs.map +1 -1
  16. package/dist/client.d.mts +9 -3
  17. package/dist/client.d.ts +9 -3
  18. package/dist/client.js +550 -194
  19. package/dist/client.js.map +1 -1
  20. package/dist/client.mjs +550 -194
  21. package/dist/client.mjs.map +1 -1
  22. package/dist/index.d.mts +10 -9
  23. package/dist/index.d.ts +10 -9
  24. package/dist/index.js +491 -284
  25. package/dist/index.js.map +1 -1
  26. package/dist/index.mjs +490 -284
  27. package/dist/index.mjs.map +1 -1
  28. package/dist/{ingest-typed-BdKmlouW.d.ts → ingest-typed-MBuMe9Pn.d.ts} +94 -2
  29. package/dist/{ingest-typed-Bo7WPmpU.d.mts → ingest-typed-UA13xTfI.d.mts} +94 -2
  30. package/dist/{phone-utils-CubQif9Z.d.mts → phone-utils-C0ds6cqZ.d.mts} +6 -0
  31. package/dist/{phone-utils-CubQif9Z.d.ts → phone-utils-C0ds6cqZ.d.ts} +6 -0
  32. package/dist/phone.d.mts +1 -1
  33. package/dist/phone.d.ts +1 -1
  34. package/dist/{sales-DG150g3o.d.ts → sales-DQC815f3.d.ts} +1 -1
  35. package/dist/{sales-D7KstKA-.d.mts → sales-w1reW_Y3.d.mts} +1 -1
  36. package/dist/sales.d.mts +2 -2
  37. package/dist/sales.d.ts +2 -2
  38. package/dist/sales.js +17 -1
  39. package/dist/sales.js.map +1 -1
  40. package/dist/sales.mjs +17 -1
  41. package/dist/sales.mjs.map +1 -1
  42. package/dist/server.d.mts +1 -1
  43. package/dist/server.d.ts +1 -1
  44. package/dist/{tracking-config-runtime-D7prdTE1.d.mts → tracking-config-runtime-C6H7ofmo.d.mts} +6 -0
  45. package/dist/{tracking-config-runtime-D7prdTE1.d.ts → tracking-config-runtime-C6H7ofmo.d.ts} +6 -0
  46. package/dist/{transport-I_cMSFQ6.d.mts → transport-BlXQZs9Z.d.mts} +120 -50
  47. package/dist/{transport-CDmGOiCq.d.ts → transport-CAZYrZbB.d.ts} +120 -50
  48. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../tracking-core/src/capabilities.ts","../../tracking-core/src/resources/http/errors.ts","../../tracking-core/src/ingest.ts","../../tracking-core/src/resources/http/request.ts","../../tracking-core/src/resources/calendar/transport.ts","../../tracking-core/src/resources/intl/date.ts","../../tracking-core/src/resources/calendar/time.ts","../../tracking-core/src/resources/calendar/read-client.ts","../../tracking-core/src/resources/calendar/schema.ts","../../tracking-core/src/resources/calendar/generated/zod.gen.ts","../../tracking-core/src/resources/calendar/slots.ts"],"sourcesContent":["// Capability self-report registry.\n//\n// Every SDK construct registers itself here at construction time, and the\n// registered set rides out on `TrackingClientContext.capabilities`, which the\n// backend persists on the session row. The dashboard derives what a client site\n// ACTUALLY has installed from those sessions, rather than trusting an operator\n// checkbox — see contracts/tracking-capabilities.json, which pins this id list\n// against the backend enum and the dashboard type union.\n//\n// Browser-only by design: a server-side client (calendar bookings) emits no\n// session, and its artifact is better evidence than a self-report anyway.\n//\n// This module deliberately imports nothing: it is the one thing every other\n// resource can import without adding an edge that could close a cycle.\n\n/**\n * Every capability an install can report. Pinned by\n * `contracts/tracking-capabilities.json` — adding one here without a catalog\n * entry fails the drift test.\n */\nexport const TRACKING_CAPABILITIES = [\n \"ad_tags_google\",\n \"ad_tags_meta\",\n \"base_tracking\",\n \"blog_rendering\",\n \"calendar_read\",\n \"calendar_write\",\n \"consent_controls\",\n \"conversion_goals_auto\",\n \"conversion_goals_manual\",\n \"cta_click_capture\",\n \"form_capture\",\n \"phone_click_capture\",\n \"phone_fields\",\n] as const;\n\nexport type TrackingCapability = (typeof TRACKING_CAPABILITIES)[number];\n\n/**\n * DOM attribute a server-rendered surface stamps to report itself.\n *\n * Blog rendering never calls the Aranova API — it reads the CDN directly — so\n * neither the ingest payload nor the request-header path can see it. The\n * renderer stamps this attribute instead and the browser context collector\n * picks it up on the next batch.\n */\nexport const CAPABILITY_DOM_ATTRIBUTE = \"data-aranova-capability\";\n\nconst registered = new Set<TrackingCapability>();\n\n/**\n * Record that a capability is present in this runtime.\n *\n * Idempotent and side-effect free — safe to call on every construction,\n * including React strict-mode double mounts.\n */\nexport function registerCapability(capability: TrackingCapability): void {\n registered.add(capability);\n}\n\n/**\n * Capabilities registered so far in this runtime, sorted for a stable wire\n * value (so the backend can hash the array to skip redundant writes).\n *\n * Also collects `data-aranova-capability` markers from the DOM, which is how\n * server-rendered surfaces that never call our API report themselves.\n */\nexport function getRegisteredCapabilities(): TrackingCapability[] {\n const all = new Set<TrackingCapability>(registered);\n for (const marker of readDomMarkers()) all.add(marker);\n return Array.from(all).sort();\n}\n\nfunction readDomMarkers(): TrackingCapability[] {\n if (typeof document === \"undefined\") return [];\n const known = new Set<string>(TRACKING_CAPABILITIES);\n const found: TrackingCapability[] = [];\n // Unknown values are dropped rather than forwarded: the backend rejects ids\n // outside its enum, and a stray attribute on a client's page must never be\n // able to fail an otherwise-valid ingest batch.\n for (const node of document.querySelectorAll(`[${CAPABILITY_DOM_ATTRIBUTE}]`)) {\n const value = node.getAttribute(CAPABILITY_DOM_ATTRIBUTE);\n if (value && known.has(value)) found.push(value as TrackingCapability);\n }\n return found;\n}\n\n/** Test-only: drop everything registered so far. */\nexport function resetRegisteredCapabilitiesForTests(): void {\n registered.clear();\n}\n","/**\n * Thrown on a non-2xx from any awaited Aranova API call.\n *\n * Lives here rather than inside one resource because more than one resource\n * needs it, and a duplicate class would be a genuine hazard: a name star-exported\n * from two modules is silently dropped unless both resolve to the same binding.\n */\nexport class AranovaApiError extends Error {\n readonly status: number;\n readonly code: string | undefined;\n readonly requestId: string | undefined;\n\n constructor(message: string, options: { status: number; code?: string; requestId?: string }) {\n super(message);\n this.name = \"AranovaApiError\";\n this.status = options.status;\n this.code = options.code;\n this.requestId = options.requestId;\n }\n}\n","// Tracking ingest client. Queues events, debounce-flushes them to the\n// /tracking/events endpoint, and degrades silently on errors so a broken\n// network never breaks the host site.\n\nimport { getRegisteredCapabilities } from \"./capabilities\";\nimport { getConsentChoice, getConsentState } from \"./consent\";\nimport { resetPageViewState } from \"./page-view\";\nimport type { PhoneConfig } from \"./phone-field\";\nimport { captureFbc, getFbcCookie, getFbpCookie } from \"./fbq\";\nimport {\n captureTrackingParamsFromLocation,\n createEmptyTrackingParams,\n getCookieValueFromDocument,\n getTrackingParamsFromCookieReader,\n mergeTrackingParams,\n} from \"./tracking\";\nimport type { TriggerRegistryConfig } from \"./events/registry\";\nimport { buildHeartbeatMetadata } from \"./heartbeat\";\nimport { buildLandingPayloadFields, getOrCaptureLandingParams } from \"./landing\";\nimport { getOrRotateSessionId, getVisitorId } from \"./session\";\nimport { stashUserDataFromFormFields } from \"./user-data\";\nimport type {\n TrackingClientContext,\n TrackingEnvironment,\n TrackingInstallSurface,\n TrackingParams,\n TrackingSessionUpsertPayload,\n} from \"./types\";\n\n/**\n * Default debounce window before queued events are flushed.\n */\nexport const DEFAULT_FLUSH_INTERVAL_MS = 2000;\n\n/**\n * Default queue size that triggers an immediate flush.\n */\nexport const DEFAULT_MAX_QUEUE_SIZE = 10;\n\n/**\n * Hard server-side maximum event count per request body.\n */\nexport const HARD_MAX_BATCH = 50;\n\n/**\n * Ceiling on the exponential backoff between failed delivery attempts.\n */\nexport const MAX_RETRY_BACKOFF_MS = 60_000;\n\n/**\n * Cap on events held in the durable retry buffer. Oldest batches are dropped\n * first once this is exceeded — an unbounded buffer would eventually blow the\n * localStorage quota and take the whole SDK down with it.\n */\nexport const MAX_BUFFERED_EVENTS = 200;\n\n/**\n * localStorage key prefix for the durable retry buffer. Versioned so a future\n * shape change can't be misread as the current one.\n */\nexport const RETRY_BUFFER_KEY_PREFIX = \"aranova_tracking_pending_v1\";\n\n/**\n * What to do with a batch after an attempted delivery.\n *\n * `retry` covers the transport failing and the server saying \"later\" (408, 429,\n * 5xx). `drop` covers a permanent rejection — a 422 from a malformed payload\n * will never succeed, and retrying it forever would wedge every batch behind it.\n */\ntype DeliveryOutcome = \"ok\" | \"retry\" | \"drop\";\n\n/**\n * Header used to authenticate public tracking ingest requests.\n */\nexport const API_KEY_HEADER = \"X-Aranova-Api-Key\";\n\n/**\n * Identity headers stamped on every ingest request. Duplicate fields already\n * present in `session.context` but survive body-parse failures so the backend\n * can attribute 422s to the offending SDK install.\n */\nexport const SDK_VERSION_HEADER = \"X-Aranova-Sdk-Version\";\nexport const SDK_PACKAGE_HEADER = \"X-Aranova-Sdk-Package\";\nexport const SDK_SURFACE_HEADER = \"X-Aranova-Sdk-Surface\";\nexport const SDK_ENVIRONMENT_HEADER = \"X-Aranova-Sdk-Environment\";\n\n/**\n * Configuration for the low-level ingest client.\n *\n * Framework packages usually create this for you through `createTracking()`.\n */\nexport interface TrackingClientConfig {\n /** Public tracking API key issued for the business. */\n apiKey: string;\n /** Tracking endpoint base URL, usually ending in `/tracking`. */\n endpoint: string;\n /** SDK surface creating this client. */\n surface: TrackingInstallSurface;\n /** Package version reported in session context and heartbeat metadata. */\n sdkVersion?: string;\n /** Package name reported in session context and heartbeat metadata. */\n packageName?: string;\n /** Trigger registry so the heartbeat can report registered events. */\n triggers?: TriggerRegistryConfig;\n /** Override the default 2s debounce window. */\n flushIntervalMs?: number;\n /** Override the default 10-event batch trigger. */\n maxQueueSize?: number;\n /** Deployment environment label reported in session context. */\n environment?: TrackingEnvironment;\n /** All active gtag IDs, keyed by label. Included in session context. */\n activeGtagIds?: Record<string, string>;\n /** `siteId` of the tracking-config reference in use; reported on the heartbeat. */\n configSiteId?: string | null;\n /** When true, swallow nothing — useful for tests. */\n debug?: boolean;\n /** Phone-field config, carried for parity; the React hook reads it via context. */\n phone?: PhoneConfig;\n}\n\n/**\n * Input accepted by the low-level stringly-typed client.\n *\n * Prefer the typed `trackEvent(eventName, metadata)` facade exposed by\n * `useTracking()` in React/Next integrations.\n */\nexport interface TrackEventInput {\n /** Event name to enqueue. */\n eventType: string;\n /** URL associated with the event. Defaults to the current page URL. */\n pageUrl?: string | null;\n /** Event-specific metadata. */\n metadata?: Record<string, unknown> | null;\n /** Timestamp override. Defaults to queue time. */\n occurredAt?: Date | string | null;\n}\n\ninterface QueuedEvent {\n event_type: string;\n page_url: string | null;\n metadata: Record<string, unknown> | null;\n occurred_at: string | null;\n}\n\n/**\n * Low-level tracking client responsible for queueing and flushing events.\n */\nexport interface TrackingClient {\n /** Enqueue an event for batched delivery. */\n trackEvent: (input: TrackEventInput) => void;\n /** Flush queued events immediately (fetch, non-keepalive). */\n flush: () => Promise<void>;\n /**\n * Flush queued events through the keepalive transport so the request\n * survives document unload. Use from `pagehide`/`visibilitychange:hidden`\n * handlers — a plain `flush()` there is aborted by the browser on unload.\n */\n flushBeacon: () => void;\n /** Return the current rolling session id. */\n getSessionId: () => string;\n /** Return the persistent visitor id. */\n getVisitorId: () => string;\n /** Remove timers/listeners and prevent future flushes. */\n destroy: () => void;\n}\n\ninterface IngestRequestBody {\n session: TrackingSessionUpsertPayload;\n events: Array<{\n event_type: string;\n page_url: string | null;\n metadata: Record<string, unknown> | null;\n occurred_at: string | null;\n }>;\n}\n\nfunction buildContext(\n surface: TrackingInstallSurface,\n sdkVersion: string | null,\n packageName: string | null,\n environment: TrackingEnvironment,\n activeGtagIds: Record<string, string> | null,\n): TrackingClientContext {\n return {\n surface,\n sdk_version: sdkVersion,\n package_name: packageName,\n site_origin: typeof window === \"undefined\" ? null : window.location.origin,\n page_title: typeof document === \"undefined\" ? null : document.title || null,\n referrer: typeof document === \"undefined\" ? null : document.referrer || null,\n environment,\n active_gtag_ids: activeGtagIds,\n // Rebuilt per flush (this runs inside the payload builder), so a client\n // constructed later on a deeper route still gets reported.\n capabilities: getRegisteredCapabilities(),\n };\n}\n\nfunction readTrackingParams(): TrackingParams {\n if (typeof window === \"undefined\") return createEmptyTrackingParams();\n // Capture from URL on every read so the first event in a session reflects the\n // landing-page params even if the cookie helper hasn't run yet.\n try {\n captureTrackingParamsFromLocation();\n } catch {\n // ignore\n }\n return getTrackingParamsFromCookieReader(getCookieValueFromDocument);\n}\n\nfunction consentSnapshot(): Record<string, unknown> | null {\n try {\n // Effective consent + provenance so the dashboard can tell default-granted\n // (opt-out model, no interaction) apart from an explicit choice.\n const choice = getConsentChoice();\n return {\n state: choice.state,\n source: choice.source,\n updated_at: choice.updatedAt,\n expires_at: choice.expiresAt,\n };\n } catch {\n return null;\n }\n}\n\ninterface IdentityHeaders {\n sdkVersion: string;\n packageName: string;\n surface: string;\n environment: string;\n}\n\nasync function postWithFetch(\n url: string,\n body: string,\n apiKey: string,\n identity: IdentityHeaders,\n keepalive: boolean,\n): Promise<DeliveryOutcome> {\n // No fetch at all (a non-browser runtime): there is nothing to retry against,\n // and scheduling one would spin a timer forever. Give up on the batch.\n if (typeof fetch !== \"function\") return \"drop\";\n try {\n const response = await fetch(url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n [API_KEY_HEADER]: apiKey,\n [SDK_VERSION_HEADER]: identity.sdkVersion,\n [SDK_PACKAGE_HEADER]: identity.packageName,\n [SDK_SURFACE_HEADER]: identity.surface,\n [SDK_ENVIRONMENT_HEADER]: identity.environment,\n },\n body,\n keepalive,\n // CORS is open on the tracking endpoint; never send cookies.\n credentials: \"omit\",\n mode: \"cors\",\n });\n if (response.ok) return \"ok\";\n if (response.status === 408 || response.status === 429 || response.status >= 500) {\n return \"retry\";\n }\n // 401/403/422 and friends: the payload or the key is wrong, and will still\n // be wrong in 30 seconds.\n return \"drop\";\n } catch {\n // Network-level failure (offline, DNS, CORS preflight, aborted). Never\n // throws to the host site; the batch stays buffered for the next attempt.\n return \"retry\";\n }\n}\n\n/**\n * A batch awaiting delivery. Each one carries its OWN session snapshot: a batch\n * can outlive the session that produced it (buffered across a reload), and\n * re-sending it under whatever session is current would silently misattribute\n * those events.\n */\ninterface PendingBatch {\n session: TrackingSessionUpsertPayload;\n events: QueuedEvent[];\n}\n\nfunction retryBufferKey(apiKey: string, endpoint: string): string {\n return `${RETRY_BUFFER_KEY_PREFIX}:${apiKey}:${endpoint}`;\n}\n\nfunction readPendingBatches(key: string): PendingBatch[] {\n if (typeof window === \"undefined\") return [];\n try {\n const raw = window.localStorage.getItem(key);\n if (!raw) return [];\n const parsed: unknown = JSON.parse(raw);\n if (!Array.isArray(parsed)) return [];\n return parsed.filter(\n (entry): entry is PendingBatch =>\n typeof entry === \"object\" &&\n entry !== null &&\n \"session\" in entry &&\n Array.isArray((entry as PendingBatch).events),\n );\n } catch {\n // Storage blocked, or a corrupt/foreign value — start clean rather than\n // letting a bad read break the host site.\n return [];\n }\n}\n\n/**\n * Mirror the buffer to storage unconditionally.\n *\n * Deliberately no read-back verification: under partitioned storage a write can\n * succeed for this document and still not be visible to a verifying read, so a\n * verify-gate false-negatives exactly where durability matters most.\n */\nfunction writePendingBatches(key: string, batches: PendingBatch[]): void {\n if (typeof window === \"undefined\") return;\n try {\n if (batches.length === 0) window.localStorage.removeItem(key);\n else window.localStorage.setItem(key, JSON.stringify(batches));\n } catch {\n // Quota exceeded or storage blocked — in-memory retry still works.\n }\n}\n\n/** Drop the oldest batches until the buffer is back under the event cap. */\nfunction trimToBufferCap(batches: PendingBatch[]): PendingBatch[] {\n let total = batches.reduce((sum, batch) => sum + batch.events.length, 0);\n const trimmed = batches.slice();\n while (total > MAX_BUFFERED_EVENTS && trimmed.length > 1) {\n const dropped = trimmed.shift();\n total -= dropped ? dropped.events.length : 0;\n }\n return trimmed;\n}\n\nlet globalClient: TrackingClient | null = null;\nlet globalClientKey: string | null = null;\n\nfunction clientConfigKey(config: TrackingClientConfig): string {\n return `${config.apiKey}@${config.endpoint}#${config.surface}`;\n}\n\n/**\n * Return a page-level singleton tracking client. Creating the client anew on\n * every component mount is wrong — React StrictMode double-mounts dev-only,\n * and destroying+recreating the client between the cleanup and re-run strips\n * away the pushState patch that SPA auto page view relies on. A singleton\n * survives all of that: the client lives for the entire page, and providers\n * just attach/detach auto page view against it.\n *\n * If `apiKey` / `endpoint` / `surface` change between calls, the previous\n * singleton is destroyed and a new one replaces it. This covers hot-config\n * changes without leaking state.\n */\nexport function getOrCreateTrackingClient(config: TrackingClientConfig): TrackingClient {\n const key = clientConfigKey(config);\n if (globalClient !== null && globalClientKey === key) {\n return globalClient;\n }\n if (globalClient !== null) {\n globalClient.destroy();\n }\n globalClient = createTrackingClient(config);\n globalClientKey = key;\n return globalClient;\n}\n\n// Global/delegated captures (document/window listeners: page_exit, scroll_depth,\n// time_on_site, form_start, cta_click, phone_click, bfcache restore) must attach ONCE\n// per page-singleton client — NOT once per React provider mount. A page can mount several\n// <TrackingProvider>s (a supported island pattern: a global provider for page-level\n// triggers plus per-component providers so `useSearchParams()` doesn't opt the whole tree\n// out of static rendering). Since the client is a singleton but each provider runs its own\n// attach effect, N mounts would stack N document listeners and every interaction would emit\n// its event N times. We ref-count attaches against the singleton client's identity: the\n// first mount attaches, later mounts are no-ops, and the last unmount detaches.\ninterface ClientCaptureEntry {\n detach: () => void;\n refCount: number;\n}\nconst clientCaptureRegistry = new WeakMap<TrackingClient, ClientCaptureEntry>();\n\n/**\n * Attach a set of global/delegated captures against a singleton `client` exactly once,\n * ref-counted across provider mounts. `build` performs the actual `document`/`window`\n * listener attachment and returns a single detacher for all of them; it runs only on the\n * first mount for a given client. The returned release decrements the ref-count and runs\n * `build`'s detacher when the last holder releases. Release is idempotent — React\n * StrictMode invokes an effect's cleanup twice in dev, and a double release must not\n * double-decrement.\n */\nexport function attachClientCapturesOnce(\n client: TrackingClient,\n build: () => () => void,\n): () => void {\n let entry = clientCaptureRegistry.get(client);\n if (entry === undefined) {\n entry = { detach: build(), refCount: 0 };\n clientCaptureRegistry.set(client, entry);\n }\n entry.refCount += 1;\n\n let released = false;\n return () => {\n if (released) return;\n released = true;\n const current = clientCaptureRegistry.get(client);\n if (current === undefined) return;\n current.refCount -= 1;\n if (current.refCount <= 0) {\n current.detach();\n clientCaptureRegistry.delete(client);\n }\n };\n}\n\n/**\n * Tear down the singleton if any. Primarily an escape hatch for tests where\n * each test should see a fresh client; production code rarely needs this.\n * Also clears the in-session SPA referrer so the next test starts with a\n * fresh referrer chain.\n */\nexport function resetGlobalTrackingClient(): void {\n if (globalClient !== null) {\n // Force-detach any captures still attached to this client so their document/window\n // listeners can't leak into the next test even if a provider didn't unmount.\n const entry = clientCaptureRegistry.get(globalClient);\n if (entry !== undefined) {\n entry.detach();\n clientCaptureRegistry.delete(globalClient);\n }\n globalClient.destroy();\n }\n globalClient = null;\n globalClientKey = null;\n resetPageViewState();\n}\n\n/**\n * Create a low-level ingest client.\n *\n * The client queues events, debounces network flushes, sends an SDK heartbeat\n * once per new session, and swallows network errors so analytics never break\n * the host site.\n */\nexport function createTrackingClient(config: TrackingClientConfig): TrackingClient {\n const flushIntervalMs = config.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS;\n const maxQueueSize = Math.min(config.maxQueueSize ?? DEFAULT_MAX_QUEUE_SIZE, HARD_MAX_BATCH);\n const sdkVersion = config.sdkVersion ?? null;\n const packageName = config.packageName ?? null;\n // Default to 'production' so the wire payload always carries a valid enum\n // value. Sending null would fail the backend's strict enum validation.\n const environment: TrackingEnvironment = config.environment ?? \"production\";\n const activeGtagIds = config.activeGtagIds ?? null;\n const endpointBase = config.endpoint.replace(/\\/$/, \"\");\n const eventsUrl = `${endpointBase}/events`;\n const identityHeaders: IdentityHeaders = {\n sdkVersion: sdkVersion ?? \"\",\n packageName: packageName ?? \"\",\n surface: config.surface,\n environment,\n };\n\n let queue: QueuedEvent[] = [];\n let flushTimer: ReturnType<typeof setTimeout> | null = null;\n // Batches that have been handed to the network at least once and not yet\n // acknowledged. Mirrored to localStorage so a reload — or a deploy window that\n // 5xxs every request — costs latency instead of data.\n const bufferKey = retryBufferKey(config.apiKey, endpointBase);\n let pending: PendingBatch[] = readPendingBatches(bufferKey);\n // Flushes are serialized through one chain. Batches are no longer removed from\n // the buffer before the response arrives, so two overlapping flushes would\n // send the same batch twice. Chaining (rather than a \"busy\" flag that returns\n // early) keeps `await flush()` meaning \"flushed\" for the caller.\n let flushChain: Promise<void> = Promise.resolve();\n let retryAttempt = 0;\n let firstPage: string | null = null;\n // Attribution captured synchronously at client creation (before any SPA router\n // can strip the query string). Merged into every session payload as a fallback\n // so a blocked-cookie webview or a stripped URL still attributes.\n let initialParams = createEmptyTrackingParams();\n let destroyed = false;\n\n // Initialize identity early so the first POST has stable values.\n const visitorId = getVisitorId();\n const initialSession = getOrRotateSessionId();\n let sessionId = initialSession.id;\n\n if (typeof window !== \"undefined\") {\n firstPage = window.location.href;\n try {\n initialParams = captureTrackingParamsFromLocation();\n } catch {\n // ignore — never break the host site\n }\n // Pin this session's LANDING params now, before any SPA router strips the\n // query string (reuses the stored record when the session already has one).\n getOrCaptureLandingParams(sessionId);\n try {\n captureFbc();\n } catch {\n // ignore\n }\n }\n\n // Queue an sdk_heartbeat event at the start of every new session.\n function enqueueHeartbeat(): void {\n const metadata = buildHeartbeatMetadata(\n config.surface,\n sdkVersion,\n packageName,\n config.triggers ?? null,\n activeGtagIds,\n config.configSiteId ?? null,\n );\n queue.push({\n event_type: \"sdk_heartbeat\",\n page_url: typeof window === \"undefined\" ? null : window.location.href,\n metadata: metadata as unknown as Record<string, unknown>,\n occurred_at: new Date().toISOString(),\n });\n }\n\n if (initialSession.isNew) {\n enqueueHeartbeat();\n }\n\n // Anything left buffered by a previous page load goes out on the normal\n // debounce, ahead of whatever this page produces.\n if (pending.length > 0) {\n scheduleFlush();\n }\n\n function buildSessionPayload(): TrackingSessionUpsertPayload {\n const rotated = getOrRotateSessionId();\n if (rotated.isNew && rotated.id !== sessionId) {\n // Session rotated mid-page (idle > 30min then user returned).\n enqueueHeartbeat();\n }\n sessionId = rotated.id;\n // Prefer the live cookie/localStorage read; fall back to the init-time\n // snapshot (covers a webview that blocked the cookie AND a router that\n // already stripped the landing URL by flush time).\n const params = mergeTrackingParams(readTrackingParams(), initialParams);\n const context = buildContext(\n config.surface,\n sdkVersion,\n packageName,\n environment,\n activeGtagIds,\n );\n return {\n session_id: sessionId,\n visitor_id: visitorId,\n gclid: params.gclid,\n wbraid: params.wbraid,\n gbraid: params.gbraid,\n ylpcid: params.ylpcid,\n fbclid: params.fbclid,\n fbc: getFbcCookie(),\n fbp: getFbpCookie(),\n utm_source: params.utm_source,\n utm_medium: params.utm_medium,\n utm_campaign: params.utm_campaign,\n utm_term: params.utm_term,\n utm_content: params.utm_content,\n // Landing params for the CURRENT session id — captured on the spot when\n // the session just rotated (the current URL is the rotated session's\n // landing), reused from the stored record otherwise. Keys are omitted\n // entirely when the landing isn't observable (SSR).\n ...buildLandingPayloadFields(sessionId),\n first_page: firstPage,\n consent_state: consentSnapshot(),\n context,\n };\n }\n\n function scheduleFlush(delayMs: number = flushIntervalMs): void {\n if (flushTimer !== null || destroyed) return;\n flushTimer = setTimeout(() => {\n flushTimer = null;\n void flush();\n }, delayMs);\n }\n\n function clearScheduledFlush(): void {\n if (flushTimer !== null) {\n clearTimeout(flushTimer);\n flushTimer = null;\n }\n }\n\n /** Move everything currently queued into the durable buffer. */\n function bufferQueued(): void {\n if (queue.length === 0) return;\n const events = queue.slice(0, HARD_MAX_BATCH);\n queue = queue.slice(events.length);\n pending = trimToBufferCap([...pending, { session: buildSessionPayload(), events }]);\n writePendingBatches(bufferKey, pending);\n }\n\n /**\n * Drain the durable buffer, oldest batch first.\n *\n * A batch leaves the buffer only on `ok` (acknowledged) or `drop` (permanently\n * rejected). Anything else keeps it, so a 5xx window costs latency, not data.\n *\n * Delivery is at-least-once: with no event-level idempotency key on the wire,\n * a response lost after the server committed will re-send that batch. That\n * window is far narrower than the \"every 5xx is permanent loss\" it replaces —\n * closing it needs a client-generated event id plus a backend uniqueness\n * constraint, which is a schema change and its own release.\n */\n function flush(): Promise<void> {\n // `.catch` keeps one unexpected throw from poisoning every later flush.\n flushChain = flushChain.then(runFlush).catch(() => {});\n return flushChain;\n }\n\n async function runFlush(): Promise<void> {\n if (destroyed) return;\n clearScheduledFlush();\n bufferQueued();\n if (pending.length === 0) return;\n\n try {\n while (pending.length > 0) {\n const batch = pending[0]!;\n const body: IngestRequestBody = { session: batch.session, events: batch.events };\n // keepalive=false: this runs on a live page, where a plain fetch\n // completes normally and its failure is observable. The unload path\n // (flushOnUnload) passes true instead — pagehide tears the document down\n // and aborts a plain fetch mid-flight, while keepalive hands the request\n // to the browser to finish after teardown.\n const outcome = await postWithFetch(\n eventsUrl,\n JSON.stringify(body),\n config.apiKey,\n identityHeaders,\n false,\n );\n if (outcome === \"retry\") {\n retryAttempt += 1;\n // Exponential from the normal flush interval, capped. Scheduling\n // happens after `flushing` is cleared, below.\n return;\n }\n pending = pending.slice(1);\n writePendingBatches(bufferKey, pending);\n retryAttempt = 0;\n }\n } finally {\n if (pending.length > 0) {\n const backoff = Math.min(flushIntervalMs * 2 ** retryAttempt, MAX_RETRY_BACKOFF_MS);\n scheduleFlush(backoff);\n } else if (queue.length > 0) {\n // `bufferQueued` takes at most HARD_MAX_BATCH per pass, so a burst\n // larger than one batch still has events waiting.\n scheduleFlush();\n }\n }\n }\n\n function trackEvent(input: TrackEventInput): void {\n if (destroyed) return;\n if (!input || typeof input.eventType !== \"string\" || input.eventType.length === 0) return;\n\n // Enhanced conversions: a form submit is the one moment the visitor's own\n // email/phone pass through the SDK — stash them (memory only) so the\n // conversion that fires next carries user_data. NOT done for phone_click:\n // its metadata holds the BUSINESS's number, not the visitor's. An explicit\n // consent decline also skips the stash entirely — egress is already gated,\n // but a decliner's identifiers shouldn't sit in page memory either.\n if (input.eventType === \"form_submit\" && getConsentState() !== \"denied\") {\n try {\n const fields = (input.metadata as { form?: { fields?: unknown } } | null)?.form?.fields;\n if (fields) stashUserDataFromFormFields(fields, config.phone?.defaultCountry);\n } catch {\n // user-data capture must never break ingest\n }\n }\n\n const occurredAt =\n input.occurredAt instanceof Date\n ? input.occurredAt.toISOString()\n : typeof input.occurredAt === \"string\"\n ? input.occurredAt\n : new Date().toISOString();\n\n queue.push({\n event_type: input.eventType,\n page_url: input.pageUrl ?? (typeof window === \"undefined\" ? null : window.location.href),\n metadata: input.metadata ?? null,\n occurred_at: occurredAt,\n });\n\n if (queue.length >= maxQueueSize) {\n void flush();\n } else {\n scheduleFlush();\n }\n }\n\n /**\n * Last-gasp send on pagehide/visibilitychange.\n *\n * The keepalive request outlives the document, so its outcome can never be\n * observed. The batch it carries is therefore removed from the buffer\n * optimistically: keeping it would re-send on the next page load every time\n * the send actually worked, which is almost always. Any batch behind it stays\n * buffered and is retried on the next load.\n */\n function flushOnUnload(): void {\n bufferQueued();\n clearScheduledFlush();\n if (pending.length === 0) return;\n\n const batch = pending[0]!;\n pending = pending.slice(1);\n writePendingBatches(bufferKey, pending);\n\n const body: IngestRequestBody = { session: batch.session, events: batch.events };\n void postWithFetch(eventsUrl, JSON.stringify(body), config.apiKey, identityHeaders, true);\n }\n\n if (typeof window !== \"undefined\") {\n window.addEventListener(\"pagehide\", flushOnUnload);\n window.addEventListener(\"visibilitychange\", () => {\n if (document.visibilityState === \"hidden\") flushOnUnload();\n });\n }\n\n return {\n trackEvent,\n flush,\n flushBeacon: flushOnUnload,\n getSessionId: () => sessionId,\n getVisitorId: () => visitorId,\n destroy: () => {\n destroyed = true;\n // Fire any pending events through the keepalive path before tearing\n // down. Critical for React StrictMode in dev, where the provider's\n // first mount is immediately unmounted and its 2s debounce would\n // otherwise drop the initial page_view on the floor. Uses fetch\n // keepalive so the request survives the component tearing down.\n if (queue.length > 0) {\n flushOnUnload();\n }\n clearScheduledFlush();\n queue = [];\n if (typeof window !== \"undefined\") {\n window.removeEventListener(\"pagehide\", flushOnUnload);\n }\n },\n };\n}\n","import {\n API_KEY_HEADER,\n SDK_ENVIRONMENT_HEADER,\n SDK_PACKAGE_HEADER,\n SDK_SURFACE_HEADER,\n SDK_VERSION_HEADER,\n} from \"../../ingest\";\nimport { AranovaApiError } from \"./errors\";\n\n/** Shared config for every awaited (non fire-and-forget) API helper. */\nexport interface ApiTransportConfig {\n /** Public (`aranv_pk_…`) or secret (`aranv_sk_…`) API key. */\n apiKey: string;\n /** Base tracking endpoint, e.g. `https://aranovainternal-production.up.railway.app/tracking`. */\n endpoint: string;\n /** Optional SDK identity headers (mirrors the event ingest client). */\n sdkVersion?: string;\n packageName?: string;\n surface?: string;\n environment?: string;\n}\n\nfunction identityHeaders(config: ApiTransportConfig): Record<string, string> {\n const headers: Record<string, string> = { [API_KEY_HEADER]: config.apiKey };\n if (config.sdkVersion) headers[SDK_VERSION_HEADER] = config.sdkVersion;\n if (config.packageName) headers[SDK_PACKAGE_HEADER] = config.packageName;\n if (config.surface) headers[SDK_SURFACE_HEADER] = config.surface;\n if (config.environment) headers[SDK_ENVIRONMENT_HEADER] = config.environment;\n return headers;\n}\n\nfunction joinUrl(endpoint: string, path: string): string {\n return `${endpoint.replace(/\\/$/, \"\")}${path}`;\n}\n\n/**\n * Single awaited request. Unlike the event queue, this surfaces failures: any\n * non-2xx rejects with an {@link AranovaApiError}. Returns `undefined` for 204.\n */\nexport async function apiRequest<T>(\n config: ApiTransportConfig,\n method: string,\n path: string,\n body?: unknown,\n extraHeaders?: Record<string, string>,\n): Promise<T> {\n const headers = { ...identityHeaders(config), ...extraHeaders };\n if (body !== undefined) headers[\"Content-Type\"] = \"application/json\";\n\n const response = await fetch(joinUrl(config.endpoint, path), {\n method,\n headers,\n body: body === undefined ? undefined : JSON.stringify(body),\n });\n\n if (!response.ok) {\n let detail: string | undefined;\n let code: string | undefined;\n try {\n const parsed: unknown = await response.json();\n if (parsed && typeof parsed === \"object\") {\n const record = parsed as Record<string, unknown>;\n if (typeof record.detail === \"string\") detail = record.detail;\n if (typeof record.code === \"string\") code = record.code;\n }\n } catch {\n // non-JSON error body — fall back to status text\n }\n throw new AranovaApiError(detail ?? response.statusText ?? \"Request failed\", {\n status: response.status,\n code,\n requestId: response.headers.get(\"x-request-id\") ?? undefined,\n });\n }\n\n if (response.status === 204) return undefined as T;\n return (await response.json()) as T;\n}\n","import type { ApiTransportConfig } from \"../http/request\";\nimport { apiRequest } from \"../http/request\";\n\nexport type CalendarTransportConfig = ApiTransportConfig;\n\n/** Prefixes `/calendar` so callers pass resource-relative paths. */\nexport async function calendarRequest<T>(\n config: CalendarTransportConfig,\n method: string,\n path: string,\n body?: unknown,\n): Promise<T> {\n return apiRequest<T>(config, method, `/calendar${path}`, body);\n}\n","/**\n * Format an ISO timestamp in a specific IANA time zone (e.g. `America/Toronto`),\n * so dashboards stop hand-rolling `Intl`. Defaults to a short date-time; pass\n * `opts` to override fields. The `timeZone` is always forced to the argument.\n */\nexport function formatDateInTz(\n iso: string,\n timeZone: string,\n opts?: Intl.DateTimeFormatOptions,\n locale?: string,\n): string {\n const date = new Date(iso);\n // Guard malformed input: Intl.format(Invalid Date) throws a RangeError.\n if (Number.isNaN(date.getTime())) return iso;\n return new Intl.DateTimeFormat(locale, {\n year: \"numeric\",\n month: \"short\",\n day: \"2-digit\",\n hour: \"2-digit\",\n minute: \"2-digit\",\n ...opts,\n timeZone,\n }).format(date);\n}\n","import { formatDateInTz } from \"../intl/date\";\n\nexport { formatDateInTz };\n\n/** The visitor's own zone, so a booking page defaults to their local times. */\nexport function resolveBrowserTimeZone(): string {\n try {\n return Intl.DateTimeFormat().resolvedOptions().timeZone || \"UTC\";\n } catch {\n return \"UTC\";\n }\n}\n\n/** Parts of an instant as seen in a specific zone. */\nfunction partsInTz(date: Date, timeZone: string): Record<string, number> {\n const formatter = new Intl.DateTimeFormat(\"en-US\", {\n timeZone,\n hour12: false,\n year: \"numeric\",\n month: \"2-digit\",\n day: \"2-digit\",\n hour: \"2-digit\",\n minute: \"2-digit\",\n second: \"2-digit\",\n });\n const out: Record<string, number> = {};\n for (const part of formatter.formatToParts(date)) {\n if (part.type !== \"literal\") out[part.type] = Number(part.value);\n }\n // `hour` comes back as 24 at midnight under hour12:false in some engines.\n if (out.hour === 24) out.hour = 0;\n return out;\n}\n\n/** Offset in minutes between UTC and `timeZone` at a given instant. */\nexport function offsetMinutesAt(date: Date, timeZone: string): number {\n const p = partsInTz(date, timeZone);\n const asUtc = Date.UTC(p.year, p.month - 1, p.day, p.hour, p.minute, p.second);\n return (asUtc - Math.floor(date.getTime() / 1000) * 1000) / 60_000;\n}\n\n/**\n * The instant matching a wall-clock time in `timeZone`.\n *\n * Two passes because the offset itself depends on the instant: guess with the\n * offset at the naive time, then re-solve with the offset that guess landed in.\n * That second pass is what makes DST-transition days come out right.\n */\nexport function instantFromWallClock(\n timeZone: string,\n year: number,\n month: number,\n day: number,\n hour = 0,\n minute = 0,\n): Date {\n const naive = Date.UTC(year, month - 1, day, hour, minute);\n const firstGuess = new Date(naive - offsetMinutesAt(new Date(naive), timeZone) * 60_000);\n const corrected = naive - offsetMinutesAt(firstGuess, timeZone) * 60_000;\n return new Date(corrected);\n}\n\n/** `YYYY-MM-DD` for an instant as seen in `timeZone`. */\nexport function localDateKey(iso: string, timeZone: string): string {\n const p = partsInTz(new Date(iso), timeZone);\n const pad = (n: number) => String(n).padStart(2, \"0\");\n return `${p.year}-${pad(p.month)}-${pad(p.day)}`;\n}\n\nexport function startOfDayInTz(dateKey: string, timeZone: string): Date {\n const [year, month, day] = dateKey.split(\"-\").map(Number);\n return instantFromWallClock(timeZone, year, month, day, 0, 0);\n}\n\nexport function endOfDayInTz(dateKey: string, timeZone: string): Date {\n const start = startOfDayInTz(dateKey, timeZone);\n // Add a calendar day rather than 24h: DST days are 23 or 25 hours long.\n const p = partsInTz(start, timeZone);\n return instantFromWallClock(timeZone, p.year, p.month, p.day + 1, 0, 0);\n}\n\nexport function durationMinutes(startIso: string, endIso: string): number {\n return Math.round((new Date(endIso).getTime() - new Date(startIso).getTime()) / 60_000);\n}\n\n/** `10:00 – 10:30` in the given zone. */\nexport function formatSlotRange(\n startIso: string,\n endIso: string,\n timeZone: string,\n locale?: string,\n): string {\n const opts: Intl.DateTimeFormatOptions = { hour: \"2-digit\", minute: \"2-digit\", timeZone };\n const fmt = new Intl.DateTimeFormat(locale, opts);\n return `${fmt.format(new Date(startIso))} – ${fmt.format(new Date(endIso))}`;\n}\n","import { registerCapability } from \"../../capabilities\";\nimport { AranovaApiError } from \"../http/errors\";\nimport type { BusyResponse, CalendarErrorCode, CalendarSummary } from \"./schema\";\nimport type { CalendarTransportConfig } from \"./transport\";\nimport { calendarRequest } from \"./transport\";\nimport { resolveBrowserTimeZone } from \"./time\";\n\n/**\n * The browser-side calendar client — reads only.\n *\n * There is deliberately no write verb on this type. A public key ships in a JS\n * bundle and its Origin allowlist is a browser-enforced header a script forges\n * trivially; a booking is not a database row but an event on a real person's\n * calendar that mails their contacts. Writes live in\n * `@aranova/tracking-next/calendar-server`, which is `server-only`.\n */\nexport interface CalendarReadClientConfig extends CalendarTransportConfig {\n /** Falls back to the visitor's own zone. */\n defaultTimeZone?: string;\n /** Required when the key is not already scoped to one business. */\n businessId?: string;\n}\n\nexport interface BusyQuery {\n /**\n * ISO 8601 instants WITH an offset — the API rejects a naive or date-only\n * value with a 400. `startOfDayInTz`/`endOfDayInTz` turn a picked date into\n * the right pair for a given zone.\n */\n since: string;\n until: string;\n timeZone?: string;\n}\n\nexport interface CalendarReadClient<TCalendarKey extends string = string> {\n /** Labels, timezones and status — everything a widget needs before booking. */\n listCalendars(): Promise<CalendarSummary[]>;\n /** Busy intervals only. Never the contents of the grantee's events. */\n busy(calendarKey: TCalendarKey, query: BusyQuery): Promise<BusyResponse>;\n}\n\nfunction withBusiness(config: CalendarReadClientConfig, path: string): string {\n if (!config.businessId) return path;\n const separator = path.includes(\"?\") ? \"&\" : \"?\";\n return `${path}${separator}business_id=${encodeURIComponent(config.businessId)}`;\n}\n\nexport function createCalendarReadClient<TCalendarKey extends string = string>(\n config: CalendarReadClientConfig,\n): CalendarReadClient<TCalendarKey> {\n registerCapability(\"calendar_read\");\n return {\n async listCalendars() {\n return calendarRequest<CalendarSummary[]>(config, \"GET\", withBusiness(config, \"/calendars\"));\n },\n async busy(calendarKey, query) {\n const params = new URLSearchParams({\n since: query.since,\n until: query.until,\n });\n const timeZone = query.timeZone ?? config.defaultTimeZone ?? resolveBrowserTimeZone();\n params.set(\"time_zone\", timeZone);\n return calendarRequest<BusyResponse>(\n config,\n \"GET\",\n withBusiness(config, `/${encodeURIComponent(calendarKey)}/busy?${params.toString()}`),\n );\n },\n };\n}\n\n/** Narrow an unknown error to a specific calendar failure code. */\nexport function isCalendarError(\n error: unknown,\n code?: CalendarErrorCode,\n): error is AranovaApiError {\n if (!(error instanceof AranovaApiError)) return false;\n return code === undefined || error.code === code;\n}\n","import * as z from \"zod/mini\";\n\nimport {\n zBookingAttendee,\n zBookingCancelSchema,\n zBookingCreateSchema,\n zBookingCustomerInput,\n zBookingMetadata,\n zBookingReminder,\n zBookingReminders,\n zBookingRescheduleSchema,\n zCustomLocation,\n zMeetLocation,\n zPhoneLocation,\n zPhysicalLocation,\n} from \"./generated/zod.gen\";\n\n/**\n * Calendar wire schemas.\n *\n * The object shapes come from the backend's own spec — see\n * `../generated/zod.gen.ts` and `packages/tracking-core/openapi-ts.config.ts`.\n * This file adds only what JSON Schema cannot express: the end/duration XOR,\n * the IANA timezone check, and defaults Pydantic omits from the spec because\n * `default_factory` is not serialized (`attendees: []`, `reminders.overrides:\n * []`) — composed onto the generated base, never duplicating its shape.\n */\n\n/** Bumped when the durable `metadata` payload changes shape. */\nexport const CALENDAR_METADATA_SCHEMA_VERSION = 1;\n\nexport const MAX_ATTENDEES = 20;\nexport const MAX_REMINDER_OVERRIDES = 5;\n/** Google's ceiling for `reminders.overrides[].minutes`: four weeks. */\nexport const MAX_REMINDER_MINUTES = 40_320;\nexport const MAX_BOOKING_MINUTES = 1_440;\n\nexport const BOOKING_SOURCES = [\"website\", \"phone\", \"walk_in\", \"import\", \"operator\"] as const;\nexport const BOOKING_STATUSES = [\"pending\", \"confirmed\", \"cancelled\", \"no_show\"] as const;\nexport const GOOGLE_SYNC_STATES = [\"queued\", \"synced\", \"uncertain\", \"failed\", \"drifted\"] as const;\nexport const CALENDAR_SYNC_STATUSES = [\n \"pending_invite\",\n \"awaiting_calendar_selection\",\n \"connected\",\n \"needs_reconnect\",\n \"revoked\",\n \"disabled\",\n] as const;\n\n/** Every failure the calendar API can hand back, so a site can branch on it. */\nexport const CALENDAR_ERROR_CODES = [\n \"slot_unavailable\",\n \"calendar_not_connected\",\n \"calendar_disconnected\",\n \"booking_not_found\",\n \"outside_booking_window\",\n \"rate_limited\",\n \"google_error\",\n] as const;\nexport type CalendarErrorCode = (typeof CALENDAR_ERROR_CODES)[number];\n\nexport type BookingStatus = (typeof BOOKING_STATUSES)[number];\nexport type GoogleSyncState = (typeof GOOGLE_SYNC_STATES)[number];\nexport type CalendarSyncStatus = (typeof CALENDAR_SYNC_STATUSES)[number];\nexport type JsonScalar = string | number | boolean | null;\n\n/**\n * Does the runtime actually know this zone?\n *\n * `Intl.DateTimeFormat` rather than `Intl.supportedValuesOf(\"timeZone\")`: the\n * latter returns canonical zones only and omits live aliases (`Asia/Calcutta`,\n * `US/Pacific`, `Europe/Kiev`) that the API's `ZoneInfo` accepts. Validating\n * against it would reject values the server stores happily.\n */\nfunction isKnownTimeZone(timeZone: string): boolean {\n try {\n new Intl.DateTimeFormat(undefined, { timeZone });\n return true;\n } catch {\n return false;\n }\n}\n\n// The refinement is invisible to `zod-to-json-schema` — unrepresentable\n// constraints are dropped — so `events.schema.json` and the drift test against\n// the Pydantic mirror are unaffected while both sides gain the real check.\nconst ianaTz = z\n .string()\n .check(\n z.minLength(1),\n z.maxLength(64),\n z.regex(/^[A-Za-z0-9_+\\-/]+$/),\n z.refine(isKnownTimeZone, { error: \"unknown IANA time zone\" }),\n );\n\nexport const bookingAttendeeSchema = zBookingAttendee;\nexport const bookingReminderSchema = zBookingReminder;\nexport const bookingCustomerSchema = zBookingCustomerInput;\n// `custom`'s value union: the generated form is `union([...]).nullable()`,\n// which nests one `anyOf` inside another where Pydantic emits a single flat\n// list. Structure is what the drift snapshot compares, so flatten it.\nexport const bookingMetadataSchema = z.extend(zBookingMetadata, {\n custom: z.nullish(\n z.record(z.string(), z.union([z.string(), z.int(), z.number(), z.boolean(), z.null()])),\n ),\n});\n\n// A plain `z.union`, matching the spec: a discriminated union would emit\n// `oneOf` + discriminator metadata where Pydantic emits `anyOf`, failing the\n// drift snapshot.\nexport const bookingLocationSchema = z.union([\n zMeetLocation,\n zPhysicalLocation,\n zPhoneLocation,\n zCustomLocation,\n]);\n\n/**\n * The nullable form, flattened.\n *\n * `bookingLocationSchema.nullable()` would nest one `anyOf` inside another,\n * while Pydantic's `BookingLocation | None` flattens to a single list — the\n * drift snapshot compares structure, so both sides emit the flat shape.\n */\nexport const bookingLocationNullableSchema = z.union([\n ...bookingLocationSchema.def.options,\n z.null(),\n]);\n\n// Mini reports issues by pushing onto the payload rather than via a\n// `RefinementCtx` helper; `path` still scopes the message to a field.\ntype MiniCheckCtx = { value: unknown; issues: Array<Record<string, unknown>> };\n\nfunction addIssue(ctx: MiniCheckCtx, path: string[], message: string): void {\n ctx.issues.push({ code: \"custom\", input: ctx.value, path, message });\n}\n\nfunction refineEndXor(\n value: { start_at: string; end_at?: string | null; duration_minutes?: number | null },\n ctx: MiniCheckCtx,\n): void {\n const hasEnd = value.end_at !== undefined && value.end_at !== null;\n const hasDuration = value.duration_minutes !== undefined && value.duration_minutes !== null;\n if (hasEnd === hasDuration) {\n addIssue(ctx, [\"end_at\"], \"provide exactly one of end_at or duration_minutes\");\n return;\n }\n if (hasEnd && new Date(value.end_at as string) <= new Date(value.start_at)) {\n addIssue(ctx, [\"end_at\"], \"end_at must be after start_at\");\n }\n}\n\n// `attendees` and `reminders.overrides` have real (mutable) defaults\n// server-side, but Pydantic doesn't serialize a `default_factory=list` into\n// the JSON Schema `default` keyword, so they're restored here.\nexport const bookingRemindersSchema = z.extend(zBookingReminders, {\n overrides: z._default(\n z.array(bookingReminderSchema).check(z.maxLength(MAX_REMINDER_OVERRIDES)),\n [],\n ),\n});\n\nexport const bookingCreateSchema = z\n .extend(zBookingCreateSchema, {\n time_zone: ianaTz,\n // The generated field is `z.union([...variants]).nullish()`, which nests one\n // `anyOf` inside another; Pydantic's `BookingLocation | None` flattens to a\n // single list and the drift snapshot compares structure. Override with the\n // flattened union.\n location: z.optional(bookingLocationNullableSchema),\n attendees: z._default(z.array(bookingAttendeeSchema).check(z.maxLength(MAX_ATTENDEES)), []),\n // These reference the un-composed generated schemas; generation can't nest\n // our compositions into its own output.\n reminders: z.nullish(bookingRemindersSchema),\n metadata: z.nullish(bookingMetadataSchema),\n })\n .check((ctx) => refineEndXor(ctx.value as never, ctx));\n\nexport const bookingRescheduleSchema = z\n .extend(zBookingRescheduleSchema, { time_zone: ianaTz })\n .check((ctx) => refineEndXor(ctx.value as never, ctx));\n\nexport const bookingCancelSchema = zBookingCancelSchema;\n\n// `z.input` so Zod defaults stay optional for callers.\nexport type BookingCreateInput = z.input<typeof bookingCreateSchema>;\nexport type BookingRescheduleInput = z.input<typeof bookingRescheduleSchema>;\nexport type BookingCancelInput = z.input<typeof bookingCancelSchema>;\nexport type BookingAttendee = z.input<typeof bookingAttendeeSchema>;\nexport type BookingReminders = z.input<typeof bookingRemindersSchema>;\nexport type BookingLocation = z.input<typeof bookingLocationSchema>;\nexport type BookingCustomer = z.input<typeof bookingCustomerSchema>;\nexport type BookingMetadata<\n TCustom extends Record<string, JsonScalar> = Record<string, JsonScalar>,\n> = Omit<z.input<typeof bookingMetadataSchema>, \"custom\"> & { custom?: TCustom | null };\n\n// ---------------------------------------------------------------------------\n// Read shapes — server responses, trusted and therefore not re-validated here.\n// ---------------------------------------------------------------------------\n\nexport interface CalendarSummary {\n calendar_key: string;\n label: string;\n time_zone: string;\n status: CalendarSyncStatus;\n /** Secret-key only. A public key gets `null`: whose Google account is behind\n * a calendar is a third party's identity, not the client site's business. */\n google_account_email?: string | null;\n granted_scopes?: string[] | null;\n connected_at?: string | null;\n}\n\nexport interface BusyInterval {\n start_at: string;\n end_at: string;\n source: \"google\" | \"booking\";\n}\n\nexport interface BusyResponse {\n calendar_key: string;\n time_zone: string;\n window: { since: string; until: string };\n busy: BusyInterval[];\n generated_at: string;\n}\n\nexport interface Booking {\n id: string;\n calendar_key: string;\n status: BookingStatus;\n google_sync_state: GoogleSyncState;\n start_at: string;\n end_at: string;\n time_zone: string;\n appointment_type: string | null;\n title: string;\n customer: BookingCustomer;\n google_html_link: string | null;\n meet_url: string | null;\n /** Returned only on create — the capability credential for manage links. */\n manage_token?: string | null;\n}\n","// This file is auto-generated by @hey-api/openapi-ts\n\nimport * as z from \"zod/mini\";\n\n/**\n * BookingAttendee\n */\nexport const zBookingAttendee = z.strictObject({\n display_name: z.nullish(z.string().check(z.maxLength(200))),\n email: z.email().check(z.maxLength(320)),\n optional: z._default(z.optional(z.boolean()), false),\n});\n\n/**\n * BookingCancelSchema\n */\nexport const zBookingCancelSchema = z.strictObject({\n notify_attendees: z._default(z.optional(z.boolean()), true),\n reason: z.nullish(z.string().check(z.maxLength(500))),\n});\n\n/**\n * BookingCustomer\n */\nexport const zBookingCustomerInput = z.strictObject({\n email: z.email().check(z.maxLength(320)),\n name: z.nullish(z.string().check(z.maxLength(200))),\n phone: z.nullish(z.string().check(z.maxLength(64))),\n sms_consent: z._default(z.optional(z.boolean()), false),\n});\n\n/**\n * BookingCustomer\n */\nexport const zBookingCustomerOutput = z.strictObject({\n email: z.email().check(z.maxLength(320)),\n name: z.nullable(z.string().check(z.maxLength(200))),\n phone: z.nullable(z.string().check(z.maxLength(64))),\n sms_consent: z._default(z.boolean(), false),\n});\n\n/**\n * BookingRescheduleSchema\n */\nexport const zBookingRescheduleSchema = z.strictObject({\n duration_minutes: z.nullish(z.int().check(z.gt(0), z.lte(1440))),\n end_at: z.nullish(z.iso.datetime({ offset: true })),\n notify_attendees: z._default(z.optional(z.boolean()), true),\n reason: z.nullish(z.string().check(z.maxLength(500))),\n start_at: z.iso.datetime({ offset: true }),\n time_zone: z.string().check(z.minLength(1), z.maxLength(64), z.regex(/^[A-Za-z0-9_+\\-\\/]+$/)),\n});\n\n/**\n * BookingResponse\n *\n * A created booking. `manage_token` is present only on the create response.\n */\nexport const zBookingResponse = z.strictObject({\n appointment_type: z.nullable(z.string()),\n calendar_key: z.string(),\n customer: zBookingCustomerOutput,\n end_at: z.iso.datetime({ offset: true }),\n google_html_link: z.nullable(z.string()),\n google_sync_state: z.string(),\n id: z.string(),\n manage_token: z.nullable(z.string()),\n meet_url: z.nullable(z.string()),\n start_at: z.iso.datetime({ offset: true }),\n status: z.string(),\n time_zone: z.string(),\n title: z.string(),\n});\n\n/**\n * BookingSource\n */\nexport const zBookingSource = z.enum([\"website\", \"phone\", \"walk_in\", \"import\", \"operator\"]);\n\n/**\n * BookingMetadata\n *\n * Structured booking metadata — named attribution keys plus a flat escape hatch.\n *\n * `custom` is flat and scalar-only on purpose: it mirrors trivially in Zod,\n * stays JSONB-queryable, and bounds what a caller can push. Only `custom` is\n * ever mirrored to Google's `extendedProperties`; the named attribution keys\n * never leave our database.\n */\nexport const zBookingMetadata = z.strictObject({\n custom: z.nullish(\n z.record(z.string(), z.nullable(z.union([z.string(), z.int(), z.number(), z.boolean()]))),\n ),\n fbclid: z.nullish(z.string().check(z.maxLength(256))),\n gclid: z.nullish(z.string().check(z.maxLength(256))),\n landing_page: z.nullish(z.string().check(z.maxLength(2048))),\n notes: z.nullish(z.string().check(z.maxLength(2000))),\n referrer: z.nullish(z.string().check(z.maxLength(2048))),\n schema_version: z._default(z.optional(z.int()), 1),\n service: z.nullish(z.string().check(z.maxLength(64))),\n session_id: z.nullish(z.string().check(z.maxLength(64))),\n source: z.nullish(zBookingSource),\n utm_campaign: z.nullish(z.string().check(z.maxLength(128))),\n utm_content: z.nullish(z.string().check(z.maxLength(128))),\n utm_medium: z.nullish(z.string().check(z.maxLength(128))),\n utm_source: z.nullish(z.string().check(z.maxLength(128))),\n utm_term: z.nullish(z.string().check(z.maxLength(128))),\n visitor_id: z.nullish(z.string().check(z.maxLength(64))),\n});\n\n/**\n * CustomLocation\n */\nexport const zCustomLocation = z.strictObject({\n kind: z.literal(\"custom\"),\n text: z.string().check(z.maxLength(1024)),\n});\n\n/**\n * ErrorCode\n *\n * Stable discriminator for a failed request. Values are contract: never rename.\n */\nexport const zErrorCode = z.enum([\n \"missing_credentials\",\n \"invalid_api_key\",\n \"not_authenticated\",\n \"wrong_key_type\",\n \"origin_missing\",\n \"origin_not_allowed\",\n \"capability_denied\",\n \"session_required\",\n \"operator_only\",\n \"cross_tenant\",\n \"business_id_required\",\n \"bad_request\",\n \"not_found\",\n \"conflict\",\n \"incompatible_payload\",\n \"repeat_confirmation_required\",\n \"idempotency_conflict\",\n \"validation_error\",\n \"unknown_service\",\n \"invalid_sale_properties\",\n \"invalid_cursor\",\n \"rate_limited\",\n \"internal_error\",\n]);\n\n/**\n * MeetLocation\n */\nexport const zMeetLocation = z.strictObject({\n kind: z.literal(\"google_meet\"),\n});\n\n/**\n * PhoneLocation\n */\nexport const zPhoneLocation = z.strictObject({\n kind: z.literal(\"phone\"),\n phone: z.string().check(z.maxLength(64)),\n});\n\n/**\n * PhysicalLocation\n */\nexport const zPhysicalLocation = z.strictObject({\n address: z.string().check(z.maxLength(1024)),\n kind: z.literal(\"physical\"),\n});\n\n/**\n * ReminderMethod\n */\nexport const zReminderMethod = z.enum([\"email\", \"popup\"]);\n\n/**\n * BookingReminder\n */\nexport const zBookingReminder = z.strictObject({\n method: zReminderMethod,\n minutes_before: z.int().check(z.gte(0), z.lte(40320)),\n});\n\n/**\n * BookingReminders\n */\nexport const zBookingReminders = z.strictObject({\n overrides: z.optional(z.array(zBookingReminder).check(z.maxLength(5))),\n use_calendar_defaults: z._default(z.optional(z.boolean()), true),\n});\n\n/**\n * BookingCreateSchema\n */\nexport const zBookingCreateSchema = z.strictObject({\n appointment_type: z.nullish(z.string().check(z.maxLength(64))),\n attendees: z.optional(z.array(zBookingAttendee).check(z.maxLength(20))),\n buffer_after_minutes: z._default(z.optional(z.int().check(z.gte(0), z.lte(1440))), 0),\n buffer_before_minutes: z._default(z.optional(z.int().check(z.gte(0), z.lte(1440))), 0),\n calendar_key: z.string().check(z.minLength(1), z.maxLength(64)),\n customer: zBookingCustomerInput,\n description: z.nullish(z.string().check(z.maxLength(8000))),\n duration_minutes: z.nullish(z.int().check(z.gt(0), z.lte(1440))),\n end_at: z.nullish(z.iso.datetime({ offset: true })),\n idempotency_key: z.nullish(z.string().check(z.maxLength(128))),\n location: z.nullish(z.union([zMeetLocation, zPhysicalLocation, zPhoneLocation, zCustomLocation])),\n metadata: z.nullish(zBookingMetadata),\n reminders: z.nullish(zBookingReminders),\n send_invites: z._default(z.optional(z.boolean()), true),\n start_at: z.iso.datetime({ offset: true }),\n time_zone: z.string().check(z.minLength(1), z.maxLength(64), z.regex(/^[A-Za-z0-9_+\\-\\/]+$/)),\n title: z.nullish(z.string().check(z.maxLength(300))),\n});\n\n/**\n * ValidationError\n */\nexport const zValidationError = z.object({\n ctx: z.optional(z.record(z.string(), z.unknown())),\n input: z.optional(z.unknown()),\n loc: z.array(z.union([z.string(), z.int()])),\n msg: z.string(),\n type: z.string(),\n});\n\n/**\n * HTTPValidationError\n */\nexport const zHttpValidationError = z.object({\n detail: z.optional(z.array(zValidationError)),\n});\n\n/**\n * ValidationErrorItem\n *\n * One field-level failure from FastAPI's request validation.\n */\nexport const zValidationErrorItem = z.object({\n loc: z.array(z.union([z.string(), z.int()])),\n msg: z.string(),\n type: z.string(),\n});\n\n/**\n * ApiErrorSchema\n *\n * The body of every non-2xx response.\n */\nexport const zApiErrorSchema = z.object({\n code: zErrorCode,\n context: z._default(z.nullish(z.record(z.string(), z.unknown())), null),\n detail: z.string(),\n errors: z._default(z.nullish(z.array(zValidationErrorItem)), null),\n request_id: z._default(z.nullish(z.string()), null),\n});\n","import type { BusyInterval } from \"./schema\";\nimport { instantFromWallClock, offsetMinutesAt } from \"./time\";\n\n/**\n * Slot computation lives in the client site, not in our database.\n *\n * Opening hours, slot length, notice and buffers differ per client and per use\n * case; modelling them as dashboard config would mean every new client needs\n * data entry before their site works. The API answers only \"when is this person\n * busy\" — this pure helper turns that plus the site's own hours into offerable\n * slots.\n */\n\nexport interface OpeningInterval {\n /** Local wall-clock `HH:MM` in the calendar's zone. */\n start: string;\n end: string;\n}\n\nexport interface OpeningHours {\n /** 0 = Sunday … 6 = Saturday. A missing weekday is closed. */\n weekly: Partial<Record<number, OpeningInterval[]>>;\n /** `YYYY-MM-DD` → intervals, replacing that day's weekly rule entirely. */\n overrides?: Record<string, OpeningInterval[]>;\n /** `YYYY-MM-DD` dates that are closed regardless of the weekly rule. */\n closedDates?: string[];\n}\n\nexport interface ComputeSlotsOptions {\n busy: BusyInterval[];\n openingHours: OpeningHours;\n timeZone: string;\n durationMinutes: number;\n granularityMinutes?: number;\n minNoticeMinutes?: number;\n bufferBeforeMinutes?: number;\n bufferAfterMinutes?: number;\n windowDays?: number;\n now?: Date;\n}\n\nexport interface Slot {\n start_at: string;\n end_at: string;\n}\n\nexport interface ComputeSlotsResult {\n slots: Slot[];\n nextAvailableAt: string | null;\n}\n\nfunction parseHhMm(value: string): [number, number] {\n const [hour, minute] = value.split(\":\").map(Number);\n return [hour || 0, minute || 0];\n}\n\nfunction dateKeyOf(date: Date, timeZone: string): string {\n const fmt = new Intl.DateTimeFormat(\"en-CA\", {\n timeZone,\n year: \"numeric\",\n month: \"2-digit\",\n day: \"2-digit\",\n });\n return fmt.format(date);\n}\n\n/** Weekday of a `YYYY-MM-DD` local date, read at noon UTC so no offset can\n * shift it across a day boundary. */\nfunction weekdayFromDateKey(dateKey: string): number {\n const [year, month, day] = dateKey.split(\"-\").map(Number);\n return new Date(Date.UTC(year, month - 1, day, 12)).getUTCDay();\n}\n\n/** Midday of the local date containing `at` — a stable anchor to step from,\n * immune to a midnight DST jump. */\nfunction startOfLocalDay(at: Date, timeZone: string): Date {\n const [year, month, day] = dateKeyOf(at, timeZone).split(\"-\").map(Number);\n return instantFromWallClock(timeZone, year, month, day, 12, 0);\n}\n\nfunction overlaps(aStart: number, aEnd: number, bStart: number, bEnd: number): boolean {\n return aStart < bEnd && aEnd > bStart;\n}\n\nexport function computeSlots(options: ComputeSlotsOptions): ComputeSlotsResult {\n const {\n busy,\n openingHours,\n timeZone,\n durationMinutes,\n granularityMinutes = 15,\n minNoticeMinutes = 0,\n bufferBeforeMinutes = 0,\n bufferAfterMinutes = 0,\n windowDays = 30,\n now = new Date(),\n } = options;\n\n const earliest = now.getTime() + minNoticeMinutes * 60_000;\n const busyRanges = busy.map((interval) => [\n new Date(interval.start_at).getTime(),\n new Date(interval.end_at).getTime(),\n ]);\n\n const slots: Slot[] = [];\n // Advance by CALENDAR days, not by 86_400_000 ms. A DST day is 23 or 25 hours\n // long, so fixed-millisecond stepping skips a local date entirely at spring\n // forward and visits one twice at fall back — in America/Toronto, adding 24h\n // from 2026-03-08T04:30Z jumps straight from the 7th to the 9th.\n let cursor = startOfLocalDay(now, timeZone);\n for (let dayOffset = 0; dayOffset < windowDays; dayOffset += 1) {\n const dateKey = dateKeyOf(cursor, timeZone);\n const [year, month, day] = dateKey.split(\"-\").map(Number);\n // `Date.UTC` normalises day 32 into the next month, so this needs no\n // month-length handling.\n cursor = instantFromWallClock(timeZone, year, month, day + 1, 12, 0);\n\n if (openingHours.closedDates?.includes(dateKey)) continue;\n\n const intervals =\n openingHours.overrides?.[dateKey] ?? openingHours.weekly[weekdayFromDateKey(dateKey)] ?? [];\n for (const interval of intervals) {\n const [openHour, openMinute] = parseHhMm(interval.start);\n const [closeHour, closeMinute] = parseHhMm(interval.end);\n const opensAt = instantFromWallClock(timeZone, year, month, day, openHour, openMinute);\n const closesAt = instantFromWallClock(timeZone, year, month, day, closeHour, closeMinute);\n\n for (\n let start = opensAt.getTime();\n start + durationMinutes * 60_000 <= closesAt.getTime();\n start += granularityMinutes * 60_000\n ) {\n const end = start + durationMinutes * 60_000;\n if (start < earliest) continue;\n\n // A slot is unavailable if its BUFFERED span touches anything busy —\n // the buffers are what keep a gap between appointments.\n const blockedStart = start - bufferBeforeMinutes * 60_000;\n const blockedEnd = end + bufferAfterMinutes * 60_000;\n const taken = busyRanges.some(([busyStart, busyEnd]) =>\n overlaps(blockedStart, blockedEnd, busyStart, busyEnd),\n );\n if (taken) continue;\n\n slots.push({\n start_at: new Date(start).toISOString(),\n end_at: new Date(end).toISOString(),\n });\n }\n }\n }\n\n slots.sort((a, b) => a.start_at.localeCompare(b.start_at));\n return { slots, nextAvailableAt: slots[0]?.start_at ?? null };\n}\n\n/** Group slots by local date, the shape every date-picker UI wants. */\nexport function groupSlotsByDay(\n slots: Slot[],\n timeZone: string,\n): { date: string; slots: Slot[] }[] {\n const byDay = new Map<string, Slot[]>();\n for (const slot of slots) {\n const key = dateKeyOf(new Date(slot.start_at), timeZone);\n const bucket = byDay.get(key);\n if (bucket) bucket.push(slot);\n else byDay.set(key, [slot]);\n }\n return [...byDay.entries()]\n .map(([date, daySlots]) => ({ date, slots: daySlots }))\n .sort((a, b) => a.date.localeCompare(b.date));\n}\n\nexport { offsetMinutesAt };\n"],"mappings":"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","names":["record","iso","iso","z","durationMinutes"]}
1
+ {"version":3,"sources":["../../tracking-core/src/tracking.ts","../../tracking-core/src/session.ts","../../tracking-core/src/resources/calendar/attribution.ts","../../tracking-core/src/capabilities.ts","../../tracking-core/src/resources/http/errors.ts","../../tracking-core/src/ingest.ts","../../tracking-core/src/resources/http/request.ts","../../tracking-core/src/resources/calendar/transport.ts","../../tracking-core/src/resources/intl/date.ts","../../tracking-core/src/resources/calendar/time.ts","../../tracking-core/src/resources/calendar/read-client.ts","../../tracking-core/src/resources/calendar/schema.ts","../../tracking-core/src/resources/calendar/generated/zod.gen.ts","../../tracking-core/src/resources/calendar/slots.ts"],"sourcesContent":["import type { TrackingParams } from \"./types\";\n\n/**\n * Default attribution cookie lifetime: 90 days.\n */\nexport const TRACKING_COOKIE_MAX_AGE_SECONDS = 7_776_000;\n\n/**\n * Attribution query/cookie keys captured by the SDK.\n */\nexport const TRACKING_PARAM_KEYS = [\n \"gclid\",\n // Google's iOS/Safari replacement click IDs — issued when privacy features\n // withhold gclid (wbraid: web-to-web, gbraid: app-to-web). First-class\n // citizens: captured, persisted, and attributed exactly like gclid.\n \"wbraid\",\n \"gbraid\",\n // Yelp Ads click id (40 hex chars), appended to ad-click URLs.\n \"ylpcid\",\n \"fbclid\",\n \"utm_source\",\n \"utm_medium\",\n \"utm_campaign\",\n \"utm_term\",\n \"utm_content\",\n] as const;\n\nexport type TrackingParamKey = (typeof TRACKING_PARAM_KEYS)[number];\n\n/**\n * Create an all-null attribution parameter object.\n */\nexport function createEmptyTrackingParams(): TrackingParams {\n return {\n gclid: null,\n wbraid: null,\n gbraid: null,\n ylpcid: null,\n fbclid: null,\n utm_source: null,\n utm_medium: null,\n utm_campaign: null,\n utm_term: null,\n utm_content: null,\n };\n}\n\n/**\n * Normalize a raw cookie value into a tracking value.\n *\n * Empty strings and non-string values become `null`.\n */\nexport function normalizeTrackingCookieValue(value: unknown): string | null {\n return typeof value === \"string\" && value.length > 0 ? value : null;\n}\n\n/**\n * Read all tracking params using the provided cookie reader.\n *\n * This is shared by browser, React, and Next server integrations.\n */\nexport function getTrackingParamsFromCookieReader(\n readCookie: (key: TrackingParamKey) => unknown,\n): TrackingParams {\n return TRACKING_PARAM_KEYS.reduce<TrackingParams>((params, key) => {\n params[key] = normalizeTrackingCookieValue(readCookie(key));\n return params;\n }, createEmptyTrackingParams());\n}\n\n/**\n * Extract tracking params from URL search params.\n *\n * Only non-empty values are returned.\n */\nexport function getTrackingQueryValues(\n searchParams: Pick<URLSearchParams, \"get\">,\n): Partial<Record<TrackingParamKey, string>> {\n return TRACKING_PARAM_KEYS.reduce<Partial<Record<TrackingParamKey, string>>>((params, key) => {\n const value = searchParams.get(key);\n\n if (typeof value === \"string\" && value.trim().length > 0) {\n params[key] = value;\n }\n\n return params;\n }, {});\n}\n\n/**\n * localStorage fallback prefix. Meta/Facebook in-app browsers (Instagram / FB\n * webviews) frequently block `document.cookie`; when a cookie write can't be\n * confirmed we mirror the value here so attribution (and `_fbc`) survives the\n * session and subsequent in-webview reloads instead of being silently lost.\n */\nconst FALLBACK_STORAGE_PREFIX = \"_aranova_track_\";\n\nfunction fallbackKey(name: string): string {\n return `${FALLBACK_STORAGE_PREFIX}${name}`;\n}\n\n/**\n * Persist a cookie value AND mirror it to localStorage. Generic over the cookie\n * name so it serves the attribution params AND the Meta `_fbc` cookie. Never\n * throws.\n *\n * The localStorage mirror is written UNCONDITIONALLY — it is NOT gated on a\n * cookie read-back. Meta/Instagram in-app webviews partition cookies: the write\n * is visible to a same-page read (so a verify-after-write would falsely pass)\n * yet is silently dropped on the next navigation/reload. Gating the fallback on\n * that read-back would leave the value unrecoverable in exactly the case it\n * exists for; a stale same-name cookie from a prior visit would also mask a\n * blocked write. So we always write both. {@link readCookieValue} prefers the\n * cookie when present, which keeps the mirror harmless when cookies work and\n * load-bearing when they don't survive navigation.\n */\nexport function persistCookieValue(\n name: string,\n value: string,\n maxAgeSeconds = TRACKING_COOKIE_MAX_AGE_SECONDS,\n): void {\n const encoded = encodeURIComponent(value);\n if (typeof document !== \"undefined\") {\n try {\n document.cookie = `${name}=${encoded}; Max-Age=${maxAgeSeconds}; Path=/; SameSite=Lax`;\n } catch {\n // cookie blocked — the localStorage mirror below is the fallback\n }\n }\n if (typeof window !== \"undefined\") {\n try {\n window.localStorage.setItem(fallbackKey(name), encoded);\n } catch {\n // both blocked — give up silently, never break the host site\n }\n }\n}\n\n/**\n * Read a cookie value, falling back to the localStorage mirror written by\n * {@link persistCookieValue} when the cookie is absent (blocked webview).\n */\nexport function readCookieValue(name: string): string | null {\n if (typeof document !== \"undefined\") {\n const cookies = document.cookie ? document.cookie.split(\"; \") : [];\n const match = cookies.find((cookie) => cookie.startsWith(`${name}=`));\n if (match) {\n const [, rawValue = \"\"] = match.split(\"=\");\n return normalizeTrackingCookieValue(decodeURIComponent(rawValue));\n }\n }\n if (typeof window !== \"undefined\") {\n try {\n const stored = window.localStorage.getItem(fallbackKey(name));\n if (stored) return normalizeTrackingCookieValue(decodeURIComponent(stored));\n } catch {\n // ignore\n }\n }\n return null;\n}\n\n/**\n * Read a tracking cookie from `document.cookie` (or the localStorage fallback).\n *\n * Returns `null` during SSR or when neither source has the value.\n */\nexport function getCookieValueFromDocument(key: TrackingParamKey): string | null {\n return readCookieValue(key);\n}\n\n/**\n * Persist one attribution value as a first-party cookie (localStorage fallback\n * when cookies are blocked).\n */\nexport function setTrackingCookie(\n key: TrackingParamKey,\n value: string,\n maxAgeSeconds = TRACKING_COOKIE_MAX_AGE_SECONDS,\n): void {\n persistCookieValue(key, value, maxAgeSeconds);\n}\n\n/**\n * Merge two attribution param sets, preferring `primary`'s non-null values and\n * filling the gaps from `fallback`. Used to combine the cookie-backed read with\n * the synchronous init-time snapshot so a value captured before an SPA router\n * stripped the URL (or before a webview blocked the cookie) still wins.\n */\nexport function mergeTrackingParams(\n primary: TrackingParams,\n fallback: TrackingParams,\n): TrackingParams {\n return TRACKING_PARAM_KEYS.reduce<TrackingParams>((merged, key) => {\n merged[key] = primary[key] ?? fallback[key];\n return merged;\n }, createEmptyTrackingParams());\n}\n\n/**\n * Capture any tracking params present in a URL search parameter source and\n * persist them to first-party cookies.\n *\n * Returns the subset of params that were found and persisted.\n */\nexport function persistTrackingParamsFromSearchParams(\n searchParams: Pick<URLSearchParams, \"get\">,\n maxAgeSeconds = TRACKING_COOKIE_MAX_AGE_SECONDS,\n): Partial<Record<TrackingParamKey, string>> {\n const trackingValues = getTrackingQueryValues(searchParams);\n\n Object.entries(trackingValues).forEach(([key, value]) => {\n setTrackingCookie(key as TrackingParamKey, value, maxAgeSeconds);\n });\n\n return trackingValues;\n}\n\n/**\n * Capture tracking params from a URL, persist them to first-party cookies, and\n * return the current cookie-backed attribution state.\n *\n * Defaults to `window.location.href` in the browser.\n */\nexport function captureTrackingParamsFromLocation(\n url = typeof window === \"undefined\" ? \"\" : window.location.href,\n maxAgeSeconds = TRACKING_COOKIE_MAX_AGE_SECONDS,\n): TrackingParams {\n const resolvedUrl =\n typeof window === \"undefined\"\n ? new URL(url || \"https://example.invalid\")\n : new URL(url, window.location.origin);\n persistTrackingParamsFromSearchParams(resolvedUrl.searchParams, maxAgeSeconds);\n return getTrackingParamsFromCookieReader(getCookieValueFromDocument);\n}\n","// Visitor + session identity for the tracking SDK.\n//\n// Visitor: persistent localStorage UUID, never expires until the user clears\n// browser storage. Used for cross-session correlation.\n//\n// Session: rolling 30-minute idle window. Regenerated when more than\n// SESSION_IDLE_MS has passed since the last event. Matches the behavior of\n// GA4, PostHog, Mixpanel, etc., so analytics is comparable.\n\nimport { clearLandingRecord } from \"./landing\";\n\n/**\n * localStorage key for the persistent visitor id.\n */\nexport const VISITOR_STORAGE_KEY = \"aranova_tracking_visitor\";\n\n/**\n * localStorage key for the rolling session id state.\n */\nexport const SESSION_STORAGE_KEY = \"aranova_tracking_session\";\n\n/**\n * Idle window before a new session id is created.\n */\nexport const SESSION_IDLE_MS = 30 * 60 * 1000;\n\n/**\n * Serialized session state stored in localStorage.\n */\nexport interface StoredSession {\n /** Client-generated session UUID. */\n id: string;\n /** Unix timestamp in milliseconds for the most recent event/session touch. */\n last_event_at: number;\n}\n\nfunction safeUuid(): string {\n if (typeof crypto !== \"undefined\" && typeof crypto.randomUUID === \"function\")\n return crypto.randomUUID();\n // Fallback for ancient browsers — not cryptographically perfect but unique enough.\n return `${Date.now().toString(16)}-${Math.random().toString(16).slice(2)}-${Math.random().toString(16).slice(2)}`;\n}\n\nfunction readLocalStorage(key: string): string | null {\n try {\n return window.localStorage.getItem(key);\n } catch {\n return null;\n }\n}\n\nfunction writeLocalStorage(key: string, value: string): void {\n try {\n window.localStorage.setItem(key, value);\n } catch {\n // Storage may be denied (private mode, blocked cookies, quota). Caller is\n // responsible for degrading gracefully.\n }\n}\n\n/**\n * Return the persistent visitor id for this browser profile.\n *\n * Creates and stores a new id when one does not already exist. During SSR,\n * returns an ephemeral id because browser storage is unavailable.\n */\nexport function getVisitorId(): string {\n if (typeof window === \"undefined\") return safeUuid();\n\n const existing = readLocalStorage(VISITOR_STORAGE_KEY);\n if (existing && existing.length > 0) return existing;\n\n const fresh = safeUuid();\n writeLocalStorage(VISITOR_STORAGE_KEY, fresh);\n return fresh;\n}\n\n/** Adopt `visitorId` only when this browser has none — an existing id is never orphaned. */\nexport function adoptVisitorId(visitorId: string): boolean {\n if (typeof window === \"undefined\" || visitorId.length === 0) return false;\n const existing = readLocalStorage(VISITOR_STORAGE_KEY);\n if (existing && existing.length > 0) return false;\n writeLocalStorage(VISITOR_STORAGE_KEY, visitorId);\n return readLocalStorage(VISITOR_STORAGE_KEY) === visitorId;\n}\n\n/**\n * Result from `getOrRotateSessionId()`.\n */\nexport interface SessionIdResult {\n /** Current session id. */\n id: string;\n /** Whether this call created a new session. */\n isNew: boolean;\n}\n\n/**\n * Return the current session id, rotating it after the idle window expires.\n *\n * Also refreshes `last_event_at` for active sessions.\n */\nexport function getOrRotateSessionId(now: number = Date.now()): SessionIdResult {\n if (typeof window === \"undefined\") return { id: safeUuid(), isNew: true };\n\n const raw = readLocalStorage(SESSION_STORAGE_KEY);\n if (raw) {\n try {\n const parsed = JSON.parse(raw) as Partial<StoredSession>;\n if (typeof parsed.id === \"string\" && typeof parsed.last_event_at === \"number\") {\n if (now - parsed.last_event_at <= SESSION_IDLE_MS) {\n const refreshed: StoredSession = { id: parsed.id, last_event_at: now };\n writeLocalStorage(SESSION_STORAGE_KEY, JSON.stringify(refreshed));\n return { id: parsed.id, isNew: false };\n }\n }\n } catch {\n // Fall through to a fresh session.\n }\n }\n\n const fresh: StoredSession = { id: safeUuid(), last_event_at: now };\n writeLocalStorage(SESSION_STORAGE_KEY, JSON.stringify(fresh));\n return { id: fresh.id, isNew: true };\n}\n\n/** This browser's visitor id, read without creating one; `null` before tracking has run. */\nexport function peekVisitorId(): string | null {\n if (typeof window === \"undefined\") return null;\n const existing = readLocalStorage(VISITOR_STORAGE_KEY);\n return existing && existing.length > 0 ? existing : null;\n}\n\n/**\n * The live session id, read without refreshing or rotating it; `null` once the idle window\n * has lapsed, since the next event would start a new session anyway.\n */\nexport function peekSessionId(now: number = Date.now()): string | null {\n if (typeof window === \"undefined\") return null;\n const raw = readLocalStorage(SESSION_STORAGE_KEY);\n if (!raw) return null;\n try {\n const parsed = JSON.parse(raw) as Partial<StoredSession>;\n if (typeof parsed.id !== \"string\" || typeof parsed.last_event_at !== \"number\") return null;\n return now - parsed.last_event_at <= SESSION_IDLE_MS ? parsed.id : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Clear visitor and session identity from localStorage, including the\n * session-scoped landing record (a landing must never outlive its session).\n *\n * Intended for tests, debugging, and explicit user reset flows.\n */\nexport function resetTrackingIdentity(): void {\n clearLandingRecord();\n if (typeof window === \"undefined\") return;\n try {\n window.localStorage.removeItem(VISITOR_STORAGE_KEY);\n window.localStorage.removeItem(SESSION_STORAGE_KEY);\n } catch {\n // ignored\n }\n}\n","import { peekSessionId, peekVisitorId } from \"../../session\";\nimport { getCookieValueFromDocument, getTrackingParamsFromCookieReader } from \"../../tracking\";\nimport type { BookingMetadata } from \"./schema\";\n\n/** The metadata keys a browser can fill: which visit made the booking, and how it arrived. */\nexport type BookingAttribution = Pick<\n BookingMetadata,\n | \"session_id\"\n | \"visitor_id\"\n | \"gclid\"\n | \"fbclid\"\n | \"utm_source\"\n | \"utm_medium\"\n | \"utm_campaign\"\n | \"utm_term\"\n | \"utm_content\"\n>;\n\n// The API's per-key limits; a longer cookie value would 422 the whole booking. Zod Mini exposes\n// no public max-length getter, so booking-attribution.test.tsx pins these to the generated schema.\nconst LIMITS = {\n session_id: 64,\n visitor_id: 64,\n gclid: 256,\n fbclid: 256,\n utm_source: 128,\n utm_medium: 128,\n utm_campaign: 128,\n utm_term: 128,\n utm_content: 128,\n} as const satisfies Record<keyof BookingAttribution, number>;\n\n/**\n * This browser's visit and click attribution, for a booking's `metadata`.\n *\n * `useBookingForm` merges it in for you. A site posting to its own route (e.g. `/api/book`)\n * sends it along from the browser and passes it into `create({ metadata })`: that is what lets\n * the dashboard tie the booking to the visitor's journey. Only keys with a value are present.\n */\nexport function getBookingAttribution(): BookingAttribution {\n if (typeof window === \"undefined\") return {};\n const params = getTrackingParamsFromCookieReader(getCookieValueFromDocument);\n const values: Record<keyof BookingAttribution, string | null> = {\n session_id: peekSessionId(),\n visitor_id: peekVisitorId(),\n gclid: params.gclid,\n fbclid: params.fbclid,\n utm_source: params.utm_source,\n utm_medium: params.utm_medium,\n utm_campaign: params.utm_campaign,\n utm_term: params.utm_term,\n utm_content: params.utm_content,\n };\n const attribution: BookingAttribution = {};\n for (const key of Object.keys(LIMITS) as (keyof BookingAttribution)[]) {\n const value = values[key];\n if (value) attribution[key] = value.slice(0, LIMITS[key]);\n }\n return attribution;\n}\n","// Capability self-report registry.\n//\n// Every SDK construct registers itself here at construction time, and the\n// registered set rides out on `TrackingClientContext.capabilities`, which the\n// backend persists on the session row. The dashboard derives what a client site\n// ACTUALLY has installed from those sessions, rather than trusting an operator\n// checkbox — see contracts/tracking-capabilities.json, which pins this id list\n// against the backend enum and the dashboard type union.\n//\n// Browser-only by design: a server-side client (calendar bookings) emits no\n// session, and its artifact is better evidence than a self-report anyway.\n//\n// This module deliberately imports nothing: it is the one thing every other\n// resource can import without adding an edge that could close a cycle.\n\n/**\n * Every capability an install can report. Pinned by\n * `contracts/tracking-capabilities.json` — adding one here without a catalog\n * entry fails the drift test.\n */\nexport const TRACKING_CAPABILITIES = [\n \"ad_tags_google\",\n \"ad_tags_meta\",\n \"base_tracking\",\n \"blog_rendering\",\n \"calendar_read\",\n \"calendar_write\",\n \"consent_controls\",\n \"conversion_goals_auto\",\n \"conversion_goals_manual\",\n \"cross_site_linker\",\n \"cta_click_capture\",\n \"form_capture\",\n \"phone_click_capture\",\n \"phone_fields\",\n] as const;\n\nexport type TrackingCapability = (typeof TRACKING_CAPABILITIES)[number];\n\n/**\n * DOM attribute a server-rendered surface stamps to report itself.\n *\n * Blog rendering never calls the Aranova API — it reads the CDN directly — so\n * neither the ingest payload nor the request-header path can see it. The\n * renderer stamps this attribute instead and the browser context collector\n * picks it up on the next batch.\n */\nexport const CAPABILITY_DOM_ATTRIBUTE = \"data-aranova-capability\";\n\nconst registered = new Set<TrackingCapability>();\n\n/**\n * Record that a capability is present in this runtime.\n *\n * Idempotent and side-effect free — safe to call on every construction,\n * including React strict-mode double mounts.\n */\nexport function registerCapability(capability: TrackingCapability): void {\n registered.add(capability);\n}\n\n/**\n * Capabilities registered so far in this runtime, sorted for a stable wire\n * value (so the backend can hash the array to skip redundant writes).\n *\n * Also collects `data-aranova-capability` markers from the DOM, which is how\n * server-rendered surfaces that never call our API report themselves.\n */\nexport function getRegisteredCapabilities(): TrackingCapability[] {\n const all = new Set<TrackingCapability>(registered);\n for (const marker of readDomMarkers()) all.add(marker);\n return Array.from(all).sort();\n}\n\nfunction readDomMarkers(): TrackingCapability[] {\n if (typeof document === \"undefined\") return [];\n const known = new Set<string>(TRACKING_CAPABILITIES);\n const found: TrackingCapability[] = [];\n // Unknown values are dropped rather than forwarded: the backend rejects ids\n // outside its enum, and a stray attribute on a client's page must never be\n // able to fail an otherwise-valid ingest batch.\n for (const node of document.querySelectorAll(`[${CAPABILITY_DOM_ATTRIBUTE}]`)) {\n const value = node.getAttribute(CAPABILITY_DOM_ATTRIBUTE);\n if (value && known.has(value)) found.push(value as TrackingCapability);\n }\n return found;\n}\n\n/** Test-only: drop everything registered so far. */\nexport function resetRegisteredCapabilitiesForTests(): void {\n registered.clear();\n}\n","/**\n * Thrown on a non-2xx from any awaited Aranova API call.\n *\n * Lives here rather than inside one resource because more than one resource\n * needs it, and a duplicate class would be a genuine hazard: a name star-exported\n * from two modules is silently dropped unless both resolve to the same binding.\n */\nexport class AranovaApiError extends Error {\n readonly status: number;\n readonly code: string | undefined;\n readonly requestId: string | undefined;\n\n constructor(message: string, options: { status: number; code?: string; requestId?: string }) {\n super(message);\n this.name = \"AranovaApiError\";\n this.status = options.status;\n this.code = options.code;\n this.requestId = options.requestId;\n }\n}\n","// Tracking ingest client. Queues events, debounce-flushes them to the\n// /tracking/events endpoint, and degrades silently on errors so a broken\n// network never breaks the host site.\n\nimport { getRegisteredCapabilities } from \"./capabilities\";\nimport { getConsentChoice, getConsentState } from \"./consent\";\nimport { consumeInboundLink, stripLinkParam, type OriginLink } from \"./cross-site-link\";\nimport { resetPageViewState } from \"./page-view\";\nimport type { PhoneConfig } from \"./phone-field\";\nimport { captureFbc, getFbcCookie, getFbpCookie } from \"./fbq\";\nimport {\n captureTrackingParamsFromLocation,\n createEmptyTrackingParams,\n getCookieValueFromDocument,\n getTrackingParamsFromCookieReader,\n mergeTrackingParams,\n} from \"./tracking\";\nimport type { TriggerRegistryConfig } from \"./events/registry\";\nimport { buildHeartbeatMetadata } from \"./heartbeat\";\nimport { buildLandingPayloadFields, getOrCaptureLandingParams } from \"./landing\";\nimport { getOrRotateSessionId, getVisitorId } from \"./session\";\nimport { stashUserDataFromFormFields } from \"./user-data\";\nimport type {\n TrackingClientContext,\n TrackingEnvironment,\n TrackingInstallSurface,\n TrackingParams,\n TrackingSessionUpsertPayload,\n} from \"./types\";\n\n/**\n * Default debounce window before queued events are flushed.\n */\nexport const DEFAULT_FLUSH_INTERVAL_MS = 2000;\n\n/**\n * Default queue size that triggers an immediate flush.\n */\nexport const DEFAULT_MAX_QUEUE_SIZE = 10;\n\n/**\n * Hard server-side maximum event count per request body.\n */\nexport const HARD_MAX_BATCH = 50;\n\n/**\n * Ceiling on the exponential backoff between failed delivery attempts.\n */\nexport const MAX_RETRY_BACKOFF_MS = 60_000;\n\n/**\n * Cap on events held in the durable retry buffer. Oldest batches are dropped\n * first once this is exceeded — an unbounded buffer would eventually blow the\n * localStorage quota and take the whole SDK down with it.\n */\nexport const MAX_BUFFERED_EVENTS = 200;\n\n/**\n * localStorage key prefix for the durable retry buffer. Versioned so a future\n * shape change can't be misread as the current one.\n */\nexport const RETRY_BUFFER_KEY_PREFIX = \"aranova_tracking_pending_v1\";\n\n/**\n * What to do with a batch after an attempted delivery.\n *\n * `retry` covers the transport failing and the server saying \"later\" (408, 429,\n * 5xx). `drop` covers a permanent rejection — a 422 from a malformed payload\n * will never succeed, and retrying it forever would wedge every batch behind it.\n */\ntype DeliveryOutcome = \"ok\" | \"retry\" | \"drop\";\n\n/**\n * Header used to authenticate public tracking ingest requests.\n */\nexport const API_KEY_HEADER = \"X-Aranova-Api-Key\";\n\n/**\n * Identity headers stamped on every ingest request. Duplicate fields already\n * present in `session.context` but survive body-parse failures so the backend\n * can attribute 422s to the offending SDK install.\n */\nexport const SDK_VERSION_HEADER = \"X-Aranova-Sdk-Version\";\nexport const SDK_PACKAGE_HEADER = \"X-Aranova-Sdk-Package\";\nexport const SDK_SURFACE_HEADER = \"X-Aranova-Sdk-Surface\";\nexport const SDK_ENVIRONMENT_HEADER = \"X-Aranova-Sdk-Environment\";\n\n/**\n * Configuration for the low-level ingest client.\n *\n * Framework packages usually create this for you through `createTracking()`.\n */\nexport interface TrackingClientConfig {\n /** Public tracking API key issued for the business. */\n apiKey: string;\n /** Tracking endpoint base URL, usually ending in `/tracking`. */\n endpoint: string;\n /** SDK surface creating this client. */\n surface: TrackingInstallSurface;\n /** Package version reported in session context and heartbeat metadata. */\n sdkVersion?: string;\n /** Package name reported in session context and heartbeat metadata. */\n packageName?: string;\n /** Trigger registry so the heartbeat can report registered events. */\n triggers?: TriggerRegistryConfig;\n /** Override the default 2s debounce window. */\n flushIntervalMs?: number;\n /** Override the default 10-event batch trigger. */\n maxQueueSize?: number;\n /** Deployment environment label reported in session context. */\n environment?: TrackingEnvironment;\n /** All active gtag IDs, keyed by label. Included in session context. */\n activeGtagIds?: Record<string, string>;\n /** `siteId` of the tracking-config reference in use; reported on the heartbeat. */\n configSiteId?: string | null;\n /** When true, swallow nothing — useful for tests. */\n debug?: boolean;\n /** Phone-field config, carried for parity; the React hook reads it via context. */\n phone?: PhoneConfig;\n}\n\n/**\n * Input accepted by the low-level stringly-typed client.\n *\n * Prefer the typed `trackEvent(eventName, metadata)` facade exposed by\n * `useTracking()` in React/Next integrations.\n */\nexport interface TrackEventInput {\n /** Event name to enqueue. */\n eventType: string;\n /** URL associated with the event. Defaults to the current page URL. */\n pageUrl?: string | null;\n /** Event-specific metadata. */\n metadata?: Record<string, unknown> | null;\n /** Timestamp override. Defaults to queue time. */\n occurredAt?: Date | string | null;\n}\n\ninterface QueuedEvent {\n event_type: string;\n page_url: string | null;\n metadata: Record<string, unknown> | null;\n occurred_at: string | null;\n}\n\n/**\n * Low-level tracking client responsible for queueing and flushing events.\n */\nexport interface TrackingClient {\n /** Enqueue an event for batched delivery. */\n trackEvent: (input: TrackEventInput) => void;\n /** Flush queued events immediately (fetch, non-keepalive). */\n flush: () => Promise<void>;\n /**\n * Flush queued events through the keepalive transport so the request\n * survives document unload. Use from `pagehide`/`visibilitychange:hidden`\n * handlers — a plain `flush()` there is aborted by the browser on unload.\n */\n flushBeacon: () => void;\n /** Return the current rolling session id. */\n getSessionId: () => string;\n /** Return the persistent visitor id. */\n getVisitorId: () => string;\n /** Remove timers/listeners and prevent future flushes. */\n destroy: () => void;\n}\n\ninterface IngestRequestBody {\n session: TrackingSessionUpsertPayload;\n events: Array<{\n event_type: string;\n page_url: string | null;\n metadata: Record<string, unknown> | null;\n occurred_at: string | null;\n }>;\n}\n\nfunction buildContext(\n surface: TrackingInstallSurface,\n sdkVersion: string | null,\n packageName: string | null,\n environment: TrackingEnvironment,\n activeGtagIds: Record<string, string> | null,\n): TrackingClientContext {\n return {\n surface,\n sdk_version: sdkVersion,\n package_name: packageName,\n site_origin: typeof window === \"undefined\" ? null : window.location.origin,\n page_title: typeof document === \"undefined\" ? null : document.title || null,\n referrer: typeof document === \"undefined\" ? null : document.referrer || null,\n environment,\n active_gtag_ids: activeGtagIds,\n // Rebuilt per flush (this runs inside the payload builder), so a client\n // constructed later on a deeper route still gets reported.\n capabilities: getRegisteredCapabilities(),\n };\n}\n\nfunction readTrackingParams(): TrackingParams {\n if (typeof window === \"undefined\") return createEmptyTrackingParams();\n // Capture from URL on every read so the first event in a session reflects the\n // landing-page params even if the cookie helper hasn't run yet.\n try {\n captureTrackingParamsFromLocation();\n } catch {\n // ignore\n }\n return getTrackingParamsFromCookieReader(getCookieValueFromDocument);\n}\n\nfunction consentSnapshot(): Record<string, unknown> | null {\n try {\n // Effective consent + provenance so the dashboard can tell default-granted\n // (opt-out model, no interaction) apart from an explicit choice.\n const choice = getConsentChoice();\n return {\n state: choice.state,\n source: choice.source,\n updated_at: choice.updatedAt,\n expires_at: choice.expiresAt,\n };\n } catch {\n return null;\n }\n}\n\ninterface IdentityHeaders {\n sdkVersion: string;\n packageName: string;\n surface: string;\n environment: string;\n}\n\nasync function postWithFetch(\n url: string,\n body: string,\n apiKey: string,\n identity: IdentityHeaders,\n keepalive: boolean,\n): Promise<DeliveryOutcome> {\n // No fetch at all (a non-browser runtime): there is nothing to retry against,\n // and scheduling one would spin a timer forever. Give up on the batch.\n if (typeof fetch !== \"function\") return \"drop\";\n try {\n const response = await fetch(url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n [API_KEY_HEADER]: apiKey,\n [SDK_VERSION_HEADER]: identity.sdkVersion,\n [SDK_PACKAGE_HEADER]: identity.packageName,\n [SDK_SURFACE_HEADER]: identity.surface,\n [SDK_ENVIRONMENT_HEADER]: identity.environment,\n },\n body,\n keepalive,\n // CORS is open on the tracking endpoint; never send cookies.\n credentials: \"omit\",\n mode: \"cors\",\n });\n if (response.ok) return \"ok\";\n if (response.status === 408 || response.status === 429 || response.status >= 500) {\n return \"retry\";\n }\n // 401/403/422 and friends: the payload or the key is wrong, and will still\n // be wrong in 30 seconds.\n return \"drop\";\n } catch {\n // Network-level failure (offline, DNS, CORS preflight, aborted). Never\n // throws to the host site; the batch stays buffered for the next attempt.\n return \"retry\";\n }\n}\n\n/**\n * A batch awaiting delivery. Each one carries its OWN session snapshot: a batch\n * can outlive the session that produced it (buffered across a reload), and\n * re-sending it under whatever session is current would silently misattribute\n * those events.\n */\ninterface PendingBatch {\n session: TrackingSessionUpsertPayload;\n events: QueuedEvent[];\n}\n\nfunction retryBufferKey(apiKey: string, endpoint: string): string {\n return `${RETRY_BUFFER_KEY_PREFIX}:${apiKey}:${endpoint}`;\n}\n\nfunction readPendingBatches(key: string): PendingBatch[] {\n if (typeof window === \"undefined\") return [];\n try {\n const raw = window.localStorage.getItem(key);\n if (!raw) return [];\n const parsed: unknown = JSON.parse(raw);\n if (!Array.isArray(parsed)) return [];\n return parsed.filter(\n (entry): entry is PendingBatch =>\n typeof entry === \"object\" &&\n entry !== null &&\n \"session\" in entry &&\n Array.isArray((entry as PendingBatch).events),\n );\n } catch {\n // Storage blocked, or a corrupt/foreign value — start clean rather than\n // letting a bad read break the host site.\n return [];\n }\n}\n\n/**\n * Mirror the buffer to storage unconditionally.\n *\n * Deliberately no read-back verification: under partitioned storage a write can\n * succeed for this document and still not be visible to a verifying read, so a\n * verify-gate false-negatives exactly where durability matters most.\n */\nfunction writePendingBatches(key: string, batches: PendingBatch[]): void {\n if (typeof window === \"undefined\") return;\n try {\n if (batches.length === 0) window.localStorage.removeItem(key);\n else window.localStorage.setItem(key, JSON.stringify(batches));\n } catch {\n // Quota exceeded or storage blocked — in-memory retry still works.\n }\n}\n\n/** Drop the oldest batches until the buffer is back under the event cap. */\nfunction trimToBufferCap(batches: PendingBatch[]): PendingBatch[] {\n let total = batches.reduce((sum, batch) => sum + batch.events.length, 0);\n const trimmed = batches.slice();\n while (total > MAX_BUFFERED_EVENTS && trimmed.length > 1) {\n const dropped = trimmed.shift();\n total -= dropped ? dropped.events.length : 0;\n }\n return trimmed;\n}\n\nlet globalClient: TrackingClient | null = null;\nlet globalClientKey: string | null = null;\n\nfunction clientConfigKey(config: TrackingClientConfig): string {\n return `${config.apiKey}@${config.endpoint}#${config.surface}`;\n}\n\n/**\n * Return a page-level singleton tracking client. Creating the client anew on\n * every component mount is wrong — React StrictMode double-mounts dev-only,\n * and destroying+recreating the client between the cleanup and re-run strips\n * away the pushState patch that SPA auto page view relies on. A singleton\n * survives all of that: the client lives for the entire page, and providers\n * just attach/detach auto page view against it.\n *\n * If `apiKey` / `endpoint` / `surface` change between calls, the previous\n * singleton is destroyed and a new one replaces it. This covers hot-config\n * changes without leaking state.\n */\nexport function getOrCreateTrackingClient(config: TrackingClientConfig): TrackingClient {\n const key = clientConfigKey(config);\n if (globalClient !== null && globalClientKey === key) {\n return globalClient;\n }\n if (globalClient !== null) {\n globalClient.destroy();\n }\n globalClient = createTrackingClient(config);\n globalClientKey = key;\n return globalClient;\n}\n\n// Global/delegated captures (document/window listeners: page_exit, scroll_depth,\n// time_on_site, form_start, cta_click, phone_click, bfcache restore) must attach ONCE\n// per page-singleton client — NOT once per React provider mount. A page can mount several\n// <TrackingProvider>s (a supported island pattern: a global provider for page-level\n// triggers plus per-component providers so `useSearchParams()` doesn't opt the whole tree\n// out of static rendering). Since the client is a singleton but each provider runs its own\n// attach effect, N mounts would stack N document listeners and every interaction would emit\n// its event N times. We ref-count attaches against the singleton client's identity: the\n// first mount attaches, later mounts are no-ops, and the last unmount detaches.\ninterface ClientCaptureEntry {\n detach: () => void;\n refCount: number;\n}\nconst clientCaptureRegistry = new WeakMap<TrackingClient, ClientCaptureEntry>();\n\n/**\n * Attach a set of global/delegated captures against a singleton `client` exactly once,\n * ref-counted across provider mounts. `build` performs the actual `document`/`window`\n * listener attachment and returns a single detacher for all of them; it runs only on the\n * first mount for a given client. The returned release decrements the ref-count and runs\n * `build`'s detacher when the last holder releases. Release is idempotent — React\n * StrictMode invokes an effect's cleanup twice in dev, and a double release must not\n * double-decrement.\n */\nexport function attachClientCapturesOnce(\n client: TrackingClient,\n build: () => () => void,\n): () => void {\n let entry = clientCaptureRegistry.get(client);\n if (entry === undefined) {\n entry = { detach: build(), refCount: 0 };\n clientCaptureRegistry.set(client, entry);\n }\n entry.refCount += 1;\n\n let released = false;\n return () => {\n if (released) return;\n released = true;\n const current = clientCaptureRegistry.get(client);\n if (current === undefined) return;\n current.refCount -= 1;\n if (current.refCount <= 0) {\n current.detach();\n clientCaptureRegistry.delete(client);\n }\n };\n}\n\n/**\n * Tear down the singleton if any. Primarily an escape hatch for tests where\n * each test should see a fresh client; production code rarely needs this.\n * Also clears the in-session SPA referrer so the next test starts with a\n * fresh referrer chain.\n */\nexport function resetGlobalTrackingClient(): void {\n if (globalClient !== null) {\n // Force-detach any captures still attached to this client so their document/window\n // listeners can't leak into the next test even if a provider didn't unmount.\n const entry = clientCaptureRegistry.get(globalClient);\n if (entry !== undefined) {\n entry.detach();\n clientCaptureRegistry.delete(globalClient);\n }\n globalClient.destroy();\n }\n globalClient = null;\n globalClientKey = null;\n resetPageViewState();\n}\n\n/**\n * Create a low-level ingest client.\n *\n * The client queues events, debounces network flushes, sends an SDK heartbeat\n * once per new session, and swallows network errors so analytics never break\n * the host site.\n */\nexport function createTrackingClient(config: TrackingClientConfig): TrackingClient {\n const flushIntervalMs = config.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS;\n const maxQueueSize = Math.min(config.maxQueueSize ?? DEFAULT_MAX_QUEUE_SIZE, HARD_MAX_BATCH);\n const sdkVersion = config.sdkVersion ?? null;\n const packageName = config.packageName ?? null;\n // Default to 'production' so the wire payload always carries a valid enum\n // value. Sending null would fail the backend's strict enum validation.\n const environment: TrackingEnvironment = config.environment ?? \"production\";\n const activeGtagIds = config.activeGtagIds ?? null;\n const endpointBase = config.endpoint.replace(/\\/$/, \"\");\n const eventsUrl = `${endpointBase}/events`;\n const identityHeaders: IdentityHeaders = {\n sdkVersion: sdkVersion ?? \"\",\n packageName: packageName ?? \"\",\n surface: config.surface,\n environment,\n };\n\n let queue: QueuedEvent[] = [];\n let flushTimer: ReturnType<typeof setTimeout> | null = null;\n // Batches that have been handed to the network at least once and not yet\n // acknowledged. Mirrored to localStorage so a reload — or a deploy window that\n // 5xxs every request — costs latency instead of data.\n const bufferKey = retryBufferKey(config.apiKey, endpointBase);\n let pending: PendingBatch[] = readPendingBatches(bufferKey);\n // Flushes are serialized through one chain. Batches are no longer removed from\n // the buffer before the response arrives, so two overlapping flushes would\n // send the same batch twice. Chaining (rather than a \"busy\" flag that returns\n // early) keeps `await flush()` meaning \"flushed\" for the caller.\n let flushChain: Promise<void> = Promise.resolve();\n let retryAttempt = 0;\n let firstPage: string | null = null;\n // Attribution captured synchronously at client creation (before any SPA router\n // can strip the query string). Merged into every session payload as a fallback\n // so a blocked-cookie webview or a stripped URL still attributes.\n let initialParams = createEmptyTrackingParams();\n let destroyed = false;\n\n // Consumed BEFORE identity is minted: it may supply the visitor id itself.\n let inboundLink: ReturnType<typeof consumeInboundLink> = null;\n if (typeof window !== \"undefined\") {\n try {\n inboundLink = consumeInboundLink();\n } catch {\n // never break the host site\n }\n }\n // The sibling-site session this page load arrived from; cleared on rotation.\n let originLink: OriginLink | null = inboundLink?.link ?? null;\n\n // Initialize identity early so the first POST has stable values.\n const visitorId = getVisitorId();\n const initialSession = getOrRotateSessionId();\n let sessionId = initialSession.id;\n\n if (typeof window !== \"undefined\") {\n firstPage = stripLinkParam(window.location.href);\n try {\n initialParams = captureTrackingParamsFromLocation();\n } catch {\n // ignore — never break the host site\n }\n // Pin this session's LANDING params now, before any SPA router strips the\n // query string (reuses the stored record when the session already has one).\n getOrCaptureLandingParams(sessionId);\n try {\n captureFbc();\n } catch {\n // ignore\n }\n }\n\n /** The live page URL as stored — never carrying the transport token. */\n function currentPageUrl(): string | null {\n return typeof window === \"undefined\" ? null : stripLinkParam(window.location.href);\n }\n\n // SDK-internal events are queued at construction, before `trackEvent` exists.\n function enqueueInternal(\n eventType: string,\n metadata: Record<string, unknown>,\n pageUrl: string | null,\n ): void {\n queue.push({\n event_type: eventType,\n page_url: pageUrl,\n metadata,\n occurred_at: new Date().toISOString(),\n });\n }\n\n // Re-read per call: a session can rotate mid-page, and the heartbeat reports\n // the page the visitor is on then, not the one the client was built on.\n function enqueueHeartbeat(): void {\n enqueueInternal(\n \"sdk_heartbeat\",\n buildHeartbeatMetadata(\n config.surface,\n sdkVersion,\n packageName,\n config.triggers ?? null,\n activeGtagIds,\n config.configSiteId ?? null,\n ),\n currentPageUrl(),\n );\n }\n\n if (initialSession.isNew) {\n enqueueHeartbeat();\n }\n\n // A rejected token is reported too: the backend never sees one, only this verdict.\n if (inboundLink !== null && typeof window !== \"undefined\") {\n const link = inboundLink.link;\n enqueueInternal(\n \"site_link_arrival\",\n {\n outcome: inboundLink.outcome,\n origin_session_id: link?.sessionId ?? null,\n origin_visitor_id: link?.visitorId ?? null,\n transit_ms: link ? Math.max(0, Date.now() - link.linkedAt) : null,\n adopted_visitor: inboundLink.adoptedVisitor,\n page: { path: window.location.pathname },\n },\n firstPage,\n );\n scheduleFlush();\n }\n\n // Anything left buffered by a previous page load goes out on the normal\n // debounce, ahead of whatever this page produces.\n if (pending.length > 0) {\n scheduleFlush();\n }\n\n function buildSessionPayload(): TrackingSessionUpsertPayload {\n const rotated = getOrRotateSessionId();\n if (rotated.isNew && rotated.id !== sessionId) {\n // Session rotated mid-page (idle > 30min then user returned).\n enqueueHeartbeat();\n originLink = null;\n }\n sessionId = rotated.id;\n // Prefer the live cookie/localStorage read; fall back to the init-time\n // snapshot (covers a webview that blocked the cookie AND a router that\n // already stripped the landing URL by flush time).\n const params = mergeTrackingParams(readTrackingParams(), initialParams);\n const context = buildContext(\n config.surface,\n sdkVersion,\n packageName,\n environment,\n activeGtagIds,\n );\n return {\n session_id: sessionId,\n visitor_id: visitorId,\n gclid: params.gclid,\n wbraid: params.wbraid,\n gbraid: params.gbraid,\n ylpcid: params.ylpcid,\n fbclid: params.fbclid,\n fbc: getFbcCookie(),\n fbp: getFbpCookie(),\n utm_source: params.utm_source,\n utm_medium: params.utm_medium,\n utm_campaign: params.utm_campaign,\n utm_term: params.utm_term,\n utm_content: params.utm_content,\n // Landing params for the CURRENT session id — captured on the spot when\n // the session just rotated (the current URL is the rotated session's\n // landing), reused from the stored record otherwise. Keys are omitted\n // entirely when the landing isn't observable (SSR).\n ...buildLandingPayloadFields(sessionId),\n ...(originLink\n ? {\n origin_session_id: originLink.sessionId,\n origin_visitor_id: originLink.visitorId,\n origin_linked_at: new Date(originLink.linkedAt).toISOString(),\n }\n : {}),\n first_page: firstPage,\n consent_state: consentSnapshot(),\n context,\n };\n }\n\n function scheduleFlush(delayMs: number = flushIntervalMs): void {\n if (flushTimer !== null || destroyed) return;\n flushTimer = setTimeout(() => {\n flushTimer = null;\n void flush();\n }, delayMs);\n }\n\n function clearScheduledFlush(): void {\n if (flushTimer !== null) {\n clearTimeout(flushTimer);\n flushTimer = null;\n }\n }\n\n /** Move everything currently queued into the durable buffer. */\n function bufferQueued(): void {\n if (queue.length === 0) return;\n const events = queue.slice(0, HARD_MAX_BATCH);\n queue = queue.slice(events.length);\n pending = trimToBufferCap([...pending, { session: buildSessionPayload(), events }]);\n writePendingBatches(bufferKey, pending);\n }\n\n /**\n * Drain the durable buffer, oldest batch first.\n *\n * A batch leaves the buffer only on `ok` (acknowledged) or `drop` (permanently\n * rejected). Anything else keeps it, so a 5xx window costs latency, not data.\n *\n * Delivery is at-least-once: with no event-level idempotency key on the wire,\n * a response lost after the server committed will re-send that batch. That\n * window is far narrower than the \"every 5xx is permanent loss\" it replaces —\n * closing it needs a client-generated event id plus a backend uniqueness\n * constraint, which is a schema change and its own release.\n */\n function flush(): Promise<void> {\n // `.catch` keeps one unexpected throw from poisoning every later flush.\n flushChain = flushChain.then(runFlush).catch(() => {});\n return flushChain;\n }\n\n async function runFlush(): Promise<void> {\n if (destroyed) return;\n clearScheduledFlush();\n bufferQueued();\n if (pending.length === 0) return;\n\n try {\n while (pending.length > 0) {\n const batch = pending[0]!;\n const body: IngestRequestBody = { session: batch.session, events: batch.events };\n // keepalive=false: this runs on a live page, where a plain fetch\n // completes normally and its failure is observable. The unload path\n // (flushOnUnload) passes true instead — pagehide tears the document down\n // and aborts a plain fetch mid-flight, while keepalive hands the request\n // to the browser to finish after teardown.\n const outcome = await postWithFetch(\n eventsUrl,\n JSON.stringify(body),\n config.apiKey,\n identityHeaders,\n false,\n );\n if (outcome === \"retry\") {\n retryAttempt += 1;\n // Exponential from the normal flush interval, capped. Scheduling\n // happens after `flushing` is cleared, below.\n return;\n }\n pending = pending.slice(1);\n writePendingBatches(bufferKey, pending);\n retryAttempt = 0;\n }\n } finally {\n if (pending.length > 0) {\n const backoff = Math.min(flushIntervalMs * 2 ** retryAttempt, MAX_RETRY_BACKOFF_MS);\n scheduleFlush(backoff);\n } else if (queue.length > 0) {\n // `bufferQueued` takes at most HARD_MAX_BATCH per pass, so a burst\n // larger than one batch still has events waiting.\n scheduleFlush();\n }\n }\n }\n\n function trackEvent(input: TrackEventInput): void {\n if (destroyed) return;\n if (!input || typeof input.eventType !== \"string\" || input.eventType.length === 0) return;\n\n // Enhanced conversions: a form submit is the one moment the visitor's own\n // email/phone pass through the SDK — stash them (memory only) so the\n // conversion that fires next carries user_data. NOT done for phone_click:\n // its metadata holds the BUSINESS's number, not the visitor's. An explicit\n // consent decline also skips the stash entirely — egress is already gated,\n // but a decliner's identifiers shouldn't sit in page memory either.\n if (input.eventType === \"form_submit\" && getConsentState() !== \"denied\") {\n try {\n const fields = (input.metadata as { form?: { fields?: unknown } } | null)?.form?.fields;\n if (fields) stashUserDataFromFormFields(fields, config.phone?.defaultCountry);\n } catch {\n // user-data capture must never break ingest\n }\n }\n\n const occurredAt =\n input.occurredAt instanceof Date\n ? input.occurredAt.toISOString()\n : typeof input.occurredAt === \"string\"\n ? input.occurredAt\n : new Date().toISOString();\n\n queue.push({\n event_type: input.eventType,\n // A caller's href still carries `_arv` until the surface strips it after hydration.\n page_url: input.pageUrl != null ? stripLinkParam(input.pageUrl) : currentPageUrl(),\n metadata: input.metadata ?? null,\n occurred_at: occurredAt,\n });\n\n if (queue.length >= maxQueueSize) {\n void flush();\n } else {\n scheduleFlush();\n }\n }\n\n /**\n * Last-gasp send on pagehide/visibilitychange.\n *\n * The keepalive request outlives the document, so its outcome can never be\n * observed. The batch it carries is therefore removed from the buffer\n * optimistically: keeping it would re-send on the next page load every time\n * the send actually worked, which is almost always. Any batch behind it stays\n * buffered and is retried on the next load.\n */\n function flushOnUnload(): void {\n bufferQueued();\n clearScheduledFlush();\n if (pending.length === 0) return;\n\n const batch = pending[0]!;\n pending = pending.slice(1);\n writePendingBatches(bufferKey, pending);\n\n const body: IngestRequestBody = { session: batch.session, events: batch.events };\n void postWithFetch(eventsUrl, JSON.stringify(body), config.apiKey, identityHeaders, true);\n }\n\n if (typeof window !== \"undefined\") {\n window.addEventListener(\"pagehide\", flushOnUnload);\n window.addEventListener(\"visibilitychange\", () => {\n if (document.visibilityState === \"hidden\") flushOnUnload();\n });\n }\n\n return {\n trackEvent,\n flush,\n flushBeacon: flushOnUnload,\n getSessionId: () => sessionId,\n getVisitorId: () => visitorId,\n destroy: () => {\n destroyed = true;\n // Fire any pending events through the keepalive path before tearing\n // down. Critical for React StrictMode in dev, where the provider's\n // first mount is immediately unmounted and its 2s debounce would\n // otherwise drop the initial page_view on the floor. Uses fetch\n // keepalive so the request survives the component tearing down.\n if (queue.length > 0) {\n flushOnUnload();\n }\n clearScheduledFlush();\n queue = [];\n if (typeof window !== \"undefined\") {\n window.removeEventListener(\"pagehide\", flushOnUnload);\n }\n },\n };\n}\n","import {\n API_KEY_HEADER,\n SDK_ENVIRONMENT_HEADER,\n SDK_PACKAGE_HEADER,\n SDK_SURFACE_HEADER,\n SDK_VERSION_HEADER,\n} from \"../../ingest\";\nimport { AranovaApiError } from \"./errors\";\n\n/** Shared config for every awaited (non fire-and-forget) API helper. */\nexport interface ApiTransportConfig {\n /** Public (`aranv_pk_…`) or secret (`aranv_sk_…`) API key. */\n apiKey: string;\n /** Base tracking endpoint, e.g. `https://aranovainternal-production.up.railway.app/tracking`. */\n endpoint: string;\n /** Optional SDK identity headers (mirrors the event ingest client). */\n sdkVersion?: string;\n packageName?: string;\n surface?: string;\n environment?: string;\n}\n\nfunction identityHeaders(config: ApiTransportConfig): Record<string, string> {\n const headers: Record<string, string> = { [API_KEY_HEADER]: config.apiKey };\n if (config.sdkVersion) headers[SDK_VERSION_HEADER] = config.sdkVersion;\n if (config.packageName) headers[SDK_PACKAGE_HEADER] = config.packageName;\n if (config.surface) headers[SDK_SURFACE_HEADER] = config.surface;\n if (config.environment) headers[SDK_ENVIRONMENT_HEADER] = config.environment;\n return headers;\n}\n\nfunction joinUrl(endpoint: string, path: string): string {\n return `${endpoint.replace(/\\/$/, \"\")}${path}`;\n}\n\n/**\n * Single awaited request. Unlike the event queue, this surfaces failures: any\n * non-2xx rejects with an {@link AranovaApiError}. Returns `undefined` for 204.\n */\nexport async function apiRequest<T>(\n config: ApiTransportConfig,\n method: string,\n path: string,\n body?: unknown,\n extraHeaders?: Record<string, string>,\n): Promise<T> {\n const headers = { ...identityHeaders(config), ...extraHeaders };\n if (body !== undefined) headers[\"Content-Type\"] = \"application/json\";\n\n const response = await fetch(joinUrl(config.endpoint, path), {\n method,\n headers,\n body: body === undefined ? undefined : JSON.stringify(body),\n });\n\n if (!response.ok) {\n let detail: string | undefined;\n let code: string | undefined;\n try {\n const parsed: unknown = await response.json();\n if (parsed && typeof parsed === \"object\") {\n const record = parsed as Record<string, unknown>;\n if (typeof record.detail === \"string\") detail = record.detail;\n if (typeof record.code === \"string\") code = record.code;\n }\n } catch {\n // non-JSON error body — fall back to status text\n }\n throw new AranovaApiError(detail ?? response.statusText ?? \"Request failed\", {\n status: response.status,\n code,\n requestId: response.headers.get(\"x-request-id\") ?? undefined,\n });\n }\n\n if (response.status === 204) return undefined as T;\n return (await response.json()) as T;\n}\n","import type { ApiTransportConfig } from \"../http/request\";\nimport { apiRequest } from \"../http/request\";\n\nexport type CalendarTransportConfig = ApiTransportConfig;\n\n/** Prefixes `/calendar` so callers pass resource-relative paths. */\nexport async function calendarRequest<T>(\n config: CalendarTransportConfig,\n method: string,\n path: string,\n body?: unknown,\n): Promise<T> {\n return apiRequest<T>(config, method, `/calendar${path}`, body);\n}\n","/**\n * Format an ISO timestamp in a specific IANA time zone (e.g. `America/Toronto`),\n * so dashboards stop hand-rolling `Intl`. Defaults to a short date-time; pass\n * `opts` to override fields. The `timeZone` is always forced to the argument.\n */\nexport function formatDateInTz(\n iso: string,\n timeZone: string,\n opts?: Intl.DateTimeFormatOptions,\n locale?: string,\n): string {\n const date = new Date(iso);\n // Guard malformed input: Intl.format(Invalid Date) throws a RangeError.\n if (Number.isNaN(date.getTime())) return iso;\n return new Intl.DateTimeFormat(locale, {\n year: \"numeric\",\n month: \"short\",\n day: \"2-digit\",\n hour: \"2-digit\",\n minute: \"2-digit\",\n ...opts,\n timeZone,\n }).format(date);\n}\n","import { formatDateInTz } from \"../intl/date\";\n\nexport { formatDateInTz };\n\n/** The visitor's own zone, so a booking page defaults to their local times. */\nexport function resolveBrowserTimeZone(): string {\n try {\n return Intl.DateTimeFormat().resolvedOptions().timeZone || \"UTC\";\n } catch {\n return \"UTC\";\n }\n}\n\n/** Parts of an instant as seen in a specific zone. */\nfunction partsInTz(date: Date, timeZone: string): Record<string, number> {\n const formatter = new Intl.DateTimeFormat(\"en-US\", {\n timeZone,\n hour12: false,\n year: \"numeric\",\n month: \"2-digit\",\n day: \"2-digit\",\n hour: \"2-digit\",\n minute: \"2-digit\",\n second: \"2-digit\",\n });\n const out: Record<string, number> = {};\n for (const part of formatter.formatToParts(date)) {\n if (part.type !== \"literal\") out[part.type] = Number(part.value);\n }\n // `hour` comes back as 24 at midnight under hour12:false in some engines.\n if (out.hour === 24) out.hour = 0;\n return out;\n}\n\n/** Offset in minutes between UTC and `timeZone` at a given instant. */\nexport function offsetMinutesAt(date: Date, timeZone: string): number {\n const p = partsInTz(date, timeZone);\n const asUtc = Date.UTC(p.year, p.month - 1, p.day, p.hour, p.minute, p.second);\n return (asUtc - Math.floor(date.getTime() / 1000) * 1000) / 60_000;\n}\n\n/**\n * The instant matching a wall-clock time in `timeZone`.\n *\n * Two passes because the offset itself depends on the instant: guess with the\n * offset at the naive time, then re-solve with the offset that guess landed in.\n * That second pass is what makes DST-transition days come out right.\n */\nexport function instantFromWallClock(\n timeZone: string,\n year: number,\n month: number,\n day: number,\n hour = 0,\n minute = 0,\n): Date {\n const naive = Date.UTC(year, month - 1, day, hour, minute);\n const firstGuess = new Date(naive - offsetMinutesAt(new Date(naive), timeZone) * 60_000);\n const corrected = naive - offsetMinutesAt(firstGuess, timeZone) * 60_000;\n return new Date(corrected);\n}\n\n/** `YYYY-MM-DD` for an instant as seen in `timeZone`. */\nexport function localDateKey(iso: string, timeZone: string): string {\n const p = partsInTz(new Date(iso), timeZone);\n const pad = (n: number) => String(n).padStart(2, \"0\");\n return `${p.year}-${pad(p.month)}-${pad(p.day)}`;\n}\n\nexport function startOfDayInTz(dateKey: string, timeZone: string): Date {\n const [year, month, day] = dateKey.split(\"-\").map(Number);\n return instantFromWallClock(timeZone, year, month, day, 0, 0);\n}\n\nexport function endOfDayInTz(dateKey: string, timeZone: string): Date {\n const start = startOfDayInTz(dateKey, timeZone);\n // Add a calendar day rather than 24h: DST days are 23 or 25 hours long.\n const p = partsInTz(start, timeZone);\n return instantFromWallClock(timeZone, p.year, p.month, p.day + 1, 0, 0);\n}\n\nexport function durationMinutes(startIso: string, endIso: string): number {\n return Math.round((new Date(endIso).getTime() - new Date(startIso).getTime()) / 60_000);\n}\n\n/** `10:00 – 10:30` in the given zone. */\nexport function formatSlotRange(\n startIso: string,\n endIso: string,\n timeZone: string,\n locale?: string,\n): string {\n const opts: Intl.DateTimeFormatOptions = { hour: \"2-digit\", minute: \"2-digit\", timeZone };\n const fmt = new Intl.DateTimeFormat(locale, opts);\n return `${fmt.format(new Date(startIso))} – ${fmt.format(new Date(endIso))}`;\n}\n","import { registerCapability } from \"../../capabilities\";\nimport { AranovaApiError } from \"../http/errors\";\nimport type { BusyResponse, CalendarErrorCode, CalendarSummary } from \"./schema\";\nimport type { CalendarTransportConfig } from \"./transport\";\nimport { calendarRequest } from \"./transport\";\nimport { resolveBrowserTimeZone } from \"./time\";\n\n/**\n * The browser-side calendar client — reads only.\n *\n * There is deliberately no write verb on this type. A public key ships in a JS\n * bundle and its Origin allowlist is a browser-enforced header a script forges\n * trivially; a booking is not a database row but an event on a real person's\n * calendar that mails their contacts. Writes live in\n * `@aranova/tracking-next/calendar-server`, which is `server-only`.\n */\nexport interface CalendarReadClientConfig extends CalendarTransportConfig {\n /** Falls back to the visitor's own zone. */\n defaultTimeZone?: string;\n /** Required when the key is not already scoped to one business. */\n businessId?: string;\n}\n\nexport interface BusyQuery {\n /**\n * ISO 8601 instants WITH an offset — the API rejects a naive or date-only\n * value with a 400. `startOfDayInTz`/`endOfDayInTz` turn a picked date into\n * the right pair for a given zone.\n */\n since: string;\n until: string;\n timeZone?: string;\n}\n\nexport interface CalendarReadClient<TCalendarKey extends string = string> {\n /** Labels, timezones and status — everything a widget needs before booking. */\n listCalendars(): Promise<CalendarSummary[]>;\n /** Busy intervals only. Never the contents of the grantee's events. */\n busy(calendarKey: TCalendarKey, query: BusyQuery): Promise<BusyResponse>;\n}\n\nfunction withBusiness(config: CalendarReadClientConfig, path: string): string {\n if (!config.businessId) return path;\n const separator = path.includes(\"?\") ? \"&\" : \"?\";\n return `${path}${separator}business_id=${encodeURIComponent(config.businessId)}`;\n}\n\nexport function createCalendarReadClient<TCalendarKey extends string = string>(\n config: CalendarReadClientConfig,\n): CalendarReadClient<TCalendarKey> {\n registerCapability(\"calendar_read\");\n return {\n async listCalendars() {\n return calendarRequest<CalendarSummary[]>(config, \"GET\", withBusiness(config, \"/calendars\"));\n },\n async busy(calendarKey, query) {\n const params = new URLSearchParams({\n since: query.since,\n until: query.until,\n });\n const timeZone = query.timeZone ?? config.defaultTimeZone ?? resolveBrowserTimeZone();\n params.set(\"time_zone\", timeZone);\n return calendarRequest<BusyResponse>(\n config,\n \"GET\",\n withBusiness(config, `/${encodeURIComponent(calendarKey)}/busy?${params.toString()}`),\n );\n },\n };\n}\n\n/** Narrow an unknown error to a specific calendar failure code. */\nexport function isCalendarError(\n error: unknown,\n code?: CalendarErrorCode,\n): error is AranovaApiError {\n if (!(error instanceof AranovaApiError)) return false;\n return code === undefined || error.code === code;\n}\n","import * as z from \"zod/mini\";\n\nimport type { zBusyInterval, zBusyResponse, zErrorCode } from \"./generated/zod.gen\";\nimport {\n zBookingAttendee,\n zBookingCancelSchema,\n zBookingCreateSchema,\n zBookingCustomerInput,\n zBookingMetadataInput,\n zBookingReminder,\n zBookingReminders,\n zBookingRescheduleSchema,\n zBookingResponse,\n zCalendarSummary,\n zCustomLocation,\n zMeetLocation,\n zPhoneLocation,\n zPhysicalLocation,\n} from \"./generated/zod.gen\";\n\n/**\n * Calendar wire schemas.\n *\n * The object shapes come from the backend's own spec — see\n * `../generated/zod.gen.ts` and `packages/tracking-core/openapi-ts.config.ts`.\n * This file adds only what JSON Schema cannot express: the start/date and end/duration XORs,\n * the IANA timezone check, and defaults Pydantic omits from the spec because\n * `default_factory` is not serialized (`attendees: []`, `reminders.overrides:\n * []`) — composed onto the generated base, never duplicating its shape.\n */\n\n/** Bumped when the durable `metadata` payload changes shape. */\nexport const CALENDAR_METADATA_SCHEMA_VERSION = 1;\n\nexport const MAX_ATTENDEES = 20;\nexport const MAX_REMINDER_OVERRIDES = 5;\n/** Google's ceiling for `reminders.overrides[].minutes`: four weeks. */\nexport const MAX_REMINDER_MINUTES = 40_320;\nexport const MAX_BOOKING_MINUTES = 1_440;\n\nexport const BOOKING_SOURCES = [\"website\", \"phone\", \"walk_in\", \"import\", \"operator\"] as const;\n// Read off the generated response enums, so the backend owns the vocabulary.\nexport const BOOKING_STATUSES = zBookingResponse.shape.status.options;\nexport const GOOGLE_SYNC_STATES = zBookingResponse.shape.google_sync_state.options;\nexport const CALENDAR_SYNC_STATUSES = zCalendarSummary.shape.status.options;\n\n/**\n * Every failure code the calendar API can hand back, so a site can branch on it.\n * `satisfies` pins each one to the backend's generated `ErrorCode` enum.\n */\nexport const CALENDAR_ERROR_CODES = [\n \"slot_unavailable\",\n \"calendar_not_connected\",\n \"calendar_disconnected\",\n \"booking_not_found\",\n \"booking_not_cancellable\",\n \"booking_not_reschedulable\",\n \"manage_token_invalid\",\n \"rate_limited\",\n \"google_error\",\n] as const satisfies readonly z.output<typeof zErrorCode>[];\nexport type CalendarErrorCode = (typeof CALENDAR_ERROR_CODES)[number];\n\nexport type BookingStatus = (typeof BOOKING_STATUSES)[number];\nexport type GoogleSyncState = (typeof GOOGLE_SYNC_STATES)[number];\nexport type JsonScalar = string | number | boolean | null;\n\n/**\n * Does the runtime actually know this zone?\n *\n * `Intl.DateTimeFormat` rather than `Intl.supportedValuesOf(\"timeZone\")`: the\n * latter returns canonical zones only and omits live aliases (`Asia/Calcutta`,\n * `US/Pacific`, `Europe/Kiev`) that the API's `ZoneInfo` accepts. Validating\n * against it would reject values the server stores happily.\n */\nfunction isKnownTimeZone(timeZone: string): boolean {\n try {\n new Intl.DateTimeFormat(undefined, { timeZone });\n return true;\n } catch {\n return false;\n }\n}\n\n// The refinement is invisible to `zod-to-json-schema` — unrepresentable\n// constraints are dropped — so `events.schema.json` and the drift test against\n// the Pydantic mirror are unaffected while both sides gain the real check.\nconst ianaTz = z\n .string()\n .check(\n z.minLength(1),\n z.maxLength(64),\n z.regex(/^[A-Za-z0-9_+\\-/]+$/),\n z.refine(isKnownTimeZone, { error: \"unknown IANA time zone\" }),\n );\n\nexport const bookingAttendeeSchema = zBookingAttendee;\nexport const bookingReminderSchema = zBookingReminder;\nexport const bookingCustomerSchema = zBookingCustomerInput;\n// `custom`'s value union: the generated form is `union([...]).nullable()`,\n// which nests one `anyOf` inside another where Pydantic emits a single flat\n// list. Structure is what the drift snapshot compares, so flatten it.\nexport const bookingMetadataSchema = z.extend(zBookingMetadataInput, {\n custom: z.nullish(\n z.record(z.string(), z.union([z.string(), z.int(), z.number(), z.boolean(), z.null()])),\n ),\n});\n\n// A plain `z.union`, matching the spec: a discriminated union would emit\n// `oneOf` + discriminator metadata where Pydantic emits `anyOf`, failing the\n// drift snapshot.\nexport const bookingLocationSchema = z.union([\n zMeetLocation,\n zPhysicalLocation,\n zPhoneLocation,\n zCustomLocation,\n]);\n\n/**\n * The nullable form, flattened.\n *\n * `bookingLocationSchema.nullable()` would nest one `anyOf` inside another,\n * while Pydantic's `BookingLocation | None` flattens to a single list — the\n * drift snapshot compares structure, so both sides emit the flat shape.\n */\nexport const bookingLocationNullableSchema = z.union([\n ...bookingLocationSchema.def.options,\n z.null(),\n]);\n\n// Mini reports issues by pushing onto the payload rather than via a\n// `RefinementCtx` helper; `path` still scopes the message to a field.\ntype MiniCheckCtx = { value: unknown; issues: Array<Record<string, unknown>> };\n\nfunction addIssue(ctx: MiniCheckCtx, path: string[], message: string): void {\n ctx.issues.push({ code: \"custom\", input: ctx.value, path, message });\n}\n\ntype TimedRange = {\n start_at?: string | null;\n end_at?: string | null;\n duration_minutes?: number | null;\n};\n\nfunction refineEndXor(value: TimedRange & { start_at: string }, ctx: MiniCheckCtx): void {\n const hasEnd = value.end_at !== undefined && value.end_at !== null;\n const hasDuration = value.duration_minutes !== undefined && value.duration_minutes !== null;\n if (hasEnd === hasDuration) {\n addIssue(ctx, [\"end_at\"], \"provide exactly one of end_at or duration_minutes\");\n return;\n }\n if (hasEnd && new Date(value.end_at as string) <= new Date(value.start_at)) {\n addIssue(ctx, [\"end_at\"], \"end_at must be after start_at\");\n }\n}\n\nfunction refineStartXor(\n value: TimedRange & {\n date?: string | null;\n buffer_before_minutes?: number;\n buffer_after_minutes?: number;\n },\n ctx: MiniCheckCtx,\n): void {\n const hasStart = value.start_at !== undefined && value.start_at !== null;\n const hasDate = value.date !== undefined && value.date !== null;\n if (hasStart === hasDate) {\n addIssue(ctx, [\"start_at\"], \"provide exactly one of start_at or date\");\n return;\n }\n if (hasStart) {\n refineEndXor(value as TimedRange & { start_at: string }, ctx);\n return;\n }\n if (value.end_at != null || value.duration_minutes != null) {\n addIssue(ctx, [\"date\"], \"an all-day booking (date) takes no end_at or duration_minutes\");\n }\n if (value.buffer_before_minutes || value.buffer_after_minutes) {\n addIssue(ctx, [\"date\"], \"an all-day booking (date) takes no buffers\");\n }\n}\n\n// `attendees` and `reminders.overrides` have real (mutable) defaults\n// server-side, but Pydantic doesn't serialize a `default_factory=list` into\n// the JSON Schema `default` keyword, so they're restored here.\nexport const bookingRemindersSchema = z.extend(zBookingReminders, {\n overrides: z._default(\n z.array(bookingReminderSchema).check(z.maxLength(MAX_REMINDER_OVERRIDES)),\n [],\n ),\n});\n\nexport const bookingCreateSchema = z\n .extend(zBookingCreateSchema, {\n time_zone: ianaTz,\n // The generated field is `z.union([...variants]).nullish()`, which nests one\n // `anyOf` inside another; Pydantic's `BookingLocation | None` flattens to a\n // single list and the drift snapshot compares structure. Override with the\n // flattened union.\n location: z.optional(bookingLocationNullableSchema),\n attendees: z._default(z.array(bookingAttendeeSchema).check(z.maxLength(MAX_ATTENDEES)), []),\n // These reference the un-composed generated schemas; generation can't nest\n // our compositions into its own output.\n reminders: z.nullish(bookingRemindersSchema),\n metadata: z.nullish(bookingMetadataSchema),\n })\n .check((ctx) => refineStartXor(ctx.value as never, ctx));\n\nexport const bookingRescheduleSchema = z\n .extend(zBookingRescheduleSchema, { time_zone: ianaTz })\n .check((ctx) => refineEndXor(ctx.value as never, ctx));\n\nexport const bookingCancelSchema = zBookingCancelSchema;\n\n// `z.input` so Zod defaults stay optional for callers.\nexport type BookingCreateInput = z.input<typeof bookingCreateSchema>;\nexport type BookingRescheduleInput = z.input<typeof bookingRescheduleSchema>;\nexport type BookingCancelInput = z.input<typeof bookingCancelSchema>;\nexport type BookingAttendee = z.input<typeof bookingAttendeeSchema>;\nexport type BookingReminders = z.input<typeof bookingRemindersSchema>;\nexport type BookingLocation = z.input<typeof bookingLocationSchema>;\nexport type BookingCustomer = z.input<typeof bookingCustomerSchema>;\nexport type BookingMetadata<\n TCustom extends Record<string, JsonScalar> = Record<string, JsonScalar>,\n> = Omit<z.input<typeof bookingMetadataSchema>, \"custom\"> & { custom?: TCustom | null };\n\n// ---------------------------------------------------------------------------\n// Read shapes — server responses, trusted and therefore not re-validated here.\n// ---------------------------------------------------------------------------\n\n/** A calendar as the site sees it. `google_account_email` and friends are null for a public key. */\nexport type CalendarSummary = z.output<typeof zCalendarSummary>;\nexport type CalendarSyncStatus = CalendarSummary[\"status\"];\nexport type BusyInterval = z.output<typeof zBusyInterval>;\nexport type BusyResponse = z.output<typeof zBusyResponse>;\n\n/** A booking as the API returns it; field docs live on the backend's `BookingResponse`. */\nexport type Booking = z.output<typeof zBookingResponse>;\n","// This file is auto-generated by @hey-api/openapi-ts\n\nimport * as z from \"zod/mini\";\n\n/**\n * BookingAttendee\n */\nexport const zBookingAttendee = z.strictObject({\n display_name: z.nullish(z.string().check(z.maxLength(200))),\n email: z.email().check(z.maxLength(320)),\n optional: z._default(z.optional(z.boolean()), false),\n});\n\n/**\n * BookingCancelSchema\n */\nexport const zBookingCancelSchema = z.strictObject({\n notify_attendees: z._default(z.optional(z.boolean()), true),\n reason: z.nullish(z.string().check(z.maxLength(500))),\n});\n\n/**\n * BookingCustomer\n */\nexport const zBookingCustomerInput = z.strictObject({\n email: z.email().check(z.maxLength(320)),\n name: z.nullish(z.string().check(z.maxLength(200))),\n phone: z.nullish(z.string().check(z.maxLength(64))),\n sms_consent: z._default(z.optional(z.boolean()), false),\n});\n\n/**\n * BookingCustomer\n */\nexport const zBookingCustomerOutput = z.strictObject({\n email: z.email().check(z.maxLength(320)),\n name: z.nullable(z.string().check(z.maxLength(200))),\n phone: z.nullable(z.string().check(z.maxLength(64))),\n sms_consent: z._default(z.boolean(), false),\n});\n\n/**\n * BookingRescheduleSchema\n */\nexport const zBookingRescheduleSchema = z.strictObject({\n duration_minutes: z.nullish(z.int().check(z.gt(0), z.lte(1440))),\n end_at: z.nullish(z.iso.datetime({ offset: true })),\n notify_attendees: z._default(z.optional(z.boolean()), true),\n reason: z.nullish(z.string().check(z.maxLength(500))),\n start_at: z.iso.datetime({ offset: true }),\n time_zone: z.string().check(z.minLength(1), z.maxLength(64), z.regex(/^[A-Za-z0-9_+\\-\\/]+$/)),\n});\n\n/**\n * BookingResponse\n *\n * A created booking. `manage_token` is present only on the create response.\n */\nexport const zBookingResponse = z.strictObject({\n all_day: z.boolean(),\n appointment_type: z.nullable(z.string()),\n calendar_key: z.string(),\n created_at: z.iso.datetime({ offset: true }),\n customer: zBookingCustomerOutput,\n end_at: z.iso.datetime({ offset: true }),\n google_event_id: z.nullable(z.string()),\n google_html_link: z.nullable(z.string()),\n google_sync_state: z.enum([\"queued\", \"synced\", \"uncertain\", \"failed\", \"drifted\"]),\n ical_uid: z.nullable(z.string()),\n id: z.string(),\n manage_token: z.nullable(z.string()),\n meet_url: z.nullable(z.string()),\n organizer_email: z.nullable(z.string()),\n start_at: z.iso.datetime({ offset: true }),\n status: z.enum([\"pending\", \"confirmed\", \"cancelled\", \"no_show\"]),\n time_zone: z.string(),\n title: z.string(),\n});\n\n/**\n * BookingSource\n */\nexport const zBookingSource = z.enum([\"website\", \"phone\", \"walk_in\", \"import\", \"operator\"]);\n\n/**\n * BookingMetadata\n *\n * Structured booking metadata — named attribution keys plus a flat escape hatch.\n *\n * `custom` is flat and scalar-only on purpose: it mirrors trivially in Zod,\n * stays JSONB-queryable, and bounds what a caller can push. Only `custom` is\n * ever mirrored to Google's `extendedProperties`; the named attribution keys\n * never leave our database.\n */\nexport const zBookingMetadataInput = z.strictObject({\n custom: z.nullish(\n z.record(z.string(), z.nullable(z.union([z.string(), z.int(), z.number(), z.boolean()]))),\n ),\n fbclid: z.nullish(z.string().check(z.maxLength(256))),\n gclid: z.nullish(z.string().check(z.maxLength(256))),\n landing_page: z.nullish(z.string().check(z.maxLength(2048))),\n notes: z.nullish(z.string().check(z.maxLength(2000))),\n referrer: z.nullish(z.string().check(z.maxLength(2048))),\n schema_version: z._default(z.optional(z.int()), 1),\n service: z.nullish(z.string().check(z.maxLength(64))),\n session_id: z.nullish(z.string().check(z.maxLength(64))),\n source: z.nullish(zBookingSource),\n utm_campaign: z.nullish(z.string().check(z.maxLength(128))),\n utm_content: z.nullish(z.string().check(z.maxLength(128))),\n utm_medium: z.nullish(z.string().check(z.maxLength(128))),\n utm_source: z.nullish(z.string().check(z.maxLength(128))),\n utm_term: z.nullish(z.string().check(z.maxLength(128))),\n visitor_id: z.nullish(z.string().check(z.maxLength(64))),\n});\n\n/**\n * BusySource\n *\n * Where a busy interval came from.\n *\n * `booking` intervals are authoritative even when Google has not seen them\n * yet: a row committed a moment ago holds its slot before `events.insert`\n * returns.\n */\nexport const zBusySource = z.enum([\"google\", \"booking\"]);\n\n/**\n * BusyInterval\n */\nexport const zBusyInterval = z.strictObject({\n end_at: z.iso.datetime({ offset: true }),\n source: zBusySource,\n start_at: z.iso.datetime({ offset: true }),\n});\n\n/**\n * BusyWindow\n */\nexport const zBusyWindow = z.strictObject({\n since: z.iso.datetime({ offset: true }),\n until: z.iso.datetime({ offset: true }),\n});\n\n/**\n * BusyResponse\n *\n * The calendar's busy truth over a window — times only, never contents.\n */\nexport const zBusyResponse = z.strictObject({\n busy: z.array(zBusyInterval),\n calendar_key: z.string(),\n generated_at: z.iso.datetime({ offset: true }),\n time_zone: z.string(),\n window: zBusyWindow,\n});\n\n/**\n * CalendarSummary\n *\n * A calendar as a client website sees it.\n *\n * The trailing fields are populated only for a secret key. A public key ships\n * in a browser bundle, and the grantee's email plus our granted scopes are a\n * third party's identity and our integration posture — not the client site's\n * business.\n */\nexport const zCalendarSummary = z.strictObject({\n allow_overlaps: z.boolean(),\n calendar_key: z.string(),\n connected_at: z.nullable(z.iso.datetime({ offset: true })),\n google_account_email: z.nullable(z.string()),\n granted_scopes: z.nullable(z.array(z.string())),\n label: z.string(),\n status: z.enum([\n \"pending_invite\",\n \"awaiting_calendar_selection\",\n \"connected\",\n \"needs_reconnect\",\n \"revoked\",\n \"disabled\",\n ]),\n time_zone: z.string(),\n});\n\n/**\n * CustomLocation\n */\nexport const zCustomLocation = z.strictObject({\n kind: z.literal(\"custom\"),\n text: z.string().check(z.maxLength(1024)),\n});\n\n/**\n * ErrorCode\n *\n * Stable discriminator for a failed request. Values are contract: never rename.\n */\nexport const zErrorCode = z.enum([\n \"missing_credentials\",\n \"invalid_api_key\",\n \"not_authenticated\",\n \"wrong_key_type\",\n \"origin_missing\",\n \"origin_not_allowed\",\n \"capability_denied\",\n \"session_required\",\n \"operator_only\",\n \"cross_tenant\",\n \"business_id_required\",\n \"bad_request\",\n \"not_found\",\n \"conflict\",\n \"gone\",\n \"incompatible_payload\",\n \"repeat_confirmation_required\",\n \"idempotency_conflict\",\n \"slot_unavailable\",\n \"calendar_not_connected\",\n \"calendar_disconnected\",\n \"booking_not_found\",\n \"booking_not_cancellable\",\n \"booking_not_reschedulable\",\n \"manage_token_invalid\",\n \"google_error\",\n \"calendar_not_owned\",\n \"validation_error\",\n \"unknown_service\",\n \"invalid_sale_properties\",\n \"invalid_cursor\",\n \"rate_limited\",\n \"internal_error\",\n]);\n\n/**\n * MeetLocation\n */\nexport const zMeetLocation = z.strictObject({\n kind: z.literal(\"google_meet\"),\n});\n\n/**\n * PhoneLocation\n */\nexport const zPhoneLocation = z.strictObject({\n kind: z.literal(\"phone\"),\n phone: z.string().check(z.maxLength(64)),\n});\n\n/**\n * PhysicalLocation\n */\nexport const zPhysicalLocation = z.strictObject({\n address: z.string().check(z.maxLength(1024)),\n kind: z.literal(\"physical\"),\n});\n\n/**\n * ReminderMethod\n */\nexport const zReminderMethod = z.enum([\"email\", \"popup\"]);\n\n/**\n * BookingReminder\n */\nexport const zBookingReminder = z.strictObject({\n method: zReminderMethod,\n minutes_before: z.int().check(z.gte(0), z.lte(40320)),\n});\n\n/**\n * BookingReminders\n */\nexport const zBookingReminders = z.strictObject({\n overrides: z.optional(z.array(zBookingReminder).check(z.maxLength(5))),\n use_calendar_defaults: z._default(z.optional(z.boolean()), true),\n});\n\n/**\n * BookingCreateSchema\n */\nexport const zBookingCreateSchema = z.strictObject({\n appointment_type: z.nullish(z.string().check(z.maxLength(64))),\n attendees: z.optional(z.array(zBookingAttendee).check(z.maxLength(20))),\n buffer_after_minutes: z._default(z.optional(z.int().check(z.gte(0), z.lte(1440))), 0),\n buffer_before_minutes: z._default(z.optional(z.int().check(z.gte(0), z.lte(1440))), 0),\n calendar_key: z.string().check(z.minLength(1), z.maxLength(64)),\n customer: zBookingCustomerInput,\n date: z.nullish(z.iso.date()),\n description: z.nullish(z.string().check(z.maxLength(8000))),\n duration_minutes: z.nullish(z.int().check(z.gt(0), z.lte(1440))),\n end_at: z.nullish(z.iso.datetime({ offset: true })),\n idempotency_key: z.nullish(z.string().check(z.maxLength(128))),\n location: z.nullish(z.union([zMeetLocation, zPhysicalLocation, zPhoneLocation, zCustomLocation])),\n metadata: z.nullish(zBookingMetadataInput),\n reminders: z.nullish(zBookingReminders),\n send_invites: z._default(z.optional(z.boolean()), true),\n start_at: z.nullish(z.iso.datetime({ offset: true })),\n time_zone: z.string().check(z.minLength(1), z.maxLength(64), z.regex(/^[A-Za-z0-9_+\\-\\/]+$/)),\n title: z.nullish(z.string().check(z.maxLength(300))),\n});\n\n/**\n * ValidationError\n */\nexport const zValidationError = z.object({\n ctx: z.optional(z.record(z.string(), z.unknown())),\n input: z.optional(z.unknown()),\n loc: z.array(z.union([z.string(), z.int()])),\n msg: z.string(),\n type: z.string(),\n});\n\n/**\n * HTTPValidationError\n */\nexport const zHttpValidationError = z.object({\n detail: z.optional(z.array(zValidationError)),\n});\n\n/**\n * ValidationErrorItem\n *\n * One field-level failure from FastAPI's request validation.\n */\nexport const zValidationErrorItem = z.object({\n loc: z.array(z.union([z.string(), z.int()])),\n msg: z.string(),\n type: z.string(),\n});\n\n/**\n * ApiErrorSchema\n *\n * The body of every non-2xx response.\n */\nexport const zApiErrorSchema = z.object({\n code: zErrorCode,\n context: z._default(z.nullish(z.record(z.string(), z.unknown())), null),\n detail: z.string(),\n errors: z._default(z.nullish(z.array(zValidationErrorItem)), null),\n request_id: z._default(z.nullish(z.string()), null),\n});\n","import type { BusyInterval } from \"./schema\";\nimport { instantFromWallClock, offsetMinutesAt } from \"./time\";\n\n/**\n * Slot computation lives in the client site, not in our database.\n *\n * Opening hours, slot length, notice and buffers differ per client and per use\n * case; modelling them as dashboard config would mean every new client needs\n * data entry before their site works. The API answers only \"when is this person\n * busy\" — this pure helper turns that plus the site's own hours into offerable\n * slots.\n */\n\nexport interface OpeningInterval {\n /** Local wall-clock `HH:MM` in the calendar's zone. */\n start: string;\n end: string;\n}\n\nexport interface OpeningHours {\n /** 0 = Sunday … 6 = Saturday. A missing weekday is closed. */\n weekly: Partial<Record<number, OpeningInterval[]>>;\n /** `YYYY-MM-DD` → intervals, replacing that day's weekly rule entirely. */\n overrides?: Record<string, OpeningInterval[]>;\n /** `YYYY-MM-DD` dates that are closed regardless of the weekly rule. */\n closedDates?: string[];\n}\n\nexport interface ComputeSlotsOptions {\n busy: BusyInterval[];\n openingHours: OpeningHours;\n timeZone: string;\n durationMinutes: number;\n granularityMinutes?: number;\n minNoticeMinutes?: number;\n bufferBeforeMinutes?: number;\n bufferAfterMinutes?: number;\n windowDays?: number;\n now?: Date;\n}\n\nexport interface Slot {\n start_at: string;\n end_at: string;\n}\n\nexport interface ComputeSlotsResult {\n slots: Slot[];\n nextAvailableAt: string | null;\n}\n\nfunction parseHhMm(value: string): [number, number] {\n const [hour, minute] = value.split(\":\").map(Number);\n return [hour || 0, minute || 0];\n}\n\nfunction dateKeyOf(date: Date, timeZone: string): string {\n const fmt = new Intl.DateTimeFormat(\"en-CA\", {\n timeZone,\n year: \"numeric\",\n month: \"2-digit\",\n day: \"2-digit\",\n });\n return fmt.format(date);\n}\n\n/** Weekday of a `YYYY-MM-DD` local date, read at noon UTC so no offset can\n * shift it across a day boundary. */\nfunction weekdayFromDateKey(dateKey: string): number {\n const [year, month, day] = dateKey.split(\"-\").map(Number);\n return new Date(Date.UTC(year, month - 1, day, 12)).getUTCDay();\n}\n\n/** Midday of the local date containing `at` — a stable anchor to step from,\n * immune to a midnight DST jump. */\nfunction startOfLocalDay(at: Date, timeZone: string): Date {\n const [year, month, day] = dateKeyOf(at, timeZone).split(\"-\").map(Number);\n return instantFromWallClock(timeZone, year, month, day, 12, 0);\n}\n\nfunction overlaps(aStart: number, aEnd: number, bStart: number, bEnd: number): boolean {\n return aStart < bEnd && aEnd > bStart;\n}\n\nexport function computeSlots(options: ComputeSlotsOptions): ComputeSlotsResult {\n const {\n busy,\n openingHours,\n timeZone,\n durationMinutes,\n granularityMinutes = 15,\n minNoticeMinutes = 0,\n bufferBeforeMinutes = 0,\n bufferAfterMinutes = 0,\n windowDays = 30,\n now = new Date(),\n } = options;\n\n const earliest = now.getTime() + minNoticeMinutes * 60_000;\n const busyRanges = busy.map((interval) => [\n new Date(interval.start_at).getTime(),\n new Date(interval.end_at).getTime(),\n ]);\n\n const slots: Slot[] = [];\n // Advance by CALENDAR days, not by 86_400_000 ms. A DST day is 23 or 25 hours\n // long, so fixed-millisecond stepping skips a local date entirely at spring\n // forward and visits one twice at fall back — in America/Toronto, adding 24h\n // from 2026-03-08T04:30Z jumps straight from the 7th to the 9th.\n let cursor = startOfLocalDay(now, timeZone);\n for (let dayOffset = 0; dayOffset < windowDays; dayOffset += 1) {\n const dateKey = dateKeyOf(cursor, timeZone);\n const [year, month, day] = dateKey.split(\"-\").map(Number);\n // `Date.UTC` normalises day 32 into the next month, so this needs no\n // month-length handling.\n cursor = instantFromWallClock(timeZone, year, month, day + 1, 12, 0);\n\n if (openingHours.closedDates?.includes(dateKey)) continue;\n\n const intervals =\n openingHours.overrides?.[dateKey] ?? openingHours.weekly[weekdayFromDateKey(dateKey)] ?? [];\n for (const interval of intervals) {\n const [openHour, openMinute] = parseHhMm(interval.start);\n const [closeHour, closeMinute] = parseHhMm(interval.end);\n const opensAt = instantFromWallClock(timeZone, year, month, day, openHour, openMinute);\n const closesAt = instantFromWallClock(timeZone, year, month, day, closeHour, closeMinute);\n\n for (\n let start = opensAt.getTime();\n start + durationMinutes * 60_000 <= closesAt.getTime();\n start += granularityMinutes * 60_000\n ) {\n const end = start + durationMinutes * 60_000;\n if (start < earliest) continue;\n\n // A slot is unavailable if its BUFFERED span touches anything busy —\n // the buffers are what keep a gap between appointments.\n const blockedStart = start - bufferBeforeMinutes * 60_000;\n const blockedEnd = end + bufferAfterMinutes * 60_000;\n const taken = busyRanges.some(([busyStart, busyEnd]) =>\n overlaps(blockedStart, blockedEnd, busyStart, busyEnd),\n );\n if (taken) continue;\n\n slots.push({\n start_at: new Date(start).toISOString(),\n end_at: new Date(end).toISOString(),\n });\n }\n }\n }\n\n slots.sort((a, b) => a.start_at.localeCompare(b.start_at));\n return { slots, nextAvailableAt: slots[0]?.start_at ?? null };\n}\n\n/** Group slots by local date, the shape every date-picker UI wants. */\nexport function groupSlotsByDay(\n slots: Slot[],\n timeZone: string,\n): { date: string; slots: Slot[] }[] {\n const byDay = new Map<string, Slot[]>();\n for (const slot of slots) {\n const key = dateKeyOf(new Date(slot.start_at), timeZone);\n const bucket = byDay.get(key);\n if (bucket) bucket.push(slot);\n else byDay.set(key, [slot]);\n }\n return [...byDay.entries()]\n .map(([date, daySlots]) => ({ date, slots: daySlots }))\n .sort((a, b) => a.date.localeCompare(b.date));\n}\n\nexport { offsetMinutesAt };\n"],"mappings":"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","names":["record","iso","iso","z","durationMinutes"]}
package/dist/client.d.mts CHANGED
@@ -1,9 +1,9 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode, CSSProperties, InputHTMLAttributes, ChangeEvent, FocusEvent } from 'react';
3
- import { h as ConsentState, f as ConsentChoiceState, g as ConsentSource, T as TrackingParams, G as GtagEnvironmentMap, M as MetaPixelEnvironmentMap, b as TrackingEnvironment, k as PhoneConfig, P as ParsedPhone, l as PhoneDisplayFormat } from './phone-utils-CubQif9Z.mjs';
3
+ import { h as ConsentState, f as ConsentChoiceState, g as ConsentSource, T as TrackingParams, G as GtagEnvironmentMap, M as MetaPixelEnvironmentMap, b as TrackingEnvironment, k as PhoneConfig, P as ParsedPhone, l as PhoneDisplayFormat } from './phone-utils-C0ds6cqZ.mjs';
4
4
  import * as react_jsx_runtime from 'react/jsx-runtime';
5
- import { T as TrackingConfigReference, C as ConversionConfig } from './tracking-config-runtime-D7prdTE1.mjs';
6
- import { N as TriggerRegistryConfig, Q as TypedTrackingClient } from './ingest-typed-Bo7WPmpU.mjs';
5
+ import { T as TrackingConfigReference, C as ConversionConfig } from './tracking-config-runtime-C6H7ofmo.mjs';
6
+ import { V as TriggerRegistryConfig, X as TypedTrackingClient } from './ingest-typed-UA13xTfI.mjs';
7
7
  import { CountryCode } from 'libphonenumber-js';
8
8
  import 'zod/mini';
9
9
  import 'src';
@@ -276,6 +276,12 @@ interface CreateTrackingResult<TRegistry extends TriggerRegistryConfig> {
276
276
  * Hook that returns the registry-typed tracking client.
277
277
  */
278
278
  useTracking: () => TypedTrackingClient<TRegistry>;
279
+ /**
280
+ * Append the cross-site identity token to a URL that points at a sibling
281
+ * site of this business, for navigations the linker cannot see (`window.open`,
282
+ * `router.push`). Returns the URL unchanged for any other target.
283
+ */
284
+ decorateUrl: (url: string) => string;
279
285
  }
280
286
  /**
281
287
  * Next.js App Router version of the `createTracking()` factory. Uses
package/dist/client.d.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode, CSSProperties, InputHTMLAttributes, ChangeEvent, FocusEvent } from 'react';
3
- import { h as ConsentState, f as ConsentChoiceState, g as ConsentSource, T as TrackingParams, G as GtagEnvironmentMap, M as MetaPixelEnvironmentMap, b as TrackingEnvironment, k as PhoneConfig, P as ParsedPhone, l as PhoneDisplayFormat } from './phone-utils-CubQif9Z.js';
3
+ import { h as ConsentState, f as ConsentChoiceState, g as ConsentSource, T as TrackingParams, G as GtagEnvironmentMap, M as MetaPixelEnvironmentMap, b as TrackingEnvironment, k as PhoneConfig, P as ParsedPhone, l as PhoneDisplayFormat } from './phone-utils-C0ds6cqZ.js';
4
4
  import * as react_jsx_runtime from 'react/jsx-runtime';
5
- import { T as TrackingConfigReference, C as ConversionConfig } from './tracking-config-runtime-D7prdTE1.js';
6
- import { N as TriggerRegistryConfig, Q as TypedTrackingClient } from './ingest-typed-BdKmlouW.js';
5
+ import { T as TrackingConfigReference, C as ConversionConfig } from './tracking-config-runtime-C6H7ofmo.js';
6
+ import { V as TriggerRegistryConfig, X as TypedTrackingClient } from './ingest-typed-MBuMe9Pn.js';
7
7
  import { CountryCode } from 'libphonenumber-js';
8
8
  import 'zod/mini';
9
9
  import 'src';
@@ -276,6 +276,12 @@ interface CreateTrackingResult<TRegistry extends TriggerRegistryConfig> {
276
276
  * Hook that returns the registry-typed tracking client.
277
277
  */
278
278
  useTracking: () => TypedTrackingClient<TRegistry>;
279
+ /**
280
+ * Append the cross-site identity token to a URL that points at a sibling
281
+ * site of this business, for navigations the linker cannot see (`window.open`,
282
+ * `router.push`). Returns the URL unchanged for any other target.
283
+ */
284
+ decorateUrl: (url: string) => string;
279
285
  }
280
286
  /**
281
287
  * Next.js App Router version of the `createTracking()` factory. Uses