/** * Consent gating — GDPR / CCPA-grade kill switches. * * Three independent dimensions, each defaulting to "granted" but * runtime-overridable: * * analytics — track(), identify(), heartbeat(), session/page auto- * emissions. Off → events drop silently, no network * calls fire. * marketing — paid-traffic click IDs (gclid/fbclid/etc) and * acquisition referrer URL. Off → these get scrubbed * before they ever land in the event bag. * errors — error / breadcrumb / Web Vitals capture. Off → no * webvitals.* events emitted, no error reporting (when * Phase 3 errors land). * * Why this granularity: real consent banners offer "Analytics", * "Marketing", "Functional" as separate boxes. The SDK has to match. * * Default state: every dimension is granted. The developer must * explicitly call `Crossdeck.consent({ analytics: false })` before * the first event to opt OUT — same convention as Google Tag Manager * Consent Mode. To start in deny mode, call `init(...)` then * immediately `consent({ analytics: false, marketing: false, errors: * false })` before any user activity. * * DNT (Do Not Track) browser header is checked once at init and * applied as an automatic deny across all dimensions when * `respectDnt: true` is set in CrossdeckOptions (default false because * the industry has effectively deprecated DNT — but opt-in support * is the polite default for privacy-first apps). */ interface ConsentState { analytics: boolean; marketing: boolean; errors: boolean; } declare class ConsentManager { private state; private dntDenied; /** GPC (navigator.globalPrivacyControl) — a legally binding opt-out in * several jurisdictions, so it is honoured ALWAYS: no flag, no opt-in. * Core enforcement, never a feature. (DNT stays behind `respectDnt` * because it is advisory and widely spoofed.) */ private gpcDenied; private identityOptInState; constructor(options?: { respectDnt?: boolean; }); /** * Set the explicit identity opt-in. DNT forces it off and locks it — once the * browser says "don't track", we don't recognise, even if code disagrees. */ setIdentityOptIn(v: boolean): void; /** Whether the visitor has explicitly opted in to being recognised. */ get identityOptIn(): boolean; /** * Merge new dimensions onto the current state. Returns the resulting * snapshot. DNT-derived denies cannot be flipped back on by a `set` * call — once the browser says "don't track", we don't track even if * the developer code disagrees. That's the contract. */ set(partial: Partial): ConsentState; /** Snapshot of the current state. */ get(): ConsentState; /** Convenience getters for hot paths. */ get analytics(): boolean; get marketing(): boolean; get errors(): boolean; /** True iff the constructor detected and applied DNT. */ get isDntDenied(): boolean; /** Either binding signal says no. */ private get signalDenied(); /** GPC — read defensively; a hostile/absent navigator must never throw. */ private detectGpc; private detectDnt; } /** * Crossdeck Consent — the branded, Stripe-premium consent surface. * * This is the CONSENT-MODE GUEST PRIMITIVE (Path B, CD-175 / CD-185): the * consent widget bundled inside the injected SDK on marketplace installs * (Webflow, Wix, Framer, WordPress, WooCommerce, Squarespace, Bubble) and the * direct install. It renders the signed-off compact bottom-left arrival banner * (Accept all / Manage / Reject all) and a four-switch Manage pane — Strictly * necessary (locked) · Analytics (anonymous) · Recognize me (identity opt-in) · * Marketing — in a style-isolated Shadow DOM, and records a withdrawable, * auditable consent choice that gates all downstream autocapture. * * Relationship to the rest of the SDK: * - It WIRES to the existing {@link ../consent ConsentManager} (analytics / * marketing dimensions) — it never forks a parallel consent state. The * NEW "Recognize me" switch drives an `identityOptIn` boolean that this * widget only RECORDS and EMITS; another module gates `identify()` on it. * - The FULL-CAPABILITY / Path-A direct build is UNAFFECTED by this file. * Error tracking, host-global monkey-patching and the Trust iframe are * deliberately absent here — this guest build is the leaner, review-safe * consent set only (CD-175 #6 / #7, CD-185 Part 2A). * * Non-negotiables baked in (Stripe / bank grade, `trust-bank-grade-no-cleverness`): * - CONSENT-FIRST — everything defaults OFF except Strictly necessary; nothing * is auto-on. No capture is implied until the visitor makes a choice. * - GENUINE CHOICE, NEVER A DARK PATTERN — refusing (Reject all) is exactly as * prominent and one-click as accepting. No pre-ticked boxes, no nag pill. * - STYLE-ISOLATED — rendered in a Shadow DOM: the host page's CSS cannot leak * in, and our CSS cannot leak out. In-page, NOT an iframe. * - AUDITABLE + WITHDRAWABLE — every choice is persisted with proof * (categories, method, timestamp, policyVersion, id) and re-openable at any * time via `[data-crossdeck-consent]` or `handle.open()`. A policyVersion * bump re-prompts (re-consent). * - POLICY-URL REQUIRED — in managed mode the widget will NOT render without * the site owner's real privacy-policy URL; it surfaces a clear diagnostic. * - DIAGNOSTIC, NEVER SILENT — risky DOM work is wrapped in `safe()`, which * logs a concise `[crossdeck] consent: …` warning rather than swallowing * the failure (Webflow finding #11). */ /** Informational "what is Crossdeck Consent" page — the attribution/acquisition link. */ declare const CONSENT_INFO_URL = "https://cross-deck.com/consent"; /** localStorage key for the auditable consent record. Origin-scoped by the browser. */ declare const CONSENT_STORAGE_KEY = "crossdeck.consent.v1"; /** * The consent choice this widget records and emits. Strictly-necessary is always * on and is NOT represented here (it is not a toggleable dimension). * * - `analytics` → maps to ConsentManager `analytics`. * - `marketing` → maps to ConsentManager `marketing`. * - `identityOptIn` → NEW. The "Recognize me" switch. NOT a ConsentManager * dimension — the site's `identify()` is gated on it by a * separate module; here it is only recorded + emitted. */ interface ConsentBannerState { analytics: boolean; marketing: boolean; identityOptIn: boolean; } /** How the visitor arrived at a choice — part of the auditable record. */ type ConsentMethod = "accept_all" | "reject_all" | "custom"; /** * The auditable, withdrawable consent record. Persisted to localStorage so the * site owner (the data controller) can demonstrate consent if challenged. */ interface ConsentRecord { /** The per-category choice. Strictly-necessary is implicit (always on). */ categories: ConsentBannerState; /** Which control produced the choice. */ method: ConsentMethod; /** Epoch ms at which the choice was recorded. */ timestamp: number; /** The site's policy version in effect when consent was given. */ policyVersion: string; /** Opaque unique id for this consent event. */ id: string; } /** * Which consent regime this install runs. `managed` = Crossdeck renders the * banner and REQUIRES the owner's `policyUrl`. Deferral to a host CMP is handled * upstream (CD-185 Part 1) — a deferred install simply does not mount this widget. */ type ConsentBannerMode = "managed"; /** Copy + tag for one category row in the Manage pane. Config is per-build. */ interface ConsentCategoryConfig { /** Row title, e.g. "Analytics". */ title: string; /** Enumerated body copy — MUST state exactly what is captured and its identity status. */ description: string; /** Optional pill next to the title, e.g. { text: "Anonymous", kind: "anon" }. */ tag?: { text: string; kind: "anon" | "locked" | "optin"; }; } /** The four-category copy set. Defaults match the signed-off Webflow build. */ interface ConsentCategoriesConfig { necessary: ConsentCategoryConfig; analytics: ConsentCategoryConfig; identity: ConsentCategoryConfig; marketing: ConsentCategoryConfig; } interface ConsentBannerOptions { /** Where to attach the widget host. Element or selector. Defaults to `document.body`. */ target?: HTMLElement | string; /** * The site owner's real privacy/cookie-policy URL. REQUIRED in managed mode — * no dead `#`, no Crossdeck-hosted stand-in. The widget refuses to render * without it and logs a diagnostic (CD-185 Part 2). */ policyUrl?: string; /** Per-category copy overrides. Omitted categories fall back to the signed-off defaults. */ categories?: Partial; /** Called on every recorded choice with the resulting state. */ onChange?: (state: ConsentBannerState) => void; /** Consent regime. Only `managed` renders a banner. Default `managed`. */ mode?: ConsentBannerMode; /** * A previously known choice (e.g. from the site owner's server). Used to seed * the widget when no localStorage record is present. If it satisfies the * current `policyVersion`, the banner stays hidden and the choice is applied. */ existingConsent?: Partial; /** * The site's current policy version. When it differs from the stored record's * version, the widget re-prompts (re-consent). Defaults to `"1"`. */ policyVersion?: string; /** * The SDK's {@link ConsentManager} (or anything with a compatible `set`). The * widget calls `.set({ analytics, marketing })` on every choice so autocapture * follows consent live. `identityOptIn` is emitted separately via `onChange`. */ consent?: Pick; /** Override the info/attribution link target (tests / self-host). */ infoUrl?: string; } interface ConsentBannerHandle { /** The host element carrying the shadow root — for layout only; never reach inside. */ readonly host: HTMLElement | null; /** Re-open the Manage pane (the withdrawal affordance). Idempotent, safe post-choice. */ open(): void; /** The current persisted consent record, or `null` if none has been recorded. */ getRecord(): ConsentRecord | null; /** The current in-memory state (defaults to all-off until a choice is recorded). */ getState(): ConsentBannerState; /** Tear down: remove the host, delegated listener, and media subscription. Idempotent. */ destroy(): void; } /** The `Crossdeck.consent` widget namespace surfaced on the SDK client. */ interface CrossdeckConsentNamespace { /** Mount the branded consent banner. See {@link mountConsentBanner}. */ banner(opts: ConsentBannerOptions): ConsentBannerHandle; } /** * Mount the Crossdeck Consent banner. Framework-agnostic and guaranteed not to * throw — any construction failure is logged and an inert handle returned so the * host app is never destabilised by our consent surface. */ declare function mountConsentBanner(opts: ConsentBannerOptions): ConsentBannerHandle; /** * Public types for @cross-deck/web. These mirror the wire format * exposed by the v1 backend API. Keep them in lockstep with * backend/src/api/v1-types.ts — same field names, same nullability. */ type Environment = "production" | "sandbox"; type Platform = "ios" | "android" | "web"; type AuditRail = "apple" | "stripe" | "google" | "manual"; interface PublicEntitlement { object: "entitlement"; key: string; isActive: boolean; validUntil?: number | null; source: { rail: AuditRail; productId: string; subscriptionId: string; }; updatedAt: number; } interface EntitlementsListResponse { object: "list"; data: PublicEntitlement[]; crossdeckCustomerId: string; env: Environment; } interface AliasResult { object: "alias_result"; crossdeckCustomerId: string; linked: Array<{ type: "developer"; id: string; } | { type: "anonymous"; id: string; }>; mergePending: boolean; env: Environment; } interface PurchaseResult { object: "purchase_result"; crossdeckCustomerId: string; env: Environment; entitlements: PublicEntitlement[]; /** True when the response came from the backend's idempotency * cache instead of fresh processing. Backend also returns * `Idempotent-Replayed: true` as a response header (v1.4.0). */ idempotent_replay?: boolean; } interface HeartbeatResponse { object: "heartbeat"; ok: true; projectId: string; appId: string; platform: Platform; env: Environment; serverTime: number; } /** * Configuration for Crossdeck.init. Three fields are mandatory — * `appId`, `publicKey`, and `environment` — per NorthStar §11.1. * * The pair of (appId, environment) is what we put on the wire envelope * (NorthStar §13.1) so the backend can correlate events against the * specific app surface and refuse mismatched env declarations loudly. */ /** * Config for the opt-in Crossdeck Consent widget (see `consentBanner`). * Types only — erased at build time, so this costs zero bytes. */ interface ConsentBannerInit { /** Where to mount. Defaults to a fixed bottom-left corner. */ target?: HTMLElement | string; /** The SITE OWNER's privacy-policy URL, linked from the widget. */ policyUrl?: string; /** Override which categories are offered. */ categories?: Partial; /** Called on every choice. */ onChange?: (state: ConsentBannerState) => void; /** * Consent-FIRST (opt-in): deny analytics + marketing until the visitor * chooses. * * Default `false` — the SDK keeps collecting and the visitor may opt OUT, * which is lawful in much of the world. Set `true` if you operate under a * regime that requires prior consent (EU/EEA: GDPR + ePrivacy). * * Crossdeck does not decide this for you. It is your site and your legal * posture; we give you the switch, not the opinion. */ denyUntilChoice?: boolean; } interface CrossdeckOptions { /** * Your Crossdeck App ID (e.g. "app_web_xxx"). Required. * * Issued in the dashboard when you create an app. Goes on the wire * envelope so the backend correlates events with the specific app * surface — useful when one project has multiple apps (web + iOS + * Android) sharing the same publishable key family. */ appId: string; /** Your Crossdeck publishable key (cd_pub_…). Required. */ publicKey: string; /** * Explicit environment declaration. Required. * * Must match the publishable key's prefix: * cd_pub_test_… → "sandbox" * cd_pub_live_… → "production" * * Mismatch is rejected at init time so a typo'd key can't silently * route prod telemetry into sandbox dashboards. */ environment: Environment; /** * Crossdeck Consent — the light switch. * * `true` (or an options object) mounts the branded Crossdeck consent * banner. Off by default: most sites already run their own CMP, so the * widget is a FEATURE you switch on, never a tax on every install. The * widget is code-split — leave this unset and you download none of it. * * The terse form is your privacy-policy URL — a consent banner must link * to one, so passing it IS the switch: * * ```js * Crossdeck.init({ * appId, publicKey, environment, * consentBanner: "https://yoursite.com/privacy", * }); * ``` * * `true` also works, and is the right form when you expect an existing CMP * to be present: we adopt its answer and render nothing. With no CMP and no * `policyUrl`, we refuse to render a policy-less banner and say so. * * Turning it on is OPT-OUT by default: the SDK keeps collecting exactly as * it does today and the visitor may decline. Crossdeck does not impose a * consent posture on your site. If you need opt-in (EU/EEA), say so * explicitly: * * ```js * consentBanner: { policyUrl: "…", denyUntilChoice: true } * ``` * * If an existing CMP (or GPC) is detected we defer to it and never render a * second banner. * * NOTE: consent ENFORCEMENT is always on regardless of this flag — * `Crossdeck.consent({ analytics: false })` is the socket any external * banner plugs into, and GPC is honoured by default. */ consentBanner?: boolean | string | ConsentBannerInit; /** * Override the API base URL. Default is https://api.cross-deck.com/v1. * Useful for self-hosted setups or pointing at the local emulator * (e.g. http://localhost:5001/crossdeck-47d8f/us-east4/v1). */ baseUrl?: string; /** * Persist anonymousId + crossdeckCustomerId across sessions. * Default: true in the browser (localStorage), false in Node (in-memory only). */ persistIdentity?: boolean; /** * Storage adapter. The SDK calls .getItem / .setItem / .removeItem. * Defaults to globalThis.localStorage when present. Pass an in-memory * adapter for Node runtimes where you want session-only persistence. */ storage?: KeyValueStorage; /** Storage key prefix for the SDK's persisted state. Default "crossdeck:". */ storagePrefix?: string; /** * Cross-subdomain identity. The anonymous-ID cookie is scoped to this domain so * a visitor is ONE person across every subdomain — your marketing site * (`example.com`) and your app (`app.example.com`) share one identity, so * first-touch source, journey, and conversion stitch into one timeline. * * - `"auto"` (default) — the registrable domain (eTLD+1), detected safely. * - a domain string (`".example.com"` / `"example.com"`) — set it explicitly. * - `"none"` — host-only (each subdomain is its own identity; pre-1.11 behaviour). * * No effect in Node/native — cookies + subdomains are browser-only. Cross-*device* * / cross-platform identity is resolved server-side by email/userId, not here. */ cookieDomain?: string; /** * Send a heartbeat to /v1/sdk/heartbeat on start(). Default true. * Disable for high-frequency boot scenarios where the heartbeat is * pure overhead. */ autoHeartbeat?: boolean; /** Maximum events buffered before forced flush. Default 20. */ eventFlushBatchSize?: number; /** Idle ms after the last track() before flushing. Default 5000. */ eventFlushIntervalMs?: number; /** Override the SDK version reported on heartbeats. Default: package version. */ sdkVersion?: string; /** * Auto-tracking. Default: every flag is `true` in browsers, all * silently no-op in Node. * * Pass `false` to disable everything, or a partial object to override * individual flags: * * Crossdeck.start({ * publicKey: "...", * autoTrack: { pageViews: false }, // sessions + deviceInfo still on * }); */ autoTrack?: boolean | Partial; /** * Your app's version (e.g. "1.2.3"). Auto-attached to every event as * `properties.appVersion` when `autoTrack.deviceInfo` is enabled. * Useful for slicing dashboards by build. */ appVersion?: string; /** * Enable verbose diagnostic logging via the NorthStar §16 debug-signal * vocabulary. Default: false. Equivalent to calling * `Crossdeck.setDebugMode(true)` after init. */ debug?: boolean; /** * Respect the browser's Do Not Track signal at init (v0.10.0+). * Default `false`. When `true` AND the user has `navigator.doNotTrack === "1"`, * the SDK boots with analytics / marketing / errors all denied — * locked off even if the developer later calls `Crossdeck.consent({...})`. * Industry has effectively deprecated DNT, but opt-in support is the * polite default for privacy-first apps. */ respectDnt?: boolean; /** * Scrub PII-shaped strings (email addresses, card numbers) from * URL paths, event properties, and acquisition referrer before they * leave the SDK. Default `true` — Stripe-grade. Disable only if your * pipeline does its own PII redaction downstream and you need the * raw strings. */ scrubPii?: boolean; /** * Run the contract self-verification suite at SDK boot. Defaults * to `true` in development (`process.env.NODE_ENV !== "production"`), * `false` in production. Pass `true` explicitly to opt-in for * production (e.g. during a staging soak); pass `false` to silence * the boot self-test in development. * * What this is: the boot self-test runs every applicable runtime * verifier against an isolated test context — `EntitlementCache`, * `deriveIdempotencyKeyForPurchase`, `crossdeckErrorFromResponse`, * etc. are exercised against synthetic state. The customer's real * SDK state is never mutated. The output proves at runtime that * the platform's structural guarantees — per-user cache isolation, * idempotency-key determinism, error-envelope shape, payload * schema-lock — actually hold, not just in Crossdeck's CI. * See `docs/contracts/index.html` for the full ledger. * * Boot-time PASS results print to the console iff * `logVerifierResults` is `true`. Boot-time FAIL results ALWAYS * print at WARN and fire `reportContractFailure(...)` to * Crossdeck's reliability channel (with `verification_phase: "boot"`) * — silencing a boot failure would defeat the purpose, since a * structural break at boot means the SDK is broken before the * customer's first user even taps. To stop the failure reporting, * use `disableContractAssertions: true`. To stop the console * passes, use `logVerifierResults: false`. The flags are * independent. */ verifyContractsAtBoot?: boolean; /** * Whether to print PASS results from the contract verifier layer * to the console (`[crossdeck.identify] ✓ per-user-cache-isolation * — slot rotated …`). Defaults to `true` in development, `false` * in production. * * Cosmetic flag — controls console output only. Failure reporting * to Crossdeck's reliability channel is NOT affected by this flag; * a contract violation always prints at WARN and always fires * `reportContractFailure(...)` regardless. To stop the reliability * reporting, use `disableContractAssertions: true` instead. * * Pass `true` in a staging or QA build to verify the SDK is * honouring its own contracts as your engineer exercises the app * — every `identify()`, `track()`, `syncPurchases()` will stream * a verifier line through the browser devtools console. */ logVerifierResults?: boolean; /** * Disable the entire contract verifier + failure-reporting layer. * Default `false`. * * When `false` (default): verifiers run on every hot-path SDK * operation (identify / track / syncPurchases / isEntitled / error * parse). PASS results are silent unless `logVerifierResults` is * `true`. FAIL results always print at WARN AND fire * `reportContractFailure(...)` to Crossdeck's reliability endpoint * over a single-fire one-way path. This is the independent- * controller flow described in Privacy Policy §6 ("Flow B"); the * payload is schema-locked to contain no end-user identifiers. * * When `true`: every verifier is disabled. The runtime continues * to behave correctly — verifiers are observers, not assertions * — but the verification + reporting layer is silent end-to-end. * No console output, no telemetry, no reliability-channel writes. * * Use this only if your sovereignty posture forbids any outbound * diagnostic telemetry to third-party controllers. This is NOT * the right tool for silencing the console — for that, set * `logVerifierResults: false` and leave this flag untouched. */ disableContractAssertions?: boolean; } /** Auto-tracking flags. See CrossdeckOptions.autoTrack. */ interface AutoTrackOptions { /** Emit `session.started` / `session.ended` automatically. Default true (browser only). */ sessions: boolean; /** Emit `page.viewed` on initial load + SPA navigation. Default true (browser only). */ pageViews: boolean; /** Auto-attach os/browser/locale/screen/etc to every event's `properties`. Default true (browser only). */ deviceInfo: boolean; /** * Click autocapture — fire `element.clicked` for every interactive * click on the page. Default true. Mixpanel/Amplitude pattern. Powers * Crossdeck's funnel-attribution USP ("clicked X then converted"). * Privacy: skips form inputs / password fields / [class~="cd-noTrack"] * subtrees. Override on individual elements with data-cd-event="custom" * or data-cd-prop-* for custom property tagging. */ clicks: boolean; /** * Web Vitals capture (v0.9.0+) — emits `webvitals.lcp`, `webvitals.inp`, * `webvitals.cls`, `webvitals.fcp`, `webvitals.ttfb` events using the * browser's `PerformanceObserver`. Defaults to true in browsers, * no-op everywhere else. Disable if you have a separate RUM provider * (DataDog, Sentry Performance) and don't want duplicates. */ webVitals: boolean; /** * Error capture (v1.0.0+) — installs window.onerror + * window.onunhandledrejection listeners, wraps fetch + XHR to catch * 5xx + network failures, ships each captured error as a Crossdeck * event (kind: error.unhandled / error.unhandledrejection / * error.handled / error.http / error.message). Errors gate on * `consent.errors`. Rate-limited per-fingerprint so a runaway loop * can't flood the queue; browser-extension noise filtered by * default. Default true in browsers, no-op everywhere else. */ errors: boolean; /** * Strip query strings + URL fragments (`#hash`) and drop the referrer from * page-view events before they leave the SDK. Default `false` (full URLs, * unchanged). Set `true` for privacy-restricted / marketplace builds where * analytics must not carry query params or referrers (Webflow requirement). */ stripUrlParams?: boolean; /** * Wrap `history.pushState`/`replaceState` to track SPA navigations as new * page views. Default `true`. Set `false` to capture only the initial page * view per load and never monkey-patch host globals — required where a host * (e.g. a marketplace guest build) forbids touching page globals. */ wrapHistory?: boolean; } /** Minimal interface for any pluggable key-value persistence. */ interface KeyValueStorage { getItem(key: string): string | null; setItem(key: string, value: string): void; removeItem(key: string): void; } /** * Identity hint + profile traits passed to identify(). * * `traits` is a free-form bag of profile data (name, plan, signupDate, * teamRole, etc.) that gets persisted on the Crossdeck customer record * and attached to every subsequent event of the identified user as * `$user.` properties for dashboard filtering. * * Like event properties, traits are validated at the SDK boundary — * functions/symbols/undefined dropped, Date / BigInt / Error coerced, * strings > 1024 chars truncated. Caller's object is never mutated. */ interface IdentifyOptions { /** Optional email to attach to the customer record. */ email?: string; /** * Optional profile traits. Examples: * `{ name: "Wes", plan: "pro", signedUpAt: "2026-05-11" }` * * Treated like event properties — values are sanitised at the SDK * boundary so a `{ avatar: , callback: () => {} }` payload * doesn't crash the alias request. Server-side, traits land on * `customers/{cdcust}.traits` (additively — existing fields are * preserved unless the new identify call overrides them). */ traits?: Record; } /** * Group context — Mixpanel-style. Identifies a customer's membership * in an organisational entity (org, account, team, workspace) so B2B * dashboards can answer "how is account X using my product". * * Attached to every event as `$groups.` until cleared via * `Crossdeck.group(type, null)`. Multiple types can coexist (e.g. * `org` + `team`) — the SDK keeps a map keyed by type. */ interface GroupTraits { [key: string]: unknown; } /** Properties payload for track(). Arbitrary key/value, JSON-serialisable, ≤ 8 KB. */ type EventProperties = Record; /** * Diagnostic snapshot returned by Crossdeck.diagnostics(). Stable shape * whether or not start() has been called — callers don't need to narrow * on `started` to read `events` or `entitlements`. Pre-start values are * sensible empties (zeros, nulls). */ interface Diagnostics { started: boolean; anonymousId: string | null; crossdeckCustomerId: string | null; developerUserId: string | null; sdkVersion: string | null; baseUrl: string | null; /** * Last `serverTime` value the SDK saw on a /sdk/heartbeat response, * along with the local clock value AT that moment. Lets dashboards * (and the developer, in debug mode) detect a wrong-system-clock * problem before it corrupts a day of analytics. Null until the * first heartbeat completes. */ clock: { /** Server's view of "now" from the last heartbeat (epoch ms). */ lastServerTime: number | null; /** Client's `Date.now()` taken at the same moment as `lastServerTime`. */ lastClientTime: number | null; /** * `lastClientTime - lastServerTime` — positive means the client * clock is AHEAD of the server. Outside ±5 minutes is suspicious * and worth surfacing to the developer. */ skewMs: number | null; }; entitlements: { count: number; lastUpdated: number; /** * True when the durable cache is knowingly serving older-than- * trustworthy data — the last refresh attempt failed (Crossdeck * unreachable) or last-known-good has aged past the staleness * window. The cache still serves last-known-good; this makes the * staleness observable instead of a silent unbounded window. */ stale: boolean; /** * Cumulative count of listener invocations that threw. Swallowed * inside the cache (a buggy consumer must not crash the SDK) but * surfaced here so developers can spot broken subscribers. */ listenerErrors: number; }; events: { buffered: number; dropped: number; inFlight: number; lastFlushAt: number; lastError: string | null; /** Consecutive flush failures since the last success. */ consecutiveFailures: number; /** * When the next retry is scheduled (epoch ms), or null if the queue * is idle / healthy. */ nextRetryAt: number | null; }; } export { type AliasResult as A, type CrossdeckOptions as C, type Diagnostics as D, type EntitlementsListResponse as E, type GroupTraits as G, type HeartbeatResponse as H, type IdentifyOptions as I, type KeyValueStorage as K, type Platform as P, type AuditRail as a, type AutoTrackOptions as b, type ConsentBannerHandle as c, type ConsentBannerMode as d, type ConsentBannerOptions as e, type ConsentBannerState as f, type ConsentCategoriesConfig as g, type ConsentCategoryConfig as h, type ConsentMethod as i, type ConsentRecord as j, type ConsentState as k, type CrossdeckConsentNamespace as l, type Environment as m, type EventProperties as n, type PublicEntitlement as o, type PurchaseResult as p, CONSENT_INFO_URL as q, CONSENT_STORAGE_KEY as r, mountConsentBanner as s };