@cross-deck/web 1.5.7 → 1.6.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "generatedAt": "2026-05-28T10:12:32.123Z",
3
+ "generatedAt": "2026-05-30T11:19:57.320Z",
4
4
  "sdk": "@cross-deck/web",
5
5
  "codes": [
6
6
  {
package/dist/index.cjs CHANGED
@@ -99,7 +99,7 @@ function typeMapForStatus(status) {
99
99
  }
100
100
 
101
101
  // src/_version.ts
102
- var SDK_VERSION = "1.5.7";
102
+ var SDK_VERSION = "1.6.1";
103
103
  var SDK_NAME = "@cross-deck/web";
104
104
 
105
105
  // src/http.ts
@@ -1448,6 +1448,8 @@ var DEFAULT_AUTO_TRACK = {
1448
1448
  errors: true
1449
1449
  };
1450
1450
  var SESSION_RESUME_THRESHOLD_MS = 30 * 60 * 1e3;
1451
+ var SESSION_STORAGE_KEY = "crossdeck:session";
1452
+ var ACTIVITY_PERSIST_THROTTLE_MS = 5e3;
1451
1453
  var EMPTY_ACQUISITION = {
1452
1454
  utm_source: "",
1453
1455
  utm_medium: "",
@@ -1463,11 +1465,13 @@ var EMPTY_ACQUISITION = {
1463
1465
  twclid: ""
1464
1466
  };
1465
1467
  var AutoTracker = class {
1466
- constructor(cfg, track) {
1468
+ constructor(cfg, track, opts) {
1467
1469
  this.cfg = cfg;
1468
1470
  this.track = track;
1469
1471
  this.session = null;
1470
1472
  this.cleanups = [];
1473
+ /** Last time we flushed lastActivityAt to storage (throttle gate). */
1474
+ this.lastPersistAt = 0;
1471
1475
  /**
1472
1476
  * Stable per-page-view identifier. Minted at every `page.viewed`
1473
1477
  * emission and attached to every subsequent event until the next
@@ -1479,6 +1483,8 @@ var AutoTracker = class {
1479
1483
  * install if `autoTrack.pageViews !== false`).
1480
1484
  */
1481
1485
  this.pageviewId = null;
1486
+ this.storage = opts?.storage ?? null;
1487
+ this.sessionKey = opts?.storageKey ?? SESSION_STORAGE_KEY;
1482
1488
  }
1483
1489
  install() {
1484
1490
  if (!isBrowserSafe()) return;
@@ -1497,6 +1503,7 @@ var AutoTracker = class {
1497
1503
  if (this.session && !this.session.endedSent) {
1498
1504
  this.emitSessionEnd();
1499
1505
  }
1506
+ this.clearStoredSession();
1500
1507
  this.session = null;
1501
1508
  }
1502
1509
  /** Exposed for tests + consumers that want to reset the session manually. */
@@ -1504,8 +1511,24 @@ var AutoTracker = class {
1504
1511
  if (this.session && !this.session.endedSent) this.emitSessionEnd();
1505
1512
  this.pageviewId = null;
1506
1513
  this.session = this.startNewSession();
1514
+ this.persistSession();
1507
1515
  this.emitSessionStart();
1508
1516
  }
1517
+ /**
1518
+ * Keep the rolling session window alive. Called by the host on EVERY
1519
+ * tracked event (auto or custom) so any activity — not just the
1520
+ * pageviews/clicks AutoTracker emits itself — pushes the 30-min idle
1521
+ * boundary forward. In-memory time is updated exactly; the storage
1522
+ * flush is throttled (pagehide forces a final flush).
1523
+ */
1524
+ markActivity() {
1525
+ if (!this.session) return;
1526
+ const now = Date.now();
1527
+ this.session.lastActivityAt = now;
1528
+ if (now - this.lastPersistAt >= ACTIVITY_PERSIST_THROTTLE_MS) {
1529
+ this.persistSession();
1530
+ }
1531
+ }
1509
1532
  /** Exposed for inspection/tests — returns the current sessionId (or null if not in a session). */
1510
1533
  get currentSessionId() {
1511
1534
  return this.session?.sessionId ?? null;
@@ -1528,26 +1551,43 @@ var AutoTracker = class {
1528
1551
  }
1529
1552
  // ---------- sessions ----------
1530
1553
  installSessionTracking() {
1531
- this.session = this.startNewSession();
1532
- this.emitSessionStart();
1554
+ const now = Date.now();
1555
+ const stored = this.readStoredSession();
1556
+ if (stored && now - stored.lastActivityAt < SESSION_RESUME_THRESHOLD_MS) {
1557
+ this.session = {
1558
+ sessionId: stored.id,
1559
+ startedAt: stored.startedAt,
1560
+ lastActivityAt: now,
1561
+ hiddenAt: null,
1562
+ endedSent: false,
1563
+ acquisition: stored.acquisition
1564
+ };
1565
+ this.persistSession();
1566
+ } else {
1567
+ this.session = this.startNewSession();
1568
+ this.persistSession();
1569
+ this.emitSessionStart();
1570
+ }
1533
1571
  const onVisChange = () => {
1534
1572
  if (!this.session) return;
1535
1573
  const doc2 = globalThis.document;
1536
1574
  if (doc2.visibilityState === "hidden") {
1537
1575
  this.session.hiddenAt = Date.now();
1576
+ this.persistSession();
1538
1577
  } else if (doc2.visibilityState === "visible") {
1539
- const hiddenFor = this.session.hiddenAt ? Date.now() - this.session.hiddenAt : 0;
1540
- if (hiddenFor >= SESSION_RESUME_THRESHOLD_MS) {
1578
+ const idleFor = Date.now() - this.session.lastActivityAt;
1579
+ if (idleFor >= SESSION_RESUME_THRESHOLD_MS) {
1541
1580
  this.emitSessionEnd();
1542
1581
  this.pageviewId = null;
1543
1582
  this.session = this.startNewSession();
1583
+ this.persistSession();
1544
1584
  this.emitSessionStart();
1545
1585
  } else {
1546
1586
  this.session.hiddenAt = null;
1547
1587
  }
1548
1588
  }
1549
1589
  };
1550
- const onPageHide = () => this.emitSessionEnd();
1590
+ const onPageHide = () => this.persistSession();
1551
1591
  const w = globalThis.window;
1552
1592
  const doc = globalThis.document;
1553
1593
  doc.addEventListener("visibilitychange", onVisChange);
@@ -1560,14 +1600,63 @@ var AutoTracker = class {
1560
1600
  });
1561
1601
  }
1562
1602
  startNewSession() {
1603
+ const now = Date.now();
1563
1604
  return {
1564
1605
  sessionId: mintSessionId(),
1565
- startedAt: Date.now(),
1606
+ startedAt: now,
1607
+ lastActivityAt: now,
1566
1608
  hiddenAt: null,
1567
1609
  endedSent: false,
1568
1610
  acquisition: captureAcquisition()
1569
1611
  };
1570
1612
  }
1613
+ /**
1614
+ * Read the persisted session continuity record. Returns null on no
1615
+ * storage, no record, malformed JSON, or a record missing its required
1616
+ * fields — every failure degrades to "no session to resume", which is
1617
+ * the safe (start-fresh) path.
1618
+ */
1619
+ readStoredSession() {
1620
+ if (!this.storage) return null;
1621
+ try {
1622
+ const raw = this.storage.getItem(this.sessionKey);
1623
+ if (!raw) return null;
1624
+ const p = JSON.parse(raw);
1625
+ if (!p || typeof p.id !== "string" || typeof p.startedAt !== "number" || typeof p.lastActivityAt !== "number") {
1626
+ return null;
1627
+ }
1628
+ return {
1629
+ id: p.id,
1630
+ startedAt: p.startedAt,
1631
+ lastActivityAt: p.lastActivityAt,
1632
+ acquisition: p.acquisition && typeof p.acquisition === "object" ? p.acquisition : EMPTY_ACQUISITION
1633
+ };
1634
+ } catch {
1635
+ return null;
1636
+ }
1637
+ }
1638
+ /** Flush the current session to storage. No-op without storage/session. */
1639
+ persistSession() {
1640
+ if (!this.storage || !this.session) return;
1641
+ this.lastPersistAt = Date.now();
1642
+ try {
1643
+ const rec = {
1644
+ id: this.session.sessionId,
1645
+ startedAt: this.session.startedAt,
1646
+ lastActivityAt: this.session.lastActivityAt,
1647
+ acquisition: this.session.acquisition
1648
+ };
1649
+ this.storage.setItem(this.sessionKey, JSON.stringify(rec));
1650
+ } catch {
1651
+ }
1652
+ }
1653
+ clearStoredSession() {
1654
+ if (!this.storage) return;
1655
+ try {
1656
+ this.storage.removeItem(this.sessionKey);
1657
+ } catch {
1658
+ }
1659
+ }
1571
1660
  emitSessionStart() {
1572
1661
  if (!this.session) return;
1573
1662
  this.track("session.started", { sessionId: this.session.sessionId });
@@ -1739,6 +1828,78 @@ function isInsidePasswordField(el) {
1739
1828
  if (el.closest('input[type="password"]')) return true;
1740
1829
  return false;
1741
1830
  }
1831
+ var INLINE_LABEL_TAGS = /* @__PURE__ */ new Set([
1832
+ "span",
1833
+ "b",
1834
+ "strong",
1835
+ "em",
1836
+ "i",
1837
+ "small",
1838
+ "mark",
1839
+ "u",
1840
+ "label",
1841
+ "abbr",
1842
+ "time",
1843
+ "bdi",
1844
+ "cite",
1845
+ "code",
1846
+ "kbd",
1847
+ "q",
1848
+ "sub",
1849
+ "sup"
1850
+ ]);
1851
+ var NON_LABEL_SUBTREES = /* @__PURE__ */ new Set(["svg", "style", "script", "noscript"]);
1852
+ var CONTAINER_MARKERS = "a[href], button, input, select, textarea, [role='button'], [role='link'], h1, h2, h3, h4, h5, h6";
1853
+ var HEADING_SELECTOR = "h1, h2, h3, h4, h5, h6";
1854
+ function collapseWs(s) {
1855
+ return s.replace(/\s+/g, " ").trim();
1856
+ }
1857
+ function boundaryText(el) {
1858
+ let out = "";
1859
+ const walk = (node) => {
1860
+ const kids = node.childNodes;
1861
+ for (let i = 0; i < kids.length; i++) {
1862
+ const child = kids[i];
1863
+ if (!child) continue;
1864
+ if (child.nodeType === 3) {
1865
+ out += child.textContent || "";
1866
+ } else if (child.nodeType === 1) {
1867
+ if (NON_LABEL_SUBTREES.has(child.tagName.toLowerCase())) continue;
1868
+ out += " ";
1869
+ walk(child);
1870
+ out += " ";
1871
+ }
1872
+ }
1873
+ };
1874
+ walk(el);
1875
+ return collapseWs(out);
1876
+ }
1877
+ function directLabel(el) {
1878
+ let out = "";
1879
+ const kids = el.childNodes;
1880
+ for (let i = 0; i < kids.length; i++) {
1881
+ const child = kids[i];
1882
+ if (!child) continue;
1883
+ if (child.nodeType === 3) {
1884
+ out += " " + (child.textContent || "");
1885
+ } else if (child.nodeType === 1 && INLINE_LABEL_TAGS.has(child.tagName.toLowerCase())) {
1886
+ out += " " + boundaryText(child);
1887
+ }
1888
+ }
1889
+ return collapseWs(out);
1890
+ }
1891
+ function visibleLabel(el) {
1892
+ const isContainer = el.querySelector(CONTAINER_MARKERS) !== null;
1893
+ if (!isContainer) return boundaryText(el);
1894
+ const direct = directLabel(el);
1895
+ if (direct) return direct;
1896
+ const heading = el.querySelector(HEADING_SELECTOR);
1897
+ if (heading) {
1898
+ const h = boundaryText(heading);
1899
+ if (h) return h;
1900
+ }
1901
+ return "";
1902
+ }
1742
1903
  function extractText(el) {
1743
1904
  const clean = (s) => s.replace(/\s+/g, " ").trim();
1744
1905
  const explicit = el.getAttribute("data-cd-track") || el.getAttribute("data-track") || el.getAttribute("data-testid");
@@ -1765,7 +1926,7 @@ function extractText(el) {
1765
1926
  const t = clean(el.value);
1766
1927
  if (t) return t;
1767
1928
  }
1768
- const text = clean(el.textContent || "");
1929
+ const text = visibleLabel(el);
1769
1930
  if (text) return text;
1770
1931
  const title = el.getAttribute("title");
1771
1932
  if (title) {
@@ -3312,19 +3473,58 @@ var DEFAULT_ERROR_CAPTURE = {
3312
3473
  "ResizeObserver loop completed with undelivered notifications",
3313
3474
  "Non-Error promise rejection captured"
3314
3475
  // NOTE: We deliberately do NOT drop cross-origin "Script error."
3315
- // events here. They used to be silently filtered as noise, but
3316
- // silent drops are the opposite of "developer sleeps well at
3317
- // night". We now capture them with a clear label and the
3318
- // `cross_origin` tag so the dashboard surfaces them as a
3319
- // distinct, actionable category (the fix is always the same
3320
- // add `crossorigin="anonymous"` to the script tag + CORS
3321
- // headers on the script's origin). Apps that genuinely want
3322
- // them muted can re-add "Script error" to ignoreErrors via
3323
- // init config.
3476
+ // events here. The actionability principle (see denyUrls
3477
+ // comment below) draws the line at "developer cannot act":
3478
+ // cross-origin CORS opacity HAS a real, code-shaped fix (add
3479
+ // `crossorigin="anonymous"` to the script tag + CORS headers
3480
+ // on the script's origin). The dashboard surfaces these with a
3481
+ // `cross_origin` tag pointing at that fix. Apps that genuinely
3482
+ // want them muted can re-add "Script error" to ignoreErrors
3483
+ // via init config.
3324
3484
  ],
3325
3485
  allowUrls: [],
3326
3486
  denyUrls: [
3327
- // Common third-party extensions that pollute error streams.
3487
+ // The actionability principle
3488
+ // ---------------------------
3489
+ // Crossdeck's default philosophy is "classify, don't silently
3490
+ // drop" — surfacing errors the developer can fix is more useful
3491
+ // than hiding them. That's right for cross-origin "Script
3492
+ // error." (real, code-shaped CORS fix) and for plain
3493
+ // application bugs (obviously real).
3494
+ //
3495
+ // It's NOT right for events the developer cannot act on.
3496
+ // - A user's installed browser extension throwing inside
3497
+ // its own `chrome-extension://` URL: the developer can't
3498
+ // ship a fix for someone else's extension.
3499
+ // - An ad blocker preventing `googletagmanager.com` from
3500
+ // loading: the developer can't unblock the user's blocker.
3501
+ //
3502
+ // Capturing these creates a noise tab that's always non-empty
3503
+ // and never actionable, which trains the dev to ignore the
3504
+ // noise tab — and then the actionable noise (CORS opacity,
3505
+ // etc.) gets ignored along with it. Same lesson as Crossdeck's
3506
+ // "0-2 notifications a week" discipline: a signal that fires
3507
+ // constantly with nothing actionable behind it stops being a
3508
+ // signal.
3509
+ //
3510
+ // So we drop at the source for the unactionable category, and
3511
+ // keep capturing for the actionable category. Same principle
3512
+ // applied to two structurally different inputs — not a new
3513
+ // philosophy.
3514
+ //
3515
+ // The bootstrap list below is the minimum that ships in code.
3516
+ // The full, versioned list arrives at boot via /v1/config —
3517
+ // see `backend/src/lib/error-noise-deny-list.ts`. The SDK
3518
+ // applies the union of (bootstrap + remote), so a freshly-
3519
+ // installed SDK protects users immediately, and remote updates
3520
+ // (new ad-network domains, new pixel hosts) reach every
3521
+ // install without an SDK release.
3522
+ //
3523
+ // The backend Layer-2 classifier
3524
+ // (`backend/src/api/lib/noise-classifier.ts`) is the safety
3525
+ // net for events that slip past these patterns — older SDK
3526
+ // versions in the wild that haven't fetched the remote list
3527
+ // yet, or brand-new patterns the list hasn't named.
3328
3528
  /^chrome-extension:\/\//,
3329
3529
  /^moz-extension:\/\//,
3330
3530
  /^safari-extension:\/\//,
@@ -4117,7 +4317,12 @@ var CrossdeckClient = class {
4117
4317
  if (autoTrack.sessions || autoTrack.pageViews) {
4118
4318
  const tracker = new AutoTracker(
4119
4319
  autoTrack,
4120
- (name, properties) => this.track(name, properties)
4320
+ (name, properties) => this.track(name, properties),
4321
+ // Persist session continuity on the SAME adapter as identity, so
4322
+ // a visit survives full-page navigations (multi-page sites
4323
+ // re-install the SDK on every page) and honours the same consent
4324
+ // posture — MemoryStorage when identity persistence is off.
4325
+ { storage: effectiveStorage, storageKey: opts.storagePrefix + "session" }
4121
4326
  );
4122
4327
  this.state.autoTracker = tracker;
4123
4328
  tracker.install();
@@ -4582,6 +4787,12 @@ var CrossdeckClient = class {
4582
4787
  * true even before the session's first network round-trip. Returns
4583
4788
  * false only for a genuinely new install that has never completed a
4584
4789
  * getEntitlements(), or for an entitlement past its own validUntil.
4790
+ *
4791
+ * Throws `not_initialized` (CrossdeckError, type
4792
+ * `configuration_error`) if called before `Crossdeck.init()`. The
4793
+ * `useEntitlement` React hook and the Vue composable both swallow
4794
+ * this and return `false`; bare callers must guard for it (or call
4795
+ * after `init()` resolves).
4585
4796
  */
4586
4797
  isEntitled(key) {
4587
4798
  const s = this.requireStarted();
@@ -4597,8 +4808,21 @@ var CrossdeckClient = class {
4597
4808
  *
4598
4809
  * The listener is invoked AFTER the cache mutates — once after a
4599
4810
  * successful `getEntitlements()` warms it, again after `syncPurchases()`
4600
- * delivers fresh entitlements, and once on `reset()` to fire the
4601
- * empty-cache state for logout flows.
4811
+ * delivers fresh entitlements, once on `reset()` to fire the empty-
4812
+ * cache state for logout flows, AND once on `identify()` after the
4813
+ * per-user cache slot rotates and re-hydrates from device storage.
4814
+ *
4815
+ * IMPORTANT — the `identify()` fire is a TRAP if you treat it as
4816
+ * authoritative network state. `identify()` does NOT fetch entitlements;
4817
+ * it switches the per-user cache slot and rehydrates from device
4818
+ * storage (which is empty for a brand-new install, and last-known-good
4819
+ * — possibly stale — for a returning user). A listener that gates a
4820
+ * paywall on the first fire after an identity switch will read
4821
+ * `false` for a paying customer on a fresh device and let them past
4822
+ * the gate as free. The network-truth fire is the one that follows
4823
+ * the next `getEntitlements()` resolution. Either call
4824
+ * `getEntitlements()` explicitly after `identify()`, or have your
4825
+ * gating code tolerate the empty-then-populated transition.
4602
4826
  *
4603
4827
  * It is NOT invoked synchronously on subscribe. Callers that need
4604
4828
  * the current state should read it via `isEntitled()` / `listEntitlements()`
@@ -4707,6 +4931,7 @@ var CrossdeckClient = class {
4707
4931
  );
4708
4932
  }
4709
4933
  }
4934
+ s.autoTracker?.markActivity();
4710
4935
  const enriched = { ...s.deviceInfo };
4711
4936
  const sessionId = s.autoTracker?.currentSessionId;
4712
4937
  if (sessionId) enriched.sessionId = sessionId;
@@ -4961,6 +5186,25 @@ var CrossdeckClient = class {
4961
5186
  events: s.events.getStats()
4962
5187
  };
4963
5188
  }
5189
+ /**
5190
+ * The stable reference to hand Stripe at checkout so the resulting
5191
+ * purchase attributes to THIS person. Stamp it on the Checkout Session
5192
+ * as `metadata.crossdeck_ref` (see docs/connect-stripe) — the platform
5193
+ * webhook reads it back, validates it, and attaches the subscription to
5194
+ * the right customer instead of stranding it on an anonymous record.
5195
+ *
5196
+ * Returns the strongest identifier the SDK currently holds, in the same
5197
+ * precedence the server resolves by:
5198
+ * crossdeckCustomerId (cdcust_…) > developerUserId > anonymousId
5199
+ * anonymousId is always present, so this always returns a usable,
5200
+ * non-empty reference — even before identify(). Identify the user as
5201
+ * early as you can, though, so the reference is their stable identity
5202
+ * and not a per-device anon id.
5203
+ */
5204
+ getCheckoutReference() {
5205
+ const s = this.requireStarted();
5206
+ return s.identity.crossdeckCustomerId ?? s.developerUserId ?? s.identity.anonymousId;
5207
+ }
4964
5208
  // ---------- private helpers ----------
4965
5209
  requireStarted() {
4966
5210
  if (!this.state) {
@@ -5282,8 +5526,8 @@ function getErrorCode(code) {
5282
5526
  }
5283
5527
 
5284
5528
  // src/_contracts-bundled.ts
5285
- var BUNDLED_IN = "@cross-deck/web@1.5.7";
5286
- var SDK_VERSION2 = "1.5.7";
5529
+ var BUNDLED_IN = "@cross-deck/web@1.6.1";
5530
+ var SDK_VERSION2 = "1.6.1";
5287
5531
  var BUNDLED_CONTRACTS = Object.freeze([
5288
5532
  {
5289
5533
  "id": "contract-failed-payload-schema-lock",
@@ -5405,7 +5649,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
5405
5649
  "legal/security/index.html#diagnostic",
5406
5650
  "legal/sdk-data/index.html#b-diagnostic"
5407
5651
  ],
5408
- "bundledIn": "@cross-deck/web@1.5.7"
5652
+ "bundledIn": "@cross-deck/web@1.6.1"
5409
5653
  },
5410
5654
  {
5411
5655
  "id": "error-envelope-shape",
@@ -5444,7 +5688,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
5444
5688
  ],
5445
5689
  "registeredAt": "2026-05-26",
5446
5690
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 8 (codifies existing contract)",
5447
- "bundledIn": "@cross-deck/web@1.5.7"
5691
+ "bundledIn": "@cross-deck/web@1.6.1"
5448
5692
  },
5449
5693
  {
5450
5694
  "id": "flush-interval-parity",
@@ -5489,7 +5733,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
5489
5733
  ],
5490
5734
  "registeredAt": "2026-05-26",
5491
5735
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 3.3",
5492
- "bundledIn": "@cross-deck/web@1.5.7"
5736
+ "bundledIn": "@cross-deck/web@1.6.1"
5493
5737
  },
5494
5738
  {
5495
5739
  "id": "idempotency-key-deterministic",
@@ -5594,7 +5838,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
5594
5838
  ],
5595
5839
  "registeredAt": "2026-05-26",
5596
5840
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 2.2.a + 2.2.b + 2.2.c",
5597
- "bundledIn": "@cross-deck/web@1.5.7"
5841
+ "bundledIn": "@cross-deck/web@1.6.1"
5598
5842
  },
5599
5843
  {
5600
5844
  "id": "init-reentry-drains-prior-queue",
@@ -5621,7 +5865,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
5621
5865
  ],
5622
5866
  "registeredAt": "2026-05-26",
5623
5867
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 5.5",
5624
- "bundledIn": "@cross-deck/web@1.5.7"
5868
+ "bundledIn": "@cross-deck/web@1.6.1"
5625
5869
  },
5626
5870
  {
5627
5871
  "id": "per-user-cache-isolation",
@@ -5700,7 +5944,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
5700
5944
  ],
5701
5945
  "registeredAt": "2026-05-26",
5702
5946
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 1.3 (web/RN) + dogfood-gap fix (swift + android)",
5703
- "bundledIn": "@cross-deck/web@1.5.7"
5947
+ "bundledIn": "@cross-deck/web@1.6.1"
5704
5948
  },
5705
5949
  {
5706
5950
  "id": "sdk-error-codes-catalogue",
@@ -5740,7 +5984,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
5740
5984
  ],
5741
5985
  "registeredAt": "2026-05-26",
5742
5986
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 6.2",
5743
- "bundledIn": "@cross-deck/web@1.5.7"
5987
+ "bundledIn": "@cross-deck/web@1.6.1"
5744
5988
  },
5745
5989
  {
5746
5990
  "id": "sync-purchases-funnel-parity",
@@ -5773,7 +6017,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
5773
6017
  ],
5774
6018
  "registeredAt": "2026-05-26",
5775
6019
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 3.5",
5776
- "bundledIn": "@cross-deck/web@1.5.7"
6020
+ "bundledIn": "@cross-deck/web@1.6.1"
5777
6021
  }
5778
6022
  ]);
5779
6023