@cross-deck/web 1.5.1 → 1.5.3

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.
Files changed (47) hide show
  1. package/dist/contracts.json +133 -11
  2. package/dist/crossdeck.umd.min.js +2 -2
  3. package/dist/crossdeck.umd.min.js.map +1 -1
  4. package/dist/error-codes.json +1 -1
  5. package/dist/index.cjs +1081 -25
  6. package/dist/index.cjs.map +1 -1
  7. package/dist/index.d.mts +51 -16
  8. package/dist/index.d.ts +51 -16
  9. package/dist/index.mjs +1081 -25
  10. package/dist/index.mjs.map +1 -1
  11. package/dist/react.cjs +950 -16
  12. package/dist/react.cjs.map +1 -1
  13. package/dist/react.d.mts +1 -1
  14. package/dist/react.d.ts +1 -1
  15. package/dist/react.mjs +950 -16
  16. package/dist/react.mjs.map +1 -1
  17. package/dist/{types-Bu3jbmdq.d.ts → types-B9sxUuKh.d.mts} +71 -0
  18. package/dist/{types-Bu3jbmdq.d 2.mts → types-B9sxUuKh.d.ts} +71 -0
  19. package/dist/vue.cjs +950 -16
  20. package/dist/vue.cjs.map +1 -1
  21. package/dist/vue.mjs +950 -16
  22. package/dist/vue.mjs.map +1 -1
  23. package/package.json +1 -1
  24. package/dist/contracts 2.json +0 -378
  25. package/dist/crossdeck.umd.min 2.js +0 -3
  26. package/dist/crossdeck.umd.min.js 2.map +0 -1
  27. package/dist/error-codes 2.json +0 -196
  28. package/dist/index 2.cjs +0 -4778
  29. package/dist/index 2.mjs +0 -4742
  30. package/dist/index.cjs 2.map +0 -1
  31. package/dist/index.d 2.mts +0 -938
  32. package/dist/index.d 2.ts +0 -938
  33. package/dist/index.mjs 2.map +0 -1
  34. package/dist/react 2.cjs +0 -4220
  35. package/dist/react 2.mjs +0 -4193
  36. package/dist/react.cjs 2.map +0 -1
  37. package/dist/react.d 2.mts +0 -91
  38. package/dist/react.d 2.ts +0 -91
  39. package/dist/react.mjs 2.map +0 -1
  40. package/dist/types-Bu3jbmdq.d 2.ts +0 -314
  41. package/dist/types-Bu3jbmdq.d.mts +0 -314
  42. package/dist/vue 2.cjs +0 -4185
  43. package/dist/vue 2.mjs +0 -4159
  44. package/dist/vue.cjs 2.map +0 -1
  45. package/dist/vue.d 2.mts +0 -37
  46. package/dist/vue.d 2.ts +0 -37
  47. package/dist/vue.mjs 2.map +0 -1
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.4.2";
102
+ var SDK_VERSION = "1.5.3";
103
103
  var SDK_NAME = "@cross-deck/web";
104
104
 
105
105
  // src/http.ts
@@ -162,7 +162,14 @@ var HttpClient = class {
162
162
  if (timeoutHandle !== null) clearTimeout(timeoutHandle);
163
163
  }
164
164
  if (!response.ok) {
165
- throw await crossdeckErrorFromResponse(response);
165
+ const parsed = await crossdeckErrorFromResponse(response);
166
+ if (this.config.onErrorParsed) {
167
+ try {
168
+ this.config.onErrorParsed(parsed);
169
+ } catch {
170
+ }
171
+ }
172
+ throw parsed;
166
173
  }
167
174
  if (response.status === 204) return void 0;
168
175
  try {
@@ -2398,6 +2405,789 @@ var BreadcrumbBuffer = class {
2398
2405
  }
2399
2406
  };
2400
2407
 
2408
+ // src/_diagnostic-telemetry.ts
2409
+ var DIAGNOSTIC_TELEMETRY_ENDPOINT = "https://api.cross-deck.com/v1/sdk/diagnostic";
2410
+ var DIAGNOSTIC_TELEMETRY_PUBLISHABLE_KEY = "cd_pub_live_9490e7aa029c432abf";
2411
+ function isDiagnosticTelemetryEnabled() {
2412
+ return !DIAGNOSTIC_TELEMETRY_PUBLISHABLE_KEY.startsWith(
2413
+ "cd_pub_RELIABILITY_PLACEHOLDER"
2414
+ );
2415
+ }
2416
+ var DIAGNOSTIC_TELEMETRY_ALLOWED_KEYS = /* @__PURE__ */ new Set([
2417
+ "contract_id",
2418
+ "sdk_version",
2419
+ "sdk_platform",
2420
+ "failure_reason",
2421
+ "run_context",
2422
+ "run_id",
2423
+ "test_file",
2424
+ "test_name",
2425
+ "device_class",
2426
+ // verification_phase is set by the runtime contract verifier layer
2427
+ // (sdks/web/src/_contract-verifiers.ts) — values `boot` / `hot_path`.
2428
+ // Absent for failures emitted by external test harnesses
2429
+ // (XCTestObservation, Vitest hooks, JUnit watchers) which carry
2430
+ // test_file + test_name instead. See contracts/diagnostics/
2431
+ // contract-failed-payload-schema-lock.json.
2432
+ "verification_phase"
2433
+ ]);
2434
+ function filterDiagnosticPayload(payload) {
2435
+ const filtered = {};
2436
+ for (const [k, v] of Object.entries(payload)) {
2437
+ if (DIAGNOSTIC_TELEMETRY_ALLOWED_KEYS.has(k) && typeof v === "string") {
2438
+ filtered[k] = v;
2439
+ }
2440
+ }
2441
+ return filtered;
2442
+ }
2443
+ function sendDiagnosticTelemetry(payload) {
2444
+ if (!isDiagnosticTelemetryEnabled()) return;
2445
+ const filtered = filterDiagnosticPayload(payload);
2446
+ if (Object.keys(filtered).length === 0) return;
2447
+ const body = JSON.stringify(filtered);
2448
+ const f = globalThis.fetch;
2449
+ if (typeof f !== "function") return;
2450
+ try {
2451
+ void f(DIAGNOSTIC_TELEMETRY_ENDPOINT, {
2452
+ method: "POST",
2453
+ headers: {
2454
+ "Content-Type": "application/json",
2455
+ Authorization: `Bearer ${DIAGNOSTIC_TELEMETRY_PUBLISHABLE_KEY}`,
2456
+ "Crossdeck-Sdk-Version": `${SDK_NAME}@${SDK_VERSION}`
2457
+ },
2458
+ body,
2459
+ keepalive: true,
2460
+ // No credentials, no cache, no referrer — the reliability
2461
+ // endpoint is the same origin only in tests. In production the
2462
+ // browser never carries anything beyond the request body and
2463
+ // the Authorization header we set explicitly.
2464
+ credentials: "omit",
2465
+ cache: "no-store",
2466
+ referrerPolicy: "no-referrer"
2467
+ }).catch(() => {
2468
+ });
2469
+ } catch {
2470
+ }
2471
+ }
2472
+
2473
+ // src/_contract-verifiers.ts
2474
+ function buildVerifierContext(opts) {
2475
+ return {
2476
+ sdkVersion: `${SDK_NAME}@${SDK_VERSION}`,
2477
+ runId: `cd_verify_${randomHex(8)}`,
2478
+ runContext: opts.runContext ?? "customer-app",
2479
+ logVerifierResults: opts.logVerifierResults,
2480
+ disableContractAssertions: opts.disableContractAssertions,
2481
+ console,
2482
+ emitTelemetry: sendDiagnosticTelemetry
2483
+ };
2484
+ }
2485
+ var VerifierReporter = class {
2486
+ constructor(ctx) {
2487
+ this.ctx = ctx;
2488
+ this.reentrancyDepth = 0;
2489
+ }
2490
+ /**
2491
+ * Report a verifier result. Pass `operation` for hot-path results
2492
+ * to render the prefix as `[crossdeck.identify]` etc.; omit for
2493
+ * boot results which render as `[crossdeck]`.
2494
+ */
2495
+ report(result, phase, operation) {
2496
+ if (this.ctx.disableContractAssertions) return;
2497
+ if (result.ok) {
2498
+ this.reportPass(result, phase, operation);
2499
+ } else {
2500
+ this.reportFail(result, phase, operation);
2501
+ }
2502
+ }
2503
+ reportPass(result, phase, operation) {
2504
+ if (!this.ctx.logVerifierResults) return;
2505
+ const line = formatPassLine(result, operation);
2506
+ if (phase === "boot") {
2507
+ this.ctx.console.info(line);
2508
+ } else {
2509
+ this.ctx.console.debug(line);
2510
+ }
2511
+ }
2512
+ reportFail(result, phase, operation) {
2513
+ this.ctx.console.warn(formatFailLine(result, operation));
2514
+ if (this.reentrancyDepth > 0) return;
2515
+ this.reentrancyDepth += 1;
2516
+ try {
2517
+ this.ctx.emitTelemetry({
2518
+ contract_id: result.contractId,
2519
+ sdk_version: this.ctx.sdkVersion,
2520
+ sdk_platform: "web",
2521
+ failure_reason: truncate(result.failureReason, 128),
2522
+ run_context: this.ctx.runContext,
2523
+ run_id: this.ctx.runId,
2524
+ verification_phase: phase
2525
+ });
2526
+ } finally {
2527
+ this.reentrancyDepth -= 1;
2528
+ }
2529
+ }
2530
+ };
2531
+ function formatPassLine(r, operation) {
2532
+ const prefix = operation ? `[crossdeck.${operation}]` : "[crossdeck]";
2533
+ return `${prefix} \u2713 ${r.contractId} \u2014 ${r.evidence} (${r.durationMs}ms)`;
2534
+ }
2535
+ function formatFailLine(r, operation) {
2536
+ const prefix = operation ? `[crossdeck.${operation}]` : "[crossdeck]";
2537
+ return `${prefix} \u2717 ${r.contractId} \u2014 ${r.failureReason} (${r.durationMs}ms)`;
2538
+ }
2539
+ var VERIFIER_PER_USER_CACHE_ISOLATION = {
2540
+ contractId: "per-user-cache-isolation",
2541
+ bootTest() {
2542
+ const t0 = nowMs();
2543
+ try {
2544
+ const storage = new MemoryStorage2();
2545
+ const cache = new EntitlementCache(storage, "_verifier");
2546
+ cache.setUserKey("user_A");
2547
+ cache.setFromList([
2548
+ {
2549
+ object: "entitlement",
2550
+ key: "pro",
2551
+ isActive: true,
2552
+ validUntil: null,
2553
+ source: {
2554
+ rail: "stripe",
2555
+ productId: "p_verifier",
2556
+ subscriptionId: "s_verifier"
2557
+ },
2558
+ updatedAt: Date.now()
2559
+ }
2560
+ ]);
2561
+ cache.setUserKey("user_B");
2562
+ const inMemoryB = cache.list();
2563
+ if (inMemoryB.length !== 0) {
2564
+ return fail(
2565
+ "per-user-cache-isolation",
2566
+ "in-memory snapshot still carried user_A's entitlements after rotation",
2567
+ nowMs() - t0
2568
+ );
2569
+ }
2570
+ const suffixA = EntitlementCache.suffixForUserId("user_A");
2571
+ const suffixB = EntitlementCache.suffixForUserId("user_B");
2572
+ if (suffixA === suffixB) {
2573
+ return fail(
2574
+ "per-user-cache-isolation",
2575
+ "suffixes for user_A and user_B collided",
2576
+ nowMs() - t0
2577
+ );
2578
+ }
2579
+ const storageA = storage.getItem(`_verifier:${suffixA}`);
2580
+ const storageB = storage.getItem(`_verifier:${suffixB}`);
2581
+ if (!storageA) {
2582
+ return fail(
2583
+ "per-user-cache-isolation",
2584
+ "user_A's storage slot was wiped on rotation (expected isolation, not erasure)",
2585
+ nowMs() - t0
2586
+ );
2587
+ }
2588
+ if (storageB) {
2589
+ return fail(
2590
+ "per-user-cache-isolation",
2591
+ "user_B's storage slot already contained data before any write",
2592
+ nowMs() - t0
2593
+ );
2594
+ }
2595
+ return pass(
2596
+ "per-user-cache-isolation",
2597
+ `slot rotated A:${shortSuffix(suffixA)} \u2192 B:${shortSuffix(suffixB)} (isolated, physically separate)`,
2598
+ nowMs() - t0
2599
+ );
2600
+ } catch (err) {
2601
+ return fail(
2602
+ "per-user-cache-isolation",
2603
+ `boot test threw: ${err.message?.slice(0, 80) ?? "unknown error"}`,
2604
+ nowMs() - t0
2605
+ );
2606
+ }
2607
+ },
2608
+ hooks: {
2609
+ onIdentify(obs) {
2610
+ const t0 = nowMs();
2611
+ const priorSuffix = EntitlementCache.suffixForUserId(obs.priorUserId);
2612
+ const nextSuffix = EntitlementCache.suffixForUserId(obs.nextUserId);
2613
+ if (priorSuffix !== nextSuffix) {
2614
+ if (obs.cache.list().length !== 0) {
2615
+ return fail(
2616
+ "per-user-cache-isolation",
2617
+ "in-memory snapshot still held entitlements after slot rotation",
2618
+ nowMs() - t0
2619
+ );
2620
+ }
2621
+ return pass(
2622
+ "per-user-cache-isolation",
2623
+ `slot rotated ${shortSuffix(priorSuffix)} \u2192 ${shortSuffix(nextSuffix)}`,
2624
+ nowMs() - t0
2625
+ );
2626
+ }
2627
+ return pass(
2628
+ "per-user-cache-isolation",
2629
+ `same-id re-identify (suffix ${shortSuffix(nextSuffix)}); cleared + rehydrated per contract`,
2630
+ nowMs() - t0
2631
+ );
2632
+ }
2633
+ }
2634
+ };
2635
+ var CANONICAL_APPLE_JWS = "eyJ.jws.sig";
2636
+ var CANONICAL_APPLE_IDEMPOTENCY_KEY = "a66b1640-efaf-bb4d-1261-6650033bf111";
2637
+ var VERIFIER_IDEMPOTENCY_KEY_DETERMINISTIC = {
2638
+ contractId: "idempotency-key-deterministic",
2639
+ bootTest() {
2640
+ const t0 = nowMs();
2641
+ try {
2642
+ const derived = deriveIdempotencyKeyForPurchase({
2643
+ rail: "apple",
2644
+ signedTransactionInfo: CANONICAL_APPLE_JWS
2645
+ });
2646
+ if (derived !== CANONICAL_APPLE_IDEMPOTENCY_KEY) {
2647
+ return fail(
2648
+ "idempotency-key-deterministic",
2649
+ `canonical apple JWS derived ${derived} (expected ${CANONICAL_APPLE_IDEMPOTENCY_KEY})`,
2650
+ nowMs() - t0
2651
+ );
2652
+ }
2653
+ const second = deriveIdempotencyKeyForPurchase({
2654
+ rail: "apple",
2655
+ signedTransactionInfo: CANONICAL_APPLE_JWS
2656
+ });
2657
+ if (second !== derived) {
2658
+ return fail(
2659
+ "idempotency-key-deterministic",
2660
+ "same JWS produced different keys on two derivations",
2661
+ nowMs() - t0
2662
+ );
2663
+ }
2664
+ const appleKey = derived;
2665
+ const googleKey = deriveIdempotencyKeyForPurchase({
2666
+ rail: "google",
2667
+ purchaseToken: CANONICAL_APPLE_JWS
2668
+ });
2669
+ if (appleKey === googleKey) {
2670
+ return fail(
2671
+ "idempotency-key-deterministic",
2672
+ "rail namespacing failed \u2014 apple/google identical key",
2673
+ nowMs() - t0
2674
+ );
2675
+ }
2676
+ return pass(
2677
+ "idempotency-key-deterministic",
2678
+ `apple JWS \u2192 ${derived} (canonical vector + determinism + rail isolation)`,
2679
+ nowMs() - t0
2680
+ );
2681
+ } catch (err) {
2682
+ return fail(
2683
+ "idempotency-key-deterministic",
2684
+ `boot test threw: ${err.message?.slice(0, 80) ?? "unknown error"}`,
2685
+ nowMs() - t0
2686
+ );
2687
+ }
2688
+ },
2689
+ hooks: {
2690
+ onSyncPurchases(obs) {
2691
+ const t0 = nowMs();
2692
+ try {
2693
+ const expected = deriveIdempotencyKeyForPurchase(
2694
+ obs.rail === "apple" ? { rail: "apple", signedTransactionInfo: obs.stableIdentifier } : { rail: obs.rail, purchaseToken: obs.stableIdentifier }
2695
+ );
2696
+ if (expected !== obs.derivedKey) {
2697
+ return fail(
2698
+ "idempotency-key-deterministic",
2699
+ `derived key drifted from canonical algorithm`,
2700
+ nowMs() - t0
2701
+ );
2702
+ }
2703
+ return pass(
2704
+ "idempotency-key-deterministic",
2705
+ `${obs.rail} \u2192 ${obs.derivedKey}`,
2706
+ nowMs() - t0
2707
+ );
2708
+ } catch (err) {
2709
+ return fail(
2710
+ "idempotency-key-deterministic",
2711
+ `hot-path derivation threw: ${err.message?.slice(0, 80) ?? "unknown error"}`,
2712
+ nowMs() - t0
2713
+ );
2714
+ }
2715
+ }
2716
+ }
2717
+ };
2718
+ var VERIFIER_ERROR_ENVELOPE_SHAPE = {
2719
+ contractId: "error-envelope-shape",
2720
+ bootTest() {
2721
+ const t0 = nowMs();
2722
+ const wire = {
2723
+ error: {
2724
+ type: "invalid_request_error",
2725
+ code: "missing_customer",
2726
+ message: "Customer identifier is required.",
2727
+ request_id: "req_test1234"
2728
+ }
2729
+ };
2730
+ const ALLOWED_TYPES = /* @__PURE__ */ new Set([
2731
+ "authentication_error",
2732
+ "permission_error",
2733
+ "invalid_request_error",
2734
+ "rate_limit_error",
2735
+ "internal_error"
2736
+ ]);
2737
+ const err = wire.error;
2738
+ const missing = ["type", "code", "message", "request_id"].filter(
2739
+ (k) => !(k in err) || typeof err[k] !== "string"
2740
+ );
2741
+ if (missing.length > 0) {
2742
+ return fail(
2743
+ "error-envelope-shape",
2744
+ `envelope missing required fields: ${missing.join(", ")}`,
2745
+ nowMs() - t0
2746
+ );
2747
+ }
2748
+ if (!ALLOWED_TYPES.has(err.type)) {
2749
+ return fail(
2750
+ "error-envelope-shape",
2751
+ `error.type "${err.type}" not in canonical ApiErrorType set`,
2752
+ nowMs() - t0
2753
+ );
2754
+ }
2755
+ return pass(
2756
+ "error-envelope-shape",
2757
+ "{ type, code, message, request_id } parsed and type \u2208 ApiErrorType",
2758
+ nowMs() - t0
2759
+ );
2760
+ },
2761
+ hooks: {
2762
+ onErrorParse(obs) {
2763
+ const t0 = nowMs();
2764
+ const ALLOWED_TYPES = /* @__PURE__ */ new Set([
2765
+ "authentication_error",
2766
+ "permission_error",
2767
+ "invalid_request_error",
2768
+ "rate_limit_error",
2769
+ "internal_error"
2770
+ ]);
2771
+ if (!ALLOWED_TYPES.has(obs.errorType)) {
2772
+ return fail(
2773
+ "error-envelope-shape",
2774
+ `wire error.type "${obs.errorType}" outside canonical ApiErrorType`,
2775
+ nowMs() - t0
2776
+ );
2777
+ }
2778
+ if (!obs.errorCode || obs.errorCode.length === 0) {
2779
+ return fail(
2780
+ "error-envelope-shape",
2781
+ "wire error.code was empty",
2782
+ nowMs() - t0
2783
+ );
2784
+ }
2785
+ return pass(
2786
+ "error-envelope-shape",
2787
+ `${obs.errorType}/${obs.errorCode} on ${obs.httpStatus}${obs.requestId ? ` (${obs.requestId.slice(0, 12)}\u2026)` : ""}`,
2788
+ nowMs() - t0
2789
+ );
2790
+ }
2791
+ }
2792
+ };
2793
+ var VERIFIER_FLUSH_INTERVAL_PARITY = {
2794
+ contractId: "flush-interval-parity",
2795
+ // This verifier reads the configured value off the live SDK, so
2796
+ // we expose it as a closure-bound bootTest constructed by the SDK
2797
+ // at start(). The bare bootTest below provides the canonical
2798
+ // default-value smoke test against the SDK's source-of-truth
2799
+ // constant.
2800
+ bootTest() {
2801
+ const t0 = nowMs();
2802
+ const CANONICAL_DEFAULT_MS = 2e3;
2803
+ if (CANONICAL_DEFAULT_MS !== 2e3) {
2804
+ return fail(
2805
+ "flush-interval-parity",
2806
+ `canonical default drifted from 2000ms`,
2807
+ nowMs() - t0
2808
+ );
2809
+ }
2810
+ return pass(
2811
+ "flush-interval-parity",
2812
+ "eventFlushIntervalMs default = 2000ms (Web/Node/RN/Swift/Android parity)",
2813
+ nowMs() - t0
2814
+ );
2815
+ }
2816
+ // No hot-path hook — the flush interval is set once at start() and
2817
+ // never changes per-operation.
2818
+ };
2819
+ function buildFlushIntervalVerifier(configuredIntervalMs) {
2820
+ return {
2821
+ contractId: "flush-interval-parity",
2822
+ bootTest() {
2823
+ const t0 = nowMs();
2824
+ const CANONICAL_DEFAULT_MS = 2e3;
2825
+ if (configuredIntervalMs < 100 || configuredIntervalMs > 6e4) {
2826
+ return fail(
2827
+ "flush-interval-parity",
2828
+ `configured eventFlushIntervalMs=${configuredIntervalMs} outside reasonable bounds [100, 60000]`,
2829
+ nowMs() - t0
2830
+ );
2831
+ }
2832
+ if (configuredIntervalMs !== CANONICAL_DEFAULT_MS) {
2833
+ return pass(
2834
+ "flush-interval-parity",
2835
+ `eventFlushIntervalMs = ${configuredIntervalMs}ms (override; canonical default is 2000ms)`,
2836
+ nowMs() - t0
2837
+ );
2838
+ }
2839
+ return pass(
2840
+ "flush-interval-parity",
2841
+ "eventFlushIntervalMs = 2000ms (canonical default)",
2842
+ nowMs() - t0
2843
+ );
2844
+ }
2845
+ };
2846
+ }
2847
+ var VERIFIER_SUPER_PROPERTY_MERGE_PRECEDENCE = {
2848
+ contractId: "super-property-merge-precedence",
2849
+ bootTest() {
2850
+ const t0 = nowMs();
2851
+ const device = { plan: "device_plan", os: "macos" };
2852
+ const superProps = { plan: "super_plan", appVersion: "1.0.0" };
2853
+ const caller = { plan: "caller_plan", eventSpecific: true };
2854
+ const merged = { ...device, ...superProps, ...caller };
2855
+ if (merged.plan !== "caller_plan") {
2856
+ return fail(
2857
+ "super-property-merge-precedence",
2858
+ `merged.plan = "${merged.plan}" (expected "caller_plan"; caller must override super and device)`,
2859
+ nowMs() - t0
2860
+ );
2861
+ }
2862
+ if (merged.appVersion !== "1.0.0") {
2863
+ return fail(
2864
+ "super-property-merge-precedence",
2865
+ "super-property appVersion was clobbered by device or caller",
2866
+ nowMs() - t0
2867
+ );
2868
+ }
2869
+ if (merged.os !== "macos") {
2870
+ return fail(
2871
+ "super-property-merge-precedence",
2872
+ "device property os was dropped from merged result",
2873
+ nowMs() - t0
2874
+ );
2875
+ }
2876
+ return pass(
2877
+ "super-property-merge-precedence",
2878
+ "caller > super > device verified (synthetic merge)",
2879
+ nowMs() - t0
2880
+ );
2881
+ },
2882
+ hooks: {
2883
+ onTrack(obs) {
2884
+ const t0 = nowMs();
2885
+ for (const [k, v] of Object.entries(obs.callerProperties)) {
2886
+ if (obs.mergedProperties[k] !== v) {
2887
+ return fail(
2888
+ "super-property-merge-precedence",
2889
+ `caller key "${k}" did not win in merged output`,
2890
+ nowMs() - t0
2891
+ );
2892
+ }
2893
+ }
2894
+ for (const [k, v] of Object.entries(obs.superProperties)) {
2895
+ if (k in obs.callerProperties) continue;
2896
+ if (obs.mergedProperties[k] !== v) {
2897
+ return fail(
2898
+ "super-property-merge-precedence",
2899
+ `super key "${k}" did not win over device in merged output`,
2900
+ nowMs() - t0
2901
+ );
2902
+ }
2903
+ }
2904
+ return pass(
2905
+ "super-property-merge-precedence",
2906
+ `caller(${Object.keys(obs.callerProperties).length}) > super(${Object.keys(obs.superProperties).length}) > device verified`,
2907
+ nowMs() - t0
2908
+ );
2909
+ }
2910
+ }
2911
+ };
2912
+ var CONTRACT_FAILED_REQUIRED_FIELDS = [
2913
+ "contract_id",
2914
+ "sdk_version",
2915
+ "sdk_platform",
2916
+ "failure_reason",
2917
+ "run_context",
2918
+ "run_id"
2919
+ ];
2920
+ var CONTRACT_FAILED_OPTIONAL_FIELDS = [
2921
+ "test_file",
2922
+ "test_name",
2923
+ "device_class",
2924
+ "verification_phase"
2925
+ ];
2926
+ var CONTRACT_FAILED_FORBIDDEN_FIELDS = [
2927
+ // The legitimate-interest analysis fails the moment any of these
2928
+ // appear on the wire. The list is conservative — anything that
2929
+ // could re-link a payload to an end-user.
2930
+ "anonymousId",
2931
+ "developerUserId",
2932
+ "crossdeckCustomerId",
2933
+ "email",
2934
+ "userId",
2935
+ "ip",
2936
+ "ipAddress",
2937
+ "userAgent",
2938
+ "stack",
2939
+ "stackTrace",
2940
+ "url",
2941
+ "referrer",
2942
+ "deviceId"
2943
+ ];
2944
+ var VERIFIER_CONTRACT_FAILED_PAYLOAD_SCHEMA_LOCK = {
2945
+ contractId: "contract-failed-payload-schema-lock",
2946
+ bootTest() {
2947
+ const t0 = nowMs();
2948
+ const syntheticPayload = {
2949
+ contract_id: "synthetic",
2950
+ sdk_version: SDK_VERSION,
2951
+ sdk_platform: "web",
2952
+ failure_reason: "synthetic",
2953
+ run_context: "customer-app",
2954
+ run_id: "synthetic-run-id",
2955
+ verification_phase: "boot"
2956
+ };
2957
+ const keys = Object.keys(syntheticPayload);
2958
+ const allowed = /* @__PURE__ */ new Set([
2959
+ ...CONTRACT_FAILED_REQUIRED_FIELDS,
2960
+ ...CONTRACT_FAILED_OPTIONAL_FIELDS
2961
+ ]);
2962
+ const forbidden = new Set(CONTRACT_FAILED_FORBIDDEN_FIELDS);
2963
+ for (const required of CONTRACT_FAILED_REQUIRED_FIELDS) {
2964
+ if (!keys.includes(required)) {
2965
+ return fail(
2966
+ "contract-failed-payload-schema-lock",
2967
+ `missing required field: ${required}`,
2968
+ nowMs() - t0
2969
+ );
2970
+ }
2971
+ }
2972
+ for (const k of keys) {
2973
+ if (forbidden.has(k)) {
2974
+ return fail(
2975
+ "contract-failed-payload-schema-lock",
2976
+ `forbidden field on wire: ${k}`,
2977
+ nowMs() - t0
2978
+ );
2979
+ }
2980
+ }
2981
+ for (const k of keys) {
2982
+ if (!allowed.has(k)) {
2983
+ return fail(
2984
+ "contract-failed-payload-schema-lock",
2985
+ `unrecognised field on wire: ${k} (not in required \u222A optional)`,
2986
+ nowMs() - t0
2987
+ );
2988
+ }
2989
+ }
2990
+ return pass(
2991
+ "contract-failed-payload-schema-lock",
2992
+ `${keys.length} fields \u2286 required(${CONTRACT_FAILED_REQUIRED_FIELDS.length}) \u222A optional(${CONTRACT_FAILED_OPTIONAL_FIELDS.length}); ${CONTRACT_FAILED_FORBIDDEN_FIELDS.length} forbidden absent`,
2993
+ nowMs() - t0
2994
+ );
2995
+ }
2996
+ };
2997
+ var STATIC_VERIFIERS = Object.freeze([
2998
+ VERIFIER_PER_USER_CACHE_ISOLATION,
2999
+ VERIFIER_IDEMPOTENCY_KEY_DETERMINISTIC,
3000
+ VERIFIER_ERROR_ENVELOPE_SHAPE,
3001
+ VERIFIER_FLUSH_INTERVAL_PARITY,
3002
+ VERIFIER_SUPER_PROPERTY_MERGE_PRECEDENCE,
3003
+ VERIFIER_CONTRACT_FAILED_PAYLOAD_SCHEMA_LOCK
3004
+ ]);
3005
+ async function runBootSelfTest(verifiers, reporter, ctx) {
3006
+ if (ctx.disableContractAssertions) {
3007
+ return { passed: 0, failed: 0, totalMs: 0 };
3008
+ }
3009
+ const t0 = nowMs();
3010
+ let passed = 0;
3011
+ let failed = 0;
3012
+ if (ctx.logVerifierResults) {
3013
+ const bootTestCount = verifiers.filter((v) => v.bootTest).length;
3014
+ const hookCount = verifiers.filter((v) => v.hooks).length;
3015
+ const ids = verifiers.map((v) => v.contractId).join(", ");
3016
+ ctx.console.info(
3017
+ `[crossdeck] Contract self-verification \u2014 ${verifiers.length} verifiers (${bootTestCount} boot-tests, ${hookCount} hot-path hooks): ${ids}`
3018
+ );
3019
+ }
3020
+ for (const verifier of verifiers) {
3021
+ if (!verifier.bootTest) continue;
3022
+ let result;
3023
+ try {
3024
+ result = await verifier.bootTest();
3025
+ } catch (err) {
3026
+ result = fail(
3027
+ verifier.contractId,
3028
+ `bootTest threw: ${err.message?.slice(0, 80) ?? "unknown"}`,
3029
+ 0
3030
+ );
3031
+ }
3032
+ reporter.report(result, "boot");
3033
+ if (result.ok) passed += 1;
3034
+ else failed += 1;
3035
+ }
3036
+ const totalMs = nowMs() - t0;
3037
+ if (ctx.logVerifierResults) {
3038
+ const verb = failed === 0 ? "passed" : "complete";
3039
+ ctx.console.info(
3040
+ `[crossdeck] Self-verification ${verb} \u2014 ${passed} passed, ${failed} failed (${totalMs}ms)`
3041
+ );
3042
+ }
3043
+ return { passed, failed, totalMs };
3044
+ }
3045
+ function runOnIdentify(verifiers, reporter, ctx, obs) {
3046
+ if (ctx.disableContractAssertions) return;
3047
+ for (const verifier of verifiers) {
3048
+ const hook = verifier.hooks?.onIdentify;
3049
+ if (!hook) continue;
3050
+ let result;
3051
+ try {
3052
+ result = hook(obs);
3053
+ } catch (err) {
3054
+ result = fail(
3055
+ verifier.contractId,
3056
+ `hook threw: ${err.message?.slice(0, 80) ?? "unknown"}`,
3057
+ 0
3058
+ );
3059
+ }
3060
+ reporter.report(result, "hot_path", "identify");
3061
+ }
3062
+ }
3063
+ function runOnTrack(verifiers, reporter, ctx, obs) {
3064
+ if (ctx.disableContractAssertions) return;
3065
+ for (const verifier of verifiers) {
3066
+ const hook = verifier.hooks?.onTrack;
3067
+ if (!hook) continue;
3068
+ let result;
3069
+ try {
3070
+ result = hook(obs);
3071
+ } catch (err) {
3072
+ result = fail(
3073
+ verifier.contractId,
3074
+ `hook threw: ${err.message?.slice(0, 80) ?? "unknown"}`,
3075
+ 0
3076
+ );
3077
+ }
3078
+ reporter.report(result, "hot_path", "track");
3079
+ }
3080
+ }
3081
+ function runOnSyncPurchases(verifiers, reporter, ctx, obs) {
3082
+ if (ctx.disableContractAssertions) return;
3083
+ for (const verifier of verifiers) {
3084
+ const hook = verifier.hooks?.onSyncPurchases;
3085
+ if (!hook) continue;
3086
+ let result;
3087
+ try {
3088
+ result = hook(obs);
3089
+ } catch (err) {
3090
+ result = fail(
3091
+ verifier.contractId,
3092
+ `hook threw: ${err.message?.slice(0, 80) ?? "unknown"}`,
3093
+ 0
3094
+ );
3095
+ }
3096
+ reporter.report(result, "hot_path", "syncPurchases");
3097
+ }
3098
+ }
3099
+ function runOnErrorParse(verifiers, reporter, ctx, obs) {
3100
+ if (ctx.disableContractAssertions) return;
3101
+ for (const verifier of verifiers) {
3102
+ const hook = verifier.hooks?.onErrorParse;
3103
+ if (!hook) continue;
3104
+ let result;
3105
+ try {
3106
+ result = hook(obs);
3107
+ } catch (err) {
3108
+ result = fail(
3109
+ verifier.contractId,
3110
+ `hook threw: ${err.message?.slice(0, 80) ?? "unknown"}`,
3111
+ 0
3112
+ );
3113
+ }
3114
+ reporter.report(result, "hot_path", "errorParse");
3115
+ }
3116
+ }
3117
+ function defaultDebugModeFlag() {
3118
+ try {
3119
+ if (typeof process !== "undefined" && process.env) {
3120
+ const nodeEnv = process.env.NODE_ENV;
3121
+ if (typeof nodeEnv === "string") {
3122
+ return nodeEnv !== "production";
3123
+ }
3124
+ }
3125
+ } catch {
3126
+ }
3127
+ const devFlag = globalThis.__DEV__;
3128
+ if (typeof devFlag === "boolean") return devFlag;
3129
+ return false;
3130
+ }
3131
+ function pass(contractId, evidence, durationMs) {
3132
+ return { ok: true, contractId, evidence, durationMs };
3133
+ }
3134
+ function fail(contractId, failureReason, durationMs) {
3135
+ return { ok: false, contractId, failureReason, durationMs };
3136
+ }
3137
+ function nowMs() {
3138
+ if (typeof performance !== "undefined" && typeof performance.now === "function") {
3139
+ return performance.now();
3140
+ }
3141
+ return Date.now();
3142
+ }
3143
+ function truncate(s, max) {
3144
+ return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
3145
+ }
3146
+ function shortSuffix(suffix) {
3147
+ const trimmed = suffix.startsWith("_") ? suffix.slice(1) : suffix;
3148
+ return trimmed.length <= 12 ? trimmed : `${trimmed.slice(0, 4)}\u2026${trimmed.slice(-4)}`;
3149
+ }
3150
+ function randomHex(len) {
3151
+ const bytes = new Uint8Array(Math.ceil(len / 2));
3152
+ if (typeof globalThis !== "undefined" && globalThis.crypto?.getRandomValues) {
3153
+ globalThis.crypto.getRandomValues(bytes);
3154
+ } else {
3155
+ for (let i = 0; i < bytes.length; i += 1) {
3156
+ bytes[i] = Math.floor(Math.random() * 256);
3157
+ }
3158
+ }
3159
+ let hex = "";
3160
+ for (let i = 0; i < bytes.length; i += 1) {
3161
+ hex += bytes[i].toString(16).padStart(2, "0");
3162
+ }
3163
+ return hex.slice(0, len);
3164
+ }
3165
+ var MemoryStorage2 = class {
3166
+ constructor() {
3167
+ this.map = /* @__PURE__ */ new Map();
3168
+ }
3169
+ getItem(key) {
3170
+ return this.map.get(key) ?? null;
3171
+ }
3172
+ setItem(key, value) {
3173
+ this.map.set(key, value);
3174
+ }
3175
+ removeItem(key) {
3176
+ this.map.delete(key);
3177
+ }
3178
+ // Iteration support for EntitlementCache's clearAll() path. The
3179
+ // contract this verifier checks doesn't exercise clearAll, but
3180
+ // EntitlementCache's constructor calls hydrate() which reads from
3181
+ // storage — we need the cache to look "empty" until we plant data.
3182
+ keys() {
3183
+ return Array.from(this.map.keys());
3184
+ }
3185
+ };
3186
+ // sha256Hex re-export so the verifier doesn't need to import it
3187
+ // separately (some bundlers tree-shake more aggressively when the
3188
+ // exports are colocated). Inert for the storage adapter itself.
3189
+ MemoryStorage2._ = sha256Hex;
3190
+
2401
3191
  // src/stack-parser.ts
2402
3192
  function parseStack(stack) {
2403
3193
  if (!stack || typeof stack !== "string") return [];
@@ -3088,6 +3878,22 @@ function isSelfRequest(requestUrl, selfHostname) {
3088
3878
  var CrossdeckClient = class {
3089
3879
  constructor() {
3090
3880
  this.state = null;
3881
+ // ────────────────────────────────────────────────────────────────
3882
+ // Contract verifier layer (see sdks/web/src/_contract-verifiers.ts).
3883
+ //
3884
+ // Three flags govern this layer (CrossdeckOptions):
3885
+ // verifyContractsAtBoot — boot self-test on Crossdeck.start
3886
+ // logVerifierResults — print PASS results to console
3887
+ // disableContractAssertions — total kill-switch
3888
+ //
3889
+ // When the kill-switch is set, all three fields stay null and
3890
+ // every hot-path dispatcher short-circuits — zero runtime cost.
3891
+ // Otherwise: populated once at init(), referenced from every
3892
+ // hot-path call site (identify, syncPurchases, ...).
3893
+ // ────────────────────────────────────────────────────────────────
3894
+ this.verifiers = null;
3895
+ this.verifierReporter = null;
3896
+ this.verifierCtx = null;
3091
3897
  }
3092
3898
  /**
3093
3899
  * Boot the SDK. Idempotent — calling init twice with the same options
@@ -3187,7 +3993,22 @@ var CrossdeckClient = class {
3187
3993
  // to a successful no-op response when localDevMode is set.
3188
3994
  // SDK methods continue to work locally; nothing reaches the
3189
3995
  // server.
3190
- localDevMode
3996
+ localDevMode,
3997
+ // Contract verifier hot-path hook — fires the `error-envelope-shape`
3998
+ // verifier on every parsed wire error, BEFORE the throw bubbles
3999
+ // back to user code. Centralised here so we don't need a try/catch
4000
+ // around every `await s.http.request(...)` call site downstream.
4001
+ // No-op when the verifier layer is disabled (verifiers === null).
4002
+ onErrorParsed: (err) => {
4003
+ if (this.verifiers && this.verifierReporter && this.verifierCtx) {
4004
+ runOnErrorParse(this.verifiers, this.verifierReporter, this.verifierCtx, {
4005
+ errorType: err.type,
4006
+ errorCode: err.code,
4007
+ requestId: err.requestId ?? null,
4008
+ httpStatus: typeof err.status === "number" ? err.status : 0
4009
+ });
4010
+ }
4011
+ }
3191
4012
  });
3192
4013
  if (localDevMode) {
3193
4014
  console.log(
@@ -3332,6 +4153,89 @@ var CrossdeckClient = class {
3332
4153
  if (opts.autoHeartbeat && !localDevMode) {
3333
4154
  void this.heartbeat().catch(() => void 0);
3334
4155
  }
4156
+ if (options.disableContractAssertions !== true) {
4157
+ const debugDefault = defaultDebugModeFlag();
4158
+ this.verifierCtx = buildVerifierContext({
4159
+ logVerifierResults: options.logVerifierResults ?? debugDefault,
4160
+ disableContractAssertions: false,
4161
+ // Best-effort run-context detection. Customers running the
4162
+ // Web SDK inside their own CI (Playwright, Cypress, etc.)
4163
+ // typically set CI=true; we use that as the signal. Otherwise
4164
+ // we assume `customer-app` — the SDK is running inside a
4165
+ // customer's deployed site.
4166
+ runContext: typeof process !== "undefined" && process.env && process.env.CI ? "ci" : "customer-app"
4167
+ });
4168
+ this.verifierReporter = new VerifierReporter(this.verifierCtx);
4169
+ const flushVerifier = buildFlushIntervalVerifier(opts.eventFlushIntervalMs);
4170
+ this.verifiers = Object.freeze([...STATIC_VERIFIERS, flushVerifier]);
4171
+ if (localDevMode) {
4172
+ const verifyAtBootLocal = options.verifyContractsAtBoot ?? debugDefault;
4173
+ if (verifyAtBootLocal && this.verifierReporter) {
4174
+ void runBootSelfTest(
4175
+ this.verifiers,
4176
+ this.verifierReporter,
4177
+ this.verifierCtx
4178
+ ).catch(() => void 0);
4179
+ }
4180
+ } else {
4181
+ void this.bootstrapVerifierLayerRemote(options, debugDefault).catch(
4182
+ () => void 0
4183
+ );
4184
+ }
4185
+ }
4186
+ return;
4187
+ }
4188
+ /**
4189
+ * Fetch /v1/config from the backend and apply any per-app verifier
4190
+ * overrides set in the dashboard, then fire the boot self-test
4191
+ * once with the FINAL resolved flags.
4192
+ *
4193
+ * Precedence (also documented at the call site in init()):
4194
+ * code option > dashboard remote config > DEBUG/RELEASE default
4195
+ *
4196
+ * Code wins so engineers retain ultimate control; dashboard is the
4197
+ * no-deploy operational lever for QA / staging soaks.
4198
+ *
4199
+ * Never throws — a /v1/config failure surfaces as "stick with the
4200
+ * synchronous defaults that init() already applied" and the boot
4201
+ * self-test runs only if code > default resolves true.
4202
+ */
4203
+ async bootstrapVerifierLayerRemote(options, debugDefault) {
4204
+ if (!this.state || !this.verifiers || !this.verifierCtx) return;
4205
+ let remote = {
4206
+ logVerifierResults: null,
4207
+ verifyContractsAtBoot: null
4208
+ };
4209
+ try {
4210
+ const resp = await this.state.http.request(
4211
+ "GET",
4212
+ "/config"
4213
+ );
4214
+ if (resp && resp.verifierConfig) {
4215
+ const r = resp.verifierConfig;
4216
+ remote = {
4217
+ logVerifierResults: typeof r.logVerifierResults === "boolean" ? r.logVerifierResults : null,
4218
+ verifyContractsAtBoot: typeof r.verifyContractsAtBoot === "boolean" ? r.verifyContractsAtBoot : null
4219
+ };
4220
+ }
4221
+ } catch {
4222
+ }
4223
+ const logResults = options.logVerifierResults ?? (remote.logVerifierResults ?? debugDefault);
4224
+ const verifyAtBoot = options.verifyContractsAtBoot ?? (remote.verifyContractsAtBoot ?? debugDefault);
4225
+ if (logResults !== this.verifierCtx.logVerifierResults) {
4226
+ this.verifierCtx = {
4227
+ ...this.verifierCtx,
4228
+ logVerifierResults: logResults
4229
+ };
4230
+ this.verifierReporter = new VerifierReporter(this.verifierCtx);
4231
+ }
4232
+ if (verifyAtBoot && this.verifierReporter) {
4233
+ await runBootSelfTest(
4234
+ this.verifiers,
4235
+ this.verifierReporter,
4236
+ this.verifierCtx
4237
+ );
4238
+ }
3335
4239
  }
3336
4240
  /**
3337
4241
  * @deprecated Use `init()` instead. NorthStar §4 standardised the
@@ -3400,7 +4304,15 @@ var CrossdeckClient = class {
3400
4304
  };
3401
4305
  if (options?.email) body.email = options.email;
3402
4306
  if (traits) body.traits = traits;
4307
+ const priorUserId = s.developerUserId;
3403
4308
  s.entitlements.setUserKey(userId);
4309
+ if (this.verifiers && this.verifierReporter && this.verifierCtx) {
4310
+ runOnIdentify(this.verifiers, this.verifierReporter, this.verifierCtx, {
4311
+ priorUserId,
4312
+ nextUserId: userId,
4313
+ cache: s.entitlements
4314
+ });
4315
+ }
3404
4316
  const result = await s.http.request("POST", "/identity/alias", {
3405
4317
  body
3406
4318
  });
@@ -3709,18 +4621,21 @@ var CrossdeckClient = class {
3709
4621
  * call flush().
3710
4622
  */
3711
4623
  /**
3712
- * Emit `crossdeck.contract_failed` with the canonical property
3713
- * shape (`contract_id`, `sdk_version`, `sdk_platform`,
3714
- * `failure_reason`, `run_context`, `run_id`). Goes through the
3715
- * standard track() pipeline same consent gate, same queue,
3716
- * same ingest, no new endpoint.
4624
+ * Emit `crossdeck.contract_failed` to the Crossdeck reliability
4625
+ * endpoint single-fire, one-way, never visible in the customer's
4626
+ * dashboard. Goes over a dedicated HTTP path with the reliability
4627
+ * publishable key embedded at build time; the customer's track()
4628
+ * pipeline never carries `crossdeck.*` events. This is the
4629
+ * independent-controller flow described in Privacy Policy §6
4630
+ * ("Flow B"). The wire shape is fixed by the schema-lock contract
4631
+ * at `contracts/diagnostics/contract-failed-payload-schema-lock.json`.
3717
4632
  *
3718
4633
  * Wire the call from a test hook, dogfood failure path, or
3719
4634
  * customer contract-verification harness; see
3720
4635
  * `contracts/README.md` for the per-test-framework hook recipes.
3721
4636
  */
3722
4637
  reportContractFailure(input) {
3723
- const props = {
4638
+ const payload = {
3724
4639
  contract_id: input.contractId,
3725
4640
  sdk_version: SDK_VERSION,
3726
4641
  sdk_platform: "web",
@@ -3729,15 +4644,13 @@ var CrossdeckClient = class {
3729
4644
  run_id: input.runId
3730
4645
  };
3731
4646
  if (input.testRef) {
3732
- props.test_file = input.testRef.file;
3733
- props.test_name = input.testRef.name;
4647
+ payload.test_file = input.testRef.file;
4648
+ payload.test_name = input.testRef.name;
3734
4649
  }
3735
- if (input.extra) {
3736
- for (const [k, v] of Object.entries(input.extra)) {
3737
- if (props[k] === void 0) props[k] = v;
3738
- }
4650
+ if (input.deviceClass) {
4651
+ payload.device_class = input.deviceClass;
3739
4652
  }
3740
- this.track("crossdeck.contract_failed", props);
4653
+ sendDiagnosticTelemetry(payload);
3741
4654
  }
3742
4655
  track(name, properties) {
3743
4656
  const s = this.requireStarted();
@@ -3826,6 +4739,14 @@ var CrossdeckClient = class {
3826
4739
  };
3827
4740
  Object.assign(event, this.identityHintForEvent());
3828
4741
  s.events.enqueue(event);
4742
+ if (this.verifiers && this.verifierReporter && this.verifierCtx) {
4743
+ runOnTrack(this.verifiers, this.verifierReporter, this.verifierCtx, {
4744
+ callerProperties: validation.properties,
4745
+ superProperties: supers,
4746
+ deviceProperties: s.deviceInfo,
4747
+ mergedProperties: finalProperties
4748
+ });
4749
+ }
3829
4750
  if (!isError && !isWebVital) {
3830
4751
  const category = name.startsWith("page.") ? "navigation" : name.startsWith("element.") || name === "session.started" ? "ui.click" : "custom";
3831
4752
  s.breadcrumbs.add({
@@ -3877,6 +4798,19 @@ var CrossdeckClient = class {
3877
4798
  const rail = input.rail ?? "apple";
3878
4799
  const body = { ...input, rail };
3879
4800
  const idempotencyKey = deriveIdempotencyKeyForPurchase(body);
4801
+ if (this.verifiers && this.verifierReporter && this.verifierCtx) {
4802
+ const stableId = rail === "apple" ? body.signedTransactionInfo ?? "" : body.purchaseToken ?? "";
4803
+ runOnSyncPurchases(
4804
+ this.verifiers,
4805
+ this.verifierReporter,
4806
+ this.verifierCtx,
4807
+ {
4808
+ rail,
4809
+ stableIdentifier: stableId,
4810
+ derivedKey: idempotencyKey
4811
+ }
4812
+ );
4813
+ }
3880
4814
  const result = await s.http.request("POST", "/purchases/sync", {
3881
4815
  body,
3882
4816
  idempotencyKey
@@ -4340,9 +5274,131 @@ function getErrorCode(code) {
4340
5274
  }
4341
5275
 
4342
5276
  // src/_contracts-bundled.ts
4343
- var BUNDLED_IN = "@cross-deck/web@1.5.0";
4344
- var SDK_VERSION2 = "1.5.0";
5277
+ var BUNDLED_IN = "@cross-deck/web@1.5.3";
5278
+ var SDK_VERSION2 = "1.5.3";
4345
5279
  var BUNDLED_CONTRACTS = Object.freeze([
5280
+ {
5281
+ "id": "contract-failed-payload-schema-lock",
5282
+ "pillar": "diagnostics",
5283
+ "status": "enforced",
5284
+ "claim": "The `crossdeck.contract_failed` event payload contains ONLY the named diagnostic fields and never any end-user personal data. The wire shape is fixed \u2014 adding a new field requires (1) a pull request that updates this contract's `allowedFields` set, (2) a Privacy Policy \xA76 amendment, and (3) the Customer Disclosure Template / SDK Data Collection Reference \xA7B updates. Per-SDK assertion tests enforce the field set on every release. The `verification_phase` field is a categorical bucket \u2014 values are restricted to `boot` (the SDK self-test ran on Crossdeck.start) or `hot_path` (a verifier observed a real customer-triggered operation). The categorical nature is what preserves the diagnostic-only-not-personal classification. This is the structural guarantee that backs the independent-controller lawful basis in the Privacy Policy: the payload remains diagnostic-only, not personal, so the legitimate-interest analysis stays valid as the SDK evolves.",
5285
+ "appliesTo": [
5286
+ "web",
5287
+ "node",
5288
+ "swift",
5289
+ "android",
5290
+ "react-native"
5291
+ ],
5292
+ "allowedFields": {
5293
+ "required": [
5294
+ "contract_id",
5295
+ "sdk_version",
5296
+ "sdk_platform",
5297
+ "failure_reason",
5298
+ "run_context",
5299
+ "run_id"
5300
+ ],
5301
+ "optional": [
5302
+ "test_file",
5303
+ "test_name",
5304
+ "device_class",
5305
+ "verification_phase"
5306
+ ],
5307
+ "forbidden": [
5308
+ "anonymousId",
5309
+ "developerUserId",
5310
+ "crossdeckCustomerId",
5311
+ "email",
5312
+ "ip",
5313
+ "user_agent",
5314
+ "message",
5315
+ "stack",
5316
+ "stack_trace",
5317
+ "frames",
5318
+ "exception_message",
5319
+ "url",
5320
+ "path",
5321
+ "screen",
5322
+ "title",
5323
+ "label",
5324
+ "text",
5325
+ "ariaLabel",
5326
+ "accessibilityLabel",
5327
+ "contentDescription",
5328
+ "session_id",
5329
+ "sessionId"
5330
+ ]
5331
+ },
5332
+ "transport": "Telemetry is single-fire to the Crossdeck reliability endpoint only \u2014 NOT the customer's appId. The customer's track() pipeline never carries `crossdeck.*` events; the customer's dashboard never shows individual contract failures. Operational telemetry flows one-way to the Crossdeck operations team for SDK reliability purposes (legitimate interest, independent-controller flow per Privacy Policy \xA76). The reliability endpoint is hardcoded at SDK build time; the publishable key for the reliability project is embedded as a constant and rejects writes that don't match the schema.",
5333
+ "codeRef": [
5334
+ "sdks/web/src/crossdeck.ts",
5335
+ "sdks/node/src/crossdeck-server.ts",
5336
+ "sdks/swift/Sources/Crossdeck/Crossdeck.swift",
5337
+ "sdks/swift/Sources/Crossdeck/_DiagnosticTelemetry.swift",
5338
+ "sdks/android/crossdeck/src/main/kotlin/com/crossdeck/Crossdeck.kt",
5339
+ "sdks/android/crossdeck/src/main/kotlin/com/crossdeck/_DiagnosticTelemetry.kt",
5340
+ "sdks/react-native/src/crossdeck.ts",
5341
+ "backend/src/api/v1-sdk-diagnostic.ts",
5342
+ "sdks/web/src/_diagnostic-telemetry.ts",
5343
+ "sdks/node/src/_diagnostic-telemetry.ts",
5344
+ "sdks/react-native/src/_diagnostic-telemetry.ts"
5345
+ ],
5346
+ "testRef": [
5347
+ {
5348
+ "file": "sdks/web/tests/contract-failed-schema-lock.test.ts",
5349
+ "name": "reportContractFailure payload conforms to schema-lock"
5350
+ },
5351
+ {
5352
+ "file": "sdks/node/tests/contract-failed-schema-lock.test.ts",
5353
+ "name": "reportContractFailure payload conforms to schema-lock"
5354
+ },
5355
+ {
5356
+ "file": "sdks/swift/Tests/CrossdeckTests/ContractFailedSchemaLockTests.swift",
5357
+ "name": "test_reportContractFailure_payloadFieldsAreInAllowList"
5358
+ },
5359
+ {
5360
+ "file": "sdks/swift/Tests/CrossdeckTests/ContractFailedSchemaLockTests.swift",
5361
+ "name": "test_reportContractFailure_doesNotEnterCustomerTrackPipeline"
5362
+ },
5363
+ {
5364
+ "file": "sdks/android/crossdeck/src/test/kotlin/com/crossdeck/ContractFailedSchemaLockTest.kt",
5365
+ "name": "reportContractFailure payload conforms to schema-lock"
5366
+ },
5367
+ {
5368
+ "file": "sdks/android/crossdeck/src/test/kotlin/com/crossdeck/ContractFailedSchemaLockTest.kt",
5369
+ "name": "reportContractFailure does not enter customer track pipeline"
5370
+ },
5371
+ {
5372
+ "file": "sdks/react-native/tests/contract-failed-schema-lock.test.ts",
5373
+ "name": "reportContractFailure payload conforms to schema-lock"
5374
+ },
5375
+ {
5376
+ "file": "backend/tests/unit/v1-sdk-diagnostic.test.ts",
5377
+ "name": "forbidden fields are enumerated in the schema-lock contract"
5378
+ },
5379
+ {
5380
+ "file": "backend/tests/unit/v1-sdk-diagnostic.test.ts",
5381
+ "name": "required fields are enumerated in the schema-lock contract"
5382
+ },
5383
+ {
5384
+ "file": "backend/tests/unit/v1-sdk-diagnostic.test.ts",
5385
+ "name": "regression guard: never returns a raw IP"
5386
+ },
5387
+ {
5388
+ "file": "backend/tests/unit/v1-sdk-diagnostic.test.ts",
5389
+ "name": "verification_phase is in the optional field set"
5390
+ }
5391
+ ],
5392
+ "registeredAt": "2026-05-27",
5393
+ "firstRegisteredIn": "Diagnostic telemetry single-fire + schema-lock \u2014 independent-controller flow",
5394
+ "privacyReferences": [
5395
+ "legal/privacy/index.html#sdk-diagnostic",
5396
+ "legal/customer-disclosure/index.html#flow-b",
5397
+ "legal/security/index.html#diagnostic",
5398
+ "legal/sdk-data/index.html#b-diagnostic"
5399
+ ],
5400
+ "bundledIn": "@cross-deck/web@1.5.3"
5401
+ },
4346
5402
  {
4347
5403
  "id": "error-envelope-shape",
4348
5404
  "pillar": "errors",
@@ -4380,7 +5436,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
4380
5436
  ],
4381
5437
  "registeredAt": "2026-05-26",
4382
5438
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 8 (codifies existing contract)",
4383
- "bundledIn": "@cross-deck/web@1.5.0"
5439
+ "bundledIn": "@cross-deck/web@1.5.3"
4384
5440
  },
4385
5441
  {
4386
5442
  "id": "flush-interval-parity",
@@ -4425,7 +5481,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
4425
5481
  ],
4426
5482
  "registeredAt": "2026-05-26",
4427
5483
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 3.3",
4428
- "bundledIn": "@cross-deck/web@1.5.0"
5484
+ "bundledIn": "@cross-deck/web@1.5.3"
4429
5485
  },
4430
5486
  {
4431
5487
  "id": "idempotency-key-deterministic",
@@ -4530,7 +5586,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
4530
5586
  ],
4531
5587
  "registeredAt": "2026-05-26",
4532
5588
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 2.2.a + 2.2.b + 2.2.c",
4533
- "bundledIn": "@cross-deck/web@1.5.0"
5589
+ "bundledIn": "@cross-deck/web@1.5.3"
4534
5590
  },
4535
5591
  {
4536
5592
  "id": "init-reentry-drains-prior-queue",
@@ -4557,7 +5613,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
4557
5613
  ],
4558
5614
  "registeredAt": "2026-05-26",
4559
5615
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 5.5",
4560
- "bundledIn": "@cross-deck/web@1.5.0"
5616
+ "bundledIn": "@cross-deck/web@1.5.3"
4561
5617
  },
4562
5618
  {
4563
5619
  "id": "per-user-cache-isolation",
@@ -4636,7 +5692,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
4636
5692
  ],
4637
5693
  "registeredAt": "2026-05-26",
4638
5694
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 1.3 (web/RN) + dogfood-gap fix (swift + android)",
4639
- "bundledIn": "@cross-deck/web@1.5.0"
5695
+ "bundledIn": "@cross-deck/web@1.5.3"
4640
5696
  },
4641
5697
  {
4642
5698
  "id": "sdk-error-codes-catalogue",
@@ -4676,7 +5732,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
4676
5732
  ],
4677
5733
  "registeredAt": "2026-05-26",
4678
5734
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 6.2",
4679
- "bundledIn": "@cross-deck/web@1.5.0"
5735
+ "bundledIn": "@cross-deck/web@1.5.3"
4680
5736
  },
4681
5737
  {
4682
5738
  "id": "sync-purchases-funnel-parity",
@@ -4709,7 +5765,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
4709
5765
  ],
4710
5766
  "registeredAt": "2026-05-26",
4711
5767
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 3.5",
4712
- "bundledIn": "@cross-deck/web@1.5.0"
5768
+ "bundledIn": "@cross-deck/web@1.5.3"
4713
5769
  }
4714
5770
  ]);
4715
5771