@aranova/tracking-next 0.27.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/blog-server.d.mts +2 -1
- package/dist/blog-server.d.ts +2 -1
- package/dist/blog-server.js +121 -98
- package/dist/blog-server.js.map +1 -1
- package/dist/blog-server.mjs +86 -63
- package/dist/blog-server.mjs.map +1 -1
- package/dist/blog.d.mts +651 -1755
- package/dist/blog.d.ts +651 -1755
- package/dist/blog.js +121 -98
- package/dist/blog.js.map +1 -1
- package/dist/blog.mjs +86 -63
- package/dist/blog.mjs.map +1 -1
- package/dist/calendar-server.d.mts +2 -2
- package/dist/calendar-server.d.ts +2 -2
- package/dist/calendar-server.js +0 -26
- package/dist/calendar-server.js.map +1 -1
- package/dist/calendar-server.mjs +0 -26
- package/dist/calendar-server.mjs.map +1 -1
- package/dist/calendar.d.mts +3 -3
- package/dist/calendar.d.ts +3 -3
- package/dist/calendar.js +227 -126
- package/dist/calendar.js.map +1 -1
- package/dist/calendar.mjs +216 -125
- package/dist/calendar.mjs.map +1 -1
- package/dist/client.d.mts +11 -4
- package/dist/client.d.ts +11 -4
- package/dist/client.js +611 -347
- package/dist/client.js.map +1 -1
- package/dist/client.mjs +611 -347
- package/dist/client.mjs.map +1 -1
- package/dist/index.d.mts +9 -8
- package/dist/index.d.ts +9 -8
- package/dist/index.js +550 -487
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +539 -487
- package/dist/index.mjs.map +1 -1
- package/dist/ingest-typed-MBuMe9Pn.d.ts +883 -0
- package/dist/ingest-typed-UA13xTfI.d.mts +883 -0
- package/dist/middleware.js +0 -467
- package/dist/middleware.js.map +1 -1
- package/dist/middleware.mjs +0 -467
- package/dist/middleware.mjs.map +1 -1
- package/dist/{phone-utils-BVzSNBf1.d.mts → phone-utils-C0ds6cqZ.d.mts} +24 -78
- package/dist/{phone-utils-BVzSNBf1.d.ts → phone-utils-C0ds6cqZ.d.ts} +24 -78
- package/dist/phone.d.mts +2 -2
- package/dist/phone.d.ts +2 -2
- package/dist/phone.js +0 -464
- package/dist/phone.js.map +1 -1
- package/dist/phone.mjs +0 -464
- package/dist/phone.mjs.map +1 -1
- package/dist/{sales-IGC2tq4J.d.ts → sales-DQC815f3.d.ts} +90 -329
- package/dist/{sales-DQGzXSgL.d.mts → sales-w1reW_Y3.d.mts} +90 -329
- package/dist/sales.d.mts +3 -3
- package/dist/sales.d.ts +3 -3
- package/dist/sales.js +270 -127
- package/dist/sales.js.map +1 -1
- package/dist/sales.mjs +259 -127
- package/dist/sales.mjs.map +1 -1
- package/dist/server.d.mts +2 -2
- package/dist/server.d.ts +2 -2
- package/dist/server.js +0 -467
- package/dist/server.js.map +1 -1
- package/dist/server.mjs +0 -467
- package/dist/server.mjs.map +1 -1
- package/dist/{tracking-config-runtime-D7prdTE1.d.mts → tracking-config-runtime-C6H7ofmo.d.mts} +6 -0
- package/dist/{tracking-config-runtime-D7prdTE1.d.ts → tracking-config-runtime-C6H7ofmo.d.ts} +6 -0
- package/dist/transport-CDmGOiCq.d.ts +253 -0
- package/dist/transport-I_cMSFQ6.d.mts +253 -0
- package/package.json +2 -2
- package/dist/ingest-typed-BxLFlZ7N.d.mts +0 -1371
- package/dist/ingest-typed-ByQVRMvP.d.ts +0 -1371
- package/dist/transport-C-_rFSR4.d.ts +0 -721
- package/dist/transport-yUF5Wize.d.mts +0 -721
package/dist/calendar.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/calendar.ts","../../tracking-core/src/capabilities.ts","../../tracking-core/src/resources/http/errors.ts","../../tracking-core/src/phone.ts","../../tracking-core/src/events/page-view.ts","../../tracking-core/src/session.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/slots.ts"],"sourcesContent":["// Browser-safe calendar SDK entry — `@aranova/tracking-next/calendar`.\n// React-free and write-free: reads take a public key. Booking writes live in\n// `./calendar-server`. Authored once in tracking-core; this is the tsup entry.\nexport * from \"../../tracking-core/src/calendar-public\";\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 \"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","// Framework-agnostic phone-number utilities — isomorphic (browser + Node/RSC),\n// zero React. Bundled `libphonenumber-js` (standard metadata) so clients add no\n// dependency. The transmitted value is ALWAYS E.164; display is the only knob.\n\nimport { AsYouType, parsePhoneNumberFromString, type CountryCode } from \"libphonenumber-js\";\n\nexport type { CountryCode };\n\n/**\n * How a phone number is shown in the UI. The transmitted value is always E.164\n * and is deliberately NOT part of this — only the display format is configurable.\n * A function form covers the long tail (`(parsed) => string`).\n */\nexport type PhoneDisplayFormat =\n | \"national\"\n | \"international\"\n | \"e164\"\n | ((parsed: ParsedPhone) => string);\n\nexport interface ParsedPhone {\n /** E.164 (`\"+14165550199\"`) or `null` when the input isn't a valid number. This is what gets transmitted. */\n e164: string | null;\n /** National display form (`\"(416) 555-0199\"`); empty string when unparseable. */\n national: string;\n /** International display form (`\"+1 416 555 0199\"`); empty string when unparseable. */\n international: string;\n /** ISO-3166 country resolved by libphonenumber, or `null`. */\n country: CountryCode | null;\n isValid: boolean;\n}\n\n/** Region assumed for numbers typed without a country code. */\nexport const DEFAULT_PHONE_COUNTRY: CountryCode = \"CA\";\n\n/** Parse a raw/display string into every representation at once (one parse → display + wire never drift). */\nexport function parsePhone(raw: string, country?: CountryCode): ParsedPhone {\n const region = country ?? DEFAULT_PHONE_COUNTRY;\n const parsed = parsePhoneNumberFromString(raw ?? \"\", region);\n if (!parsed) {\n return { e164: null, national: \"\", international: \"\", country: region, isValid: false };\n }\n const isValid = parsed.isValid();\n return {\n // E.164 is only surfaced for a *valid* number — a possible-but-invalid input\n // (e.g. too few digits) still parses but must not be transmitted.\n e164: isValid ? parsed.number : null,\n national: parsed.formatNational(),\n international: parsed.formatInternational(),\n country: parsed.country ?? region,\n isValid,\n };\n}\n\n/** Normalize any raw/display value to E.164, or `null` if it isn't a valid number. */\nexport function toE164(raw: string, country?: CountryCode): string | null {\n return parsePhone(raw, country).e164;\n}\n\n/** Format a value for display. Defaults to `'national'`. Never affects the wire value. */\nexport function formatPhone(\n value: string,\n format: PhoneDisplayFormat = \"national\",\n country?: CountryCode,\n): string {\n const parsed = parsePhone(value, country);\n if (typeof format === \"function\") return format(parsed);\n switch (format) {\n case \"international\":\n return parsed.international || value;\n case \"e164\":\n return parsed.e164 ?? value;\n case \"national\":\n default:\n return parsed.national || value;\n }\n}\n\n/** Live, incremental formatting for an `<input>` as the user types (`AsYouType`). */\nexport function formatPhoneAsTyped(raw: string, country?: CountryCode): string {\n return new AsYouType(country ?? DEFAULT_PHONE_COUNTRY).input(raw ?? \"\");\n}\n","import { z } from \"zod\";\n\n/**\n * Metadata for the automatic `page_view` event.\n *\n * The SDK emits this on initial load, SPA route changes, and bfcache restores.\n * Consumers do not call `trackEvent('page_view', ...)`; registering\n * `automatic: { page_view: {} }` enables the SDK-owned trigger.\n */\nexport const pageViewMetadataSchema = z\n .object({\n page: z\n .object({\n title: z.string().nullable(),\n path: z.string(),\n search: z.string(),\n hash: z.string(),\n })\n .strict(),\n referrer: z.string().nullable(),\n // `.nullable().optional()` — absent (undefined) OR explicit null OR a\n // real viewport object. Mirrors Pydantic's `_Viewport | None = None`\n // on the backend side so the drift test stays clean.\n viewport: z\n .object({\n w: z.number(),\n h: z.number(),\n })\n .strict()\n .nullable()\n .optional(),\n })\n .strict();\n\nexport type PageViewMetadata = z.infer<typeof pageViewMetadataSchema>;\n\n/**\n * Registration config for automatic `page_view`.\n *\n * `page_view` is required in every trigger registry and currently has no\n * options. Use `{ page_view: {} }`.\n */\nexport const pageViewConfigSchema = z.object({}).strict();\nexport type PageViewConfig = z.infer<typeof pageViewConfigSchema>;\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/**\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/**\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","// 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 { z } from \"zod\";\n\n// Mirrored by `apps/api/src/schemas/tracking_calendar.py` and pinned by the\n// event-schema drift test. Keep both sides in step.\n//\n// The location union is a plain `z.union`, deliberately: a discriminated union\n// emits `oneOf` + discriminator metadata on the Python side and `anyOf` here,\n// which would fail the drift snapshot. Both sides stay plain.\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\nconst isoInstant = z.string().datetime({ offset: true });\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 .min(1)\n .max(64)\n .regex(/^[A-Za-z0-9_+\\-/]+$/)\n .refine(isKnownTimeZone, { message: \"unknown IANA time zone\" });\n\nexport const bookingAttendeeSchema = z\n .object({\n email: z.string().max(320).email(),\n display_name: z.string().max(200).nullable().optional(),\n optional: z.boolean().default(false),\n })\n .strict();\n\nexport const bookingReminderSchema = z\n .object({\n method: z.enum([\"email\", \"popup\"]),\n minutes_before: z.number().int().min(0).max(MAX_REMINDER_MINUTES),\n })\n .strict();\n\nexport const bookingRemindersSchema = z\n .object({\n use_calendar_defaults: z.boolean().default(true),\n overrides: z.array(bookingReminderSchema).max(MAX_REMINDER_OVERRIDES).default([]),\n })\n .strict();\n\nexport const bookingLocationSchema = z.union([\n z.object({ kind: z.literal(\"google_meet\") }).strict(),\n z.object({ kind: z.literal(\"physical\"), address: z.string().max(1024) }).strict(),\n z.object({ kind: z.literal(\"phone\"), phone: z.string().max(64) }).strict(),\n z.object({ kind: z.literal(\"custom\"), text: z.string().max(1024) }).strict(),\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([...bookingLocationSchema.options, z.null()]);\n\nexport const bookingCustomerSchema = z\n .object({\n // Required: Google needs an address to send the invite that confirms the\n // booking, and that invite is the whole confirmation mechanism.\n email: z.string().max(320).email(),\n name: z.string().max(200).nullable().optional(),\n phone: z.string().max(64).nullable().optional(),\n sms_consent: z.boolean().default(false),\n })\n .strict();\n\nconst jsonScalar = z.union([z.string(), z.number(), z.boolean(), z.null()]);\n\n/**\n * Named attribution keys plus a flat escape hatch.\n *\n * `custom` is flat and scalar-only on purpose: it mirrors trivially in Pydantic,\n * stays JSONB-queryable, and bounds what a caller can push. Only `custom` is\n * ever mirrored to Google's `extendedProperties`; the named keys stay in our\n * database.\n */\nexport const bookingMetadataSchema = z\n .object({\n schema_version: z.number().int().default(CALENDAR_METADATA_SCHEMA_VERSION),\n source: z.enum(BOOKING_SOURCES).nullable().optional(),\n service: z.string().max(64).nullable().optional(),\n visitor_id: z.string().max(64).nullable().optional(),\n session_id: z.string().max(64).nullable().optional(),\n gclid: z.string().max(256).nullable().optional(),\n fbclid: z.string().max(256).nullable().optional(),\n utm_source: z.string().max(128).nullable().optional(),\n utm_medium: z.string().max(128).nullable().optional(),\n utm_campaign: z.string().max(128).nullable().optional(),\n utm_term: z.string().max(128).nullable().optional(),\n utm_content: z.string().max(128).nullable().optional(),\n landing_page: z.string().max(2048).nullable().optional(),\n referrer: z.string().max(2048).nullable().optional(),\n notes: z.string().max(2000).nullable().optional(),\n custom: z.record(jsonScalar).nullable().optional(),\n })\n .strict();\n\nfunction refineEndXor(\n value: { start_at: string; end_at?: string | null; duration_minutes?: number | null },\n ctx: z.RefinementCtx,\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 ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: \"provide exactly one of end_at or duration_minutes\",\n path: [\"end_at\"],\n });\n return;\n }\n if (hasEnd && new Date(value.end_at as string) <= new Date(value.start_at)) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: \"end_at must be after start_at\",\n path: [\"end_at\"],\n });\n }\n}\n\nexport const bookingCreateSchema = z\n .object({\n calendar_key: z.string().min(1).max(64),\n // Free-form: appointment types are the client site's own vocabulary, not\n // configuration the dashboard holds.\n appointment_type: z.string().max(64).nullable().optional(),\n start_at: isoInstant,\n end_at: isoInstant.nullable().optional(),\n duration_minutes: z.number().int().positive().max(MAX_BOOKING_MINUTES).nullable().optional(),\n /** The booker's zone — distinct from the business's and the calendar's. */\n time_zone: ianaTz,\n title: z.string().max(300).nullable().optional(),\n description: z.string().max(8000).nullable().optional(),\n location: bookingLocationNullableSchema.optional(),\n customer: bookingCustomerSchema,\n attendees: z.array(bookingAttendeeSchema).max(MAX_ATTENDEES).default([]),\n send_invites: z.boolean().default(true),\n reminders: bookingRemindersSchema.nullable().optional(),\n metadata: bookingMetadataSchema.nullable().optional(),\n idempotency_key: z.string().max(128).nullable().optional(),\n buffer_before_minutes: z.number().int().min(0).max(MAX_BOOKING_MINUTES).default(0),\n buffer_after_minutes: z.number().int().min(0).max(MAX_BOOKING_MINUTES).default(0),\n })\n .strict()\n .superRefine(refineEndXor);\n\nexport const bookingRescheduleSchema = z\n .object({\n start_at: isoInstant,\n end_at: isoInstant.nullable().optional(),\n duration_minutes: z.number().int().positive().max(MAX_BOOKING_MINUTES).nullable().optional(),\n time_zone: ianaTz,\n notify_attendees: z.boolean().default(true),\n reason: z.string().max(500).nullable().optional(),\n })\n .strict()\n .superRefine(refineEndXor);\n\nexport const bookingCancelSchema = z\n .object({\n reason: z.string().max(500).nullable().optional(),\n notify_attendees: z.boolean().default(true),\n })\n .strict();\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","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":["import_zod","durationMinutes"]}
|
|
1
|
+
{"version":3,"sources":["../src/calendar.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":["// Browser-safe calendar SDK entry — `@aranova/tracking-next/calendar`.\n// React-free and write-free: reads take a public key. Booking writes live in\n// `./calendar-server`. Authored once in tracking-core; this is the tsup entry.\nexport * from \"../../tracking-core/src/calendar-public\";\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 {\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"]}
|