@cross-deck/web 1.11.2 → 1.13.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "generatedAt": "2026-07-23T09:07:15.132Z",
3
+ "generatedAt": "2026-07-26T13:09:24.739Z",
4
4
  "sdk": "@cross-deck/web",
5
5
  "codes": [
6
6
  {
package/dist/index.cjs CHANGED
@@ -29,7 +29,9 @@ __export(index_exports, {
29
29
  MemoryStorage: () => MemoryStorage,
30
30
  SDK_NAME: () => SDK_NAME,
31
31
  SDK_VERSION: () => SDK_VERSION,
32
- getErrorCode: () => getErrorCode
32
+ TRUST_PANEL_ORIGIN: () => TRUST_PANEL_ORIGIN,
33
+ getErrorCode: () => getErrorCode,
34
+ mountTrustPanel: () => mountTrustPanel
33
35
  });
34
36
  module.exports = __toCommonJS(index_exports);
35
37
 
@@ -104,7 +106,7 @@ function typeMapForStatus(status) {
104
106
  }
105
107
 
106
108
  // src/_version.ts
107
- var SDK_VERSION = "1.11.2";
109
+ var SDK_VERSION = "1.13.0";
108
110
  var SDK_NAME = "@cross-deck/web";
109
111
 
110
112
  // src/http.ts
@@ -361,6 +363,125 @@ function randomChars(count) {
361
363
  return out.join("");
362
364
  }
363
365
 
366
+ // src/trust.ts
367
+ var TRUST_PANEL_ORIGIN = "https://trust.cross-deck.com";
368
+ var MAX_WIDTH_PX = 360;
369
+ var INITIAL_HEIGHT_PX = 132;
370
+ var MIN_HEIGHT_PX = 96;
371
+ var MAX_HEIGHT_PX = 220;
372
+ var DEFAULT_TIMEOUT_MS2 = 15e3;
373
+ function mountTrustPanel(opts) {
374
+ const origin = normalizeOrigin(opts.origin) || TRUST_PANEL_ORIGIN;
375
+ const timeoutMs = clampInt(opts.timeoutMs, DEFAULT_TIMEOUT_MS2, 1e3, 12e4);
376
+ let settled = false;
377
+ let onMessage = null;
378
+ let timer = null;
379
+ let frame = null;
380
+ let resolveReady;
381
+ const ready = new Promise((res) => {
382
+ resolveReady = res;
383
+ });
384
+ function settle(result, reason) {
385
+ if (settled) return;
386
+ settled = true;
387
+ if (timer) safe(() => clearTimeout(timer));
388
+ if (onMessage) safe(() => window.removeEventListener("message", onMessage));
389
+ if (result) {
390
+ safe(() => opts.onToken?.(result));
391
+ resolveReady(result);
392
+ } else {
393
+ const why = reason || "unavailable";
394
+ safe(() => opts.onUnavailable?.(why));
395
+ resolveReady({ token: null, expiresAt: null, reason: why });
396
+ }
397
+ }
398
+ try {
399
+ if (typeof window === "undefined" || typeof document === "undefined") {
400
+ queueMicrotask(() => settle(null, "no_dom"));
401
+ return makeHandle();
402
+ }
403
+ const host = resolveTarget(opts.target);
404
+ frame = createFrame(origin, opts.publicKey);
405
+ if (!host) {
406
+ queueMicrotask(() => settle(null, "no_mount_target"));
407
+ return makeHandle();
408
+ }
409
+ onMessage = (ev) => {
410
+ if (ev.origin !== origin) return;
411
+ if (!ev.data || !frame || ev.source !== frame.contentWindow) return;
412
+ const d = ev.data;
413
+ if (d.type === "cd-trust-size" && typeof d.height === "number") {
414
+ safe(() => {
415
+ if (frame) frame.style.height = clampInt(d.height, INITIAL_HEIGHT_PX, MIN_HEIGHT_PX, MAX_HEIGHT_PX) + "px";
416
+ });
417
+ } else if (d.type === "cd-trust-token" && typeof d.token === "string") {
418
+ settle({ token: d.token, expiresAt: typeof d.expMs === "number" ? d.expMs : null });
419
+ }
420
+ };
421
+ window.addEventListener("message", onMessage);
422
+ host.appendChild(frame);
423
+ timer = setTimeout(() => settle(null, "timeout"), timeoutMs);
424
+ } catch (err) {
425
+ queueMicrotask(() => settle(null, "mount_error:" + safeMessage(err)));
426
+ }
427
+ return makeHandle();
428
+ function makeHandle() {
429
+ return {
430
+ frame,
431
+ ready,
432
+ destroy() {
433
+ if (onMessage) safe(() => window.removeEventListener("message", onMessage));
434
+ if (timer) safe(() => clearTimeout(timer));
435
+ safe(() => frame?.parentNode?.removeChild(frame));
436
+ settle(null, "destroyed");
437
+ }
438
+ };
439
+ }
440
+ }
441
+ function createFrame(origin, publicKey) {
442
+ const frame = document.createElement("iframe");
443
+ frame.title = "Protected by Crossdeck Trust";
444
+ frame.setAttribute("aria-label", "Protected by Crossdeck Trust");
445
+ frame.setAttribute("scrolling", "no");
446
+ frame.setAttribute("allowtransparency", "true");
447
+ frame.style.cssText = "border:0;display:block;width:100%;max-width:" + MAX_WIDTH_PX + "px;height:" + INITIAL_HEIGHT_PX + "px;margin:0;background:transparent;color-scheme:light;";
448
+ frame.src = origin + "/panel?k=" + encodeURIComponent(publicKey || "");
449
+ return frame;
450
+ }
451
+ function resolveTarget(target) {
452
+ try {
453
+ if (typeof target === "string") return document.querySelector(target);
454
+ return target || null;
455
+ } catch {
456
+ return null;
457
+ }
458
+ }
459
+ function normalizeOrigin(o) {
460
+ if (!o || typeof o !== "string") return null;
461
+ try {
462
+ return new URL(o).origin;
463
+ } catch {
464
+ return null;
465
+ }
466
+ }
467
+ function clampInt(v, fallback, min, max) {
468
+ const n = typeof v === "number" && isFinite(v) ? v : fallback;
469
+ return Math.min(Math.max(n, min), max);
470
+ }
471
+ function safe(fn) {
472
+ try {
473
+ fn();
474
+ } catch {
475
+ }
476
+ }
477
+ function safeMessage(err) {
478
+ try {
479
+ return err instanceof Error ? err.message : String(err);
480
+ } catch {
481
+ return "unknown";
482
+ }
483
+ }
484
+
364
485
  // src/hash.ts
365
486
  var K = new Uint32Array([
366
487
  1116352408,
@@ -1594,6 +1715,7 @@ var AutoTracker = class {
1594
1715
  this.pageviewId = null;
1595
1716
  this.storage = opts?.storage ?? null;
1596
1717
  this.sessionKey = opts?.storageKey ?? SESSION_STORAGE_KEY;
1718
+ this.cookieStorage = opts?.cookieStorage ?? null;
1597
1719
  }
1598
1720
  install() {
1599
1721
  if (!isBrowserSafe()) return;
@@ -1745,8 +1867,73 @@ var AutoTracker = class {
1745
1867
  lastActivityAt: now,
1746
1868
  hiddenAt: null,
1747
1869
  endedSent: false,
1748
- acquisition: captureAcquisition()
1870
+ acquisition: this.resolveAcquisition()
1871
+ };
1872
+ }
1873
+ /**
1874
+ * Session acquisition, with FIRST-TOUCH survival across sessions.
1875
+ *
1876
+ * WHY: `captureAcquisition()` reads the CURRENT page. That is correct for a
1877
+ * fresh arrival, but a session boundary (30-min idle, or a return visit next
1878
+ * week) starts a new session — and a visitor returning directly has no utm_*
1879
+ * and no click id, so the acquisition that actually WON them was silently
1880
+ * replaced with empty. Someone who arrives from LinkedIn on Monday, returns
1881
+ * direct on Tuesday and signs up on Wednesday converted with no source at all.
1882
+ *
1883
+ * That is exactly the question the product exists to answer — "where did this
1884
+ * PAYING CUSTOMER come from" — so origin has to outlive the session that
1885
+ * captured it. We persist the first touch that carried a real signal and,
1886
+ * whenever a later session arrives with nothing, fall back to it.
1887
+ *
1888
+ * Deliberately NOT last-touch-wins: a real new campaign click overwrites
1889
+ * nothing, it simply records a newer touch for that session while the stored
1890
+ * first touch stays put. Write-once by design.
1891
+ */
1892
+ resolveAcquisition() {
1893
+ const current = captureAcquisition();
1894
+ if (!this.storage && !this.cookieStorage) return current;
1895
+ const key = `${this.sessionKey}_origin_first`;
1896
+ const readFirstTouch = () => {
1897
+ try {
1898
+ const v = this.storage?.getItem(key);
1899
+ if (v) return v;
1900
+ } catch {
1901
+ }
1902
+ try {
1903
+ return this.cookieStorage?.getItem(key) ?? null;
1904
+ } catch {
1905
+ return null;
1906
+ }
1907
+ };
1908
+ const writeFirstTouch = (v) => {
1909
+ try {
1910
+ this.storage?.setItem(key, v);
1911
+ } catch {
1912
+ }
1913
+ try {
1914
+ this.cookieStorage?.setItem(key, v);
1915
+ } catch {
1916
+ }
1749
1917
  };
1918
+ const hasSignal = (a) => Boolean(a.utm_source || a.utm_medium || a.utm_campaign || a.utm_content || a.utm_term || a.gclid || a.fbclid || a.msclkid || a.ttclid || a.li_fat_id || a.twclid);
1919
+ try {
1920
+ if (hasSignal(current)) {
1921
+ if (readFirstTouch() === null) writeFirstTouch(JSON.stringify(current));
1922
+ return current;
1923
+ }
1924
+ const raw = readFirstTouch();
1925
+ if (raw) {
1926
+ const stored = JSON.parse(raw);
1927
+ return {
1928
+ ...EMPTY_ACQUISITION,
1929
+ ...stored,
1930
+ // Referrer always describes THIS visit, never the remembered one.
1931
+ referrer: current.referrer
1932
+ };
1933
+ }
1934
+ } catch {
1935
+ }
1936
+ return current;
1750
1937
  }
1751
1938
  /**
1752
1939
  * Read the persisted session continuity record. Returns null on no
@@ -4794,7 +4981,15 @@ var CrossdeckClient = class {
4794
4981
  // a visit survives full-page navigations (multi-page sites
4795
4982
  // re-install the SDK on every page) and honours the same consent
4796
4983
  // posture — MemoryStorage when identity persistence is off.
4797
- { storage: effectiveStorage, storageKey: opts.storagePrefix + "session" }
4984
+ // cookieStorage is the SAME registrable-domain cookie identity uses.
4985
+ // First-touch origin rides it so the campaign that won a visitor on the
4986
+ // marketing site is still known when they land on the app subdomain —
4987
+ // localStorage alone is per-origin and would lose it at exactly that hop.
4988
+ {
4989
+ storage: effectiveStorage,
4990
+ storageKey: opts.storagePrefix + "session",
4991
+ cookieStorage: cookieStore ?? void 0
4992
+ }
4798
4993
  );
4799
4994
  this.state.autoTracker = tracker;
4800
4995
  tracker.install();
@@ -5749,6 +5944,30 @@ var CrossdeckClient = class {
5749
5944
  getAnonymousId() {
5750
5945
  return this.state ? this.state.identity.anonymousId : null;
5751
5946
  }
5947
+ /**
5948
+ * **Crossdeck Trust** — human-proof at your signup, native to the SDK.
5949
+ *
5950
+ * `Crossdeck.trust.panel({ target, onToken })` renders the branded, un-restylable
5951
+ * Trust panel (the same cross-origin iframe on every install) and mints a
5952
+ * single-use attestation. Hand the token to your server and verify it at the gate
5953
+ * (`crossdeck.trust.gate(...)` in @cross-deck/node). The publishable key is taken
5954
+ * from your `init()` — you don't pass it again.
5955
+ *
5956
+ * Fail-open by contract: if the panel can't mint (adblocker, offline, our outage),
5957
+ * `ready` resolves with `{ token: null }` and your signup proceeds — the server
5958
+ * scores the missing token. It never throws and never blocks your form.
5959
+ *
5960
+ * @example
5961
+ * const { ready } = Crossdeck.trust.panel({ target: "#cd-trust" });
5962
+ * const result = await ready; // { token, expiresAt } | { token: null }
5963
+ * await createUser({ email, cdTrustToken: result.token });
5964
+ */
5965
+ get trust() {
5966
+ const publicKey = this.state ? this.state.options.publicKey : "";
5967
+ return {
5968
+ panel: (input) => mountTrustPanel({ ...input, publicKey })
5969
+ };
5970
+ }
5752
5971
  // ---------- private helpers ----------
5753
5972
  requireStarted() {
5754
5973
  if (!this.state) {
@@ -5874,8 +6093,8 @@ function installUnloadFlush(onUnload) {
5874
6093
  }
5875
6094
 
5876
6095
  // src/_contracts-bundled.ts
5877
- var BUNDLED_IN = "@cross-deck/web@1.8.0";
5878
- var SDK_VERSION2 = "1.8.0";
6096
+ var BUNDLED_IN = "@cross-deck/web@1.12.0";
6097
+ var SDK_VERSION2 = "1.12.0";
5879
6098
  var BUNDLED_CONTRACTS = Object.freeze([
5880
6099
  {
5881
6100
  "id": "contract-failed-payload-schema-lock",
@@ -5997,7 +6216,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
5997
6216
  "legal/security/index.html#diagnostic",
5998
6217
  "legal/sdk-data/index.html#b-diagnostic"
5999
6218
  ],
6000
- "bundledIn": "@cross-deck/web@1.8.0",
6219
+ "bundledIn": "@cross-deck/web@1.12.0",
6001
6220
  "runtimeVerified": true
6002
6221
  },
6003
6222
  {
@@ -6037,7 +6256,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
6037
6256
  ],
6038
6257
  "registeredAt": "2026-05-26",
6039
6258
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 8 (codifies existing contract)",
6040
- "bundledIn": "@cross-deck/web@1.8.0",
6259
+ "bundledIn": "@cross-deck/web@1.12.0",
6041
6260
  "runtimeVerified": true
6042
6261
  },
6043
6262
  {
@@ -6083,7 +6302,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
6083
6302
  ],
6084
6303
  "registeredAt": "2026-05-26",
6085
6304
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 3.3",
6086
- "bundledIn": "@cross-deck/web@1.8.0",
6305
+ "bundledIn": "@cross-deck/web@1.12.0",
6087
6306
  "runtimeVerified": true
6088
6307
  },
6089
6308
  {
@@ -6189,7 +6408,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
6189
6408
  ],
6190
6409
  "registeredAt": "2026-05-26",
6191
6410
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 2.2.a + 2.2.b + 2.2.c",
6192
- "bundledIn": "@cross-deck/web@1.8.0",
6411
+ "bundledIn": "@cross-deck/web@1.12.0",
6193
6412
  "runtimeVerified": true
6194
6413
  },
6195
6414
  {
@@ -6217,7 +6436,64 @@ var BUNDLED_CONTRACTS = Object.freeze([
6217
6436
  ],
6218
6437
  "registeredAt": "2026-05-26",
6219
6438
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 5.5",
6220
- "bundledIn": "@cross-deck/web@1.8.0",
6439
+ "bundledIn": "@cross-deck/web@1.12.0",
6440
+ "runtimeVerified": false
6441
+ },
6442
+ {
6443
+ "id": "invalid-input-rejected-natively",
6444
+ "pillar": "errors",
6445
+ "status": "enforced",
6446
+ "claim": "No public SDK API ever crashes the host app, and invalid input never reaches the wire. Invalid input (empty event name, empty userId, out-of-range config such as a non-positive breadcrumb capacity, NaN/Infinity/oversize/cyclic property values) is rejected at the call site WITHOUT a fatal trap. The signalling IDIOM is per-language and intentionally NOT uniform: Web, Node, and React Native THROW a typed CrossdeckError synchronously (code missing_event_name / missing_user_id / invalid_request_error) \u2014 a normal, catchable JavaScript convention where an uncaught throw logs and the app continues; Swift DROPS with a debug-log signal (track_dropped / identify_dropped) to match its non-throwing fire-and-forget surface, and exposes the throwing equivalent only via identifyAndWait(userId:). What is UNIFORM is the invariant, not the mechanism: every SDK rejects the same inputs, no public fire-and-forget API contains a fatalError / assertionFailure / precondition reachable from customer input, and no rejected input is enqueued or transmitted. Swift additionally proves this in BOTH debug and release configuration, because precondition fires under -O while assertionFailure does not. The bug class this contract closes was never the per-language difference \u2014 it was the UNDECLARED difference: each SDK's public API documentation must state its own semantics explicitly (TS docs: 'throws on empty name'; Swift docs: 'drops and logs').",
6447
+ "appliesTo": [
6448
+ "web",
6449
+ "node",
6450
+ "react-native",
6451
+ "swift"
6452
+ ],
6453
+ "codeRef": [
6454
+ "sdks/web/src/crossdeck.ts",
6455
+ "sdks/node/src/crossdeck-server.ts",
6456
+ "sdks/react-native/src/crossdeck.ts",
6457
+ "sdks/swift/Sources/Crossdeck/Crossdeck.swift",
6458
+ "sdks/swift/Sources/Crossdeck/Breadcrumbs.swift"
6459
+ ],
6460
+ "testRef": [
6461
+ {
6462
+ "file": "sdks/web/tests/crossdeck.test.ts",
6463
+ "name": "track with empty name throws synchronously"
6464
+ },
6465
+ {
6466
+ "file": "sdks/web/tests/crossdeck.test.ts",
6467
+ "name": "rejects empty userId"
6468
+ },
6469
+ {
6470
+ "file": "sdks/node/tests/crossdeck-server.test.ts",
6471
+ "name": "track() throws CrossdeckError with code 'missing_event_name' when event name is empty"
6472
+ },
6473
+ {
6474
+ "file": "sdks/react-native/tests/crossdeck.test.ts",
6475
+ "name": "track('') throws CrossdeckError(missing_event_name) synchronously"
6476
+ },
6477
+ {
6478
+ "file": "sdks/react-native/tests/crossdeck.test.ts",
6479
+ "name": "identify('') rejects with CrossdeckError(missing_user_id)"
6480
+ },
6481
+ {
6482
+ "file": "sdks/swift/Tests/CrossdeckTests/CrossdeckPublicAPITests.swift",
6483
+ "name": "test_track_dropsEmptyName"
6484
+ },
6485
+ {
6486
+ "file": "sdks/swift/Tests/CrossdeckTests/CrossdeckPublicAPITests.swift",
6487
+ "name": "test_identifyAndWait_rejectsEmptyId"
6488
+ },
6489
+ {
6490
+ "file": "sdks/swift/Tests/CrossdeckTests/PublicAPIInputSafetyTests.swift",
6491
+ "name": "test_start_withZeroBreadcrumbCapacity_doesNotTrap"
6492
+ }
6493
+ ],
6494
+ "registeredAt": "2026-06-11",
6495
+ "firstRegisteredIn": "swift trap-on-input class fix \u2014 first machine-tested Swift release",
6496
+ "bundledIn": "@cross-deck/web@1.12.0",
6221
6497
  "runtimeVerified": false
6222
6498
  },
6223
6499
  {
@@ -6297,7 +6573,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
6297
6573
  ],
6298
6574
  "registeredAt": "2026-05-26",
6299
6575
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 1.3 (web/RN) + dogfood-gap fix (swift + android)",
6300
- "bundledIn": "@cross-deck/web@1.8.0",
6576
+ "bundledIn": "@cross-deck/web@1.12.0",
6301
6577
  "runtimeVerified": true
6302
6578
  },
6303
6579
  {
@@ -6343,7 +6619,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
6343
6619
  ],
6344
6620
  "registeredAt": "2026-05-26",
6345
6621
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 6.2",
6346
- "bundledIn": "@cross-deck/web@1.8.0",
6622
+ "bundledIn": "@cross-deck/web@1.12.0",
6347
6623
  "runtimeVerified": true
6348
6624
  },
6349
6625
  {
@@ -6385,7 +6661,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
6385
6661
  ],
6386
6662
  "registeredAt": "2026-05-26",
6387
6663
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 3.2",
6388
- "bundledIn": "@cross-deck/web@1.8.0",
6664
+ "bundledIn": "@cross-deck/web@1.12.0",
6389
6665
  "runtimeVerified": true
6390
6666
  },
6391
6667
  {
@@ -6419,7 +6695,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
6419
6695
  ],
6420
6696
  "registeredAt": "2026-05-26",
6421
6697
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 3.5",
6422
- "bundledIn": "@cross-deck/web@1.8.0",
6698
+ "bundledIn": "@cross-deck/web@1.12.0",
6423
6699
  "runtimeVerified": false
6424
6700
  }
6425
6701
  ]);
@@ -6484,6 +6760,8 @@ var CrossdeckContracts = {
6484
6760
  MemoryStorage,
6485
6761
  SDK_NAME,
6486
6762
  SDK_VERSION,
6487
- getErrorCode
6763
+ TRUST_PANEL_ORIGIN,
6764
+ getErrorCode,
6765
+ mountTrustPanel
6488
6766
  });
6489
6767
  //# sourceMappingURL=index.cjs.map