@cross-deck/web 0.7.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -139,6 +139,23 @@ interface CrossdeckOptions {
139
139
  * `Crossdeck.setDebugMode(true)` after init.
140
140
  */
141
141
  debug?: boolean;
142
+ /**
143
+ * Respect the browser's Do Not Track signal at init (v0.10.0+).
144
+ * Default `false`. When `true` AND the user has `navigator.doNotTrack === "1"`,
145
+ * the SDK boots with analytics / marketing / errors all denied —
146
+ * locked off even if the developer later calls `Crossdeck.consent({...})`.
147
+ * Industry has effectively deprecated DNT, but opt-in support is the
148
+ * polite default for privacy-first apps.
149
+ */
150
+ respectDnt?: boolean;
151
+ /**
152
+ * Scrub PII-shaped strings (email addresses, card numbers) from
153
+ * URL paths, event properties, and acquisition referrer before they
154
+ * leave the SDK. Default `true` — Stripe-grade. Disable only if your
155
+ * pipeline does its own PII redaction downstream and you need the
156
+ * raw strings.
157
+ */
158
+ scrubPii?: boolean;
142
159
  }
143
160
  /** Auto-tracking flags. See CrossdeckOptions.autoTrack. */
144
161
  interface AutoTrackOptions {
@@ -157,6 +174,14 @@ interface AutoTrackOptions {
157
174
  * or data-cd-prop-* for custom property tagging.
158
175
  */
159
176
  clicks: boolean;
177
+ /**
178
+ * Web Vitals capture (v0.9.0+) — emits `webvitals.lcp`, `webvitals.inp`,
179
+ * `webvitals.cls`, `webvitals.fcp`, `webvitals.ttfb` events using the
180
+ * browser's `PerformanceObserver`. Defaults to true in browsers,
181
+ * no-op everywhere else. Disable if you have a separate RUM provider
182
+ * (DataDog, Sentry Performance) and don't want duplicates.
183
+ */
184
+ webVitals: boolean;
160
185
  }
161
186
  /** Minimal interface for any pluggable key-value persistence. */
162
187
  interface KeyValueStorage {
@@ -164,10 +189,44 @@ interface KeyValueStorage {
164
189
  setItem(key: string, value: string): void;
165
190
  removeItem(key: string): void;
166
191
  }
167
- /** Identity hint object passed to identify() — at least one field required. */
192
+ /**
193
+ * Identity hint + profile traits passed to identify().
194
+ *
195
+ * `traits` is a free-form bag of profile data (name, plan, signupDate,
196
+ * teamRole, etc.) that gets persisted on the Crossdeck customer record
197
+ * and attached to every subsequent event of the identified user as
198
+ * `$user.<key>` properties for dashboard filtering.
199
+ *
200
+ * Like event properties, traits are validated at the SDK boundary —
201
+ * functions/symbols/undefined dropped, Date / BigInt / Error coerced,
202
+ * strings > 1024 chars truncated. Caller's object is never mutated.
203
+ */
168
204
  interface IdentifyOptions {
169
205
  /** Optional email to attach to the customer record. */
170
206
  email?: string;
207
+ /**
208
+ * Optional profile traits. Examples:
209
+ * `{ name: "Wes", plan: "pro", signedUpAt: "2026-05-11" }`
210
+ *
211
+ * Treated like event properties — values are sanitised at the SDK
212
+ * boundary so a `{ avatar: <File>, callback: () => {} }` payload
213
+ * doesn't crash the alias request. Server-side, traits land on
214
+ * `customers/{cdcust}.traits` (additively — existing fields are
215
+ * preserved unless the new identify call overrides them).
216
+ */
217
+ traits?: Record<string, unknown>;
218
+ }
219
+ /**
220
+ * Group context — Mixpanel-style. Identifies a customer's membership
221
+ * in an organisational entity (org, account, team, workspace) so B2B
222
+ * dashboards can answer "how is account X using my product".
223
+ *
224
+ * Attached to every event as `$groups.<type>` until cleared via
225
+ * `Crossdeck.group(type, null)`. Multiple types can coexist (e.g.
226
+ * `org` + `team`) — the SDK keeps a map keyed by type.
227
+ */
228
+ interface GroupTraits {
229
+ [key: string]: unknown;
171
230
  }
172
231
  /** Properties payload for track(). Arbitrary key/value, JSON-serialisable, ≤ 8 KB. */
173
232
  type EventProperties = Record<string, unknown>;
@@ -184,9 +243,34 @@ interface Diagnostics {
184
243
  developerUserId: string | null;
185
244
  sdkVersion: string | null;
186
245
  baseUrl: string | null;
246
+ /**
247
+ * Last `serverTime` value the SDK saw on a /sdk/heartbeat response,
248
+ * along with the local clock value AT that moment. Lets dashboards
249
+ * (and the developer, in debug mode) detect a wrong-system-clock
250
+ * problem before it corrupts a day of analytics. Null until the
251
+ * first heartbeat completes.
252
+ */
253
+ clock: {
254
+ /** Server's view of "now" from the last heartbeat (epoch ms). */
255
+ lastServerTime: number | null;
256
+ /** Client's `Date.now()` taken at the same moment as `lastServerTime`. */
257
+ lastClientTime: number | null;
258
+ /**
259
+ * `lastClientTime - lastServerTime` — positive means the client
260
+ * clock is AHEAD of the server. Outside ±5 minutes is suspicious
261
+ * and worth surfacing to the developer.
262
+ */
263
+ skewMs: number | null;
264
+ };
187
265
  entitlements: {
188
266
  count: number;
189
267
  lastUpdated: number;
268
+ /**
269
+ * Cumulative count of listener invocations that threw. Swallowed
270
+ * inside the cache (a buggy consumer must not crash the SDK) but
271
+ * surfaced here so developers can spot broken subscribers.
272
+ */
273
+ listenerErrors: number;
190
274
  };
191
275
  events: {
192
276
  buffered: number;
@@ -194,6 +278,13 @@ interface Diagnostics {
194
278
  inFlight: number;
195
279
  lastFlushAt: number;
196
280
  lastError: string | null;
281
+ /** Consecutive flush failures since the last success. */
282
+ consecutiveFailures: number;
283
+ /**
284
+ * When the next retry is scheduled (epoch ms), or null if the queue
285
+ * is idle / healthy.
286
+ */
287
+ nextRetryAt: number | null;
197
288
  };
198
289
  }
199
290
 
@@ -239,6 +330,44 @@ interface Diagnostics {
239
330
 
240
331
  type EntitlementsListener = (entitlements: PublicEntitlement[]) => void;
241
332
 
333
+ /**
334
+ * Consent gating — GDPR / CCPA-grade kill switches.
335
+ *
336
+ * Three independent dimensions, each defaulting to "granted" but
337
+ * runtime-overridable:
338
+ *
339
+ * analytics — track(), identify(), heartbeat(), session/page auto-
340
+ * emissions. Off → events drop silently, no network
341
+ * calls fire.
342
+ * marketing — paid-traffic click IDs (gclid/fbclid/etc) and
343
+ * acquisition referrer URL. Off → these get scrubbed
344
+ * before they ever land in the event bag.
345
+ * errors — error / breadcrumb / Web Vitals capture. Off → no
346
+ * webvitals.* events emitted, no error reporting (when
347
+ * Phase 3 errors land).
348
+ *
349
+ * Why this granularity: real consent banners offer "Analytics",
350
+ * "Marketing", "Functional" as separate boxes. The SDK has to match.
351
+ *
352
+ * Default state: every dimension is granted. The developer must
353
+ * explicitly call `Crossdeck.consent({ analytics: false })` before
354
+ * the first event to opt OUT — same convention as Google Tag Manager
355
+ * Consent Mode. To start in deny mode, call `init(...)` then
356
+ * immediately `consent({ analytics: false, marketing: false, errors:
357
+ * false })` before any user activity.
358
+ *
359
+ * DNT (Do Not Track) browser header is checked once at init and
360
+ * applied as an automatic deny across all dimensions when
361
+ * `respectDnt: true` is set in CrossdeckOptions (default false because
362
+ * the industry has effectively deprecated DNT — but opt-in support
363
+ * is the polite default for privacy-first apps).
364
+ */
365
+ interface ConsentState {
366
+ analytics: boolean;
367
+ marketing: boolean;
368
+ errors: boolean;
369
+ }
370
+
242
371
  /**
243
372
  * Public API surface for @cross-deck/web.
244
373
  *
@@ -294,8 +423,88 @@ declare class CrossdeckClient {
294
423
  /**
295
424
  * Link the anonymous device to a developer-supplied user ID. Cache
296
425
  * the resolved Crossdeck customer for follow-up calls.
426
+ *
427
+ * v0.9.0+ accepts an optional `traits` bag — profile data (name,
428
+ * plan, signupDate, role) persisted on the Crossdeck customer record
429
+ * and queryable from dashboards. Traits are sanitised through the
430
+ * same validator that gates `track()` properties, so a `{ avatar:
431
+ * <File>, onSave: () => {} }` payload can't corrupt the alias call.
432
+ *
433
+ * Crossdeck.identify("user_847", {
434
+ * email: "wes@pinet.co.za",
435
+ * traits: { name: "Wes", plan: "pro", signedUpAt: "2026-05-11" },
436
+ * });
297
437
  */
298
- identify(userId: string, _options?: IdentifyOptions): Promise<AliasResult>;
438
+ identify(userId: string, options?: IdentifyOptions): Promise<AliasResult>;
439
+ /**
440
+ * Register super-properties — Mixpanel pattern. Once set, every
441
+ * subsequent event of THIS SDK instance carries these keys on its
442
+ * properties bag automatically.
443
+ *
444
+ * Crossdeck.register({ plan: "pro", releaseChannel: "beta" });
445
+ * Crossdeck.track("paywall_shown"); // includes plan + releaseChannel
446
+ *
447
+ * Values that are `null` are deleted (the explicit "stop tracking
448
+ * this key" idiom). Returns the resulting bag.
449
+ *
450
+ * Sanitised through `validateEventProperties` so a `{ avatar: File }`
451
+ * payload can't poison the queue at flush time.
452
+ */
453
+ register(properties: Record<string, unknown>): Record<string, unknown>;
454
+ /** Remove a single super-property key. Idempotent. */
455
+ unregister(key: string): void;
456
+ /** Snapshot of the current super-property bag. */
457
+ getSuperProperties(): Record<string, unknown>;
458
+ /**
459
+ * Associate the current user with a group (org, team, account, etc.).
460
+ * Mixpanel / Segment "Group Analytics" pattern.
461
+ *
462
+ * Crossdeck.group("org", "acme_inc");
463
+ * Crossdeck.group("team", "design", { headcount: 12 });
464
+ *
465
+ * Once set, every subsequent event carries `$groups.<type>: id` on
466
+ * its properties bag, enabling B2B dashboards ("how is Acme using
467
+ * the product"). Pass `id: null` to clear a group membership.
468
+ */
469
+ group(type: string, id: string | null, traits?: GroupTraits): void;
470
+ /** Snapshot of the current groups map keyed by type. */
471
+ getGroups(): Record<string, {
472
+ id: string;
473
+ traits?: Record<string, unknown>;
474
+ }>;
475
+ /**
476
+ * Update consent state. Three independent dimensions:
477
+ *
478
+ * analytics — track() + identify() + auto-emissions
479
+ * marketing — paid-traffic click IDs + referrer URL on events
480
+ * errors — Web Vitals + (future) error reporting
481
+ *
482
+ * Each defaults to `true` (granted). Pass partial state — only the
483
+ * keys you provide are changed.
484
+ *
485
+ * Crossdeck.consent({ analytics: false });
486
+ * Crossdeck.consent({ marketing: true, errors: true });
487
+ *
488
+ * DNT-derived denies cannot be flipped back on; if the browser said
489
+ * "don't track" we don't track even if the developer code disagrees.
490
+ */
491
+ consent(state: Partial<ConsentState>): ConsentState;
492
+ /** Snapshot of the current consent state. */
493
+ consentStatus(): ConsentState;
494
+ /**
495
+ * GDPR/CCPA "right to be forgotten" — calls the backend's
496
+ * /v1/identity/forget endpoint to schedule a server-side deletion of
497
+ * the customer's events and profile, then wipes all local state
498
+ * (identity, entitlements, queue, super-props, persistent stores).
499
+ *
500
+ * Idempotent. Safe to call when no identity has been established
501
+ * (it just wipes the empty local state).
502
+ *
503
+ * After forget() resolves, the SDK is in the same shape as if the
504
+ * developer had called `Crossdeck.reset()` — a fresh anonymousId is
505
+ * minted and the next session is a brand new identity-graph entry.
506
+ */
507
+ forget(): Promise<void>;
299
508
  /**
300
509
  * Read the current customer's active entitlements from the server.
301
510
  * Updates the local cache so subsequent isEntitled() calls answer
@@ -458,12 +667,21 @@ interface CrossdeckErrorPayload {
458
667
  requestId?: string;
459
668
  /** HTTP status code if the error came from an API response. */
460
669
  status?: number;
670
+ /**
671
+ * Server-suggested wait (in milliseconds) before retrying. Populated
672
+ * from the `Retry-After` response header on 429 / 503. The header
673
+ * spec allows either delta-seconds or an HTTP-date; the parser below
674
+ * normalises both to milliseconds. Consumers MUST honour this — the
675
+ * server is telling you the safe rate.
676
+ */
677
+ retryAfterMs?: number;
461
678
  }
462
679
  declare class CrossdeckError extends Error {
463
680
  readonly type: CrossdeckErrorType;
464
681
  readonly code: string;
465
682
  readonly requestId?: string;
466
683
  readonly status?: number;
684
+ readonly retryAfterMs?: number;
467
685
  constructor(payload: CrossdeckErrorPayload);
468
686
  }
469
687
 
@@ -526,9 +744,45 @@ declare class MemoryStorage implements KeyValueStorage {
526
744
  * fetch shim, no transitive deps.
527
745
  */
528
746
  declare const SDK_NAME = "@cross-deck/web";
529
- declare const SDK_VERSION = "0.6.0";
747
+ declare const SDK_VERSION = "0.10.0";
530
748
  declare const DEFAULT_BASE_URL = "https://api.cross-deck.com/v1";
531
749
 
750
+ /**
751
+ * Machine-readable index of every error code the SDK can throw, with
752
+ * a short description and a hint on what action to take. Published
753
+ * verbatim as `crossdeck-error-codes.json` in the npm tarball so AI
754
+ * integration assistants, error-aggregator dashboards (Sentry,
755
+ * DataDog), and the Crossdeck dashboard can render human-friendly
756
+ * messages without parsing freeform `message` strings.
757
+ *
758
+ * Stripe publishes the same surface at stripe.com/docs/error-codes;
759
+ * developers love it because every code has a canonical "what does
760
+ * this mean / what should I do" answer.
761
+ *
762
+ * Adding a new error code:
763
+ * 1. Add the code string to the union in `errors.ts` (where used).
764
+ * 2. Add an entry here.
765
+ * 3. The next `npm run build` regenerates the JSON sidecar.
766
+ *
767
+ * Keep entries terse — the consumer surfaces this in tooltips and
768
+ * automated tickets, not in long-form docs.
769
+ */
770
+ interface ErrorCodeEntry {
771
+ /** The string thrown as CrossdeckError.code. */
772
+ code: string;
773
+ /** CrossdeckError.type — broad category. */
774
+ type: "authentication_error" | "permission_error" | "invalid_request_error" | "rate_limit_error" | "internal_error" | "network_error" | "configuration_error";
775
+ /** One-sentence description. Surfaced verbatim in dashboards. */
776
+ description: string;
777
+ /** What the developer should do. Imperative phrasing. */
778
+ resolution: string;
779
+ /** True for codes the SDK can auto-recover from (no developer action). */
780
+ retryable: boolean;
781
+ }
782
+ declare const CROSSDECK_ERROR_CODES: readonly ErrorCodeEntry[];
783
+ /** Lookup helper — returns the entry matching a CrossdeckError.code, or undefined. */
784
+ declare function getErrorCode(code: string): ErrorCodeEntry | undefined;
785
+
532
786
  /**
533
787
  * Device + environment enrichment.
534
788
  *
@@ -563,4 +817,4 @@ interface DeviceInfo {
563
817
  appVersion?: string;
564
818
  }
565
819
 
566
- export { type AliasResult, type AuditRail, type AutoTrackOptions, Crossdeck, CrossdeckClient, CrossdeckError, type CrossdeckErrorPayload, type CrossdeckErrorType, type CrossdeckOptions, DEFAULT_BASE_URL, type DeviceInfo, type Diagnostics, type EntitlementsListResponse, type Environment, type EventProperties, type HeartbeatResponse, type IdentifyOptions, type KeyValueStorage, MemoryStorage, type Platform, type PublicEntitlement, type PurchaseResult, SDK_NAME, SDK_VERSION };
820
+ export { type AliasResult, type AuditRail, type AutoTrackOptions, CROSSDECK_ERROR_CODES, type ConsentState, Crossdeck, CrossdeckClient, CrossdeckError, type CrossdeckErrorPayload, type CrossdeckErrorType, type CrossdeckOptions, DEFAULT_BASE_URL, type DeviceInfo, type Diagnostics, type EntitlementsListResponse, type Environment, type ErrorCodeEntry, type EventProperties, type GroupTraits, type HeartbeatResponse, type IdentifyOptions, type KeyValueStorage, MemoryStorage, type Platform, type PublicEntitlement, type PurchaseResult, SDK_NAME, SDK_VERSION, getErrorCode };
package/dist/index.d.ts CHANGED
@@ -139,6 +139,23 @@ interface CrossdeckOptions {
139
139
  * `Crossdeck.setDebugMode(true)` after init.
140
140
  */
141
141
  debug?: boolean;
142
+ /**
143
+ * Respect the browser's Do Not Track signal at init (v0.10.0+).
144
+ * Default `false`. When `true` AND the user has `navigator.doNotTrack === "1"`,
145
+ * the SDK boots with analytics / marketing / errors all denied —
146
+ * locked off even if the developer later calls `Crossdeck.consent({...})`.
147
+ * Industry has effectively deprecated DNT, but opt-in support is the
148
+ * polite default for privacy-first apps.
149
+ */
150
+ respectDnt?: boolean;
151
+ /**
152
+ * Scrub PII-shaped strings (email addresses, card numbers) from
153
+ * URL paths, event properties, and acquisition referrer before they
154
+ * leave the SDK. Default `true` — Stripe-grade. Disable only if your
155
+ * pipeline does its own PII redaction downstream and you need the
156
+ * raw strings.
157
+ */
158
+ scrubPii?: boolean;
142
159
  }
143
160
  /** Auto-tracking flags. See CrossdeckOptions.autoTrack. */
144
161
  interface AutoTrackOptions {
@@ -157,6 +174,14 @@ interface AutoTrackOptions {
157
174
  * or data-cd-prop-* for custom property tagging.
158
175
  */
159
176
  clicks: boolean;
177
+ /**
178
+ * Web Vitals capture (v0.9.0+) — emits `webvitals.lcp`, `webvitals.inp`,
179
+ * `webvitals.cls`, `webvitals.fcp`, `webvitals.ttfb` events using the
180
+ * browser's `PerformanceObserver`. Defaults to true in browsers,
181
+ * no-op everywhere else. Disable if you have a separate RUM provider
182
+ * (DataDog, Sentry Performance) and don't want duplicates.
183
+ */
184
+ webVitals: boolean;
160
185
  }
161
186
  /** Minimal interface for any pluggable key-value persistence. */
162
187
  interface KeyValueStorage {
@@ -164,10 +189,44 @@ interface KeyValueStorage {
164
189
  setItem(key: string, value: string): void;
165
190
  removeItem(key: string): void;
166
191
  }
167
- /** Identity hint object passed to identify() — at least one field required. */
192
+ /**
193
+ * Identity hint + profile traits passed to identify().
194
+ *
195
+ * `traits` is a free-form bag of profile data (name, plan, signupDate,
196
+ * teamRole, etc.) that gets persisted on the Crossdeck customer record
197
+ * and attached to every subsequent event of the identified user as
198
+ * `$user.<key>` properties for dashboard filtering.
199
+ *
200
+ * Like event properties, traits are validated at the SDK boundary —
201
+ * functions/symbols/undefined dropped, Date / BigInt / Error coerced,
202
+ * strings > 1024 chars truncated. Caller's object is never mutated.
203
+ */
168
204
  interface IdentifyOptions {
169
205
  /** Optional email to attach to the customer record. */
170
206
  email?: string;
207
+ /**
208
+ * Optional profile traits. Examples:
209
+ * `{ name: "Wes", plan: "pro", signedUpAt: "2026-05-11" }`
210
+ *
211
+ * Treated like event properties — values are sanitised at the SDK
212
+ * boundary so a `{ avatar: <File>, callback: () => {} }` payload
213
+ * doesn't crash the alias request. Server-side, traits land on
214
+ * `customers/{cdcust}.traits` (additively — existing fields are
215
+ * preserved unless the new identify call overrides them).
216
+ */
217
+ traits?: Record<string, unknown>;
218
+ }
219
+ /**
220
+ * Group context — Mixpanel-style. Identifies a customer's membership
221
+ * in an organisational entity (org, account, team, workspace) so B2B
222
+ * dashboards can answer "how is account X using my product".
223
+ *
224
+ * Attached to every event as `$groups.<type>` until cleared via
225
+ * `Crossdeck.group(type, null)`. Multiple types can coexist (e.g.
226
+ * `org` + `team`) — the SDK keeps a map keyed by type.
227
+ */
228
+ interface GroupTraits {
229
+ [key: string]: unknown;
171
230
  }
172
231
  /** Properties payload for track(). Arbitrary key/value, JSON-serialisable, ≤ 8 KB. */
173
232
  type EventProperties = Record<string, unknown>;
@@ -184,9 +243,34 @@ interface Diagnostics {
184
243
  developerUserId: string | null;
185
244
  sdkVersion: string | null;
186
245
  baseUrl: string | null;
246
+ /**
247
+ * Last `serverTime` value the SDK saw on a /sdk/heartbeat response,
248
+ * along with the local clock value AT that moment. Lets dashboards
249
+ * (and the developer, in debug mode) detect a wrong-system-clock
250
+ * problem before it corrupts a day of analytics. Null until the
251
+ * first heartbeat completes.
252
+ */
253
+ clock: {
254
+ /** Server's view of "now" from the last heartbeat (epoch ms). */
255
+ lastServerTime: number | null;
256
+ /** Client's `Date.now()` taken at the same moment as `lastServerTime`. */
257
+ lastClientTime: number | null;
258
+ /**
259
+ * `lastClientTime - lastServerTime` — positive means the client
260
+ * clock is AHEAD of the server. Outside ±5 minutes is suspicious
261
+ * and worth surfacing to the developer.
262
+ */
263
+ skewMs: number | null;
264
+ };
187
265
  entitlements: {
188
266
  count: number;
189
267
  lastUpdated: number;
268
+ /**
269
+ * Cumulative count of listener invocations that threw. Swallowed
270
+ * inside the cache (a buggy consumer must not crash the SDK) but
271
+ * surfaced here so developers can spot broken subscribers.
272
+ */
273
+ listenerErrors: number;
190
274
  };
191
275
  events: {
192
276
  buffered: number;
@@ -194,6 +278,13 @@ interface Diagnostics {
194
278
  inFlight: number;
195
279
  lastFlushAt: number;
196
280
  lastError: string | null;
281
+ /** Consecutive flush failures since the last success. */
282
+ consecutiveFailures: number;
283
+ /**
284
+ * When the next retry is scheduled (epoch ms), or null if the queue
285
+ * is idle / healthy.
286
+ */
287
+ nextRetryAt: number | null;
197
288
  };
198
289
  }
199
290
 
@@ -239,6 +330,44 @@ interface Diagnostics {
239
330
 
240
331
  type EntitlementsListener = (entitlements: PublicEntitlement[]) => void;
241
332
 
333
+ /**
334
+ * Consent gating — GDPR / CCPA-grade kill switches.
335
+ *
336
+ * Three independent dimensions, each defaulting to "granted" but
337
+ * runtime-overridable:
338
+ *
339
+ * analytics — track(), identify(), heartbeat(), session/page auto-
340
+ * emissions. Off → events drop silently, no network
341
+ * calls fire.
342
+ * marketing — paid-traffic click IDs (gclid/fbclid/etc) and
343
+ * acquisition referrer URL. Off → these get scrubbed
344
+ * before they ever land in the event bag.
345
+ * errors — error / breadcrumb / Web Vitals capture. Off → no
346
+ * webvitals.* events emitted, no error reporting (when
347
+ * Phase 3 errors land).
348
+ *
349
+ * Why this granularity: real consent banners offer "Analytics",
350
+ * "Marketing", "Functional" as separate boxes. The SDK has to match.
351
+ *
352
+ * Default state: every dimension is granted. The developer must
353
+ * explicitly call `Crossdeck.consent({ analytics: false })` before
354
+ * the first event to opt OUT — same convention as Google Tag Manager
355
+ * Consent Mode. To start in deny mode, call `init(...)` then
356
+ * immediately `consent({ analytics: false, marketing: false, errors:
357
+ * false })` before any user activity.
358
+ *
359
+ * DNT (Do Not Track) browser header is checked once at init and
360
+ * applied as an automatic deny across all dimensions when
361
+ * `respectDnt: true` is set in CrossdeckOptions (default false because
362
+ * the industry has effectively deprecated DNT — but opt-in support
363
+ * is the polite default for privacy-first apps).
364
+ */
365
+ interface ConsentState {
366
+ analytics: boolean;
367
+ marketing: boolean;
368
+ errors: boolean;
369
+ }
370
+
242
371
  /**
243
372
  * Public API surface for @cross-deck/web.
244
373
  *
@@ -294,8 +423,88 @@ declare class CrossdeckClient {
294
423
  /**
295
424
  * Link the anonymous device to a developer-supplied user ID. Cache
296
425
  * the resolved Crossdeck customer for follow-up calls.
426
+ *
427
+ * v0.9.0+ accepts an optional `traits` bag — profile data (name,
428
+ * plan, signupDate, role) persisted on the Crossdeck customer record
429
+ * and queryable from dashboards. Traits are sanitised through the
430
+ * same validator that gates `track()` properties, so a `{ avatar:
431
+ * <File>, onSave: () => {} }` payload can't corrupt the alias call.
432
+ *
433
+ * Crossdeck.identify("user_847", {
434
+ * email: "wes@pinet.co.za",
435
+ * traits: { name: "Wes", plan: "pro", signedUpAt: "2026-05-11" },
436
+ * });
297
437
  */
298
- identify(userId: string, _options?: IdentifyOptions): Promise<AliasResult>;
438
+ identify(userId: string, options?: IdentifyOptions): Promise<AliasResult>;
439
+ /**
440
+ * Register super-properties — Mixpanel pattern. Once set, every
441
+ * subsequent event of THIS SDK instance carries these keys on its
442
+ * properties bag automatically.
443
+ *
444
+ * Crossdeck.register({ plan: "pro", releaseChannel: "beta" });
445
+ * Crossdeck.track("paywall_shown"); // includes plan + releaseChannel
446
+ *
447
+ * Values that are `null` are deleted (the explicit "stop tracking
448
+ * this key" idiom). Returns the resulting bag.
449
+ *
450
+ * Sanitised through `validateEventProperties` so a `{ avatar: File }`
451
+ * payload can't poison the queue at flush time.
452
+ */
453
+ register(properties: Record<string, unknown>): Record<string, unknown>;
454
+ /** Remove a single super-property key. Idempotent. */
455
+ unregister(key: string): void;
456
+ /** Snapshot of the current super-property bag. */
457
+ getSuperProperties(): Record<string, unknown>;
458
+ /**
459
+ * Associate the current user with a group (org, team, account, etc.).
460
+ * Mixpanel / Segment "Group Analytics" pattern.
461
+ *
462
+ * Crossdeck.group("org", "acme_inc");
463
+ * Crossdeck.group("team", "design", { headcount: 12 });
464
+ *
465
+ * Once set, every subsequent event carries `$groups.<type>: id` on
466
+ * its properties bag, enabling B2B dashboards ("how is Acme using
467
+ * the product"). Pass `id: null` to clear a group membership.
468
+ */
469
+ group(type: string, id: string | null, traits?: GroupTraits): void;
470
+ /** Snapshot of the current groups map keyed by type. */
471
+ getGroups(): Record<string, {
472
+ id: string;
473
+ traits?: Record<string, unknown>;
474
+ }>;
475
+ /**
476
+ * Update consent state. Three independent dimensions:
477
+ *
478
+ * analytics — track() + identify() + auto-emissions
479
+ * marketing — paid-traffic click IDs + referrer URL on events
480
+ * errors — Web Vitals + (future) error reporting
481
+ *
482
+ * Each defaults to `true` (granted). Pass partial state — only the
483
+ * keys you provide are changed.
484
+ *
485
+ * Crossdeck.consent({ analytics: false });
486
+ * Crossdeck.consent({ marketing: true, errors: true });
487
+ *
488
+ * DNT-derived denies cannot be flipped back on; if the browser said
489
+ * "don't track" we don't track even if the developer code disagrees.
490
+ */
491
+ consent(state: Partial<ConsentState>): ConsentState;
492
+ /** Snapshot of the current consent state. */
493
+ consentStatus(): ConsentState;
494
+ /**
495
+ * GDPR/CCPA "right to be forgotten" — calls the backend's
496
+ * /v1/identity/forget endpoint to schedule a server-side deletion of
497
+ * the customer's events and profile, then wipes all local state
498
+ * (identity, entitlements, queue, super-props, persistent stores).
499
+ *
500
+ * Idempotent. Safe to call when no identity has been established
501
+ * (it just wipes the empty local state).
502
+ *
503
+ * After forget() resolves, the SDK is in the same shape as if the
504
+ * developer had called `Crossdeck.reset()` — a fresh anonymousId is
505
+ * minted and the next session is a brand new identity-graph entry.
506
+ */
507
+ forget(): Promise<void>;
299
508
  /**
300
509
  * Read the current customer's active entitlements from the server.
301
510
  * Updates the local cache so subsequent isEntitled() calls answer
@@ -458,12 +667,21 @@ interface CrossdeckErrorPayload {
458
667
  requestId?: string;
459
668
  /** HTTP status code if the error came from an API response. */
460
669
  status?: number;
670
+ /**
671
+ * Server-suggested wait (in milliseconds) before retrying. Populated
672
+ * from the `Retry-After` response header on 429 / 503. The header
673
+ * spec allows either delta-seconds or an HTTP-date; the parser below
674
+ * normalises both to milliseconds. Consumers MUST honour this — the
675
+ * server is telling you the safe rate.
676
+ */
677
+ retryAfterMs?: number;
461
678
  }
462
679
  declare class CrossdeckError extends Error {
463
680
  readonly type: CrossdeckErrorType;
464
681
  readonly code: string;
465
682
  readonly requestId?: string;
466
683
  readonly status?: number;
684
+ readonly retryAfterMs?: number;
467
685
  constructor(payload: CrossdeckErrorPayload);
468
686
  }
469
687
 
@@ -526,9 +744,45 @@ declare class MemoryStorage implements KeyValueStorage {
526
744
  * fetch shim, no transitive deps.
527
745
  */
528
746
  declare const SDK_NAME = "@cross-deck/web";
529
- declare const SDK_VERSION = "0.6.0";
747
+ declare const SDK_VERSION = "0.10.0";
530
748
  declare const DEFAULT_BASE_URL = "https://api.cross-deck.com/v1";
531
749
 
750
+ /**
751
+ * Machine-readable index of every error code the SDK can throw, with
752
+ * a short description and a hint on what action to take. Published
753
+ * verbatim as `crossdeck-error-codes.json` in the npm tarball so AI
754
+ * integration assistants, error-aggregator dashboards (Sentry,
755
+ * DataDog), and the Crossdeck dashboard can render human-friendly
756
+ * messages without parsing freeform `message` strings.
757
+ *
758
+ * Stripe publishes the same surface at stripe.com/docs/error-codes;
759
+ * developers love it because every code has a canonical "what does
760
+ * this mean / what should I do" answer.
761
+ *
762
+ * Adding a new error code:
763
+ * 1. Add the code string to the union in `errors.ts` (where used).
764
+ * 2. Add an entry here.
765
+ * 3. The next `npm run build` regenerates the JSON sidecar.
766
+ *
767
+ * Keep entries terse — the consumer surfaces this in tooltips and
768
+ * automated tickets, not in long-form docs.
769
+ */
770
+ interface ErrorCodeEntry {
771
+ /** The string thrown as CrossdeckError.code. */
772
+ code: string;
773
+ /** CrossdeckError.type — broad category. */
774
+ type: "authentication_error" | "permission_error" | "invalid_request_error" | "rate_limit_error" | "internal_error" | "network_error" | "configuration_error";
775
+ /** One-sentence description. Surfaced verbatim in dashboards. */
776
+ description: string;
777
+ /** What the developer should do. Imperative phrasing. */
778
+ resolution: string;
779
+ /** True for codes the SDK can auto-recover from (no developer action). */
780
+ retryable: boolean;
781
+ }
782
+ declare const CROSSDECK_ERROR_CODES: readonly ErrorCodeEntry[];
783
+ /** Lookup helper — returns the entry matching a CrossdeckError.code, or undefined. */
784
+ declare function getErrorCode(code: string): ErrorCodeEntry | undefined;
785
+
532
786
  /**
533
787
  * Device + environment enrichment.
534
788
  *
@@ -563,4 +817,4 @@ interface DeviceInfo {
563
817
  appVersion?: string;
564
818
  }
565
819
 
566
- export { type AliasResult, type AuditRail, type AutoTrackOptions, Crossdeck, CrossdeckClient, CrossdeckError, type CrossdeckErrorPayload, type CrossdeckErrorType, type CrossdeckOptions, DEFAULT_BASE_URL, type DeviceInfo, type Diagnostics, type EntitlementsListResponse, type Environment, type EventProperties, type HeartbeatResponse, type IdentifyOptions, type KeyValueStorage, MemoryStorage, type Platform, type PublicEntitlement, type PurchaseResult, SDK_NAME, SDK_VERSION };
820
+ export { type AliasResult, type AuditRail, type AutoTrackOptions, CROSSDECK_ERROR_CODES, type ConsentState, Crossdeck, CrossdeckClient, CrossdeckError, type CrossdeckErrorPayload, type CrossdeckErrorType, type CrossdeckOptions, DEFAULT_BASE_URL, type DeviceInfo, type Diagnostics, type EntitlementsListResponse, type Environment, type ErrorCodeEntry, type EventProperties, type GroupTraits, type HeartbeatResponse, type IdentifyOptions, type KeyValueStorage, MemoryStorage, type Platform, type PublicEntitlement, type PurchaseResult, SDK_NAME, SDK_VERSION, getErrorCode };