@cross-deck/web 1.5.0 → 1.5.1

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.
@@ -0,0 +1,938 @@
1
+ import { P as PublicEntitlement, C as CrossdeckOptions, I as IdentifyOptions, A as AliasResult, G as GroupTraits, E as EventProperties, a as PurchaseResult, H as HeartbeatResponse, D as Diagnostics, K as KeyValueStorage } from './types-Bu3jbmdq.js';
2
+ export { b as AuditRail, c as AutoTrackOptions, d as EntitlementsListResponse, e as Environment, f as Platform } from './types-Bu3jbmdq.js';
3
+
4
+ /**
5
+ * Durable last-known-good cache of the customer's entitlements.
6
+ *
7
+ * This cache is NOT a second source of truth. Crossdeck remains the
8
+ * only source; this is the SDK's local copy of what the server last
9
+ * told us — a cache that doesn't forget during a network partition.
10
+ *
11
+ * Durability contract (the RevenueCat model):
12
+ * - Every successful server read is persisted to device storage
13
+ * (localStorage, via the SDK's storage adapter).
14
+ * - On SDK boot the cache hydrates from storage synchronously, so
15
+ * isEntitled() answers correctly from the very first call — there
16
+ * is no cold-start window where a returning Pro customer reads as
17
+ * free.
18
+ * - When the server is unreachable, the SDK keeps serving the last
19
+ * entitlements it successfully fetched. A failed refresh never
20
+ * reaches setFromList(), so it cannot clear the cache; only a
21
+ * SUCCESSFUL fetch replaces it. An outage can never fail a paying
22
+ * customer down to free.
23
+ * - Staleness alone never returns false. Each entitlement is honoured
24
+ * against its OWN validUntil instead — a time-based trial expiry
25
+ * still applies even mid-partition, a still-valid Pro entitlement
26
+ * rides the outage out.
27
+ * - Staleness is VISIBLE, not silent. validUntil covers time-based
28
+ * expiry; it does NOT cover an event-based revoke (chargeback,
29
+ * refund, fraud) — that has no validUntil, so the cache would keep
30
+ * serving a revoked customer through an outage. Serving them is the
31
+ * right trade (don't lock real payers out), but unbounded-and-
32
+ * invisible is the bug. So: once a refresh ATTEMPT fails (or the
33
+ * data ages past staleAfterMs) the cache is marked stale —
34
+ * isStale / freshness are surfaced in diagnostics(). It keeps
35
+ * serving last-known-good; the staleness is just no longer hidden.
36
+ *
37
+ * The cache is wiped only on reset() (logout) and on an identity switch
38
+ * — never by a TTL.
39
+ *
40
+ * Reactive listener API
41
+ * ---------------------
42
+ * `subscribe(listener)` registers a callback fired every time the cache
43
+ * mutates (setFromList or clear) — the foundation for the
44
+ * `useEntitlement` React hook and other framework bindings. Semantics:
45
+ * - Fired AFTER the mutation, so the listener sees fresh state.
46
+ * - Fire-and-forget: a throwing listener is swallowed (and counted)
47
+ * so a buggy consumer can't crash the SDK or other listeners.
48
+ * - Unsubscribe is idempotent.
49
+ * - Listeners are NOT fired on subscribe — a caller that wants the
50
+ * initial state reads isEntitled() / list() synchronously, which
51
+ * work from boot thanks to hydration above.
52
+ */
53
+
54
+ type EntitlementsListener = (entitlements: PublicEntitlement[]) => void;
55
+
56
+ /**
57
+ * Consent gating — GDPR / CCPA-grade kill switches.
58
+ *
59
+ * Three independent dimensions, each defaulting to "granted" but
60
+ * runtime-overridable:
61
+ *
62
+ * analytics — track(), identify(), heartbeat(), session/page auto-
63
+ * emissions. Off → events drop silently, no network
64
+ * calls fire.
65
+ * marketing — paid-traffic click IDs (gclid/fbclid/etc) and
66
+ * acquisition referrer URL. Off → these get scrubbed
67
+ * before they ever land in the event bag.
68
+ * errors — error / breadcrumb / Web Vitals capture. Off → no
69
+ * webvitals.* events emitted, no error reporting (when
70
+ * Phase 3 errors land).
71
+ *
72
+ * Why this granularity: real consent banners offer "Analytics",
73
+ * "Marketing", "Functional" as separate boxes. The SDK has to match.
74
+ *
75
+ * Default state: every dimension is granted. The developer must
76
+ * explicitly call `Crossdeck.consent({ analytics: false })` before
77
+ * the first event to opt OUT — same convention as Google Tag Manager
78
+ * Consent Mode. To start in deny mode, call `init(...)` then
79
+ * immediately `consent({ analytics: false, marketing: false, errors:
80
+ * false })` before any user activity.
81
+ *
82
+ * DNT (Do Not Track) browser header is checked once at init and
83
+ * applied as an automatic deny across all dimensions when
84
+ * `respectDnt: true` is set in CrossdeckOptions (default false because
85
+ * the industry has effectively deprecated DNT — but opt-in support
86
+ * is the polite default for privacy-first apps).
87
+ */
88
+ interface ConsentState {
89
+ analytics: boolean;
90
+ marketing: boolean;
91
+ errors: boolean;
92
+ }
93
+
94
+ /**
95
+ * Breadcrumb ring buffer — context attached to every error report.
96
+ *
97
+ * Sentry / Datadog / Bugsnag all ship the same idea: keep a rolling
98
+ * record of the last N "things the user did" (page views, clicks,
99
+ * custom events, network calls, console logs). When an error fires,
100
+ * attach the buffer so the engineer reading the error can see exactly
101
+ * how the user got into the broken state. The single most powerful
102
+ * debugging signal in error monitoring — without breadcrumbs, errors
103
+ * are stack traces with no story.
104
+ *
105
+ * Implementation: a circular buffer with a fixed cap. Old entries are
106
+ * evicted as new ones arrive. The default cap (50) is enough to cover
107
+ * ~5 minutes of typical user activity without ballooning the error
108
+ * payload — Sentry uses 100 by default but the SDK is more aggressive
109
+ * about size since we ship breadcrumbs over the wire with every error,
110
+ * not as a separate batch.
111
+ *
112
+ * Privacy: breadcrumbs auto-emit from the same auto-tracking sources
113
+ * as analytics events (page.viewed, element.clicked). Those already
114
+ * skip password fields, form inputs, and cd-noTrack subtrees. Custom
115
+ * crumbs added via Crossdeck.addBreadcrumb() pass through the same
116
+ * property sanitiser as track() events.
117
+ */
118
+ type BreadcrumbCategory = "navigation" | "ui.click" | "ui.input" | "http" | "console" | "custom" | "info";
119
+ type BreadcrumbLevel = "debug" | "info" | "warning" | "error";
120
+ interface Breadcrumb {
121
+ /** epoch ms */
122
+ timestamp: number;
123
+ category: BreadcrumbCategory;
124
+ level?: BreadcrumbLevel;
125
+ /** Short human-readable description. */
126
+ message?: string;
127
+ /** Arbitrary key/value context for the crumb. */
128
+ data?: Record<string, unknown>;
129
+ }
130
+
131
+ /**
132
+ * Public, typed accessor for the bank-grade behavioural contracts
133
+ * this SDK ships. The full architecture — schema, distribution,
134
+ * audit loop, pillar taxonomy — lives in `contracts/README.md`
135
+ * at the monorepo root.
136
+ *
137
+ * Why a typed surface (vs. plain JSON access): contract IDs and
138
+ * pillar names are part of Crossdeck's public commitment to
139
+ * customers. Reading them through `CrossdeckContracts` means the
140
+ * compiler catches drift the moment a contract is renamed or
141
+ * retired. Tools that consume contracts at runtime (dashboards,
142
+ * AI assistants, customer integration tests) get the exact same
143
+ * shape every SDK ships, with no parsing layer to drift.
144
+ *
145
+ * --- BINARY STABILITY ---
146
+ * `Contract` is treated as an evolving — but back-compat — wire
147
+ * shape. Fields may be added in any minor release. Existing
148
+ * fields will not be removed or repurposed except in a major
149
+ * version bump, even if all known contracts stop using them.
150
+ * Customers can rely on `id`, `pillar`, `status`, `appliesTo`,
151
+ * `codeRef`, `testRef`, `registeredAt`, `firstRegisteredIn`,
152
+ * and `bundledIn` being present on every contract in every
153
+ * future minor/patch release of this SDK.
154
+ */
155
+ /**
156
+ * Which bank-grade pillar a contract belongs to. The taxonomy is
157
+ * deliberately small — every contract maps to exactly one. New
158
+ * pillars require a Crossdeck major-version bump.
159
+ */
160
+ type ContractPillar = "revenue" | "entitlements" | "analytics" | "webhooks" | "errors" | "lifecycle" | "identity";
161
+ /**
162
+ * Lifecycle stage of a contract.
163
+ * - `enforced`: live in this SDK and exercised by `testRef`.
164
+ * - `proposed`: registered for an upcoming release; `testRef`
165
+ * may point to a not-yet-existing file.
166
+ * - `retired`: kept for history only; the behaviour no longer
167
+ * ships. Filtered out of `CrossdeckContracts.all()` by default.
168
+ */
169
+ type ContractStatus = "enforced" | "proposed" | "retired";
170
+ /** Which SDKs (and/or `backend`) a contract is binding on. */
171
+ type ContractAppliesTo = "web" | "node" | "react-native" | "swift" | "android" | "backend";
172
+ /**
173
+ * Pointer to the test that exercises a contract clause. The
174
+ * `name` is matched verbatim against the file's text by
175
+ * `scripts/contract-audit.mjs`, so a rename without updating
176
+ * the contract aborts CI.
177
+ */
178
+ interface ContractTestRef {
179
+ readonly file: string;
180
+ readonly name: string;
181
+ }
182
+ /** One bank-grade behavioural guarantee — see `contracts/README.md`. */
183
+ interface Contract {
184
+ readonly id: string;
185
+ readonly pillar: ContractPillar;
186
+ readonly status: ContractStatus;
187
+ readonly claim: string;
188
+ readonly appliesTo: readonly ContractAppliesTo[];
189
+ readonly codeRef: readonly string[];
190
+ readonly testRef: readonly ContractTestRef[];
191
+ /** ISO-8601 date the contract was first registered. */
192
+ readonly registeredAt: string;
193
+ /** The release note / phase the contract first appeared in. Immutable. */
194
+ readonly firstRegisteredIn: string;
195
+ /** The SDK release this snapshot was bundled with, stamped at build time. */
196
+ readonly bundledIn: string;
197
+ }
198
+ /**
199
+ * Typed entry point to the bank-grade contracts bundled with this
200
+ * SDK release. Stable, side-effect-free, tree-shakeable.
201
+ *
202
+ * @example Audit at app boot
203
+ * ```ts
204
+ * import { CrossdeckContracts } from "@cross-deck/web";
205
+ *
206
+ * for (const c of CrossdeckContracts.all()) {
207
+ * console.log(`[crossdeck] ${c.id} (${c.pillar})`);
208
+ * }
209
+ * ```
210
+ *
211
+ * @example Assert a specific clause is in force
212
+ * ```ts
213
+ * const isolation = CrossdeckContracts.byId("per-user-cache-isolation");
214
+ * if (!isolation || isolation.status !== "enforced") {
215
+ * throw new Error("entitlement isolation contract is not enforced — refusing to start");
216
+ * }
217
+ * ```
218
+ */
219
+ declare const CrossdeckContracts: {
220
+ /** Every contract that applies to this SDK and is currently enforced. */
221
+ readonly all: () => readonly Contract[];
222
+ /**
223
+ * Every contract bundled with this SDK release, including
224
+ * `proposed` and `retired` entries. Use `all()` for the
225
+ * enforced-only view.
226
+ */
227
+ readonly allIncludingHistorical: () => readonly Contract[];
228
+ /** Look up a contract by its stable `id`. */
229
+ readonly byId: (id: string) => Contract | undefined;
230
+ /** Every enforced contract within a pillar. */
231
+ readonly byPillar: (pillar: ContractPillar) => readonly Contract[];
232
+ /** Filter by lifecycle status. */
233
+ readonly withStatus: (status: ContractStatus) => readonly Contract[];
234
+ /** Semver of the SDK release these contracts were bundled with. */
235
+ readonly sdkVersion: "1.5.0";
236
+ /** Fully-qualified bundle identifier — e.g. `@cross-deck/web@1.4.2`. */
237
+ readonly bundledIn: "@cross-deck/web@1.5.0";
238
+ /**
239
+ * Resolve a failing test back to the contract it exercises.
240
+ * Used by test-framework hooks (Vitest `afterEach`, XCTest
241
+ * observation, JUnit `TestWatcher`) to find the contract id of
242
+ * a failed contract test so `reportContractFailure(...)` can
243
+ * stamp the right `contract_id` on the emitted event.
244
+ *
245
+ * Match is on `testRef.name` (case-sensitive, exact). Returns
246
+ * the first contract whose `testRef` list contains a matching
247
+ * entry, regardless of pillar or status.
248
+ */
249
+ readonly findByTestName: (name: string) => Contract | undefined;
250
+ };
251
+ /**
252
+ * Input to {@link Crossdeck.reportContractFailure}. Lets a test
253
+ * harness / dogfood app / customer integration report a contract
254
+ * violation back to Crossdeck through the standard `track()` API
255
+ * with a typed property bag — no new ingest endpoint, no bespoke
256
+ * code, just a `crossdeck.contract_failed` custom event.
257
+ *
258
+ * `sdk_version` and `sdk_platform` are auto-stamped by the SDK so
259
+ * every emitted event carries them correctly without the caller
260
+ * needing to read them out of `CrossdeckContracts.sdkVersion`.
261
+ */
262
+ interface ContractFailureInput {
263
+ /** Stable contract id (`per-user-cache-isolation` etc.). */
264
+ contractId: string;
265
+ /** Human-readable failure reason — the assertion message. */
266
+ failureReason: string;
267
+ /**
268
+ * Where the failure was observed:
269
+ * - `ci` — the SDK's own test suite on CI
270
+ * - `dogfood` — Crossdeck's internal dogfood project
271
+ * - `customer-app` — a customer's app verifying contracts
272
+ */
273
+ runContext: "ci" | "dogfood" | "customer-app";
274
+ /**
275
+ * Stable identifier for this verification run. CI: `GITHUB_RUN_ID`
276
+ * or equivalent. Dogfood: per-launch UUID. Customer app: any
277
+ * stable handle the customer chooses to group fires by run.
278
+ */
279
+ runId: string;
280
+ /**
281
+ * Optional pointer back to the failing test, for triage. The SDK
282
+ * sends both `test_file` and `test_name` on the wire when set.
283
+ */
284
+ testRef?: {
285
+ file: string;
286
+ name: string;
287
+ };
288
+ /** Optional extra properties merged into the event payload. */
289
+ extra?: Record<string, unknown>;
290
+ }
291
+
292
+ /**
293
+ * Stack-trace parser — normalises Chrome / Firefox / Safari / Edge
294
+ * stack strings into a common frame shape.
295
+ *
296
+ * Why hand-rolled, not stack-trace-js or error-stack-parser libraries:
297
+ * those weigh 5–15 KB after minification and we'd be pulling in their
298
+ * full feature matrix just for the parser. The patterns below cover
299
+ * the four shapes any modern browser emits, totalling ~80 lines.
300
+ *
301
+ * The output frame shape mirrors what Sentry's `mechanism: { type:
302
+ * 'generic' }` events ship, so future source-map symbolication on the
303
+ * Crossdeck backend has a stable input to work against.
304
+ *
305
+ * Defensive: never throws. An unparseable line becomes a `raw` frame
306
+ * with just the literal text. Engineers reading errors still get the
307
+ * raw stack as fallback.
308
+ */
309
+ interface StackFrame {
310
+ /** Function name, or "?" if anonymous / unparseable. */
311
+ function: string;
312
+ /** Source file URL the frame ran in. Empty when unknown. */
313
+ filename: string;
314
+ /** 1-indexed line number, or 0 when unknown. */
315
+ lineno: number;
316
+ /** 1-indexed column number, or 0 when unknown. */
317
+ colno: number;
318
+ /**
319
+ * True when the frame is in the app's own code (best-effort:
320
+ * detected by URL not starting with chrome-extension://, etc.).
321
+ * Helps the dashboard's "your code vs library code" view.
322
+ */
323
+ in_app: boolean;
324
+ /** Raw line from the stack string for debugging when parse fails. */
325
+ raw: string;
326
+ }
327
+
328
+ /**
329
+ * Error capture — the third Crossdeck USP.
330
+ *
331
+ * Catches every error source the browser can hand us and ships them as
332
+ * Crossdeck events. The pipeline reuses the analytics queue:
333
+ * - Same durable persistence (errors survive crashes / hard closes)
334
+ * - Same exponential backoff (a flapping server doesn't flood
335
+ * errors past the rate limit)
336
+ * - Same Idempotency-Key (duplicate batches dedup server-side)
337
+ * - Same consent gate (consent.errors)
338
+ * - Same PII scrub on properties before they leave
339
+ *
340
+ * Error sources captured (each toggleable):
341
+ * 1. window.onerror — uncaught synchronous errors
342
+ * 2. window.onunhandledrejection — unhandled promise rejections
343
+ * 3. fetch() wrap — HTTP errors the app code didn't catch
344
+ * 4. XMLHttpRequest wrap — same, for legacy XHR consumers
345
+ * 5. Crossdeck.captureError(err) — manual API for try/catch blocks
346
+ * 6. Crossdeck.captureMessage(msg) — non-error events you want to
347
+ * surface as issues (e.g. "we hit the soft-deprecated path")
348
+ *
349
+ * Defensive design rules:
350
+ * - The error handler must NEVER throw — if our own code crashes
351
+ * while reporting an error, we'd take down the host app's error
352
+ * handler too. Every callback is wrapped in try/swallow.
353
+ * - Recursion guard: a `_reporting` flag prevents the SDK from
354
+ * reporting its own errors recursively forever.
355
+ * - Rate limited per-fingerprint: max N reports per second to defend
356
+ * against runaway loops (e.g. an error in setInterval).
357
+ * - Browser-extension noise is filtered by default — those errors
358
+ * aren't the developer's fault and would otherwise drown the
359
+ * signal.
360
+ */
361
+
362
+ type ErrorLevel = "error" | "warning" | "info";
363
+ interface CapturedError {
364
+ /** When the error fired (epoch ms). */
365
+ timestamp: number;
366
+ /** error.unhandled, error.unhandledrejection, error.handled, error.message, error.http */
367
+ kind: "error.unhandled" | "error.unhandledrejection" | "error.handled" | "error.message" | "error.http";
368
+ level: ErrorLevel;
369
+ message: string;
370
+ /** The error class name when we have it (TypeError, ReferenceError, etc.) */
371
+ errorType: string | null;
372
+ /** Parsed stack frames, empty when unavailable. */
373
+ frames: StackFrame[];
374
+ /** Raw stack string for fallback display. */
375
+ rawStack: string | null;
376
+ /** Origin URL when available (window.onerror's `source` arg). */
377
+ filename: string | null;
378
+ lineno: number | null;
379
+ colno: number | null;
380
+ /** djb2 hash of message + top frames — group identical errors. */
381
+ fingerprint: string;
382
+ /** Snapshot of the breadcrumb buffer at the moment the error fired. */
383
+ breadcrumbs: Breadcrumb[];
384
+ /** Free-form context attached via Crossdeck.setContext(). */
385
+ context: Record<string, unknown>;
386
+ /** Free-form tags attached via Crossdeck.setTag(). */
387
+ tags: Record<string, string>;
388
+ /** "TypeError: x is not a function" → "TypeError" + "x is not a function". */
389
+ /** Whether the error happened during a fetch / XHR. */
390
+ http?: {
391
+ url: string;
392
+ method: string;
393
+ status: number;
394
+ statusText?: string;
395
+ };
396
+ }
397
+
398
+ /**
399
+ * Public API surface for @cross-deck/web.
400
+ *
401
+ * Usage (browser):
402
+ *
403
+ * import { Crossdeck } from "@cross-deck/web";
404
+ *
405
+ * Crossdeck.init({
406
+ * appId: "app_web_xxx",
407
+ * publicKey: "cd_pub_live_…",
408
+ * environment: "production",
409
+ * });
410
+ *
411
+ * await Crossdeck.identify("user_847");
412
+ * const ents = await Crossdeck.getEntitlements();
413
+ * if (Crossdeck.isEntitled("pro")) {
414
+ * showPro();
415
+ * }
416
+ * Crossdeck.track("paywall_shown", { variant: "v3" });
417
+ *
418
+ *
419
+ * Usage (Node):
420
+ *
421
+ * import { Crossdeck, MemoryStorage } from "@cross-deck/web";
422
+ *
423
+ * Crossdeck.init({
424
+ * appId: "app_node_xxx",
425
+ * publicKey: "cd_pub_test_…",
426
+ * environment: "sandbox",
427
+ * storage: new MemoryStorage(), // session-only persistence
428
+ * autoHeartbeat: false, // skip the boot ping in scripts
429
+ * });
430
+ */
431
+
432
+ declare class CrossdeckClient {
433
+ private state;
434
+ /**
435
+ * Boot the SDK. Idempotent — calling init twice with the same options
436
+ * is a no-op; calling with different options replaces the previous
437
+ * configuration.
438
+ *
439
+ * NorthStar §11.1: signature is `Crossdeck.init({ appId, publicKey,
440
+ * environment })`. The trio is validated up-front so a typo'd key or a
441
+ * mismatched env fails fast at boot rather than at first event-flush.
442
+ */
443
+ init(options: CrossdeckOptions): void;
444
+ /**
445
+ * @deprecated Use `init()` instead. NorthStar §4 standardised the
446
+ * lifecycle method name across SDKs as `init` (formerly `start` /
447
+ * `configure`). `start` will be removed in a future major version.
448
+ */
449
+ start(options: CrossdeckOptions): void;
450
+ /**
451
+ * Link the anonymous device to a developer-supplied user ID. Cache
452
+ * the resolved Crossdeck customer for follow-up calls.
453
+ *
454
+ * v0.9.0+ accepts an optional `traits` bag — profile data (name,
455
+ * plan, signupDate, role) persisted on the Crossdeck customer record
456
+ * and queryable from dashboards. Traits are sanitised through the
457
+ * same validator that gates `track()` properties, so a `{ avatar:
458
+ * <File>, onSave: () => {} }` payload can't corrupt the alias call.
459
+ *
460
+ * Crossdeck.identify("user_847", {
461
+ * email: "wes@pinet.co.za",
462
+ * traits: { name: "Wes", plan: "pro", signedUpAt: "2026-05-11" },
463
+ * });
464
+ */
465
+ identify(userId: string, options?: IdentifyOptions): Promise<AliasResult>;
466
+ /**
467
+ * Register super-properties — Mixpanel pattern. Once set, every
468
+ * subsequent event of THIS SDK instance carries these keys on its
469
+ * properties bag automatically.
470
+ *
471
+ * Crossdeck.register({ plan: "pro", releaseChannel: "beta" });
472
+ * Crossdeck.track("paywall_shown"); // includes plan + releaseChannel
473
+ *
474
+ * Values that are `null` are deleted (the explicit "stop tracking
475
+ * this key" idiom). Returns the resulting bag.
476
+ *
477
+ * Sanitised through `validateEventProperties` so a `{ avatar: File }`
478
+ * payload can't poison the queue at flush time.
479
+ */
480
+ register(properties: Record<string, unknown>): Record<string, unknown>;
481
+ /** Remove a single super-property key. Idempotent. */
482
+ unregister(key: string): void;
483
+ /** Snapshot of the current super-property bag. */
484
+ getSuperProperties(): Record<string, unknown>;
485
+ /**
486
+ * Associate the current user with a group (org, team, account, etc.).
487
+ * Mixpanel / Segment "Group Analytics" pattern.
488
+ *
489
+ * Crossdeck.group("org", "acme_inc");
490
+ * Crossdeck.group("team", "design", { headcount: 12 });
491
+ *
492
+ * Once set, every subsequent event carries `$groups.<type>: id` on
493
+ * its properties bag, enabling B2B dashboards ("how is Acme using
494
+ * the product"). Pass `id: null` to clear a group membership.
495
+ */
496
+ group(type: string, id: string | null, traits?: GroupTraits): void;
497
+ /** Snapshot of the current groups map keyed by type. */
498
+ getGroups(): Record<string, {
499
+ id: string;
500
+ traits?: Record<string, unknown>;
501
+ }>;
502
+ /**
503
+ * Update consent state. Three independent dimensions:
504
+ *
505
+ * analytics — track() + identify() + auto-emissions
506
+ * marketing — paid-traffic click IDs + referrer URL on events
507
+ * errors — Web Vitals + (future) error reporting
508
+ *
509
+ * Each defaults to `true` (granted). Pass partial state — only the
510
+ * keys you provide are changed.
511
+ *
512
+ * Crossdeck.consent({ analytics: false });
513
+ * Crossdeck.consent({ marketing: true, errors: true });
514
+ *
515
+ * DNT-derived denies cannot be flipped back on; if the browser said
516
+ * "don't track" we don't track even if the developer code disagrees.
517
+ */
518
+ consent(state: Partial<ConsentState>): ConsentState;
519
+ /** Snapshot of the current consent state. */
520
+ consentStatus(): ConsentState;
521
+ /**
522
+ * Manually capture an error from a try/catch block.
523
+ *
524
+ * try { …risky… } catch (err) {
525
+ * Crossdeck.captureError(err, { context: { plan: "pro" } });
526
+ * }
527
+ *
528
+ * The error is shipped through the same event queue as analytics
529
+ * (durable, retried, rate-limited per fingerprint). Sends are gated
530
+ * by `consent.errors`. Returns silently — never throws, even if the
531
+ * SDK isn't initialised yet.
532
+ */
533
+ captureError(error: unknown, options?: {
534
+ context?: Record<string, unknown>;
535
+ tags?: Record<string, string>;
536
+ level?: ErrorLevel;
537
+ }): void;
538
+ /**
539
+ * Capture a non-error event you want to surface as an issue
540
+ * ("deprecated path hit", "we entered the slow code path"). Sentry
541
+ * captureMessage pattern. Returns silently if not initialised.
542
+ */
543
+ captureMessage(message: string, level?: ErrorLevel): void;
544
+ /**
545
+ * Attach a tag to every subsequent error report. Tags are key/value
546
+ * strings (Sentry pattern): `setTag("flow", "checkout")` → every
547
+ * error from this point on carries `tags.flow === "checkout"`.
548
+ */
549
+ setTag(key: string, value: string): void;
550
+ /** Bulk-set tags. Merges with existing tags. */
551
+ setTags(tags: Record<string, string>): void;
552
+ /**
553
+ * Attach a structured context blob to every subsequent error report.
554
+ * Unlike tags (flat key/value), context is a named bag of arbitrary
555
+ * data: `setContext("cart", { items: 3, total: 42.99 })`.
556
+ */
557
+ setContext(name: string, data: Record<string, unknown>): void;
558
+ /**
559
+ * Add a custom breadcrumb to the rolling buffer. Useful for marking
560
+ * domain-meaningful moments ("user opened paywall") that aren't
561
+ * already auto-captured. The buffer caps at 50 entries; old ones
562
+ * evict.
563
+ */
564
+ addBreadcrumb(crumb: Breadcrumb): void;
565
+ /**
566
+ * Install a pre-send hook for errors. Return null to drop, or a
567
+ * modified CapturedError to scrub / rewrite. Sentry's beforeSend
568
+ * pattern — the only way to redact app-specific PII (auth tokens
569
+ * in URLs, etc.) before the report leaves the browser.
570
+ */
571
+ setErrorBeforeSend(hook: ((err: CapturedError) => CapturedError | null) | null): void;
572
+ /**
573
+ * Internal: turn a CapturedError into a Crossdeck event and enqueue
574
+ * it. Goes through the same queue / persistence / consent / scrub
575
+ * pipeline as analytics events.
576
+ */
577
+ private reportError;
578
+ /**
579
+ * GDPR/CCPA "right to be forgotten" — calls the backend's
580
+ * /v1/identity/forget endpoint to schedule a server-side deletion of
581
+ * the customer's events and profile, then wipes all local state
582
+ * (identity, entitlements, queue, super-props, persistent stores).
583
+ *
584
+ * Idempotent. Safe to call when no identity has been established
585
+ * (it just wipes the empty local state).
586
+ *
587
+ * After forget() resolves, the SDK is in the same shape as if the
588
+ * developer had called `Crossdeck.reset()` — a fresh anonymousId is
589
+ * minted and the next session is a brand new identity-graph entry.
590
+ */
591
+ forget(): Promise<void>;
592
+ /**
593
+ * Read the current customer's active entitlements from the server.
594
+ * Updates the local cache so subsequent isEntitled() calls answer
595
+ * synchronously.
596
+ */
597
+ getEntitlements(): Promise<PublicEntitlement[]>;
598
+ /**
599
+ * Synchronous read from the durable local cache — answers from
600
+ * last-known-good. The cache hydrates from device storage on boot and
601
+ * survives a Crossdeck outage, so a returning paying customer reads
602
+ * true even before the session's first network round-trip. Returns
603
+ * false only for a genuinely new install that has never completed a
604
+ * getEntitlements(), or for an entitlement past its own validUntil.
605
+ */
606
+ isEntitled(key: string): boolean;
607
+ /** Snapshot of the local entitlement cache. */
608
+ listEntitlements(): PublicEntitlement[];
609
+ /**
610
+ * Subscribe to entitlement-cache changes. Returns an unsubscribe fn.
611
+ *
612
+ * The listener is invoked AFTER the cache mutates — once after a
613
+ * successful `getEntitlements()` warms it, again after `syncPurchases()`
614
+ * delivers fresh entitlements, and once on `reset()` to fire the
615
+ * empty-cache state for logout flows.
616
+ *
617
+ * It is NOT invoked synchronously on subscribe. Callers that need
618
+ * the current state should read it via `isEntitled()` / `listEntitlements()`
619
+ * inline; the listener fires only on FUTURE changes.
620
+ *
621
+ * This is the foundation of the `useEntitlement` React hook in
622
+ * `@cross-deck/web/react` — without it, React (or SwiftUI / Compose
623
+ * / Vue) would have no way to re-render when entitlements arrive
624
+ * asynchronously after init. The naive pattern of calling
625
+ * `Crossdeck.isEntitled("pro")` directly inside a render path
626
+ * shows the empty-cache result forever; binding the result to
627
+ * component state via `onEntitlementsChange` is the correct
628
+ * pattern.
629
+ *
630
+ * Idempotent unsubscribe — calling the returned function multiple
631
+ * times is safe.
632
+ *
633
+ * Listener errors are swallowed (a buggy listener can't crash the
634
+ * SDK or other listeners).
635
+ */
636
+ onEntitlementsChange(listener: EntitlementsListener): () => void;
637
+ /**
638
+ * Queue a telemetry event. Returns immediately — the network round-
639
+ * trip happens in the background. To flush before the page unloads,
640
+ * call flush().
641
+ */
642
+ /**
643
+ * Emit `crossdeck.contract_failed` with the canonical property
644
+ * shape (`contract_id`, `sdk_version`, `sdk_platform`,
645
+ * `failure_reason`, `run_context`, `run_id`). Goes through the
646
+ * standard track() pipeline — same consent gate, same queue,
647
+ * same ingest, no new endpoint.
648
+ *
649
+ * Wire the call from a test hook, dogfood failure path, or
650
+ * customer contract-verification harness; see
651
+ * `contracts/README.md` for the per-test-framework hook recipes.
652
+ */
653
+ reportContractFailure(input: ContractFailureInput): void;
654
+ track(name: string, properties?: EventProperties): void;
655
+ /**
656
+ * Force-flush queued events. Useful to call from page-unload handlers.
657
+ *
658
+ * Pass `{ keepalive: true }` from terminal handlers (pagehide /
659
+ * visibilitychange→hidden / beforeunload). The browser keeps the
660
+ * request alive after the page tears down, so the final batch
661
+ * actually lands instead of being cancelled with the unload.
662
+ *
663
+ * NorthStar §4: standard method name across all Crossdeck SDKs.
664
+ */
665
+ flush(options?: {
666
+ keepalive?: boolean;
667
+ }): Promise<void>;
668
+ /** @deprecated Use `flush()` instead. NorthStar §4 standardised the name. */
669
+ flushEvents(): Promise<void>;
670
+ /**
671
+ * Forward purchase evidence to the backend for verification + entitlement
672
+ * projection. NorthStar §4 + §13 canonical name.
673
+ *
674
+ * Today the web SDK only supports Apple StoreKit 2 forwarding (web apps
675
+ * that sit alongside an iOS app). Stripe doesn't need this method —
676
+ * Stripe webhooks deliver evidence server-side without a client round-trip.
677
+ */
678
+ syncPurchases(input: {
679
+ rail?: "apple";
680
+ signedTransactionInfo: string;
681
+ signedRenewalInfo?: string;
682
+ appAccountToken?: string;
683
+ }): Promise<PurchaseResult>;
684
+ /** @deprecated Use `syncPurchases()` instead. NorthStar §4 standardised the name. */
685
+ purchaseApple(input: {
686
+ signedTransactionInfo: string;
687
+ signedRenewalInfo?: string;
688
+ appAccountToken?: string;
689
+ }): Promise<PurchaseResult>;
690
+ /**
691
+ * Toggle verbose diagnostic logging — NorthStar §16. When enabled, the
692
+ * SDK emits a fixed vocabulary of debug signals to console.info that the
693
+ * dashboard's onboarding checklist can also surface as live events.
694
+ */
695
+ setDebugMode(enabled: boolean): void;
696
+ /**
697
+ * Send the boot heartbeat. Called automatically by start() unless
698
+ * autoHeartbeat:false. Safe to call manually as a "we're still here" ping.
699
+ */
700
+ heartbeat(): Promise<HeartbeatResponse>;
701
+ /**
702
+ * Wipe persisted identity + entitlement cache. Use on logout. The
703
+ * next pre-login session generates a fresh anonymousId and starts a
704
+ * new identity-graph entry.
705
+ */
706
+ reset(): void;
707
+ /**
708
+ * Diagnostic: current state + queue stats. Useful for the dashboard's
709
+ * heartbeat row and debugging in dev.
710
+ *
711
+ * Returns a stable shape regardless of whether start() has been called —
712
+ * callers don't need to narrow on `started` to access `events` or
713
+ * `entitlements`. Pre-start values are sensible empties.
714
+ */
715
+ diagnostics(): Diagnostics;
716
+ private requireStarted;
717
+ /**
718
+ * Build the identity query for /v1/entitlements. Priority:
719
+ * crossdeckCustomerId > developerUserId > anonymousId
720
+ * — matches the resolveCrossdeckCustomerId precedence on the server.
721
+ */
722
+ private identityQueryParams;
723
+ /**
724
+ * Embed every known identity axis on the event. Earlier this returned
725
+ * just the highest-priority hint (cdcust → developerUserId → anonymousId)
726
+ * to keep payloads small, but that leaked into analytics: once a user
727
+ * was logged in, every subsequent page.viewed shipped without
728
+ * anonymousId, and `uniqExact(anonymous_id)` on the warehouse side
729
+ * counted 0 visitors for the entire authenticated app.
730
+ *
731
+ * Bank-grade rule: the server is the single source of truth on
732
+ * dedup. Send everything we know; let CH count by whichever axis
733
+ * matches the question. Each field is at most 32 bytes — sending
734
+ * three on every event costs ~80 bytes per request, which is
735
+ * trivial compared to the analytics correctness it buys.
736
+ */
737
+ private identityHintForEvent;
738
+ private mintEventId;
739
+ }
740
+ /**
741
+ * Default singleton — most consumers want one SDK instance per app.
742
+ * Creating extra instances is fine; just `new CrossdeckClient()`.
743
+ */
744
+ declare const Crossdeck: CrossdeckClient;
745
+
746
+ /**
747
+ * Stripe-style error wrapper for @cross-deck/web.
748
+ *
749
+ * Mirrors the wire shape returned by the v1 backend (see
750
+ * backend/src/api/v1-errors.ts) so SDK consumers can `catch`
751
+ * with consistent fields:
752
+ *
753
+ * try {
754
+ * await crossdeck.identify("user_847");
755
+ * } catch (err) {
756
+ * if (err instanceof CrossdeckError && err.code === "invalid_api_key") {
757
+ * // ...
758
+ * }
759
+ * }
760
+ */
761
+ type CrossdeckErrorType = "authentication_error" | "permission_error" | "invalid_request_error" | "rate_limit_error" | "internal_error" | "network_error" | "configuration_error";
762
+ interface CrossdeckErrorPayload {
763
+ type: CrossdeckErrorType;
764
+ code: string;
765
+ message: string;
766
+ /** Server-issued request ID. Echoed in support tickets. */
767
+ requestId?: string;
768
+ /** HTTP status code if the error came from an API response. */
769
+ status?: number;
770
+ /**
771
+ * Server-suggested wait (in milliseconds) before retrying. Populated
772
+ * from the `Retry-After` response header on 429 / 503. The header
773
+ * spec allows either delta-seconds or an HTTP-date; the parser below
774
+ * normalises both to milliseconds. Consumers MUST honour this — the
775
+ * server is telling you the safe rate.
776
+ */
777
+ retryAfterMs?: number;
778
+ }
779
+ declare class CrossdeckError extends Error {
780
+ readonly type: CrossdeckErrorType;
781
+ readonly code: string;
782
+ readonly requestId?: string;
783
+ readonly status?: number;
784
+ readonly retryAfterMs?: number;
785
+ constructor(payload: CrossdeckErrorPayload);
786
+ }
787
+
788
+ /**
789
+ * Storage adapters for SDK-persisted state.
790
+ *
791
+ * Three flavours:
792
+ * - browser localStorage (default in browsers)
793
+ * - 1st-party document.cookie (redundancy for cleared localStorage)
794
+ * - in-memory (default in Node, or as an explicit fallback)
795
+ *
796
+ * Detection is at construction time, not at every call — picking the
797
+ * adapter once means we don't hit `typeof window` checks on hot paths.
798
+ *
799
+ * ----- Bank-grade identity continuity -----
800
+ *
801
+ * Plain localStorage is not enough. ITP, private browsing, "clear site
802
+ * data" actions, and aggressive privacy extensions all wipe it. When
803
+ * that happens, the SDK mints a fresh anonymousId on next page load
804
+ * and the customer's analytics see one human as multiple "new
805
+ * visitors" — a credibility hit on every dashboard chart that depends
806
+ * on visitor uniqueness (new vs returning, retention, funnels).
807
+ *
808
+ * The fix is redundancy: we write the same identity to BOTH
809
+ * localStorage AND a 1st-party cookie. On boot we read both; whichever
810
+ * survived wins. On set, we write to both stores so a future clear of
811
+ * either doesn't lose the user.
812
+ *
813
+ * Caveats (documented honestly):
814
+ * 1. Safari ITP caps client-set 1st-party cookies at 7 days. Cookie
815
+ * redundancy protects against localStorage clears WITHIN that
816
+ * 7-day window, not beyond it. The full ITP-bypass story (server-
817
+ * set cookies via a customer-CNAMEd subdomain) is a Phase 2
818
+ * follow-up that requires customer DNS configuration.
819
+ * 2. We never write fingerprintable data — only the same anonymousId
820
+ * already in localStorage. Privacy posture is unchanged from
821
+ * single-store identity.
822
+ * 3. `persistIdentity: false` disables BOTH stores so customers
823
+ * running strict consent flows can defer cookie writes until the
824
+ * user opts in.
825
+ */
826
+
827
+ /**
828
+ * In-memory storage. Cleared on process exit. Useful for Node runtimes
829
+ * where you want session-scoped identity that doesn't persist to disk.
830
+ */
831
+ declare class MemoryStorage implements KeyValueStorage {
832
+ private store;
833
+ getItem(key: string): string | null;
834
+ setItem(key: string, value: string): void;
835
+ removeItem(key: string): void;
836
+ }
837
+
838
+ /**
839
+ * SDK version constant — generated by `scripts/sync-sdk-versions.mjs`.
840
+ *
841
+ * Single source of truth: the `version` field in this package's
842
+ * package.json. The sync script writes this file so that
843
+ * `SDK_VERSION` is a plain TypeScript literal at runtime — no
844
+ * runtime JSON-import gotcha (Node ESM requires
845
+ * `with { type: "json" }` to import JSON as ESM, and the published
846
+ * dist file would otherwise fail to load).
847
+ *
848
+ * Drift protection: `node scripts/sync-sdk-versions.mjs --check` (the
849
+ * CI gate) flags this file when it falls out of sync with package.json.
850
+ * Bumping `package.json` without re-running the sync script fails CI.
851
+ *
852
+ * Do NOT edit by hand — `node scripts/sync-sdk-versions.mjs`.
853
+ */
854
+ declare const SDK_VERSION = "1.4.2";
855
+ declare const SDK_NAME = "@cross-deck/web";
856
+
857
+ /**
858
+ * HTTP transport for the SDK. Single fetch wrapper used by every endpoint
859
+ * call. Adds the Bearer token and SDK version header, parses responses,
860
+ * normalises errors to CrossdeckError.
861
+ *
862
+ * Uses platform-native fetch (browser + Node 18+). No axios, no isomorphic-
863
+ * fetch shim, no transitive deps.
864
+ */
865
+
866
+ declare const DEFAULT_BASE_URL = "https://api.cross-deck.com/v1";
867
+
868
+ /**
869
+ * Machine-readable index of every error code the SDK can throw, with
870
+ * a short description and a hint on what action to take. Published
871
+ * verbatim as `crossdeck-error-codes.json` in the npm tarball so AI
872
+ * integration assistants, error-aggregator dashboards (Sentry,
873
+ * DataDog), and the Crossdeck dashboard can render human-friendly
874
+ * messages without parsing freeform `message` strings.
875
+ *
876
+ * Stripe publishes the same surface at stripe.com/docs/error-codes;
877
+ * developers love it because every code has a canonical "what does
878
+ * this mean / what should I do" answer.
879
+ *
880
+ * Adding a new error code:
881
+ * 1. Add the code string to the union in `errors.ts` (where used).
882
+ * 2. Add an entry here.
883
+ * 3. The next `npm run build` regenerates the JSON sidecar.
884
+ *
885
+ * Keep entries terse — the consumer surfaces this in tooltips and
886
+ * automated tickets, not in long-form docs.
887
+ */
888
+ interface ErrorCodeEntry {
889
+ /** The string thrown as CrossdeckError.code. */
890
+ code: string;
891
+ /** CrossdeckError.type — broad category. */
892
+ type: "authentication_error" | "permission_error" | "invalid_request_error" | "rate_limit_error" | "internal_error" | "network_error" | "configuration_error";
893
+ /** One-sentence description. Surfaced verbatim in dashboards. */
894
+ description: string;
895
+ /** What the developer should do. Imperative phrasing. */
896
+ resolution: string;
897
+ /** True for codes the SDK can auto-recover from (no developer action). */
898
+ retryable: boolean;
899
+ }
900
+ declare const CROSSDECK_ERROR_CODES: readonly ErrorCodeEntry[];
901
+ /** Lookup helper — returns the entry matching a CrossdeckError.code, or undefined. */
902
+ declare function getErrorCode(code: string): ErrorCodeEntry | undefined;
903
+
904
+ /**
905
+ * Device + environment enrichment.
906
+ *
907
+ * Auto-attached to every event the SDK emits when `autoTrack.deviceInfo` is
908
+ * enabled (default). Caller-supplied event properties always override
909
+ * auto-detected ones (so a developer can manually set `app.version` per
910
+ * event if they want to A/B between builds).
911
+ *
912
+ * Privacy posture:
913
+ * - No fingerprinting (no canvas hashes, no font enumeration).
914
+ * - No precise geolocation (only timezone + locale, both of which the
915
+ * browser exposes to every page anyway).
916
+ * - No IP collection — the backend logs the request IP for rate-limit
917
+ * purposes; it isn't stored on the event document.
918
+ * - All fields are typed enums or short strings; we never echo back
919
+ * full User-Agent strings to avoid surfacing fingerprintable detail
920
+ * in dashboards.
921
+ */
922
+ interface DeviceInfo {
923
+ os?: string;
924
+ osVersion?: string;
925
+ browser?: string;
926
+ browserVersion?: string;
927
+ locale?: string;
928
+ timezone?: string;
929
+ screenWidth?: number;
930
+ screenHeight?: number;
931
+ viewportWidth?: number;
932
+ viewportHeight?: number;
933
+ devicePixelRatio?: number;
934
+ /** Caller-supplied. Set via Crossdeck.start({ appVersion: "1.2.3" }). */
935
+ appVersion?: string;
936
+ }
937
+
938
+ export { AliasResult, type Breadcrumb, type BreadcrumbCategory, type BreadcrumbLevel, CROSSDECK_ERROR_CODES, type CapturedError, type ConsentState, type Contract, type ContractAppliesTo, type ContractFailureInput, type ContractPillar, type ContractStatus, type ContractTestRef, Crossdeck, CrossdeckClient, CrossdeckContracts, CrossdeckError, type CrossdeckErrorPayload, type CrossdeckErrorType, CrossdeckOptions, DEFAULT_BASE_URL, type DeviceInfo, Diagnostics, type ErrorCodeEntry, type ErrorLevel, EventProperties, GroupTraits, HeartbeatResponse, IdentifyOptions, KeyValueStorage, MemoryStorage, PublicEntitlement, PurchaseResult, SDK_NAME, SDK_VERSION, type StackFrame, getErrorCode };