@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/vue.cjs CHANGED
@@ -92,7 +92,7 @@ function typeMapForStatus(status) {
92
92
  }
93
93
 
94
94
  // src/_version.ts
95
- var SDK_VERSION = "1.4.2";
95
+ var SDK_VERSION = "1.5.3";
96
96
  var SDK_NAME = "@cross-deck/web";
97
97
 
98
98
  // src/http.ts
@@ -155,7 +155,14 @@ var HttpClient = class {
155
155
  if (timeoutHandle !== null) clearTimeout(timeoutHandle);
156
156
  }
157
157
  if (!response.ok) {
158
- throw await crossdeckErrorFromResponse(response);
158
+ const parsed = await crossdeckErrorFromResponse(response);
159
+ if (this.config.onErrorParsed) {
160
+ try {
161
+ this.config.onErrorParsed(parsed);
162
+ } catch {
163
+ }
164
+ }
165
+ throw parsed;
159
166
  }
160
167
  if (response.status === 204) return void 0;
161
168
  try {
@@ -2391,6 +2398,789 @@ var BreadcrumbBuffer = class {
2391
2398
  }
2392
2399
  };
2393
2400
 
2401
+ // src/_diagnostic-telemetry.ts
2402
+ var DIAGNOSTIC_TELEMETRY_ENDPOINT = "https://api.cross-deck.com/v1/sdk/diagnostic";
2403
+ var DIAGNOSTIC_TELEMETRY_PUBLISHABLE_KEY = "cd_pub_live_9490e7aa029c432abf";
2404
+ function isDiagnosticTelemetryEnabled() {
2405
+ return !DIAGNOSTIC_TELEMETRY_PUBLISHABLE_KEY.startsWith(
2406
+ "cd_pub_RELIABILITY_PLACEHOLDER"
2407
+ );
2408
+ }
2409
+ var DIAGNOSTIC_TELEMETRY_ALLOWED_KEYS = /* @__PURE__ */ new Set([
2410
+ "contract_id",
2411
+ "sdk_version",
2412
+ "sdk_platform",
2413
+ "failure_reason",
2414
+ "run_context",
2415
+ "run_id",
2416
+ "test_file",
2417
+ "test_name",
2418
+ "device_class",
2419
+ // verification_phase is set by the runtime contract verifier layer
2420
+ // (sdks/web/src/_contract-verifiers.ts) — values `boot` / `hot_path`.
2421
+ // Absent for failures emitted by external test harnesses
2422
+ // (XCTestObservation, Vitest hooks, JUnit watchers) which carry
2423
+ // test_file + test_name instead. See contracts/diagnostics/
2424
+ // contract-failed-payload-schema-lock.json.
2425
+ "verification_phase"
2426
+ ]);
2427
+ function filterDiagnosticPayload(payload) {
2428
+ const filtered = {};
2429
+ for (const [k, v] of Object.entries(payload)) {
2430
+ if (DIAGNOSTIC_TELEMETRY_ALLOWED_KEYS.has(k) && typeof v === "string") {
2431
+ filtered[k] = v;
2432
+ }
2433
+ }
2434
+ return filtered;
2435
+ }
2436
+ function sendDiagnosticTelemetry(payload) {
2437
+ if (!isDiagnosticTelemetryEnabled()) return;
2438
+ const filtered = filterDiagnosticPayload(payload);
2439
+ if (Object.keys(filtered).length === 0) return;
2440
+ const body = JSON.stringify(filtered);
2441
+ const f = globalThis.fetch;
2442
+ if (typeof f !== "function") return;
2443
+ try {
2444
+ void f(DIAGNOSTIC_TELEMETRY_ENDPOINT, {
2445
+ method: "POST",
2446
+ headers: {
2447
+ "Content-Type": "application/json",
2448
+ Authorization: `Bearer ${DIAGNOSTIC_TELEMETRY_PUBLISHABLE_KEY}`,
2449
+ "Crossdeck-Sdk-Version": `${SDK_NAME}@${SDK_VERSION}`
2450
+ },
2451
+ body,
2452
+ keepalive: true,
2453
+ // No credentials, no cache, no referrer — the reliability
2454
+ // endpoint is the same origin only in tests. In production the
2455
+ // browser never carries anything beyond the request body and
2456
+ // the Authorization header we set explicitly.
2457
+ credentials: "omit",
2458
+ cache: "no-store",
2459
+ referrerPolicy: "no-referrer"
2460
+ }).catch(() => {
2461
+ });
2462
+ } catch {
2463
+ }
2464
+ }
2465
+
2466
+ // src/_contract-verifiers.ts
2467
+ function buildVerifierContext(opts) {
2468
+ return {
2469
+ sdkVersion: `${SDK_NAME}@${SDK_VERSION}`,
2470
+ runId: `cd_verify_${randomHex(8)}`,
2471
+ runContext: opts.runContext ?? "customer-app",
2472
+ logVerifierResults: opts.logVerifierResults,
2473
+ disableContractAssertions: opts.disableContractAssertions,
2474
+ console,
2475
+ emitTelemetry: sendDiagnosticTelemetry
2476
+ };
2477
+ }
2478
+ var VerifierReporter = class {
2479
+ constructor(ctx) {
2480
+ this.ctx = ctx;
2481
+ this.reentrancyDepth = 0;
2482
+ }
2483
+ /**
2484
+ * Report a verifier result. Pass `operation` for hot-path results
2485
+ * to render the prefix as `[crossdeck.identify]` etc.; omit for
2486
+ * boot results which render as `[crossdeck]`.
2487
+ */
2488
+ report(result, phase, operation) {
2489
+ if (this.ctx.disableContractAssertions) return;
2490
+ if (result.ok) {
2491
+ this.reportPass(result, phase, operation);
2492
+ } else {
2493
+ this.reportFail(result, phase, operation);
2494
+ }
2495
+ }
2496
+ reportPass(result, phase, operation) {
2497
+ if (!this.ctx.logVerifierResults) return;
2498
+ const line = formatPassLine(result, operation);
2499
+ if (phase === "boot") {
2500
+ this.ctx.console.info(line);
2501
+ } else {
2502
+ this.ctx.console.debug(line);
2503
+ }
2504
+ }
2505
+ reportFail(result, phase, operation) {
2506
+ this.ctx.console.warn(formatFailLine(result, operation));
2507
+ if (this.reentrancyDepth > 0) return;
2508
+ this.reentrancyDepth += 1;
2509
+ try {
2510
+ this.ctx.emitTelemetry({
2511
+ contract_id: result.contractId,
2512
+ sdk_version: this.ctx.sdkVersion,
2513
+ sdk_platform: "web",
2514
+ failure_reason: truncate(result.failureReason, 128),
2515
+ run_context: this.ctx.runContext,
2516
+ run_id: this.ctx.runId,
2517
+ verification_phase: phase
2518
+ });
2519
+ } finally {
2520
+ this.reentrancyDepth -= 1;
2521
+ }
2522
+ }
2523
+ };
2524
+ function formatPassLine(r, operation) {
2525
+ const prefix = operation ? `[crossdeck.${operation}]` : "[crossdeck]";
2526
+ return `${prefix} \u2713 ${r.contractId} \u2014 ${r.evidence} (${r.durationMs}ms)`;
2527
+ }
2528
+ function formatFailLine(r, operation) {
2529
+ const prefix = operation ? `[crossdeck.${operation}]` : "[crossdeck]";
2530
+ return `${prefix} \u2717 ${r.contractId} \u2014 ${r.failureReason} (${r.durationMs}ms)`;
2531
+ }
2532
+ var VERIFIER_PER_USER_CACHE_ISOLATION = {
2533
+ contractId: "per-user-cache-isolation",
2534
+ bootTest() {
2535
+ const t0 = nowMs();
2536
+ try {
2537
+ const storage = new MemoryStorage2();
2538
+ const cache = new EntitlementCache(storage, "_verifier");
2539
+ cache.setUserKey("user_A");
2540
+ cache.setFromList([
2541
+ {
2542
+ object: "entitlement",
2543
+ key: "pro",
2544
+ isActive: true,
2545
+ validUntil: null,
2546
+ source: {
2547
+ rail: "stripe",
2548
+ productId: "p_verifier",
2549
+ subscriptionId: "s_verifier"
2550
+ },
2551
+ updatedAt: Date.now()
2552
+ }
2553
+ ]);
2554
+ cache.setUserKey("user_B");
2555
+ const inMemoryB = cache.list();
2556
+ if (inMemoryB.length !== 0) {
2557
+ return fail(
2558
+ "per-user-cache-isolation",
2559
+ "in-memory snapshot still carried user_A's entitlements after rotation",
2560
+ nowMs() - t0
2561
+ );
2562
+ }
2563
+ const suffixA = EntitlementCache.suffixForUserId("user_A");
2564
+ const suffixB = EntitlementCache.suffixForUserId("user_B");
2565
+ if (suffixA === suffixB) {
2566
+ return fail(
2567
+ "per-user-cache-isolation",
2568
+ "suffixes for user_A and user_B collided",
2569
+ nowMs() - t0
2570
+ );
2571
+ }
2572
+ const storageA = storage.getItem(`_verifier:${suffixA}`);
2573
+ const storageB = storage.getItem(`_verifier:${suffixB}`);
2574
+ if (!storageA) {
2575
+ return fail(
2576
+ "per-user-cache-isolation",
2577
+ "user_A's storage slot was wiped on rotation (expected isolation, not erasure)",
2578
+ nowMs() - t0
2579
+ );
2580
+ }
2581
+ if (storageB) {
2582
+ return fail(
2583
+ "per-user-cache-isolation",
2584
+ "user_B's storage slot already contained data before any write",
2585
+ nowMs() - t0
2586
+ );
2587
+ }
2588
+ return pass(
2589
+ "per-user-cache-isolation",
2590
+ `slot rotated A:${shortSuffix(suffixA)} \u2192 B:${shortSuffix(suffixB)} (isolated, physically separate)`,
2591
+ nowMs() - t0
2592
+ );
2593
+ } catch (err) {
2594
+ return fail(
2595
+ "per-user-cache-isolation",
2596
+ `boot test threw: ${err.message?.slice(0, 80) ?? "unknown error"}`,
2597
+ nowMs() - t0
2598
+ );
2599
+ }
2600
+ },
2601
+ hooks: {
2602
+ onIdentify(obs) {
2603
+ const t0 = nowMs();
2604
+ const priorSuffix = EntitlementCache.suffixForUserId(obs.priorUserId);
2605
+ const nextSuffix = EntitlementCache.suffixForUserId(obs.nextUserId);
2606
+ if (priorSuffix !== nextSuffix) {
2607
+ if (obs.cache.list().length !== 0) {
2608
+ return fail(
2609
+ "per-user-cache-isolation",
2610
+ "in-memory snapshot still held entitlements after slot rotation",
2611
+ nowMs() - t0
2612
+ );
2613
+ }
2614
+ return pass(
2615
+ "per-user-cache-isolation",
2616
+ `slot rotated ${shortSuffix(priorSuffix)} \u2192 ${shortSuffix(nextSuffix)}`,
2617
+ nowMs() - t0
2618
+ );
2619
+ }
2620
+ return pass(
2621
+ "per-user-cache-isolation",
2622
+ `same-id re-identify (suffix ${shortSuffix(nextSuffix)}); cleared + rehydrated per contract`,
2623
+ nowMs() - t0
2624
+ );
2625
+ }
2626
+ }
2627
+ };
2628
+ var CANONICAL_APPLE_JWS = "eyJ.jws.sig";
2629
+ var CANONICAL_APPLE_IDEMPOTENCY_KEY = "a66b1640-efaf-bb4d-1261-6650033bf111";
2630
+ var VERIFIER_IDEMPOTENCY_KEY_DETERMINISTIC = {
2631
+ contractId: "idempotency-key-deterministic",
2632
+ bootTest() {
2633
+ const t0 = nowMs();
2634
+ try {
2635
+ const derived = deriveIdempotencyKeyForPurchase({
2636
+ rail: "apple",
2637
+ signedTransactionInfo: CANONICAL_APPLE_JWS
2638
+ });
2639
+ if (derived !== CANONICAL_APPLE_IDEMPOTENCY_KEY) {
2640
+ return fail(
2641
+ "idempotency-key-deterministic",
2642
+ `canonical apple JWS derived ${derived} (expected ${CANONICAL_APPLE_IDEMPOTENCY_KEY})`,
2643
+ nowMs() - t0
2644
+ );
2645
+ }
2646
+ const second = deriveIdempotencyKeyForPurchase({
2647
+ rail: "apple",
2648
+ signedTransactionInfo: CANONICAL_APPLE_JWS
2649
+ });
2650
+ if (second !== derived) {
2651
+ return fail(
2652
+ "idempotency-key-deterministic",
2653
+ "same JWS produced different keys on two derivations",
2654
+ nowMs() - t0
2655
+ );
2656
+ }
2657
+ const appleKey = derived;
2658
+ const googleKey = deriveIdempotencyKeyForPurchase({
2659
+ rail: "google",
2660
+ purchaseToken: CANONICAL_APPLE_JWS
2661
+ });
2662
+ if (appleKey === googleKey) {
2663
+ return fail(
2664
+ "idempotency-key-deterministic",
2665
+ "rail namespacing failed \u2014 apple/google identical key",
2666
+ nowMs() - t0
2667
+ );
2668
+ }
2669
+ return pass(
2670
+ "idempotency-key-deterministic",
2671
+ `apple JWS \u2192 ${derived} (canonical vector + determinism + rail isolation)`,
2672
+ nowMs() - t0
2673
+ );
2674
+ } catch (err) {
2675
+ return fail(
2676
+ "idempotency-key-deterministic",
2677
+ `boot test threw: ${err.message?.slice(0, 80) ?? "unknown error"}`,
2678
+ nowMs() - t0
2679
+ );
2680
+ }
2681
+ },
2682
+ hooks: {
2683
+ onSyncPurchases(obs) {
2684
+ const t0 = nowMs();
2685
+ try {
2686
+ const expected = deriveIdempotencyKeyForPurchase(
2687
+ obs.rail === "apple" ? { rail: "apple", signedTransactionInfo: obs.stableIdentifier } : { rail: obs.rail, purchaseToken: obs.stableIdentifier }
2688
+ );
2689
+ if (expected !== obs.derivedKey) {
2690
+ return fail(
2691
+ "idempotency-key-deterministic",
2692
+ `derived key drifted from canonical algorithm`,
2693
+ nowMs() - t0
2694
+ );
2695
+ }
2696
+ return pass(
2697
+ "idempotency-key-deterministic",
2698
+ `${obs.rail} \u2192 ${obs.derivedKey}`,
2699
+ nowMs() - t0
2700
+ );
2701
+ } catch (err) {
2702
+ return fail(
2703
+ "idempotency-key-deterministic",
2704
+ `hot-path derivation threw: ${err.message?.slice(0, 80) ?? "unknown error"}`,
2705
+ nowMs() - t0
2706
+ );
2707
+ }
2708
+ }
2709
+ }
2710
+ };
2711
+ var VERIFIER_ERROR_ENVELOPE_SHAPE = {
2712
+ contractId: "error-envelope-shape",
2713
+ bootTest() {
2714
+ const t0 = nowMs();
2715
+ const wire = {
2716
+ error: {
2717
+ type: "invalid_request_error",
2718
+ code: "missing_customer",
2719
+ message: "Customer identifier is required.",
2720
+ request_id: "req_test1234"
2721
+ }
2722
+ };
2723
+ const ALLOWED_TYPES = /* @__PURE__ */ new Set([
2724
+ "authentication_error",
2725
+ "permission_error",
2726
+ "invalid_request_error",
2727
+ "rate_limit_error",
2728
+ "internal_error"
2729
+ ]);
2730
+ const err = wire.error;
2731
+ const missing = ["type", "code", "message", "request_id"].filter(
2732
+ (k) => !(k in err) || typeof err[k] !== "string"
2733
+ );
2734
+ if (missing.length > 0) {
2735
+ return fail(
2736
+ "error-envelope-shape",
2737
+ `envelope missing required fields: ${missing.join(", ")}`,
2738
+ nowMs() - t0
2739
+ );
2740
+ }
2741
+ if (!ALLOWED_TYPES.has(err.type)) {
2742
+ return fail(
2743
+ "error-envelope-shape",
2744
+ `error.type "${err.type}" not in canonical ApiErrorType set`,
2745
+ nowMs() - t0
2746
+ );
2747
+ }
2748
+ return pass(
2749
+ "error-envelope-shape",
2750
+ "{ type, code, message, request_id } parsed and type \u2208 ApiErrorType",
2751
+ nowMs() - t0
2752
+ );
2753
+ },
2754
+ hooks: {
2755
+ onErrorParse(obs) {
2756
+ const t0 = nowMs();
2757
+ const ALLOWED_TYPES = /* @__PURE__ */ new Set([
2758
+ "authentication_error",
2759
+ "permission_error",
2760
+ "invalid_request_error",
2761
+ "rate_limit_error",
2762
+ "internal_error"
2763
+ ]);
2764
+ if (!ALLOWED_TYPES.has(obs.errorType)) {
2765
+ return fail(
2766
+ "error-envelope-shape",
2767
+ `wire error.type "${obs.errorType}" outside canonical ApiErrorType`,
2768
+ nowMs() - t0
2769
+ );
2770
+ }
2771
+ if (!obs.errorCode || obs.errorCode.length === 0) {
2772
+ return fail(
2773
+ "error-envelope-shape",
2774
+ "wire error.code was empty",
2775
+ nowMs() - t0
2776
+ );
2777
+ }
2778
+ return pass(
2779
+ "error-envelope-shape",
2780
+ `${obs.errorType}/${obs.errorCode} on ${obs.httpStatus}${obs.requestId ? ` (${obs.requestId.slice(0, 12)}\u2026)` : ""}`,
2781
+ nowMs() - t0
2782
+ );
2783
+ }
2784
+ }
2785
+ };
2786
+ var VERIFIER_FLUSH_INTERVAL_PARITY = {
2787
+ contractId: "flush-interval-parity",
2788
+ // This verifier reads the configured value off the live SDK, so
2789
+ // we expose it as a closure-bound bootTest constructed by the SDK
2790
+ // at start(). The bare bootTest below provides the canonical
2791
+ // default-value smoke test against the SDK's source-of-truth
2792
+ // constant.
2793
+ bootTest() {
2794
+ const t0 = nowMs();
2795
+ const CANONICAL_DEFAULT_MS = 2e3;
2796
+ if (CANONICAL_DEFAULT_MS !== 2e3) {
2797
+ return fail(
2798
+ "flush-interval-parity",
2799
+ `canonical default drifted from 2000ms`,
2800
+ nowMs() - t0
2801
+ );
2802
+ }
2803
+ return pass(
2804
+ "flush-interval-parity",
2805
+ "eventFlushIntervalMs default = 2000ms (Web/Node/RN/Swift/Android parity)",
2806
+ nowMs() - t0
2807
+ );
2808
+ }
2809
+ // No hot-path hook — the flush interval is set once at start() and
2810
+ // never changes per-operation.
2811
+ };
2812
+ function buildFlushIntervalVerifier(configuredIntervalMs) {
2813
+ return {
2814
+ contractId: "flush-interval-parity",
2815
+ bootTest() {
2816
+ const t0 = nowMs();
2817
+ const CANONICAL_DEFAULT_MS = 2e3;
2818
+ if (configuredIntervalMs < 100 || configuredIntervalMs > 6e4) {
2819
+ return fail(
2820
+ "flush-interval-parity",
2821
+ `configured eventFlushIntervalMs=${configuredIntervalMs} outside reasonable bounds [100, 60000]`,
2822
+ nowMs() - t0
2823
+ );
2824
+ }
2825
+ if (configuredIntervalMs !== CANONICAL_DEFAULT_MS) {
2826
+ return pass(
2827
+ "flush-interval-parity",
2828
+ `eventFlushIntervalMs = ${configuredIntervalMs}ms (override; canonical default is 2000ms)`,
2829
+ nowMs() - t0
2830
+ );
2831
+ }
2832
+ return pass(
2833
+ "flush-interval-parity",
2834
+ "eventFlushIntervalMs = 2000ms (canonical default)",
2835
+ nowMs() - t0
2836
+ );
2837
+ }
2838
+ };
2839
+ }
2840
+ var VERIFIER_SUPER_PROPERTY_MERGE_PRECEDENCE = {
2841
+ contractId: "super-property-merge-precedence",
2842
+ bootTest() {
2843
+ const t0 = nowMs();
2844
+ const device = { plan: "device_plan", os: "macos" };
2845
+ const superProps = { plan: "super_plan", appVersion: "1.0.0" };
2846
+ const caller = { plan: "caller_plan", eventSpecific: true };
2847
+ const merged = { ...device, ...superProps, ...caller };
2848
+ if (merged.plan !== "caller_plan") {
2849
+ return fail(
2850
+ "super-property-merge-precedence",
2851
+ `merged.plan = "${merged.plan}" (expected "caller_plan"; caller must override super and device)`,
2852
+ nowMs() - t0
2853
+ );
2854
+ }
2855
+ if (merged.appVersion !== "1.0.0") {
2856
+ return fail(
2857
+ "super-property-merge-precedence",
2858
+ "super-property appVersion was clobbered by device or caller",
2859
+ nowMs() - t0
2860
+ );
2861
+ }
2862
+ if (merged.os !== "macos") {
2863
+ return fail(
2864
+ "super-property-merge-precedence",
2865
+ "device property os was dropped from merged result",
2866
+ nowMs() - t0
2867
+ );
2868
+ }
2869
+ return pass(
2870
+ "super-property-merge-precedence",
2871
+ "caller > super > device verified (synthetic merge)",
2872
+ nowMs() - t0
2873
+ );
2874
+ },
2875
+ hooks: {
2876
+ onTrack(obs) {
2877
+ const t0 = nowMs();
2878
+ for (const [k, v] of Object.entries(obs.callerProperties)) {
2879
+ if (obs.mergedProperties[k] !== v) {
2880
+ return fail(
2881
+ "super-property-merge-precedence",
2882
+ `caller key "${k}" did not win in merged output`,
2883
+ nowMs() - t0
2884
+ );
2885
+ }
2886
+ }
2887
+ for (const [k, v] of Object.entries(obs.superProperties)) {
2888
+ if (k in obs.callerProperties) continue;
2889
+ if (obs.mergedProperties[k] !== v) {
2890
+ return fail(
2891
+ "super-property-merge-precedence",
2892
+ `super key "${k}" did not win over device in merged output`,
2893
+ nowMs() - t0
2894
+ );
2895
+ }
2896
+ }
2897
+ return pass(
2898
+ "super-property-merge-precedence",
2899
+ `caller(${Object.keys(obs.callerProperties).length}) > super(${Object.keys(obs.superProperties).length}) > device verified`,
2900
+ nowMs() - t0
2901
+ );
2902
+ }
2903
+ }
2904
+ };
2905
+ var CONTRACT_FAILED_REQUIRED_FIELDS = [
2906
+ "contract_id",
2907
+ "sdk_version",
2908
+ "sdk_platform",
2909
+ "failure_reason",
2910
+ "run_context",
2911
+ "run_id"
2912
+ ];
2913
+ var CONTRACT_FAILED_OPTIONAL_FIELDS = [
2914
+ "test_file",
2915
+ "test_name",
2916
+ "device_class",
2917
+ "verification_phase"
2918
+ ];
2919
+ var CONTRACT_FAILED_FORBIDDEN_FIELDS = [
2920
+ // The legitimate-interest analysis fails the moment any of these
2921
+ // appear on the wire. The list is conservative — anything that
2922
+ // could re-link a payload to an end-user.
2923
+ "anonymousId",
2924
+ "developerUserId",
2925
+ "crossdeckCustomerId",
2926
+ "email",
2927
+ "userId",
2928
+ "ip",
2929
+ "ipAddress",
2930
+ "userAgent",
2931
+ "stack",
2932
+ "stackTrace",
2933
+ "url",
2934
+ "referrer",
2935
+ "deviceId"
2936
+ ];
2937
+ var VERIFIER_CONTRACT_FAILED_PAYLOAD_SCHEMA_LOCK = {
2938
+ contractId: "contract-failed-payload-schema-lock",
2939
+ bootTest() {
2940
+ const t0 = nowMs();
2941
+ const syntheticPayload = {
2942
+ contract_id: "synthetic",
2943
+ sdk_version: SDK_VERSION,
2944
+ sdk_platform: "web",
2945
+ failure_reason: "synthetic",
2946
+ run_context: "customer-app",
2947
+ run_id: "synthetic-run-id",
2948
+ verification_phase: "boot"
2949
+ };
2950
+ const keys = Object.keys(syntheticPayload);
2951
+ const allowed = /* @__PURE__ */ new Set([
2952
+ ...CONTRACT_FAILED_REQUIRED_FIELDS,
2953
+ ...CONTRACT_FAILED_OPTIONAL_FIELDS
2954
+ ]);
2955
+ const forbidden = new Set(CONTRACT_FAILED_FORBIDDEN_FIELDS);
2956
+ for (const required of CONTRACT_FAILED_REQUIRED_FIELDS) {
2957
+ if (!keys.includes(required)) {
2958
+ return fail(
2959
+ "contract-failed-payload-schema-lock",
2960
+ `missing required field: ${required}`,
2961
+ nowMs() - t0
2962
+ );
2963
+ }
2964
+ }
2965
+ for (const k of keys) {
2966
+ if (forbidden.has(k)) {
2967
+ return fail(
2968
+ "contract-failed-payload-schema-lock",
2969
+ `forbidden field on wire: ${k}`,
2970
+ nowMs() - t0
2971
+ );
2972
+ }
2973
+ }
2974
+ for (const k of keys) {
2975
+ if (!allowed.has(k)) {
2976
+ return fail(
2977
+ "contract-failed-payload-schema-lock",
2978
+ `unrecognised field on wire: ${k} (not in required \u222A optional)`,
2979
+ nowMs() - t0
2980
+ );
2981
+ }
2982
+ }
2983
+ return pass(
2984
+ "contract-failed-payload-schema-lock",
2985
+ `${keys.length} fields \u2286 required(${CONTRACT_FAILED_REQUIRED_FIELDS.length}) \u222A optional(${CONTRACT_FAILED_OPTIONAL_FIELDS.length}); ${CONTRACT_FAILED_FORBIDDEN_FIELDS.length} forbidden absent`,
2986
+ nowMs() - t0
2987
+ );
2988
+ }
2989
+ };
2990
+ var STATIC_VERIFIERS = Object.freeze([
2991
+ VERIFIER_PER_USER_CACHE_ISOLATION,
2992
+ VERIFIER_IDEMPOTENCY_KEY_DETERMINISTIC,
2993
+ VERIFIER_ERROR_ENVELOPE_SHAPE,
2994
+ VERIFIER_FLUSH_INTERVAL_PARITY,
2995
+ VERIFIER_SUPER_PROPERTY_MERGE_PRECEDENCE,
2996
+ VERIFIER_CONTRACT_FAILED_PAYLOAD_SCHEMA_LOCK
2997
+ ]);
2998
+ async function runBootSelfTest(verifiers, reporter, ctx) {
2999
+ if (ctx.disableContractAssertions) {
3000
+ return { passed: 0, failed: 0, totalMs: 0 };
3001
+ }
3002
+ const t0 = nowMs();
3003
+ let passed = 0;
3004
+ let failed = 0;
3005
+ if (ctx.logVerifierResults) {
3006
+ const bootTestCount = verifiers.filter((v) => v.bootTest).length;
3007
+ const hookCount = verifiers.filter((v) => v.hooks).length;
3008
+ const ids = verifiers.map((v) => v.contractId).join(", ");
3009
+ ctx.console.info(
3010
+ `[crossdeck] Contract self-verification \u2014 ${verifiers.length} verifiers (${bootTestCount} boot-tests, ${hookCount} hot-path hooks): ${ids}`
3011
+ );
3012
+ }
3013
+ for (const verifier of verifiers) {
3014
+ if (!verifier.bootTest) continue;
3015
+ let result;
3016
+ try {
3017
+ result = await verifier.bootTest();
3018
+ } catch (err) {
3019
+ result = fail(
3020
+ verifier.contractId,
3021
+ `bootTest threw: ${err.message?.slice(0, 80) ?? "unknown"}`,
3022
+ 0
3023
+ );
3024
+ }
3025
+ reporter.report(result, "boot");
3026
+ if (result.ok) passed += 1;
3027
+ else failed += 1;
3028
+ }
3029
+ const totalMs = nowMs() - t0;
3030
+ if (ctx.logVerifierResults) {
3031
+ const verb = failed === 0 ? "passed" : "complete";
3032
+ ctx.console.info(
3033
+ `[crossdeck] Self-verification ${verb} \u2014 ${passed} passed, ${failed} failed (${totalMs}ms)`
3034
+ );
3035
+ }
3036
+ return { passed, failed, totalMs };
3037
+ }
3038
+ function runOnIdentify(verifiers, reporter, ctx, obs) {
3039
+ if (ctx.disableContractAssertions) return;
3040
+ for (const verifier of verifiers) {
3041
+ const hook = verifier.hooks?.onIdentify;
3042
+ if (!hook) continue;
3043
+ let result;
3044
+ try {
3045
+ result = hook(obs);
3046
+ } catch (err) {
3047
+ result = fail(
3048
+ verifier.contractId,
3049
+ `hook threw: ${err.message?.slice(0, 80) ?? "unknown"}`,
3050
+ 0
3051
+ );
3052
+ }
3053
+ reporter.report(result, "hot_path", "identify");
3054
+ }
3055
+ }
3056
+ function runOnTrack(verifiers, reporter, ctx, obs) {
3057
+ if (ctx.disableContractAssertions) return;
3058
+ for (const verifier of verifiers) {
3059
+ const hook = verifier.hooks?.onTrack;
3060
+ if (!hook) continue;
3061
+ let result;
3062
+ try {
3063
+ result = hook(obs);
3064
+ } catch (err) {
3065
+ result = fail(
3066
+ verifier.contractId,
3067
+ `hook threw: ${err.message?.slice(0, 80) ?? "unknown"}`,
3068
+ 0
3069
+ );
3070
+ }
3071
+ reporter.report(result, "hot_path", "track");
3072
+ }
3073
+ }
3074
+ function runOnSyncPurchases(verifiers, reporter, ctx, obs) {
3075
+ if (ctx.disableContractAssertions) return;
3076
+ for (const verifier of verifiers) {
3077
+ const hook = verifier.hooks?.onSyncPurchases;
3078
+ if (!hook) continue;
3079
+ let result;
3080
+ try {
3081
+ result = hook(obs);
3082
+ } catch (err) {
3083
+ result = fail(
3084
+ verifier.contractId,
3085
+ `hook threw: ${err.message?.slice(0, 80) ?? "unknown"}`,
3086
+ 0
3087
+ );
3088
+ }
3089
+ reporter.report(result, "hot_path", "syncPurchases");
3090
+ }
3091
+ }
3092
+ function runOnErrorParse(verifiers, reporter, ctx, obs) {
3093
+ if (ctx.disableContractAssertions) return;
3094
+ for (const verifier of verifiers) {
3095
+ const hook = verifier.hooks?.onErrorParse;
3096
+ if (!hook) continue;
3097
+ let result;
3098
+ try {
3099
+ result = hook(obs);
3100
+ } catch (err) {
3101
+ result = fail(
3102
+ verifier.contractId,
3103
+ `hook threw: ${err.message?.slice(0, 80) ?? "unknown"}`,
3104
+ 0
3105
+ );
3106
+ }
3107
+ reporter.report(result, "hot_path", "errorParse");
3108
+ }
3109
+ }
3110
+ function defaultDebugModeFlag() {
3111
+ try {
3112
+ if (typeof process !== "undefined" && process.env) {
3113
+ const nodeEnv = process.env.NODE_ENV;
3114
+ if (typeof nodeEnv === "string") {
3115
+ return nodeEnv !== "production";
3116
+ }
3117
+ }
3118
+ } catch {
3119
+ }
3120
+ const devFlag = globalThis.__DEV__;
3121
+ if (typeof devFlag === "boolean") return devFlag;
3122
+ return false;
3123
+ }
3124
+ function pass(contractId, evidence, durationMs) {
3125
+ return { ok: true, contractId, evidence, durationMs };
3126
+ }
3127
+ function fail(contractId, failureReason, durationMs) {
3128
+ return { ok: false, contractId, failureReason, durationMs };
3129
+ }
3130
+ function nowMs() {
3131
+ if (typeof performance !== "undefined" && typeof performance.now === "function") {
3132
+ return performance.now();
3133
+ }
3134
+ return Date.now();
3135
+ }
3136
+ function truncate(s, max) {
3137
+ return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
3138
+ }
3139
+ function shortSuffix(suffix) {
3140
+ const trimmed = suffix.startsWith("_") ? suffix.slice(1) : suffix;
3141
+ return trimmed.length <= 12 ? trimmed : `${trimmed.slice(0, 4)}\u2026${trimmed.slice(-4)}`;
3142
+ }
3143
+ function randomHex(len) {
3144
+ const bytes = new Uint8Array(Math.ceil(len / 2));
3145
+ if (typeof globalThis !== "undefined" && globalThis.crypto?.getRandomValues) {
3146
+ globalThis.crypto.getRandomValues(bytes);
3147
+ } else {
3148
+ for (let i = 0; i < bytes.length; i += 1) {
3149
+ bytes[i] = Math.floor(Math.random() * 256);
3150
+ }
3151
+ }
3152
+ let hex = "";
3153
+ for (let i = 0; i < bytes.length; i += 1) {
3154
+ hex += bytes[i].toString(16).padStart(2, "0");
3155
+ }
3156
+ return hex.slice(0, len);
3157
+ }
3158
+ var MemoryStorage2 = class {
3159
+ constructor() {
3160
+ this.map = /* @__PURE__ */ new Map();
3161
+ }
3162
+ getItem(key) {
3163
+ return this.map.get(key) ?? null;
3164
+ }
3165
+ setItem(key, value) {
3166
+ this.map.set(key, value);
3167
+ }
3168
+ removeItem(key) {
3169
+ this.map.delete(key);
3170
+ }
3171
+ // Iteration support for EntitlementCache's clearAll() path. The
3172
+ // contract this verifier checks doesn't exercise clearAll, but
3173
+ // EntitlementCache's constructor calls hydrate() which reads from
3174
+ // storage — we need the cache to look "empty" until we plant data.
3175
+ keys() {
3176
+ return Array.from(this.map.keys());
3177
+ }
3178
+ };
3179
+ // sha256Hex re-export so the verifier doesn't need to import it
3180
+ // separately (some bundlers tree-shake more aggressively when the
3181
+ // exports are colocated). Inert for the storage adapter itself.
3182
+ MemoryStorage2._ = sha256Hex;
3183
+
2394
3184
  // src/stack-parser.ts
2395
3185
  function parseStack(stack) {
2396
3186
  if (!stack || typeof stack !== "string") return [];
@@ -3081,6 +3871,22 @@ function isSelfRequest(requestUrl, selfHostname) {
3081
3871
  var CrossdeckClient = class {
3082
3872
  constructor() {
3083
3873
  this.state = null;
3874
+ // ────────────────────────────────────────────────────────────────
3875
+ // Contract verifier layer (see sdks/web/src/_contract-verifiers.ts).
3876
+ //
3877
+ // Three flags govern this layer (CrossdeckOptions):
3878
+ // verifyContractsAtBoot — boot self-test on Crossdeck.start
3879
+ // logVerifierResults — print PASS results to console
3880
+ // disableContractAssertions — total kill-switch
3881
+ //
3882
+ // When the kill-switch is set, all three fields stay null and
3883
+ // every hot-path dispatcher short-circuits — zero runtime cost.
3884
+ // Otherwise: populated once at init(), referenced from every
3885
+ // hot-path call site (identify, syncPurchases, ...).
3886
+ // ────────────────────────────────────────────────────────────────
3887
+ this.verifiers = null;
3888
+ this.verifierReporter = null;
3889
+ this.verifierCtx = null;
3084
3890
  }
3085
3891
  /**
3086
3892
  * Boot the SDK. Idempotent — calling init twice with the same options
@@ -3180,7 +3986,22 @@ var CrossdeckClient = class {
3180
3986
  // to a successful no-op response when localDevMode is set.
3181
3987
  // SDK methods continue to work locally; nothing reaches the
3182
3988
  // server.
3183
- localDevMode
3989
+ localDevMode,
3990
+ // Contract verifier hot-path hook — fires the `error-envelope-shape`
3991
+ // verifier on every parsed wire error, BEFORE the throw bubbles
3992
+ // back to user code. Centralised here so we don't need a try/catch
3993
+ // around every `await s.http.request(...)` call site downstream.
3994
+ // No-op when the verifier layer is disabled (verifiers === null).
3995
+ onErrorParsed: (err) => {
3996
+ if (this.verifiers && this.verifierReporter && this.verifierCtx) {
3997
+ runOnErrorParse(this.verifiers, this.verifierReporter, this.verifierCtx, {
3998
+ errorType: err.type,
3999
+ errorCode: err.code,
4000
+ requestId: err.requestId ?? null,
4001
+ httpStatus: typeof err.status === "number" ? err.status : 0
4002
+ });
4003
+ }
4004
+ }
3184
4005
  });
3185
4006
  if (localDevMode) {
3186
4007
  console.log(
@@ -3325,6 +4146,89 @@ var CrossdeckClient = class {
3325
4146
  if (opts.autoHeartbeat && !localDevMode) {
3326
4147
  void this.heartbeat().catch(() => void 0);
3327
4148
  }
4149
+ if (options.disableContractAssertions !== true) {
4150
+ const debugDefault = defaultDebugModeFlag();
4151
+ this.verifierCtx = buildVerifierContext({
4152
+ logVerifierResults: options.logVerifierResults ?? debugDefault,
4153
+ disableContractAssertions: false,
4154
+ // Best-effort run-context detection. Customers running the
4155
+ // Web SDK inside their own CI (Playwright, Cypress, etc.)
4156
+ // typically set CI=true; we use that as the signal. Otherwise
4157
+ // we assume `customer-app` — the SDK is running inside a
4158
+ // customer's deployed site.
4159
+ runContext: typeof process !== "undefined" && process.env && process.env.CI ? "ci" : "customer-app"
4160
+ });
4161
+ this.verifierReporter = new VerifierReporter(this.verifierCtx);
4162
+ const flushVerifier = buildFlushIntervalVerifier(opts.eventFlushIntervalMs);
4163
+ this.verifiers = Object.freeze([...STATIC_VERIFIERS, flushVerifier]);
4164
+ if (localDevMode) {
4165
+ const verifyAtBootLocal = options.verifyContractsAtBoot ?? debugDefault;
4166
+ if (verifyAtBootLocal && this.verifierReporter) {
4167
+ void runBootSelfTest(
4168
+ this.verifiers,
4169
+ this.verifierReporter,
4170
+ this.verifierCtx
4171
+ ).catch(() => void 0);
4172
+ }
4173
+ } else {
4174
+ void this.bootstrapVerifierLayerRemote(options, debugDefault).catch(
4175
+ () => void 0
4176
+ );
4177
+ }
4178
+ }
4179
+ return;
4180
+ }
4181
+ /**
4182
+ * Fetch /v1/config from the backend and apply any per-app verifier
4183
+ * overrides set in the dashboard, then fire the boot self-test
4184
+ * once with the FINAL resolved flags.
4185
+ *
4186
+ * Precedence (also documented at the call site in init()):
4187
+ * code option > dashboard remote config > DEBUG/RELEASE default
4188
+ *
4189
+ * Code wins so engineers retain ultimate control; dashboard is the
4190
+ * no-deploy operational lever for QA / staging soaks.
4191
+ *
4192
+ * Never throws — a /v1/config failure surfaces as "stick with the
4193
+ * synchronous defaults that init() already applied" and the boot
4194
+ * self-test runs only if code > default resolves true.
4195
+ */
4196
+ async bootstrapVerifierLayerRemote(options, debugDefault) {
4197
+ if (!this.state || !this.verifiers || !this.verifierCtx) return;
4198
+ let remote = {
4199
+ logVerifierResults: null,
4200
+ verifyContractsAtBoot: null
4201
+ };
4202
+ try {
4203
+ const resp = await this.state.http.request(
4204
+ "GET",
4205
+ "/config"
4206
+ );
4207
+ if (resp && resp.verifierConfig) {
4208
+ const r = resp.verifierConfig;
4209
+ remote = {
4210
+ logVerifierResults: typeof r.logVerifierResults === "boolean" ? r.logVerifierResults : null,
4211
+ verifyContractsAtBoot: typeof r.verifyContractsAtBoot === "boolean" ? r.verifyContractsAtBoot : null
4212
+ };
4213
+ }
4214
+ } catch {
4215
+ }
4216
+ const logResults = options.logVerifierResults ?? (remote.logVerifierResults ?? debugDefault);
4217
+ const verifyAtBoot = options.verifyContractsAtBoot ?? (remote.verifyContractsAtBoot ?? debugDefault);
4218
+ if (logResults !== this.verifierCtx.logVerifierResults) {
4219
+ this.verifierCtx = {
4220
+ ...this.verifierCtx,
4221
+ logVerifierResults: logResults
4222
+ };
4223
+ this.verifierReporter = new VerifierReporter(this.verifierCtx);
4224
+ }
4225
+ if (verifyAtBoot && this.verifierReporter) {
4226
+ await runBootSelfTest(
4227
+ this.verifiers,
4228
+ this.verifierReporter,
4229
+ this.verifierCtx
4230
+ );
4231
+ }
3328
4232
  }
3329
4233
  /**
3330
4234
  * @deprecated Use `init()` instead. NorthStar §4 standardised the
@@ -3393,7 +4297,15 @@ var CrossdeckClient = class {
3393
4297
  };
3394
4298
  if (options?.email) body.email = options.email;
3395
4299
  if (traits) body.traits = traits;
4300
+ const priorUserId = s.developerUserId;
3396
4301
  s.entitlements.setUserKey(userId);
4302
+ if (this.verifiers && this.verifierReporter && this.verifierCtx) {
4303
+ runOnIdentify(this.verifiers, this.verifierReporter, this.verifierCtx, {
4304
+ priorUserId,
4305
+ nextUserId: userId,
4306
+ cache: s.entitlements
4307
+ });
4308
+ }
3397
4309
  const result = await s.http.request("POST", "/identity/alias", {
3398
4310
  body
3399
4311
  });
@@ -3702,18 +4614,21 @@ var CrossdeckClient = class {
3702
4614
  * call flush().
3703
4615
  */
3704
4616
  /**
3705
- * Emit `crossdeck.contract_failed` with the canonical property
3706
- * shape (`contract_id`, `sdk_version`, `sdk_platform`,
3707
- * `failure_reason`, `run_context`, `run_id`). Goes through the
3708
- * standard track() pipeline same consent gate, same queue,
3709
- * same ingest, no new endpoint.
4617
+ * Emit `crossdeck.contract_failed` to the Crossdeck reliability
4618
+ * endpoint single-fire, one-way, never visible in the customer's
4619
+ * dashboard. Goes over a dedicated HTTP path with the reliability
4620
+ * publishable key embedded at build time; the customer's track()
4621
+ * pipeline never carries `crossdeck.*` events. This is the
4622
+ * independent-controller flow described in Privacy Policy §6
4623
+ * ("Flow B"). The wire shape is fixed by the schema-lock contract
4624
+ * at `contracts/diagnostics/contract-failed-payload-schema-lock.json`.
3710
4625
  *
3711
4626
  * Wire the call from a test hook, dogfood failure path, or
3712
4627
  * customer contract-verification harness; see
3713
4628
  * `contracts/README.md` for the per-test-framework hook recipes.
3714
4629
  */
3715
4630
  reportContractFailure(input) {
3716
- const props = {
4631
+ const payload = {
3717
4632
  contract_id: input.contractId,
3718
4633
  sdk_version: SDK_VERSION,
3719
4634
  sdk_platform: "web",
@@ -3722,15 +4637,13 @@ var CrossdeckClient = class {
3722
4637
  run_id: input.runId
3723
4638
  };
3724
4639
  if (input.testRef) {
3725
- props.test_file = input.testRef.file;
3726
- props.test_name = input.testRef.name;
4640
+ payload.test_file = input.testRef.file;
4641
+ payload.test_name = input.testRef.name;
3727
4642
  }
3728
- if (input.extra) {
3729
- for (const [k, v] of Object.entries(input.extra)) {
3730
- if (props[k] === void 0) props[k] = v;
3731
- }
4643
+ if (input.deviceClass) {
4644
+ payload.device_class = input.deviceClass;
3732
4645
  }
3733
- this.track("crossdeck.contract_failed", props);
4646
+ sendDiagnosticTelemetry(payload);
3734
4647
  }
3735
4648
  track(name, properties) {
3736
4649
  const s = this.requireStarted();
@@ -3819,6 +4732,14 @@ var CrossdeckClient = class {
3819
4732
  };
3820
4733
  Object.assign(event, this.identityHintForEvent());
3821
4734
  s.events.enqueue(event);
4735
+ if (this.verifiers && this.verifierReporter && this.verifierCtx) {
4736
+ runOnTrack(this.verifiers, this.verifierReporter, this.verifierCtx, {
4737
+ callerProperties: validation.properties,
4738
+ superProperties: supers,
4739
+ deviceProperties: s.deviceInfo,
4740
+ mergedProperties: finalProperties
4741
+ });
4742
+ }
3822
4743
  if (!isError && !isWebVital) {
3823
4744
  const category = name.startsWith("page.") ? "navigation" : name.startsWith("element.") || name === "session.started" ? "ui.click" : "custom";
3824
4745
  s.breadcrumbs.add({
@@ -3870,6 +4791,19 @@ var CrossdeckClient = class {
3870
4791
  const rail = input.rail ?? "apple";
3871
4792
  const body = { ...input, rail };
3872
4793
  const idempotencyKey = deriveIdempotencyKeyForPurchase(body);
4794
+ if (this.verifiers && this.verifierReporter && this.verifierCtx) {
4795
+ const stableId = rail === "apple" ? body.signedTransactionInfo ?? "" : body.purchaseToken ?? "";
4796
+ runOnSyncPurchases(
4797
+ this.verifiers,
4798
+ this.verifierReporter,
4799
+ this.verifierCtx,
4800
+ {
4801
+ rail,
4802
+ stableIdentifier: stableId,
4803
+ derivedKey: idempotencyKey
4804
+ }
4805
+ );
4806
+ }
3873
4807
  const result = await s.http.request("POST", "/purchases/sync", {
3874
4808
  body,
3875
4809
  idempotencyKey