import { o as PublicEntitlement, C as CrossdeckOptions, I as IdentifyOptions, A as AliasResult, G as GroupTraits, k as ConsentState, n as EventProperties, p as PurchaseResult, H as HeartbeatResponse, D as Diagnostics, c as ConsentBannerHandle, m as Environment, f as ConsentBannerState } from './types-DfdHJNUG.js'; import { C as CrossdeckTrustNamespace } from './trust-sji5vuxH.js'; /** * Durable last-known-good cache of the customer's entitlements. * * This cache is NOT a second source of truth. Crossdeck remains the * only source; this is the SDK's local copy of what the server last * told us — a cache that doesn't forget during a network partition. * * Durability contract (the RevenueCat model): * - Every successful server read is persisted to device storage * (localStorage, via the SDK's storage adapter). * - On SDK boot the cache hydrates from storage synchronously, so * isEntitled() answers correctly from the very first call — there * is no cold-start window where a returning Pro customer reads as * free. * - When the server is unreachable, the SDK keeps serving the last * entitlements it successfully fetched. A failed refresh never * reaches setFromList(), so it cannot clear the cache; only a * SUCCESSFUL fetch replaces it. An outage can never fail a paying * customer down to free. * - Staleness alone never returns false. Each entitlement is honoured * against its OWN validUntil instead — a time-based trial expiry * still applies even mid-partition, a still-valid Pro entitlement * rides the outage out. * - Staleness is VISIBLE, not silent. validUntil covers time-based * expiry; it does NOT cover an event-based revoke (chargeback, * refund, fraud) — that has no validUntil, so the cache would keep * serving a revoked customer through an outage. Serving them is the * right trade (don't lock real payers out), but unbounded-and- * invisible is the bug. So: once a refresh ATTEMPT fails (or the * data ages past staleAfterMs) the cache is marked stale — * isStale / freshness are surfaced in diagnostics(). It keeps * serving last-known-good; the staleness is just no longer hidden. * * The cache is wiped only on reset() (logout) and on an identity switch * — never by a TTL. * * Reactive listener API * --------------------- * `subscribe(listener)` registers a callback fired every time the cache * mutates (setFromList or clear) — the foundation for the * `useEntitlement` React hook and other framework bindings. Semantics: * - Fired AFTER the mutation, so the listener sees fresh state. * - Fire-and-forget: a throwing listener is swallowed (and counted) * so a buggy consumer can't crash the SDK or other listeners. * - Unsubscribe is idempotent. * - Listeners are NOT fired on subscribe — a caller that wants the * initial state reads isEntitled() / list() synchronously, which * work from boot thanks to hydration above. */ type EntitlementsListener = (entitlements: PublicEntitlement[]) => void; /** * Breadcrumb ring buffer — context attached to every error report. * * Sentry / Datadog / Bugsnag all ship the same idea: keep a rolling * record of the last N "things the user did" (page views, clicks, * custom events, network calls, console logs). When an error fires, * attach the buffer so the engineer reading the error can see exactly * how the user got into the broken state. The single most powerful * debugging signal in error monitoring — without breadcrumbs, errors * are stack traces with no story. * * Implementation: a circular buffer with a fixed cap. Old entries are * evicted as new ones arrive. The default cap (50) is enough to cover * ~5 minutes of typical user activity without ballooning the error * payload — Sentry uses 100 by default but the SDK is more aggressive * about size since we ship breadcrumbs over the wire with every error, * not as a separate batch. * * Privacy: breadcrumbs auto-emit from the same auto-tracking sources * as analytics events (page.viewed, element.clicked). Those already * skip password fields, form inputs, and cd-noTrack subtrees. Custom * crumbs added via Crossdeck.addBreadcrumb() pass through the same * property sanitiser as track() events. */ type BreadcrumbCategory = "navigation" | "ui.click" | "ui.input" | "http" | "console" | "custom" | "info"; type BreadcrumbLevel = "debug" | "info" | "warning" | "error"; interface Breadcrumb { /** epoch ms */ timestamp: number; category: BreadcrumbCategory; level?: BreadcrumbLevel; /** Short human-readable description. */ message?: string; /** Arbitrary key/value context for the crumb. */ data?: Record; } /** * Public, typed accessor for the bank-grade behavioural contracts * this SDK ships. The full architecture — schema, distribution, * audit loop, pillar taxonomy — lives in `contracts/README.md` * at the monorepo root. * * Why a typed surface (vs. plain JSON access): contract IDs and * pillar names are part of Crossdeck's public commitment to * customers. Reading them through `CrossdeckContracts` means the * compiler catches drift the moment a contract is renamed or * retired. Tools that consume contracts at runtime (dashboards, * AI assistants, customer integration tests) get the exact same * shape every SDK ships, with no parsing layer to drift. * * --- BINARY STABILITY --- * `Contract` is treated as an evolving — but back-compat — wire * shape. Fields may be added in any minor release. Existing * fields will not be removed or repurposed except in a major * version bump, even if all known contracts stop using them. * Customers can rely on `id`, `pillar`, `status`, `appliesTo`, * `codeRef`, `testRef`, `registeredAt`, `firstRegisteredIn`, * and `bundledIn` being present on every contract in every * future minor/patch release of this SDK. */ /** * Which bank-grade pillar a contract belongs to. The taxonomy is * deliberately small — every contract maps to exactly one. New * pillars require a Crossdeck major-version bump. */ type ContractPillar = "revenue" | "entitlements" | "analytics" | "webhooks" | "errors" | "lifecycle" | "identity"; /** * Lifecycle stage of a contract. * - `enforced`: live in this SDK and exercised by `testRef`. * - `proposed`: registered for an upcoming release; `testRef` * may point to a not-yet-existing file. * - `retired`: kept for history only; the behaviour no longer * ships. Filtered out of `CrossdeckContracts.all()` by default. */ type ContractStatus = "enforced" | "proposed" | "retired"; /** Which SDKs (and/or `backend`) a contract is binding on. */ type ContractAppliesTo = "web" | "node" | "react-native" | "swift" | "android" | "backend"; /** * Pointer to the test that exercises a contract clause. The * `name` is matched verbatim against the file's text by * `scripts/contract-audit.mjs`, so a rename without updating * the contract aborts CI. */ interface ContractTestRef { readonly file: string; readonly name: string; } /** One bank-grade behavioural guarantee — see `contracts/README.md`. */ interface Contract { readonly id: string; readonly pillar: ContractPillar; readonly status: ContractStatus; readonly claim: string; readonly appliesTo: readonly ContractAppliesTo[]; readonly codeRef: readonly string[]; readonly testRef: readonly ContractTestRef[]; /** ISO-8601 date the contract was first registered. */ readonly registeredAt: string; /** The release note / phase the contract first appeared in. Immutable. */ readonly firstRegisteredIn: string; /** The SDK release this snapshot was bundled with, stamped at build time. */ readonly bundledIn: string; /** * Whether THIS SDK self-verifies this contract at runtime (a verifier is * registered in the SDK's `STATIC_VERIFIERS` harness, emitting * `crossdeck.contract_failed` live), vs. proven by CI tests only. * * DERIVED at bundle time from the verifier registry — never hand-set — * so the registry can never disagree with what actually runs. Runtime * status is a property of (contract × SDK): the same contract can be * `true` here (web) and `false` in another SDK that lacks the harness. * * `true` → surfaces in the console as "watch it pass live". * `false` → "CI-proven every release" (still enforced, just not a * live toggle on this platform). */ readonly runtimeVerified: boolean; } /** * Typed entry point to the bank-grade contracts bundled with this * SDK release. Stable, side-effect-free, tree-shakeable. * * @example Audit at app boot * ```ts * import { CrossdeckContracts } from "@cross-deck/web"; * * for (const c of CrossdeckContracts.all()) { * console.log(`[crossdeck] ${c.id} (${c.pillar})`); * } * ``` * * @example Assert a specific clause is in force * ```ts * const isolation = CrossdeckContracts.byId("per-user-cache-isolation"); * if (!isolation || isolation.status !== "enforced") { * throw new Error("entitlement isolation contract is not enforced — refusing to start"); * } * ``` */ declare const CrossdeckContracts: { /** Every contract that applies to this SDK and is currently enforced. */ readonly all: () => readonly Contract[]; /** * Every contract bundled with this SDK release, including * `proposed` and `retired` entries. Use `all()` for the * enforced-only view. */ readonly allIncludingHistorical: () => readonly Contract[]; /** Look up a contract by its stable `id`. */ readonly byId: (id: string) => Contract | undefined; /** Every enforced contract within a pillar. */ readonly byPillar: (pillar: ContractPillar) => readonly Contract[]; /** Filter by lifecycle status. */ readonly withStatus: (status: ContractStatus) => readonly Contract[]; /** Semver of the SDK release these contracts were bundled with. */ readonly sdkVersion: "1.14.1"; /** Fully-qualified bundle identifier — e.g. `@cross-deck/web@1.4.2`. */ readonly bundledIn: "@cross-deck/web@1.14.1"; /** * Resolve a failing test back to the contract it exercises. * Used by test-framework hooks (Vitest `afterEach`, XCTest * observation, JUnit `TestWatcher`) to find the contract id of * a failed contract test so `reportContractFailure(...)` can * stamp the right `contract_id` on the emitted event. * * Match is on `testRef.name` (case-sensitive, exact). Returns * the first contract whose `testRef` list contains a matching * entry, regardless of pillar or status. */ readonly findByTestName: (name: string) => Contract | undefined; }; /** * Input to {@link Crossdeck.reportContractFailure}. Lets a test * harness / dogfood app / customer integration report a contract * violation back to Crossdeck on the dedicated reliability channel — * single-fire, never visible in the customer's dashboard. * * SCHEMA-LOCK: this interface's field set is exhaustively named. No * free-form `extra: Record` — the schema-lock * contract at * `contracts/diagnostics/contract-failed-payload-schema-lock.json` * forbids unbounded fields. Adding a field requires a PR that * amends the contract first, then the public interface. * * `sdk_version` and `sdk_platform` are auto-stamped by the SDK so * every emitted event carries them correctly without the caller * needing to read them out of `CrossdeckContracts.sdkVersion`. */ interface ContractFailureInput { /** Stable contract id (`per-user-cache-isolation` etc.). */ contractId: string; /** * Short categorical-ish label — the SDK convention is to keep this * under 128 chars and stable across runs (so dashboards can group). * Never an end-user-supplied string. */ failureReason: string; /** * Where the failure was observed: * - `ci` — the SDK's own test suite on CI * - `dogfood` — Crossdeck's internal dogfood project * - `customer-app` — a customer's app verifying contracts */ runContext: "ci" | "dogfood" | "customer-app"; /** * Stable identifier for this verification run. CI: `GITHUB_RUN_ID` * or equivalent. Dogfood: per-launch UUID. Customer app: any * stable handle the customer chooses to group fires by run. */ runId: string; /** * Optional pointer back to the failing test, for triage. The SDK * sends both `test_file` and `test_name` on the wire when set. */ testRef?: { file: string; name: string; }; /** * Optional coarse device class, e.g. "desktop", "mobile-web", * "ssr". A categorical bucket, not a device identifier. */ deviceClass?: string; } /** * Stack-trace parser — normalises Chrome / Firefox / Safari / Edge * stack strings into a common frame shape. * * Why hand-rolled, not stack-trace-js or error-stack-parser libraries: * those weigh 5–15 KB after minification and we'd be pulling in their * full feature matrix just for the parser. The patterns below cover * the four shapes any modern browser emits, totalling ~80 lines. * * The output frame shape mirrors what Sentry's `mechanism: { type: * 'generic' }` events ship, so future source-map symbolication on the * Crossdeck backend has a stable input to work against. * * Defensive: never throws. An unparseable line becomes a `raw` frame * with just the literal text. Engineers reading errors still get the * raw stack as fallback. */ interface StackFrame { /** Function name, or "?" if anonymous / unparseable. */ function: string; /** Source file URL the frame ran in. Empty when unknown. */ filename: string; /** 1-indexed line number, or 0 when unknown. */ lineno: number; /** 1-indexed column number, or 0 when unknown. */ colno: number; /** * True when the frame is in the app's own code (best-effort: * detected by URL not starting with chrome-extension://, etc.). * Helps the dashboard's "your code vs library code" view. */ in_app: boolean; /** Raw line from the stack string for debugging when parse fails. */ raw: string; } /** * Error capture — the third Crossdeck USP. * * Catches every error source the browser can hand us and ships them as * Crossdeck events. The pipeline reuses the analytics queue: * - Same durable persistence (errors survive crashes / hard closes) * - Same exponential backoff (a flapping server doesn't flood * errors past the rate limit) * - Same Idempotency-Key (duplicate batches dedup server-side) * - Same consent gate (consent.errors) * - Same PII scrub on properties before they leave * * Error sources captured (each toggleable): * 1. window.onerror — uncaught synchronous errors * 2. window.onunhandledrejection — unhandled promise rejections * 3. fetch() wrap — HTTP errors the app code didn't catch * 4. XMLHttpRequest wrap — same, for legacy XHR consumers * 5. Crossdeck.captureError(err) — manual API for try/catch blocks * 6. Crossdeck.captureMessage(msg) — non-error events you want to * surface as issues (e.g. "we hit the soft-deprecated path") * * Defensive design rules: * - The error handler must NEVER throw — if our own code crashes * while reporting an error, we'd take down the host app's error * handler too. Every callback is wrapped in try/swallow. * - Recursion guard: a `_reporting` flag prevents the SDK from * reporting its own errors recursively forever. * - Rate limited per-fingerprint: max N reports per second to defend * against runaway loops (e.g. an error in setInterval). * - Browser-extension noise is filtered by default — those errors * aren't the developer's fault and would otherwise drown the * signal. */ type ErrorLevel = "error" | "warning" | "info"; interface CapturedError { /** When the error fired (epoch ms). */ timestamp: number; /** error.unhandled, error.unhandledrejection, error.handled, error.message, error.http */ kind: "error.unhandled" | "error.unhandledrejection" | "error.handled" | "error.message" | "error.http"; level: ErrorLevel; message: string; /** The error class name when we have it (TypeError, ReferenceError, etc.) */ errorType: string | null; /** Parsed stack frames, empty when unavailable. */ frames: StackFrame[]; /** Raw stack string for fallback display. */ rawStack: string | null; /** Origin URL when available (window.onerror's `source` arg). */ filename: string | null; lineno: number | null; colno: number | null; /** djb2 hash of message + top frames — group identical errors. */ fingerprint: string; /** Snapshot of the breadcrumb buffer at the moment the error fired. */ breadcrumbs: Breadcrumb[]; /** Free-form context attached via Crossdeck.setContext(). */ context: Record; /** Free-form tags attached via Crossdeck.setTag(). */ tags: Record; /** "TypeError: x is not a function" → "TypeError" + "x is not a function". */ /** Whether the error happened during a fetch / XHR. */ http?: { url: string; method: string; status: number; statusText?: string; }; } /** * Public API surface for @cross-deck/web. * * Usage (browser): * * import { Crossdeck } from "@cross-deck/web"; * * Crossdeck.init({ * appId: "app_web_xxx", * publicKey: "cd_pub_live_…", * environment: "production", * }); * * await Crossdeck.identify("user_847"); * const ents = await Crossdeck.getEntitlements(); * if (Crossdeck.isEntitled("pro")) { * showPro(); * } * Crossdeck.track("paywall_shown", { variant: "v3" }); * * * Usage (Node): * * import { Crossdeck, MemoryStorage } from "@cross-deck/web"; * * Crossdeck.init({ * appId: "app_node_xxx", * publicKey: "cd_pub_test_…", * environment: "sandbox", * storage: new MemoryStorage(), // session-only persistence * autoHeartbeat: false, // skip the boot ping in scripts * }); */ declare class CrossdeckClient { private state; private verifiers; private verifierReporter; private verifierCtx; /** * Boot the SDK. Idempotent — calling init twice with the same options * is a no-op; calling with different options replaces the previous * configuration. * * NorthStar §11.1: signature is `Crossdeck.init({ appId, publicKey, * environment })`. The trio is validated up-front so a typo'd key or a * mismatched env fails fast at boot rather than at first event-flush. */ init(options: CrossdeckOptions): void; /** * Crossdeck Consent — mount the opt-in widget ("the light switch"). * * The import is DYNAMIC on purpose: bundlers code-split the widget, so a * customer who never flips the switch never downloads a byte of it. That is * what keeps consent a FEATURE rather than a tax on every install. * * Consent-FIRST: analytics + marketing are denied until the visitor * chooses. A banner that gates nothing is the lawsuit, not the fix. * * Never a second banner: if GPC or an existing CMP is detected we adopt its * answer, subscribe to its changes, and render nothing. * * Fail-soft: any failure here logs one diagnostic and leaves the SDK * running. Enforcement is unaffected — it lives in core, not in the widget. */ private mountConsentWidget; /** * Fetch /v1/config from the backend and apply any per-app verifier * overrides set in the dashboard, then fire the boot self-test * once with the FINAL resolved flags. * * Precedence (also documented at the call site in init()): * code option > dashboard remote config > DEBUG/RELEASE default * * Code wins so engineers retain ultimate control; dashboard is the * no-deploy operational lever for QA / staging soaks. * * Never throws — a /v1/config failure surfaces as "stick with the * synchronous defaults that init() already applied" and the boot * self-test runs only if code > default resolves true. */ private bootstrapVerifierLayerRemote; /** * @deprecated Use `init()` instead. NorthStar §4 standardised the * lifecycle method name across SDKs as `init` (formerly `start` / * `configure`). `start` will be removed in a future major version. */ start(options: CrossdeckOptions): void; /** * Campaign-arrival connect. If the landing URL carries a Crossdeck tag * (`cd_ref`), post it to the arrival endpoint so the backend binds this * anonymous session to the tagged person and pulls their integration * record (deals, pipeline) onto their journey. Browser-only; fire-and- * forget (never throws); backend-idempotent so a re-fire is harmless. */ private captureCampaignArrival; /** * Link the anonymous device to a developer-supplied user ID. Cache * the resolved Crossdeck customer for follow-up calls. * * v0.9.0+ accepts an optional `traits` bag — profile data (name, * plan, signupDate, role) persisted on the Crossdeck customer record * and queryable from dashboards. Traits are sanitised through the * same validator that gates `track()` properties, so a `{ avatar: * , onSave: () => {} }` payload can't corrupt the alias call. * * Crossdeck.identify("user_847", { * email: "wes@pinet.co.za", * traits: { name: "Wes", plan: "pro", signedUpAt: "2026-05-11" }, * }); */ identify(userId: string, options?: IdentifyOptions): Promise; /** * Register super-properties — Mixpanel pattern. Once set, every * subsequent event of THIS SDK instance carries these keys on its * properties bag automatically. * * Crossdeck.register({ plan: "pro", releaseChannel: "beta" }); * Crossdeck.track("paywall_shown"); // includes plan + releaseChannel * * Values that are `null` are deleted (the explicit "stop tracking * this key" idiom). Returns the resulting bag. * * Sanitised through `validateEventProperties` so a `{ avatar: File }` * payload can't poison the queue at flush time. */ register(properties: Record): Record; /** Remove a single super-property key. Idempotent. */ unregister(key: string): void; /** Snapshot of the current super-property bag. */ getSuperProperties(): Record; /** * Associate the current user with a group (org, team, account, etc.). * Mixpanel / Segment "Group Analytics" pattern. * * Crossdeck.group("org", "acme_inc"); * Crossdeck.group("team", "design", { headcount: 12 }); * * Once set, every subsequent event carries `$groups.: id` on * its properties bag, enabling B2B dashboards ("how is Acme using * the product"). Pass `id: null` to clear a group membership. */ group(type: string, id: string | null, traits?: GroupTraits): void; /** Snapshot of the current groups map keyed by type. */ getGroups(): Record; }>; /** * Update consent state. Three independent dimensions: * * analytics — track() + identify() + auto-emissions * marketing — paid-traffic click IDs + referrer URL on events * errors — Web Vitals + (future) error reporting * * Each defaults to `true` (granted). Pass partial state — only the * keys you provide are changed. * * Crossdeck.consent({ analytics: false }); * Crossdeck.consent({ marketing: true, errors: true }); * * DNT-derived denies cannot be flipped back on; if the browser said * "don't track" we don't track even if the developer code disagrees. */ consent(state: Partial): ConsentState; /** * Set the explicit identity opt-in — the "Recognize me" choice. Default is * `true` (core SDK identifies as normal); the consent-mode guest build sets it * `false` at boot and flips it `true` only when the visitor opts in. While * `false`, `identify()` no-ops. DNT forces it off and locks it. */ setIdentityOptIn(optedIn: boolean): void; /** Snapshot of the current consent state. */ consentStatus(): ConsentState; /** * Manually capture an error from a try/catch block. * * try { …risky… } catch (err) { * Crossdeck.captureError(err, { context: { plan: "pro" } }); * } * * The error is shipped through the same event queue as analytics * (durable, retried, rate-limited per fingerprint). Sends are gated * by `consent.errors`. Returns silently — never throws, even if the * SDK isn't initialised yet. */ captureError(error: unknown, options?: { context?: Record; tags?: Record; level?: ErrorLevel; }): void; /** * Capture a non-error event you want to surface as an issue * ("deprecated path hit", "we entered the slow code path"). Sentry * captureMessage pattern. Returns silently if not initialised. */ captureMessage(message: string, level?: ErrorLevel): void; /** * Attach a tag to every subsequent error report. Tags are key/value * strings (Sentry pattern): `setTag("flow", "checkout")` → every * error from this point on carries `tags.flow === "checkout"`. */ setTag(key: string, value: string): void; /** Bulk-set tags. Merges with existing tags. */ setTags(tags: Record): void; /** * Attach a structured context blob to every subsequent error report. * Unlike tags (flat key/value), context is a named bag of arbitrary * data: `setContext("cart", { items: 3, total: 42.99 })`. */ setContext(name: string, data: Record): void; /** * Add a custom breadcrumb to the rolling buffer. Useful for marking * domain-meaningful moments ("user opened paywall") that aren't * already auto-captured. The buffer caps at 50 entries; old ones * evict. */ addBreadcrumb(crumb: Breadcrumb): void; /** * Install a pre-send hook for errors. Return null to drop, or a * modified CapturedError to scrub / rewrite. Sentry's beforeSend * pattern — the only way to redact app-specific PII (auth tokens * in URLs, etc.) before the report leaves the browser. */ setErrorBeforeSend(hook: ((err: CapturedError) => CapturedError | null) | null): void; /** * Internal: turn a CapturedError into a Crossdeck event and enqueue * it. Goes through the same queue / persistence / consent / scrub * pipeline as analytics events. */ private reportError; /** * GDPR/CCPA "right to be forgotten" — calls the backend's * /v1/identity/forget endpoint to schedule a server-side deletion of * the customer's events and profile, then wipes all local state * (identity, entitlements, queue, super-props, persistent stores). * * Idempotent. Safe to call when no identity has been established * (it just wipes the empty local state). * * After forget() resolves, the SDK is in the same shape as if the * developer had called `Crossdeck.reset()` — a fresh anonymousId is * minted and the next session is a brand new identity-graph entry. */ forget(): Promise; /** * Read the current customer's active entitlements from the server. * Updates the local cache so subsequent isEntitled() calls answer * synchronously. */ getEntitlements(): Promise; /** * Synchronous read from the durable local cache — answers from * last-known-good. The cache hydrates from device storage on boot and * survives a Crossdeck outage, so a returning paying customer reads * true even before the session's first network round-trip. Returns * false only for a genuinely new install that has never completed a * getEntitlements(), or for an entitlement past its own validUntil. * * Throws `not_initialized` (CrossdeckError, type * `configuration_error`) if called before `Crossdeck.init()`. The * `useEntitlement` React hook and the Vue composable both swallow * this and return `false`; bare callers must guard for it (or call * after `init()` resolves). */ isEntitled(key: string): boolean; /** Snapshot of the local entitlement cache. */ listEntitlements(): PublicEntitlement[]; /** * Subscribe to entitlement-cache changes. Returns an unsubscribe fn. * * The listener is invoked AFTER the cache mutates — once after a * successful `getEntitlements()` warms it, again after `syncPurchases()` * delivers fresh entitlements, once on `reset()` to fire the empty- * cache state for logout flows, AND once on `identify()` after the * per-user cache slot rotates and re-hydrates from device storage. * * IMPORTANT — the `identify()` fire is a TRAP if you treat it as * authoritative network state. `identify()` does NOT fetch entitlements; * it switches the per-user cache slot and rehydrates from device * storage (which is empty for a brand-new install, and last-known-good * — possibly stale — for a returning user). A listener that gates a * paywall on the first fire after an identity switch will read * `false` for a paying customer on a fresh device and let them past * the gate as free. The network-truth fire is the one that follows * the next `getEntitlements()` resolution. Either call * `getEntitlements()` explicitly after `identify()`, or have your * gating code tolerate the empty-then-populated transition. * * It is NOT invoked synchronously on subscribe. Callers that need * the current state should read it via `isEntitled()` / `listEntitlements()` * inline; the listener fires only on FUTURE changes. * * This is the foundation of the `useEntitlement` React hook in * `@cross-deck/web/react` — without it, React (or SwiftUI / Compose * / Vue) would have no way to re-render when entitlements arrive * asynchronously after init. The naive pattern of calling * `Crossdeck.isEntitled("pro")` directly inside a render path * shows the empty-cache result forever; binding the result to * component state via `onEntitlementsChange` is the correct * pattern. * * Idempotent unsubscribe — calling the returned function multiple * times is safe. * * Listener errors are swallowed (a buggy listener can't crash the * SDK or other listeners). */ onEntitlementsChange(listener: EntitlementsListener): () => void; /** * Queue a telemetry event. Returns immediately — the network round- * trip happens in the background. To flush before the page unloads, * call flush(). */ /** * Emit `crossdeck.contract_failed` to the Crossdeck reliability * endpoint — single-fire, one-way, never visible in the customer's * dashboard. Goes over a dedicated HTTP path with the reliability * publishable key embedded at build time; the customer's track() * pipeline never carries `crossdeck.*` events. This is the * independent-controller flow described in Privacy Policy §6 * ("Flow B"). The wire shape is fixed by the schema-lock contract * at `contracts/diagnostics/contract-failed-payload-schema-lock.json`. * * Wire the call from a test hook, dogfood failure path, or * customer contract-verification harness; see * `contracts/README.md` for the per-test-framework hook recipes. */ reportContractFailure(input: ContractFailureInput): void; track(name: string, properties?: EventProperties): void; /** * Force-flush queued events. Useful to call from page-unload handlers. * * Pass `{ keepalive: true }` from terminal handlers (pagehide / * visibilitychange→hidden / beforeunload). The browser keeps the * request alive after the page tears down, so the final batch * actually lands instead of being cancelled with the unload. * * NorthStar §4: standard method name across all Crossdeck SDKs. */ flush(options?: { keepalive?: boolean; }): Promise; /** @deprecated Use `flush()` instead. NorthStar §4 standardised the name. */ flushEvents(): Promise; /** * Forward purchase evidence to the backend for verification + entitlement * projection. NorthStar §4 + §13 canonical name. * * Today the web SDK only supports Apple StoreKit 2 forwarding (web apps * that sit alongside an iOS app). Stripe doesn't need this method — * Stripe webhooks deliver evidence server-side without a client round-trip. */ syncPurchases(input: { rail?: "apple"; signedTransactionInfo: string; signedRenewalInfo?: string; appAccountToken?: string; }): Promise; /** @deprecated Use `syncPurchases()` instead. NorthStar §4 standardised the name. */ purchaseApple(input: { signedTransactionInfo: string; signedRenewalInfo?: string; appAccountToken?: string; }): Promise; /** * Toggle verbose diagnostic logging — NorthStar §16. When enabled, the * SDK emits a fixed vocabulary of debug signals to console.info that the * dashboard's onboarding checklist can also surface as live events. */ setDebugMode(enabled: boolean): void; /** * Send the boot heartbeat. Called automatically by start() unless * autoHeartbeat:false. Safe to call manually as a "we're still here" ping. */ heartbeat(): Promise; /** * Wipe persisted identity + entitlement cache. Use on logout. The * next pre-login session generates a fresh anonymousId and starts a * new identity-graph entry. */ reset(): void; /** * Diagnostic: current state + queue stats. Useful for the dashboard's * heartbeat row and debugging in dev. * * Returns a stable shape regardless of whether start() has been called — * callers don't need to narrow on `started` to access `events` or * `entitlements`. Pre-start values are sensible empties. */ diagnostics(): Diagnostics; /** * The stable reference to hand Stripe at checkout so the resulting * purchase attributes to THIS person. Stamp it on the Checkout Session * as `metadata.crossdeck_ref` (see docs/connect-stripe) — the platform * webhook reads it back, validates it, and attaches the subscription to * the right customer instead of stranding it on an anonymous record. * * Returns the strongest identifier the SDK currently holds, in the same * precedence the server resolves by: * crossdeckCustomerId (cdcust_…) > developerUserId > anonymousId * anonymousId is always present, so this always returns a usable, * non-empty reference — even before identify(). Identify the user as * early as you can, though, so the reference is their stable identity * and not a per-device anon id. */ getCheckoutReference(): string; /** * The device-scoped anonymous id the SDK minted on first boot and persists * across launches (stable until reset()). Public accessor so a server-to- * server flow or a block/suspension gate can pass the device identity to * POST /v1/resolve without reaching into private storage. * * Returns `null` BEFORE init() — there is no anon id yet, and a gate that * fires during early app boot should get a clean falsy, not a throw. (This is * deliberately softer than getCheckoutReference(), which requires init.) * * Note: /v1/resolve also accepts a VERIFIED identity (userId + idToken) * without an anonymousId, and that path is higher-trust — prefer it where the * user is authenticated. */ getAnonymousId(): string | null; /** * **Crossdeck Trust** — human-proof at your signup, native to the SDK. * * `Crossdeck.trust.panel({ target, onToken })` renders the branded, un-restylable * Trust panel (the same cross-origin iframe on every install) and mints a * single-use attestation. Hand the token to your server and verify it at the gate * (`crossdeck.trust.gate(...)` in @cross-deck/node). The publishable key is taken * from your `init()` — you don't pass it again. * * Fail-open by contract: if the panel can't mint (adblocker, offline, our outage), * `ready` resolves with `{ token: null }` and your signup proceeds — the server * scores the missing token. It never throws and never blocks your form. * * @example * const { ready } = Crossdeck.trust.panel({ target: "#cd-trust" }); * const result = await ready; // { token, expiresAt } | { token: null } * await createUser({ email, cdTrustToken: result.token }); */ get trust(): CrossdeckTrustNamespace; private requireStarted; /** * Build the identity query for /v1/entitlements. Priority: * crossdeckCustomerId > developerUserId > anonymousId * — matches the resolveCrossdeckCustomerId precedence on the server. */ private identityQueryParams; /** * Embed every known identity axis on the event. Earlier this returned * just the highest-priority hint (cdcust → developerUserId → anonymousId) * to keep payloads small, but that leaked into analytics: once a user * was logged in, every subsequent page.viewed shipped without * anonymousId, and `uniqExact(anonymous_id)` on the warehouse side * counted 0 visitors for the entire authenticated app. * * Bank-grade rule: the server is the single source of truth on * dedup. Send everything we know; let CH count by whichever axis * matches the question. Each field is at most 32 bytes — sending * three on every event costs ~80 bytes per request, which is * trivial compared to the analytics correctness it buys. */ private identityHintForEvent; private mintEventId; } /** Who owns the consent moment on this site. */ type ConsentOwner = "auto" | "crossdeck" | "external"; interface ConsentModeOptions { /** Client-safe publishable key (`cd_pub_…`). Ingest-only — never a secret key. */ publicKey: string; /** The Crossdeck app id the connector provisioned. */ appId: string; /** Environment. Defaults to "production" (marketplace installs are live). */ environment?: Environment; /** * The SITE OWNER's privacy-policy URL. REQUIRED — the Crossdeck-managed banner * will not render without it (it is the site owner's tool, reflecting their * policy). Ignored when we defer to an existing CMP. */ policyUrl: string; /** Where the banner mounts (element or selector). Defaults to document.body. */ target?: HTMLElement | string; /** Identity cookie scope. Defaults to "none" (host-only) for the guest build. */ cookieDomain?: string; /** * Who manages consent. "auto" (default) → render the Crossdeck banner only if * no other CMP is detected. "crossdeck" → always render ours. "external" → * always defer, never render ours. */ consentOwner?: ConsentOwner; /** Called whenever the visitor's choice changes. */ onChange?: (state: ConsentBannerState) => void; /** The Crossdeck client to drive. Defaults to the singleton. */ client?: CrossdeckClient; } interface ConsentModeHandle { /** The mounted banner, or null when we deferred to an existing CMP. */ banner: ConsentBannerHandle | null; /** The consent mechanism we deferred to (its label), or null if we own it. */ deferredTo: string | null; } /** * Boot Crossdeck in consent-mode and wire the widget + co-existence. One call. */ declare function startConsentMode(opts: ConsentModeOptions): ConsentModeHandle; /** * Consent co-existence — "defer, never double up" (CD-185, PART 1). * * Crossdeck Consent is a branded banner bundled into the injected SDK. It must * NEVER render a second consent banner on a site that already manages consent — * two banners is a poor-experience marketplace auto-reject. On init we detect any * existing consent mechanism; if one is present we do NOT render our banner and * instead READ its signal and gate autocapture off it. * * Detection order (first match wins → defer & read): * 1. Explicit site-owner config — "I use my own CMP" → read the configured source. * 2. GPC — navigator.globalPrivacyControl; a legally-binding opt-out in several * US states → ALWAYS honour (force marketing + analytics OFF). * 3. IAB TCF v2.2 — window.__tcfapi → subscribe, map purpose consents. * 4. IAB GPP — window.__gpp (supersedes US Privacy __uspapi) → read US signals. * 5. Google Consent Mode v2 — analytics_storage / ad_storage / ad_user_data in * the dataLayer → read. * 6. Known CMP plugins — Cookiebot, OneTrust, CookieYes, Osano, Termly. * 7. Platform-native — Webflow / Wix / Squarespace cookie banners. * * DESIGN CONTRACT (mirrors trust.ts): * - ISOLATED — every global / DOM / postMessage touch is wrapped in `safe()`; a * stumble in a detector can never throw into the host page or the rest of the SDK. * - CONSERVATIVE — external purposes map to our {analytics, marketing} categories * "when in doubt, deny." A detected-but-unreadable CMP defers (no banner) AND * keeps capture OFF until we can positively read a grant. * - SUBSCRIBE, DON'T SNAPSHOT — where the source emits changes (TCF / GPP / * Consent Mode / Cookiebot / OneTrust) we subscribe and update capture live when * the visitor changes their choice in the OTHER tool. Snapshot-only sources * return a no-op unsubscribe. * * API-VERIFICATION LEDGER ("pack your bags" — confirmed against current docs): * - GPC CONFIRMED — https://globalprivacycontrol.github.io/gpc-spec/ * - TCF v2.2 CONFIRMED — IAB Tech Lab CMP API v2 (__tcfapi signature, * addEventListener/removeEventListener, eventStatus, * purpose.consents): * https://github.com/InteractiveAdvertisingBureau/GDPR-Transparency-and-Consent-Framework/blob/master/TCFv2/IAB%20Tech%20Lab%20-%20CMP%20API%20v2.md * Purpose IDs 1-10: https://support.didomi.io/iab-tcf-v2.2-purposes/features-summary * - Cookiebot CONFIRMED — Cookiebot.consent.{statistics,marketing} + * CookiebotOnConsentReady/OnAccept/OnDecline events: * https://www.cookiebot.com/en/developer/ * - OneTrust CONFIRMED — window.OnetrustActiveGroups (',C0002,'/',C0004,') * + OneTrust.OnConsentChanged: * https://developer.onetrust.com/onetrust/docs/javascript-api * - IAB GPP NEEDS-FINAL-VERIFICATION — __gpp(command,cb,parameter) signature + * ping/addEventListener/getSection confirmed * (https://github.com/InteractiveAdvertisingBureau/Global-Privacy-Platform/blob/main/Core/CMP%20API%20Specification.md), * but the per-section US-state FIELD layouts (uspv1 / * usnat / usca) vary — confirm field names before GA. * - Google Consent Mode NEEDS-FINAL-VERIFICATION — gtag('consent','default'|'update',{...}) * and the storage keys are confirmed * (https://developers.google.com/tag-platform/security/guides/consent), * BUT Google publishes NO official read/subscribe API * for current consent state. We scan `dataLayer` and * chain `dataLayer.push` (non-destructive, reversible). * Re-confirm against the gtag reference before GA. * - Osano / CookieYes / Termly NEEDS-FINAL-VERIFICATION — presence-detected only; we do * NOT fabricate a read API, so a detected instance defers * and stays conservative-deny until a verified reader lands. * - Webflow / Wix / Squarespace native NEEDS-FINAL-VERIFICATION — presence-detected only, * same conservative-deny handling as above. */ /** The consent mechanisms we can detect and defer to. */ type ConsentMechanism = "site-config" | "gpc" | "tcf-v2" | "gpp" | "google-consent-mode" | "cookiebot" | "onetrust" | "cookieyes" | "osano" | "termly" | "webflow-native" | "wix-native" | "squarespace-native"; /** How confident we are in the read/subscribe wiring for a mechanism. */ type VerificationStatus = "confirmed" | "needs-verification"; /** * A detected existing consent mechanism. If `detectExistingConsent` returns one of * these, Crossdeck Consent MUST NOT render its own banner — read `read()` for the * current mapped grant and (if `emitsChanges`) call `subscribeToExternalConsent` to * follow live changes. */ interface ExistingConsentSource { /** Which mechanism was detected. */ readonly mechanism: ConsentMechanism; /** Human-readable label for logs / the "deferring to X" disclosure. */ readonly source: string; /** * Snapshot read of the source, mapped conservatively to our categories. Only the * keys the source speaks to are present; absent keys leave our defaults untouched. * GPC returns { analytics:false, marketing:false }. */ read(): Partial; /** True iff `subscribeToExternalConsent` can deliver live updates for this source. */ readonly emitsChanges: boolean; /** * Categories this source FORCES off regardless of anything else (GPC → analytics + * marketing). Enforced on top of `read()` — a legally-binding opt-out cannot be * flipped back on by any later signal. */ readonly forcesDeny?: ReadonlyArray; /** API-confirmation status for this mechanism (see the file-top ledger). */ readonly verification: VerificationStatus; } /** Options for {@link detectExistingConsent}. */ interface DetectExistingConsentOptions { /** * The site owner explicitly declared (at install) that they run their own consent * tool ("I use my own CMP"). Highest priority — we defer unconditionally. */ ownConsentTool?: boolean; /** * Optional reader for the owner's configured source, used only when `ownConsentTool` * is set. Returns their current grant mapped to our categories. If omitted we defer * (render nothing) and stay conservative-deny until the owner wires a reader. */ readOwnConsent?: () => Partial; /** * Global scope to probe. Defaults to the real window. Injectable for tests and for * SSR guards (pass a plain object to probe nothing). */ scope?: ConsentGlobals; } /** IAB TCF v2.2 CMP API. Signature: __tcfapi(command, version, callback, parameter?). */ type TcfApi = (command: string, version: number, callback: (data: unknown, success: boolean) => void, parameter?: unknown) => void; /** IAB GPP CMP API. Signature: __gpp(command, callback, parameter?, version?). */ type GppApi = (command: string, callback: (data: unknown, success: boolean) => void, parameter?: unknown, version?: number) => void; /** Cookiebot's window.Cookiebot object (the fields we read). */ interface CookiebotGlobal { consent?: { necessary?: boolean; preferences?: boolean; statistics?: boolean; marketing?: boolean; }; consented?: boolean; } /** OneTrust's window.OneTrust object (the change hook we use). */ interface OneTrustGlobal { OnConsentChanged?: (callback: (event: unknown) => void) => void; } /** * The globals we probe. Everything is optional — presence is the detection signal. * Kept as one interface so `scope` is fully typed with no `any`. */ interface ConsentGlobals { navigator?: Navigator & { globalPrivacyControl?: boolean; }; __tcfapi?: TcfApi; __gpp?: GppApi; dataLayer?: unknown[]; Cookiebot?: CookiebotGlobal; OneTrust?: OneTrustGlobal; OnetrustActiveGroups?: string; OptanonActiveGroups?: string; Osano?: unknown; CookieYes?: unknown; cookieyes?: unknown; getCkyConsent?: unknown; Termly?: unknown; Webflow?: unknown; consentPolicyManager?: unknown; Static?: unknown; addEventListener?: Window["addEventListener"]; removeEventListener?: Window["removeEventListener"]; } /** * Detect the first existing consent mechanism on the page, in the CD-185 priority * order. Returns the source to defer to, or `null` if nothing is present (in which * case Crossdeck Consent may render its own banner, subject to Part 3's "who owns * consent" choice). * * Guaranteed not to throw — any probe failure is swallowed and treated as "absent". */ declare function detectExistingConsent(opts?: DetectExistingConsentOptions): ExistingConsentSource | null; /** * Subscribe to a source that emits changes and forward each new mapped grant to `cb`. * Returns an unsubscribe function. Snapshot-only sources (GPC, site-config, and any * presence-only `deferUnread` source) return a no-op unsubscribe. * * Guaranteed not to throw — wiring failures degrade to a no-op unsubscribe. */ declare function subscribeToExternalConsent(source: ExistingConsentSource, cb: (state: Partial) => void, opts?: { scope?: ConsentGlobals; }): () => void; export { type Breadcrumb as B, CrossdeckClient as C, type DetectExistingConsentOptions as D, type ErrorLevel as E, type StackFrame as S, type VerificationStatus as V, type BreadcrumbCategory as a, type BreadcrumbLevel as b, type CapturedError as c, type ConsentGlobals as d, type ConsentMechanism as e, type ConsentModeHandle as f, type ConsentModeOptions as g, type ConsentOwner as h, type Contract as i, type ContractAppliesTo as j, type ContractFailureInput as k, type ContractPillar as l, type ContractStatus as m, type ContractTestRef as n, CrossdeckContracts as o, type ExistingConsentSource as p, detectExistingConsent as q, subscribeToExternalConsent as r, startConsentMode as s };