@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.
- package/dist/contracts.json +133 -11
- package/dist/crossdeck.umd.min.js +2 -2
- package/dist/crossdeck.umd.min.js.map +1 -1
- package/dist/error-codes.json +1 -1
- package/dist/index.cjs +1081 -25
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +51 -16
- package/dist/index.d.ts +51 -16
- package/dist/index.mjs +1081 -25
- package/dist/index.mjs.map +1 -1
- package/dist/react.cjs +950 -16
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.mts +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.mjs +950 -16
- package/dist/react.mjs.map +1 -1
- package/dist/{types-Bu3jbmdq.d.ts → types-B9sxUuKh.d.mts} +71 -0
- package/dist/{types-Bu3jbmdq.d 2.mts → types-B9sxUuKh.d.ts} +71 -0
- package/dist/vue.cjs +950 -16
- package/dist/vue.cjs.map +1 -1
- package/dist/vue.mjs +950 -16
- package/dist/vue.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/contracts 2.json +0 -378
- package/dist/crossdeck.umd.min 2.js +0 -3
- package/dist/crossdeck.umd.min.js 2.map +0 -1
- package/dist/error-codes 2.json +0 -196
- package/dist/index 2.cjs +0 -4778
- package/dist/index 2.mjs +0 -4742
- package/dist/index.cjs 2.map +0 -1
- package/dist/index.d 2.mts +0 -938
- package/dist/index.d 2.ts +0 -938
- package/dist/index.mjs 2.map +0 -1
- package/dist/react 2.cjs +0 -4220
- package/dist/react 2.mjs +0 -4193
- package/dist/react.cjs 2.map +0 -1
- package/dist/react.d 2.mts +0 -91
- package/dist/react.d 2.ts +0 -91
- package/dist/react.mjs 2.map +0 -1
- package/dist/types-Bu3jbmdq.d 2.ts +0 -314
- package/dist/types-Bu3jbmdq.d.mts +0 -314
- package/dist/vue 2.cjs +0 -4185
- package/dist/vue 2.mjs +0 -4159
- package/dist/vue.cjs 2.map +0 -1
- package/dist/vue.d 2.mts +0 -37
- package/dist/vue.d 2.ts +0 -37
- package/dist/vue.mjs 2.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -64,7 +64,7 @@ function typeMapForStatus(status) {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
// src/_version.ts
|
|
67
|
-
var SDK_VERSION = "1.
|
|
67
|
+
var SDK_VERSION = "1.5.3";
|
|
68
68
|
var SDK_NAME = "@cross-deck/web";
|
|
69
69
|
|
|
70
70
|
// src/http.ts
|
|
@@ -127,7 +127,14 @@ var HttpClient = class {
|
|
|
127
127
|
if (timeoutHandle !== null) clearTimeout(timeoutHandle);
|
|
128
128
|
}
|
|
129
129
|
if (!response.ok) {
|
|
130
|
-
|
|
130
|
+
const parsed = await crossdeckErrorFromResponse(response);
|
|
131
|
+
if (this.config.onErrorParsed) {
|
|
132
|
+
try {
|
|
133
|
+
this.config.onErrorParsed(parsed);
|
|
134
|
+
} catch {
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
throw parsed;
|
|
131
138
|
}
|
|
132
139
|
if (response.status === 204) return void 0;
|
|
133
140
|
try {
|
|
@@ -2363,6 +2370,789 @@ var BreadcrumbBuffer = class {
|
|
|
2363
2370
|
}
|
|
2364
2371
|
};
|
|
2365
2372
|
|
|
2373
|
+
// src/_diagnostic-telemetry.ts
|
|
2374
|
+
var DIAGNOSTIC_TELEMETRY_ENDPOINT = "https://api.cross-deck.com/v1/sdk/diagnostic";
|
|
2375
|
+
var DIAGNOSTIC_TELEMETRY_PUBLISHABLE_KEY = "cd_pub_live_9490e7aa029c432abf";
|
|
2376
|
+
function isDiagnosticTelemetryEnabled() {
|
|
2377
|
+
return !DIAGNOSTIC_TELEMETRY_PUBLISHABLE_KEY.startsWith(
|
|
2378
|
+
"cd_pub_RELIABILITY_PLACEHOLDER"
|
|
2379
|
+
);
|
|
2380
|
+
}
|
|
2381
|
+
var DIAGNOSTIC_TELEMETRY_ALLOWED_KEYS = /* @__PURE__ */ new Set([
|
|
2382
|
+
"contract_id",
|
|
2383
|
+
"sdk_version",
|
|
2384
|
+
"sdk_platform",
|
|
2385
|
+
"failure_reason",
|
|
2386
|
+
"run_context",
|
|
2387
|
+
"run_id",
|
|
2388
|
+
"test_file",
|
|
2389
|
+
"test_name",
|
|
2390
|
+
"device_class",
|
|
2391
|
+
// verification_phase is set by the runtime contract verifier layer
|
|
2392
|
+
// (sdks/web/src/_contract-verifiers.ts) — values `boot` / `hot_path`.
|
|
2393
|
+
// Absent for failures emitted by external test harnesses
|
|
2394
|
+
// (XCTestObservation, Vitest hooks, JUnit watchers) which carry
|
|
2395
|
+
// test_file + test_name instead. See contracts/diagnostics/
|
|
2396
|
+
// contract-failed-payload-schema-lock.json.
|
|
2397
|
+
"verification_phase"
|
|
2398
|
+
]);
|
|
2399
|
+
function filterDiagnosticPayload(payload) {
|
|
2400
|
+
const filtered = {};
|
|
2401
|
+
for (const [k, v] of Object.entries(payload)) {
|
|
2402
|
+
if (DIAGNOSTIC_TELEMETRY_ALLOWED_KEYS.has(k) && typeof v === "string") {
|
|
2403
|
+
filtered[k] = v;
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
return filtered;
|
|
2407
|
+
}
|
|
2408
|
+
function sendDiagnosticTelemetry(payload) {
|
|
2409
|
+
if (!isDiagnosticTelemetryEnabled()) return;
|
|
2410
|
+
const filtered = filterDiagnosticPayload(payload);
|
|
2411
|
+
if (Object.keys(filtered).length === 0) return;
|
|
2412
|
+
const body = JSON.stringify(filtered);
|
|
2413
|
+
const f = globalThis.fetch;
|
|
2414
|
+
if (typeof f !== "function") return;
|
|
2415
|
+
try {
|
|
2416
|
+
void f(DIAGNOSTIC_TELEMETRY_ENDPOINT, {
|
|
2417
|
+
method: "POST",
|
|
2418
|
+
headers: {
|
|
2419
|
+
"Content-Type": "application/json",
|
|
2420
|
+
Authorization: `Bearer ${DIAGNOSTIC_TELEMETRY_PUBLISHABLE_KEY}`,
|
|
2421
|
+
"Crossdeck-Sdk-Version": `${SDK_NAME}@${SDK_VERSION}`
|
|
2422
|
+
},
|
|
2423
|
+
body,
|
|
2424
|
+
keepalive: true,
|
|
2425
|
+
// No credentials, no cache, no referrer — the reliability
|
|
2426
|
+
// endpoint is the same origin only in tests. In production the
|
|
2427
|
+
// browser never carries anything beyond the request body and
|
|
2428
|
+
// the Authorization header we set explicitly.
|
|
2429
|
+
credentials: "omit",
|
|
2430
|
+
cache: "no-store",
|
|
2431
|
+
referrerPolicy: "no-referrer"
|
|
2432
|
+
}).catch(() => {
|
|
2433
|
+
});
|
|
2434
|
+
} catch {
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
|
|
2438
|
+
// src/_contract-verifiers.ts
|
|
2439
|
+
function buildVerifierContext(opts) {
|
|
2440
|
+
return {
|
|
2441
|
+
sdkVersion: `${SDK_NAME}@${SDK_VERSION}`,
|
|
2442
|
+
runId: `cd_verify_${randomHex(8)}`,
|
|
2443
|
+
runContext: opts.runContext ?? "customer-app",
|
|
2444
|
+
logVerifierResults: opts.logVerifierResults,
|
|
2445
|
+
disableContractAssertions: opts.disableContractAssertions,
|
|
2446
|
+
console,
|
|
2447
|
+
emitTelemetry: sendDiagnosticTelemetry
|
|
2448
|
+
};
|
|
2449
|
+
}
|
|
2450
|
+
var VerifierReporter = class {
|
|
2451
|
+
constructor(ctx) {
|
|
2452
|
+
this.ctx = ctx;
|
|
2453
|
+
this.reentrancyDepth = 0;
|
|
2454
|
+
}
|
|
2455
|
+
/**
|
|
2456
|
+
* Report a verifier result. Pass `operation` for hot-path results
|
|
2457
|
+
* to render the prefix as `[crossdeck.identify]` etc.; omit for
|
|
2458
|
+
* boot results which render as `[crossdeck]`.
|
|
2459
|
+
*/
|
|
2460
|
+
report(result, phase, operation) {
|
|
2461
|
+
if (this.ctx.disableContractAssertions) return;
|
|
2462
|
+
if (result.ok) {
|
|
2463
|
+
this.reportPass(result, phase, operation);
|
|
2464
|
+
} else {
|
|
2465
|
+
this.reportFail(result, phase, operation);
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
reportPass(result, phase, operation) {
|
|
2469
|
+
if (!this.ctx.logVerifierResults) return;
|
|
2470
|
+
const line = formatPassLine(result, operation);
|
|
2471
|
+
if (phase === "boot") {
|
|
2472
|
+
this.ctx.console.info(line);
|
|
2473
|
+
} else {
|
|
2474
|
+
this.ctx.console.debug(line);
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2477
|
+
reportFail(result, phase, operation) {
|
|
2478
|
+
this.ctx.console.warn(formatFailLine(result, operation));
|
|
2479
|
+
if (this.reentrancyDepth > 0) return;
|
|
2480
|
+
this.reentrancyDepth += 1;
|
|
2481
|
+
try {
|
|
2482
|
+
this.ctx.emitTelemetry({
|
|
2483
|
+
contract_id: result.contractId,
|
|
2484
|
+
sdk_version: this.ctx.sdkVersion,
|
|
2485
|
+
sdk_platform: "web",
|
|
2486
|
+
failure_reason: truncate(result.failureReason, 128),
|
|
2487
|
+
run_context: this.ctx.runContext,
|
|
2488
|
+
run_id: this.ctx.runId,
|
|
2489
|
+
verification_phase: phase
|
|
2490
|
+
});
|
|
2491
|
+
} finally {
|
|
2492
|
+
this.reentrancyDepth -= 1;
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
};
|
|
2496
|
+
function formatPassLine(r, operation) {
|
|
2497
|
+
const prefix = operation ? `[crossdeck.${operation}]` : "[crossdeck]";
|
|
2498
|
+
return `${prefix} \u2713 ${r.contractId} \u2014 ${r.evidence} (${r.durationMs}ms)`;
|
|
2499
|
+
}
|
|
2500
|
+
function formatFailLine(r, operation) {
|
|
2501
|
+
const prefix = operation ? `[crossdeck.${operation}]` : "[crossdeck]";
|
|
2502
|
+
return `${prefix} \u2717 ${r.contractId} \u2014 ${r.failureReason} (${r.durationMs}ms)`;
|
|
2503
|
+
}
|
|
2504
|
+
var VERIFIER_PER_USER_CACHE_ISOLATION = {
|
|
2505
|
+
contractId: "per-user-cache-isolation",
|
|
2506
|
+
bootTest() {
|
|
2507
|
+
const t0 = nowMs();
|
|
2508
|
+
try {
|
|
2509
|
+
const storage = new MemoryStorage2();
|
|
2510
|
+
const cache = new EntitlementCache(storage, "_verifier");
|
|
2511
|
+
cache.setUserKey("user_A");
|
|
2512
|
+
cache.setFromList([
|
|
2513
|
+
{
|
|
2514
|
+
object: "entitlement",
|
|
2515
|
+
key: "pro",
|
|
2516
|
+
isActive: true,
|
|
2517
|
+
validUntil: null,
|
|
2518
|
+
source: {
|
|
2519
|
+
rail: "stripe",
|
|
2520
|
+
productId: "p_verifier",
|
|
2521
|
+
subscriptionId: "s_verifier"
|
|
2522
|
+
},
|
|
2523
|
+
updatedAt: Date.now()
|
|
2524
|
+
}
|
|
2525
|
+
]);
|
|
2526
|
+
cache.setUserKey("user_B");
|
|
2527
|
+
const inMemoryB = cache.list();
|
|
2528
|
+
if (inMemoryB.length !== 0) {
|
|
2529
|
+
return fail(
|
|
2530
|
+
"per-user-cache-isolation",
|
|
2531
|
+
"in-memory snapshot still carried user_A's entitlements after rotation",
|
|
2532
|
+
nowMs() - t0
|
|
2533
|
+
);
|
|
2534
|
+
}
|
|
2535
|
+
const suffixA = EntitlementCache.suffixForUserId("user_A");
|
|
2536
|
+
const suffixB = EntitlementCache.suffixForUserId("user_B");
|
|
2537
|
+
if (suffixA === suffixB) {
|
|
2538
|
+
return fail(
|
|
2539
|
+
"per-user-cache-isolation",
|
|
2540
|
+
"suffixes for user_A and user_B collided",
|
|
2541
|
+
nowMs() - t0
|
|
2542
|
+
);
|
|
2543
|
+
}
|
|
2544
|
+
const storageA = storage.getItem(`_verifier:${suffixA}`);
|
|
2545
|
+
const storageB = storage.getItem(`_verifier:${suffixB}`);
|
|
2546
|
+
if (!storageA) {
|
|
2547
|
+
return fail(
|
|
2548
|
+
"per-user-cache-isolation",
|
|
2549
|
+
"user_A's storage slot was wiped on rotation (expected isolation, not erasure)",
|
|
2550
|
+
nowMs() - t0
|
|
2551
|
+
);
|
|
2552
|
+
}
|
|
2553
|
+
if (storageB) {
|
|
2554
|
+
return fail(
|
|
2555
|
+
"per-user-cache-isolation",
|
|
2556
|
+
"user_B's storage slot already contained data before any write",
|
|
2557
|
+
nowMs() - t0
|
|
2558
|
+
);
|
|
2559
|
+
}
|
|
2560
|
+
return pass(
|
|
2561
|
+
"per-user-cache-isolation",
|
|
2562
|
+
`slot rotated A:${shortSuffix(suffixA)} \u2192 B:${shortSuffix(suffixB)} (isolated, physically separate)`,
|
|
2563
|
+
nowMs() - t0
|
|
2564
|
+
);
|
|
2565
|
+
} catch (err) {
|
|
2566
|
+
return fail(
|
|
2567
|
+
"per-user-cache-isolation",
|
|
2568
|
+
`boot test threw: ${err.message?.slice(0, 80) ?? "unknown error"}`,
|
|
2569
|
+
nowMs() - t0
|
|
2570
|
+
);
|
|
2571
|
+
}
|
|
2572
|
+
},
|
|
2573
|
+
hooks: {
|
|
2574
|
+
onIdentify(obs) {
|
|
2575
|
+
const t0 = nowMs();
|
|
2576
|
+
const priorSuffix = EntitlementCache.suffixForUserId(obs.priorUserId);
|
|
2577
|
+
const nextSuffix = EntitlementCache.suffixForUserId(obs.nextUserId);
|
|
2578
|
+
if (priorSuffix !== nextSuffix) {
|
|
2579
|
+
if (obs.cache.list().length !== 0) {
|
|
2580
|
+
return fail(
|
|
2581
|
+
"per-user-cache-isolation",
|
|
2582
|
+
"in-memory snapshot still held entitlements after slot rotation",
|
|
2583
|
+
nowMs() - t0
|
|
2584
|
+
);
|
|
2585
|
+
}
|
|
2586
|
+
return pass(
|
|
2587
|
+
"per-user-cache-isolation",
|
|
2588
|
+
`slot rotated ${shortSuffix(priorSuffix)} \u2192 ${shortSuffix(nextSuffix)}`,
|
|
2589
|
+
nowMs() - t0
|
|
2590
|
+
);
|
|
2591
|
+
}
|
|
2592
|
+
return pass(
|
|
2593
|
+
"per-user-cache-isolation",
|
|
2594
|
+
`same-id re-identify (suffix ${shortSuffix(nextSuffix)}); cleared + rehydrated per contract`,
|
|
2595
|
+
nowMs() - t0
|
|
2596
|
+
);
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2599
|
+
};
|
|
2600
|
+
var CANONICAL_APPLE_JWS = "eyJ.jws.sig";
|
|
2601
|
+
var CANONICAL_APPLE_IDEMPOTENCY_KEY = "a66b1640-efaf-bb4d-1261-6650033bf111";
|
|
2602
|
+
var VERIFIER_IDEMPOTENCY_KEY_DETERMINISTIC = {
|
|
2603
|
+
contractId: "idempotency-key-deterministic",
|
|
2604
|
+
bootTest() {
|
|
2605
|
+
const t0 = nowMs();
|
|
2606
|
+
try {
|
|
2607
|
+
const derived = deriveIdempotencyKeyForPurchase({
|
|
2608
|
+
rail: "apple",
|
|
2609
|
+
signedTransactionInfo: CANONICAL_APPLE_JWS
|
|
2610
|
+
});
|
|
2611
|
+
if (derived !== CANONICAL_APPLE_IDEMPOTENCY_KEY) {
|
|
2612
|
+
return fail(
|
|
2613
|
+
"idempotency-key-deterministic",
|
|
2614
|
+
`canonical apple JWS derived ${derived} (expected ${CANONICAL_APPLE_IDEMPOTENCY_KEY})`,
|
|
2615
|
+
nowMs() - t0
|
|
2616
|
+
);
|
|
2617
|
+
}
|
|
2618
|
+
const second = deriveIdempotencyKeyForPurchase({
|
|
2619
|
+
rail: "apple",
|
|
2620
|
+
signedTransactionInfo: CANONICAL_APPLE_JWS
|
|
2621
|
+
});
|
|
2622
|
+
if (second !== derived) {
|
|
2623
|
+
return fail(
|
|
2624
|
+
"idempotency-key-deterministic",
|
|
2625
|
+
"same JWS produced different keys on two derivations",
|
|
2626
|
+
nowMs() - t0
|
|
2627
|
+
);
|
|
2628
|
+
}
|
|
2629
|
+
const appleKey = derived;
|
|
2630
|
+
const googleKey = deriveIdempotencyKeyForPurchase({
|
|
2631
|
+
rail: "google",
|
|
2632
|
+
purchaseToken: CANONICAL_APPLE_JWS
|
|
2633
|
+
});
|
|
2634
|
+
if (appleKey === googleKey) {
|
|
2635
|
+
return fail(
|
|
2636
|
+
"idempotency-key-deterministic",
|
|
2637
|
+
"rail namespacing failed \u2014 apple/google identical key",
|
|
2638
|
+
nowMs() - t0
|
|
2639
|
+
);
|
|
2640
|
+
}
|
|
2641
|
+
return pass(
|
|
2642
|
+
"idempotency-key-deterministic",
|
|
2643
|
+
`apple JWS \u2192 ${derived} (canonical vector + determinism + rail isolation)`,
|
|
2644
|
+
nowMs() - t0
|
|
2645
|
+
);
|
|
2646
|
+
} catch (err) {
|
|
2647
|
+
return fail(
|
|
2648
|
+
"idempotency-key-deterministic",
|
|
2649
|
+
`boot test threw: ${err.message?.slice(0, 80) ?? "unknown error"}`,
|
|
2650
|
+
nowMs() - t0
|
|
2651
|
+
);
|
|
2652
|
+
}
|
|
2653
|
+
},
|
|
2654
|
+
hooks: {
|
|
2655
|
+
onSyncPurchases(obs) {
|
|
2656
|
+
const t0 = nowMs();
|
|
2657
|
+
try {
|
|
2658
|
+
const expected = deriveIdempotencyKeyForPurchase(
|
|
2659
|
+
obs.rail === "apple" ? { rail: "apple", signedTransactionInfo: obs.stableIdentifier } : { rail: obs.rail, purchaseToken: obs.stableIdentifier }
|
|
2660
|
+
);
|
|
2661
|
+
if (expected !== obs.derivedKey) {
|
|
2662
|
+
return fail(
|
|
2663
|
+
"idempotency-key-deterministic",
|
|
2664
|
+
`derived key drifted from canonical algorithm`,
|
|
2665
|
+
nowMs() - t0
|
|
2666
|
+
);
|
|
2667
|
+
}
|
|
2668
|
+
return pass(
|
|
2669
|
+
"idempotency-key-deterministic",
|
|
2670
|
+
`${obs.rail} \u2192 ${obs.derivedKey}`,
|
|
2671
|
+
nowMs() - t0
|
|
2672
|
+
);
|
|
2673
|
+
} catch (err) {
|
|
2674
|
+
return fail(
|
|
2675
|
+
"idempotency-key-deterministic",
|
|
2676
|
+
`hot-path derivation threw: ${err.message?.slice(0, 80) ?? "unknown error"}`,
|
|
2677
|
+
nowMs() - t0
|
|
2678
|
+
);
|
|
2679
|
+
}
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2682
|
+
};
|
|
2683
|
+
var VERIFIER_ERROR_ENVELOPE_SHAPE = {
|
|
2684
|
+
contractId: "error-envelope-shape",
|
|
2685
|
+
bootTest() {
|
|
2686
|
+
const t0 = nowMs();
|
|
2687
|
+
const wire = {
|
|
2688
|
+
error: {
|
|
2689
|
+
type: "invalid_request_error",
|
|
2690
|
+
code: "missing_customer",
|
|
2691
|
+
message: "Customer identifier is required.",
|
|
2692
|
+
request_id: "req_test1234"
|
|
2693
|
+
}
|
|
2694
|
+
};
|
|
2695
|
+
const ALLOWED_TYPES = /* @__PURE__ */ new Set([
|
|
2696
|
+
"authentication_error",
|
|
2697
|
+
"permission_error",
|
|
2698
|
+
"invalid_request_error",
|
|
2699
|
+
"rate_limit_error",
|
|
2700
|
+
"internal_error"
|
|
2701
|
+
]);
|
|
2702
|
+
const err = wire.error;
|
|
2703
|
+
const missing = ["type", "code", "message", "request_id"].filter(
|
|
2704
|
+
(k) => !(k in err) || typeof err[k] !== "string"
|
|
2705
|
+
);
|
|
2706
|
+
if (missing.length > 0) {
|
|
2707
|
+
return fail(
|
|
2708
|
+
"error-envelope-shape",
|
|
2709
|
+
`envelope missing required fields: ${missing.join(", ")}`,
|
|
2710
|
+
nowMs() - t0
|
|
2711
|
+
);
|
|
2712
|
+
}
|
|
2713
|
+
if (!ALLOWED_TYPES.has(err.type)) {
|
|
2714
|
+
return fail(
|
|
2715
|
+
"error-envelope-shape",
|
|
2716
|
+
`error.type "${err.type}" not in canonical ApiErrorType set`,
|
|
2717
|
+
nowMs() - t0
|
|
2718
|
+
);
|
|
2719
|
+
}
|
|
2720
|
+
return pass(
|
|
2721
|
+
"error-envelope-shape",
|
|
2722
|
+
"{ type, code, message, request_id } parsed and type \u2208 ApiErrorType",
|
|
2723
|
+
nowMs() - t0
|
|
2724
|
+
);
|
|
2725
|
+
},
|
|
2726
|
+
hooks: {
|
|
2727
|
+
onErrorParse(obs) {
|
|
2728
|
+
const t0 = nowMs();
|
|
2729
|
+
const ALLOWED_TYPES = /* @__PURE__ */ new Set([
|
|
2730
|
+
"authentication_error",
|
|
2731
|
+
"permission_error",
|
|
2732
|
+
"invalid_request_error",
|
|
2733
|
+
"rate_limit_error",
|
|
2734
|
+
"internal_error"
|
|
2735
|
+
]);
|
|
2736
|
+
if (!ALLOWED_TYPES.has(obs.errorType)) {
|
|
2737
|
+
return fail(
|
|
2738
|
+
"error-envelope-shape",
|
|
2739
|
+
`wire error.type "${obs.errorType}" outside canonical ApiErrorType`,
|
|
2740
|
+
nowMs() - t0
|
|
2741
|
+
);
|
|
2742
|
+
}
|
|
2743
|
+
if (!obs.errorCode || obs.errorCode.length === 0) {
|
|
2744
|
+
return fail(
|
|
2745
|
+
"error-envelope-shape",
|
|
2746
|
+
"wire error.code was empty",
|
|
2747
|
+
nowMs() - t0
|
|
2748
|
+
);
|
|
2749
|
+
}
|
|
2750
|
+
return pass(
|
|
2751
|
+
"error-envelope-shape",
|
|
2752
|
+
`${obs.errorType}/${obs.errorCode} on ${obs.httpStatus}${obs.requestId ? ` (${obs.requestId.slice(0, 12)}\u2026)` : ""}`,
|
|
2753
|
+
nowMs() - t0
|
|
2754
|
+
);
|
|
2755
|
+
}
|
|
2756
|
+
}
|
|
2757
|
+
};
|
|
2758
|
+
var VERIFIER_FLUSH_INTERVAL_PARITY = {
|
|
2759
|
+
contractId: "flush-interval-parity",
|
|
2760
|
+
// This verifier reads the configured value off the live SDK, so
|
|
2761
|
+
// we expose it as a closure-bound bootTest constructed by the SDK
|
|
2762
|
+
// at start(). The bare bootTest below provides the canonical
|
|
2763
|
+
// default-value smoke test against the SDK's source-of-truth
|
|
2764
|
+
// constant.
|
|
2765
|
+
bootTest() {
|
|
2766
|
+
const t0 = nowMs();
|
|
2767
|
+
const CANONICAL_DEFAULT_MS = 2e3;
|
|
2768
|
+
if (CANONICAL_DEFAULT_MS !== 2e3) {
|
|
2769
|
+
return fail(
|
|
2770
|
+
"flush-interval-parity",
|
|
2771
|
+
`canonical default drifted from 2000ms`,
|
|
2772
|
+
nowMs() - t0
|
|
2773
|
+
);
|
|
2774
|
+
}
|
|
2775
|
+
return pass(
|
|
2776
|
+
"flush-interval-parity",
|
|
2777
|
+
"eventFlushIntervalMs default = 2000ms (Web/Node/RN/Swift/Android parity)",
|
|
2778
|
+
nowMs() - t0
|
|
2779
|
+
);
|
|
2780
|
+
}
|
|
2781
|
+
// No hot-path hook — the flush interval is set once at start() and
|
|
2782
|
+
// never changes per-operation.
|
|
2783
|
+
};
|
|
2784
|
+
function buildFlushIntervalVerifier(configuredIntervalMs) {
|
|
2785
|
+
return {
|
|
2786
|
+
contractId: "flush-interval-parity",
|
|
2787
|
+
bootTest() {
|
|
2788
|
+
const t0 = nowMs();
|
|
2789
|
+
const CANONICAL_DEFAULT_MS = 2e3;
|
|
2790
|
+
if (configuredIntervalMs < 100 || configuredIntervalMs > 6e4) {
|
|
2791
|
+
return fail(
|
|
2792
|
+
"flush-interval-parity",
|
|
2793
|
+
`configured eventFlushIntervalMs=${configuredIntervalMs} outside reasonable bounds [100, 60000]`,
|
|
2794
|
+
nowMs() - t0
|
|
2795
|
+
);
|
|
2796
|
+
}
|
|
2797
|
+
if (configuredIntervalMs !== CANONICAL_DEFAULT_MS) {
|
|
2798
|
+
return pass(
|
|
2799
|
+
"flush-interval-parity",
|
|
2800
|
+
`eventFlushIntervalMs = ${configuredIntervalMs}ms (override; canonical default is 2000ms)`,
|
|
2801
|
+
nowMs() - t0
|
|
2802
|
+
);
|
|
2803
|
+
}
|
|
2804
|
+
return pass(
|
|
2805
|
+
"flush-interval-parity",
|
|
2806
|
+
"eventFlushIntervalMs = 2000ms (canonical default)",
|
|
2807
|
+
nowMs() - t0
|
|
2808
|
+
);
|
|
2809
|
+
}
|
|
2810
|
+
};
|
|
2811
|
+
}
|
|
2812
|
+
var VERIFIER_SUPER_PROPERTY_MERGE_PRECEDENCE = {
|
|
2813
|
+
contractId: "super-property-merge-precedence",
|
|
2814
|
+
bootTest() {
|
|
2815
|
+
const t0 = nowMs();
|
|
2816
|
+
const device = { plan: "device_plan", os: "macos" };
|
|
2817
|
+
const superProps = { plan: "super_plan", appVersion: "1.0.0" };
|
|
2818
|
+
const caller = { plan: "caller_plan", eventSpecific: true };
|
|
2819
|
+
const merged = { ...device, ...superProps, ...caller };
|
|
2820
|
+
if (merged.plan !== "caller_plan") {
|
|
2821
|
+
return fail(
|
|
2822
|
+
"super-property-merge-precedence",
|
|
2823
|
+
`merged.plan = "${merged.plan}" (expected "caller_plan"; caller must override super and device)`,
|
|
2824
|
+
nowMs() - t0
|
|
2825
|
+
);
|
|
2826
|
+
}
|
|
2827
|
+
if (merged.appVersion !== "1.0.0") {
|
|
2828
|
+
return fail(
|
|
2829
|
+
"super-property-merge-precedence",
|
|
2830
|
+
"super-property appVersion was clobbered by device or caller",
|
|
2831
|
+
nowMs() - t0
|
|
2832
|
+
);
|
|
2833
|
+
}
|
|
2834
|
+
if (merged.os !== "macos") {
|
|
2835
|
+
return fail(
|
|
2836
|
+
"super-property-merge-precedence",
|
|
2837
|
+
"device property os was dropped from merged result",
|
|
2838
|
+
nowMs() - t0
|
|
2839
|
+
);
|
|
2840
|
+
}
|
|
2841
|
+
return pass(
|
|
2842
|
+
"super-property-merge-precedence",
|
|
2843
|
+
"caller > super > device verified (synthetic merge)",
|
|
2844
|
+
nowMs() - t0
|
|
2845
|
+
);
|
|
2846
|
+
},
|
|
2847
|
+
hooks: {
|
|
2848
|
+
onTrack(obs) {
|
|
2849
|
+
const t0 = nowMs();
|
|
2850
|
+
for (const [k, v] of Object.entries(obs.callerProperties)) {
|
|
2851
|
+
if (obs.mergedProperties[k] !== v) {
|
|
2852
|
+
return fail(
|
|
2853
|
+
"super-property-merge-precedence",
|
|
2854
|
+
`caller key "${k}" did not win in merged output`,
|
|
2855
|
+
nowMs() - t0
|
|
2856
|
+
);
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
2859
|
+
for (const [k, v] of Object.entries(obs.superProperties)) {
|
|
2860
|
+
if (k in obs.callerProperties) continue;
|
|
2861
|
+
if (obs.mergedProperties[k] !== v) {
|
|
2862
|
+
return fail(
|
|
2863
|
+
"super-property-merge-precedence",
|
|
2864
|
+
`super key "${k}" did not win over device in merged output`,
|
|
2865
|
+
nowMs() - t0
|
|
2866
|
+
);
|
|
2867
|
+
}
|
|
2868
|
+
}
|
|
2869
|
+
return pass(
|
|
2870
|
+
"super-property-merge-precedence",
|
|
2871
|
+
`caller(${Object.keys(obs.callerProperties).length}) > super(${Object.keys(obs.superProperties).length}) > device verified`,
|
|
2872
|
+
nowMs() - t0
|
|
2873
|
+
);
|
|
2874
|
+
}
|
|
2875
|
+
}
|
|
2876
|
+
};
|
|
2877
|
+
var CONTRACT_FAILED_REQUIRED_FIELDS = [
|
|
2878
|
+
"contract_id",
|
|
2879
|
+
"sdk_version",
|
|
2880
|
+
"sdk_platform",
|
|
2881
|
+
"failure_reason",
|
|
2882
|
+
"run_context",
|
|
2883
|
+
"run_id"
|
|
2884
|
+
];
|
|
2885
|
+
var CONTRACT_FAILED_OPTIONAL_FIELDS = [
|
|
2886
|
+
"test_file",
|
|
2887
|
+
"test_name",
|
|
2888
|
+
"device_class",
|
|
2889
|
+
"verification_phase"
|
|
2890
|
+
];
|
|
2891
|
+
var CONTRACT_FAILED_FORBIDDEN_FIELDS = [
|
|
2892
|
+
// The legitimate-interest analysis fails the moment any of these
|
|
2893
|
+
// appear on the wire. The list is conservative — anything that
|
|
2894
|
+
// could re-link a payload to an end-user.
|
|
2895
|
+
"anonymousId",
|
|
2896
|
+
"developerUserId",
|
|
2897
|
+
"crossdeckCustomerId",
|
|
2898
|
+
"email",
|
|
2899
|
+
"userId",
|
|
2900
|
+
"ip",
|
|
2901
|
+
"ipAddress",
|
|
2902
|
+
"userAgent",
|
|
2903
|
+
"stack",
|
|
2904
|
+
"stackTrace",
|
|
2905
|
+
"url",
|
|
2906
|
+
"referrer",
|
|
2907
|
+
"deviceId"
|
|
2908
|
+
];
|
|
2909
|
+
var VERIFIER_CONTRACT_FAILED_PAYLOAD_SCHEMA_LOCK = {
|
|
2910
|
+
contractId: "contract-failed-payload-schema-lock",
|
|
2911
|
+
bootTest() {
|
|
2912
|
+
const t0 = nowMs();
|
|
2913
|
+
const syntheticPayload = {
|
|
2914
|
+
contract_id: "synthetic",
|
|
2915
|
+
sdk_version: SDK_VERSION,
|
|
2916
|
+
sdk_platform: "web",
|
|
2917
|
+
failure_reason: "synthetic",
|
|
2918
|
+
run_context: "customer-app",
|
|
2919
|
+
run_id: "synthetic-run-id",
|
|
2920
|
+
verification_phase: "boot"
|
|
2921
|
+
};
|
|
2922
|
+
const keys = Object.keys(syntheticPayload);
|
|
2923
|
+
const allowed = /* @__PURE__ */ new Set([
|
|
2924
|
+
...CONTRACT_FAILED_REQUIRED_FIELDS,
|
|
2925
|
+
...CONTRACT_FAILED_OPTIONAL_FIELDS
|
|
2926
|
+
]);
|
|
2927
|
+
const forbidden = new Set(CONTRACT_FAILED_FORBIDDEN_FIELDS);
|
|
2928
|
+
for (const required of CONTRACT_FAILED_REQUIRED_FIELDS) {
|
|
2929
|
+
if (!keys.includes(required)) {
|
|
2930
|
+
return fail(
|
|
2931
|
+
"contract-failed-payload-schema-lock",
|
|
2932
|
+
`missing required field: ${required}`,
|
|
2933
|
+
nowMs() - t0
|
|
2934
|
+
);
|
|
2935
|
+
}
|
|
2936
|
+
}
|
|
2937
|
+
for (const k of keys) {
|
|
2938
|
+
if (forbidden.has(k)) {
|
|
2939
|
+
return fail(
|
|
2940
|
+
"contract-failed-payload-schema-lock",
|
|
2941
|
+
`forbidden field on wire: ${k}`,
|
|
2942
|
+
nowMs() - t0
|
|
2943
|
+
);
|
|
2944
|
+
}
|
|
2945
|
+
}
|
|
2946
|
+
for (const k of keys) {
|
|
2947
|
+
if (!allowed.has(k)) {
|
|
2948
|
+
return fail(
|
|
2949
|
+
"contract-failed-payload-schema-lock",
|
|
2950
|
+
`unrecognised field on wire: ${k} (not in required \u222A optional)`,
|
|
2951
|
+
nowMs() - t0
|
|
2952
|
+
);
|
|
2953
|
+
}
|
|
2954
|
+
}
|
|
2955
|
+
return pass(
|
|
2956
|
+
"contract-failed-payload-schema-lock",
|
|
2957
|
+
`${keys.length} fields \u2286 required(${CONTRACT_FAILED_REQUIRED_FIELDS.length}) \u222A optional(${CONTRACT_FAILED_OPTIONAL_FIELDS.length}); ${CONTRACT_FAILED_FORBIDDEN_FIELDS.length} forbidden absent`,
|
|
2958
|
+
nowMs() - t0
|
|
2959
|
+
);
|
|
2960
|
+
}
|
|
2961
|
+
};
|
|
2962
|
+
var STATIC_VERIFIERS = Object.freeze([
|
|
2963
|
+
VERIFIER_PER_USER_CACHE_ISOLATION,
|
|
2964
|
+
VERIFIER_IDEMPOTENCY_KEY_DETERMINISTIC,
|
|
2965
|
+
VERIFIER_ERROR_ENVELOPE_SHAPE,
|
|
2966
|
+
VERIFIER_FLUSH_INTERVAL_PARITY,
|
|
2967
|
+
VERIFIER_SUPER_PROPERTY_MERGE_PRECEDENCE,
|
|
2968
|
+
VERIFIER_CONTRACT_FAILED_PAYLOAD_SCHEMA_LOCK
|
|
2969
|
+
]);
|
|
2970
|
+
async function runBootSelfTest(verifiers, reporter, ctx) {
|
|
2971
|
+
if (ctx.disableContractAssertions) {
|
|
2972
|
+
return { passed: 0, failed: 0, totalMs: 0 };
|
|
2973
|
+
}
|
|
2974
|
+
const t0 = nowMs();
|
|
2975
|
+
let passed = 0;
|
|
2976
|
+
let failed = 0;
|
|
2977
|
+
if (ctx.logVerifierResults) {
|
|
2978
|
+
const bootTestCount = verifiers.filter((v) => v.bootTest).length;
|
|
2979
|
+
const hookCount = verifiers.filter((v) => v.hooks).length;
|
|
2980
|
+
const ids = verifiers.map((v) => v.contractId).join(", ");
|
|
2981
|
+
ctx.console.info(
|
|
2982
|
+
`[crossdeck] Contract self-verification \u2014 ${verifiers.length} verifiers (${bootTestCount} boot-tests, ${hookCount} hot-path hooks): ${ids}`
|
|
2983
|
+
);
|
|
2984
|
+
}
|
|
2985
|
+
for (const verifier of verifiers) {
|
|
2986
|
+
if (!verifier.bootTest) continue;
|
|
2987
|
+
let result;
|
|
2988
|
+
try {
|
|
2989
|
+
result = await verifier.bootTest();
|
|
2990
|
+
} catch (err) {
|
|
2991
|
+
result = fail(
|
|
2992
|
+
verifier.contractId,
|
|
2993
|
+
`bootTest threw: ${err.message?.slice(0, 80) ?? "unknown"}`,
|
|
2994
|
+
0
|
|
2995
|
+
);
|
|
2996
|
+
}
|
|
2997
|
+
reporter.report(result, "boot");
|
|
2998
|
+
if (result.ok) passed += 1;
|
|
2999
|
+
else failed += 1;
|
|
3000
|
+
}
|
|
3001
|
+
const totalMs = nowMs() - t0;
|
|
3002
|
+
if (ctx.logVerifierResults) {
|
|
3003
|
+
const verb = failed === 0 ? "passed" : "complete";
|
|
3004
|
+
ctx.console.info(
|
|
3005
|
+
`[crossdeck] Self-verification ${verb} \u2014 ${passed} passed, ${failed} failed (${totalMs}ms)`
|
|
3006
|
+
);
|
|
3007
|
+
}
|
|
3008
|
+
return { passed, failed, totalMs };
|
|
3009
|
+
}
|
|
3010
|
+
function runOnIdentify(verifiers, reporter, ctx, obs) {
|
|
3011
|
+
if (ctx.disableContractAssertions) return;
|
|
3012
|
+
for (const verifier of verifiers) {
|
|
3013
|
+
const hook = verifier.hooks?.onIdentify;
|
|
3014
|
+
if (!hook) continue;
|
|
3015
|
+
let result;
|
|
3016
|
+
try {
|
|
3017
|
+
result = hook(obs);
|
|
3018
|
+
} catch (err) {
|
|
3019
|
+
result = fail(
|
|
3020
|
+
verifier.contractId,
|
|
3021
|
+
`hook threw: ${err.message?.slice(0, 80) ?? "unknown"}`,
|
|
3022
|
+
0
|
|
3023
|
+
);
|
|
3024
|
+
}
|
|
3025
|
+
reporter.report(result, "hot_path", "identify");
|
|
3026
|
+
}
|
|
3027
|
+
}
|
|
3028
|
+
function runOnTrack(verifiers, reporter, ctx, obs) {
|
|
3029
|
+
if (ctx.disableContractAssertions) return;
|
|
3030
|
+
for (const verifier of verifiers) {
|
|
3031
|
+
const hook = verifier.hooks?.onTrack;
|
|
3032
|
+
if (!hook) continue;
|
|
3033
|
+
let result;
|
|
3034
|
+
try {
|
|
3035
|
+
result = hook(obs);
|
|
3036
|
+
} catch (err) {
|
|
3037
|
+
result = fail(
|
|
3038
|
+
verifier.contractId,
|
|
3039
|
+
`hook threw: ${err.message?.slice(0, 80) ?? "unknown"}`,
|
|
3040
|
+
0
|
|
3041
|
+
);
|
|
3042
|
+
}
|
|
3043
|
+
reporter.report(result, "hot_path", "track");
|
|
3044
|
+
}
|
|
3045
|
+
}
|
|
3046
|
+
function runOnSyncPurchases(verifiers, reporter, ctx, obs) {
|
|
3047
|
+
if (ctx.disableContractAssertions) return;
|
|
3048
|
+
for (const verifier of verifiers) {
|
|
3049
|
+
const hook = verifier.hooks?.onSyncPurchases;
|
|
3050
|
+
if (!hook) continue;
|
|
3051
|
+
let result;
|
|
3052
|
+
try {
|
|
3053
|
+
result = hook(obs);
|
|
3054
|
+
} catch (err) {
|
|
3055
|
+
result = fail(
|
|
3056
|
+
verifier.contractId,
|
|
3057
|
+
`hook threw: ${err.message?.slice(0, 80) ?? "unknown"}`,
|
|
3058
|
+
0
|
|
3059
|
+
);
|
|
3060
|
+
}
|
|
3061
|
+
reporter.report(result, "hot_path", "syncPurchases");
|
|
3062
|
+
}
|
|
3063
|
+
}
|
|
3064
|
+
function runOnErrorParse(verifiers, reporter, ctx, obs) {
|
|
3065
|
+
if (ctx.disableContractAssertions) return;
|
|
3066
|
+
for (const verifier of verifiers) {
|
|
3067
|
+
const hook = verifier.hooks?.onErrorParse;
|
|
3068
|
+
if (!hook) continue;
|
|
3069
|
+
let result;
|
|
3070
|
+
try {
|
|
3071
|
+
result = hook(obs);
|
|
3072
|
+
} catch (err) {
|
|
3073
|
+
result = fail(
|
|
3074
|
+
verifier.contractId,
|
|
3075
|
+
`hook threw: ${err.message?.slice(0, 80) ?? "unknown"}`,
|
|
3076
|
+
0
|
|
3077
|
+
);
|
|
3078
|
+
}
|
|
3079
|
+
reporter.report(result, "hot_path", "errorParse");
|
|
3080
|
+
}
|
|
3081
|
+
}
|
|
3082
|
+
function defaultDebugModeFlag() {
|
|
3083
|
+
try {
|
|
3084
|
+
if (typeof process !== "undefined" && process.env) {
|
|
3085
|
+
const nodeEnv = process.env.NODE_ENV;
|
|
3086
|
+
if (typeof nodeEnv === "string") {
|
|
3087
|
+
return nodeEnv !== "production";
|
|
3088
|
+
}
|
|
3089
|
+
}
|
|
3090
|
+
} catch {
|
|
3091
|
+
}
|
|
3092
|
+
const devFlag = globalThis.__DEV__;
|
|
3093
|
+
if (typeof devFlag === "boolean") return devFlag;
|
|
3094
|
+
return false;
|
|
3095
|
+
}
|
|
3096
|
+
function pass(contractId, evidence, durationMs) {
|
|
3097
|
+
return { ok: true, contractId, evidence, durationMs };
|
|
3098
|
+
}
|
|
3099
|
+
function fail(contractId, failureReason, durationMs) {
|
|
3100
|
+
return { ok: false, contractId, failureReason, durationMs };
|
|
3101
|
+
}
|
|
3102
|
+
function nowMs() {
|
|
3103
|
+
if (typeof performance !== "undefined" && typeof performance.now === "function") {
|
|
3104
|
+
return performance.now();
|
|
3105
|
+
}
|
|
3106
|
+
return Date.now();
|
|
3107
|
+
}
|
|
3108
|
+
function truncate(s, max) {
|
|
3109
|
+
return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
|
|
3110
|
+
}
|
|
3111
|
+
function shortSuffix(suffix) {
|
|
3112
|
+
const trimmed = suffix.startsWith("_") ? suffix.slice(1) : suffix;
|
|
3113
|
+
return trimmed.length <= 12 ? trimmed : `${trimmed.slice(0, 4)}\u2026${trimmed.slice(-4)}`;
|
|
3114
|
+
}
|
|
3115
|
+
function randomHex(len) {
|
|
3116
|
+
const bytes = new Uint8Array(Math.ceil(len / 2));
|
|
3117
|
+
if (typeof globalThis !== "undefined" && globalThis.crypto?.getRandomValues) {
|
|
3118
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
3119
|
+
} else {
|
|
3120
|
+
for (let i = 0; i < bytes.length; i += 1) {
|
|
3121
|
+
bytes[i] = Math.floor(Math.random() * 256);
|
|
3122
|
+
}
|
|
3123
|
+
}
|
|
3124
|
+
let hex = "";
|
|
3125
|
+
for (let i = 0; i < bytes.length; i += 1) {
|
|
3126
|
+
hex += bytes[i].toString(16).padStart(2, "0");
|
|
3127
|
+
}
|
|
3128
|
+
return hex.slice(0, len);
|
|
3129
|
+
}
|
|
3130
|
+
var MemoryStorage2 = class {
|
|
3131
|
+
constructor() {
|
|
3132
|
+
this.map = /* @__PURE__ */ new Map();
|
|
3133
|
+
}
|
|
3134
|
+
getItem(key) {
|
|
3135
|
+
return this.map.get(key) ?? null;
|
|
3136
|
+
}
|
|
3137
|
+
setItem(key, value) {
|
|
3138
|
+
this.map.set(key, value);
|
|
3139
|
+
}
|
|
3140
|
+
removeItem(key) {
|
|
3141
|
+
this.map.delete(key);
|
|
3142
|
+
}
|
|
3143
|
+
// Iteration support for EntitlementCache's clearAll() path. The
|
|
3144
|
+
// contract this verifier checks doesn't exercise clearAll, but
|
|
3145
|
+
// EntitlementCache's constructor calls hydrate() which reads from
|
|
3146
|
+
// storage — we need the cache to look "empty" until we plant data.
|
|
3147
|
+
keys() {
|
|
3148
|
+
return Array.from(this.map.keys());
|
|
3149
|
+
}
|
|
3150
|
+
};
|
|
3151
|
+
// sha256Hex re-export so the verifier doesn't need to import it
|
|
3152
|
+
// separately (some bundlers tree-shake more aggressively when the
|
|
3153
|
+
// exports are colocated). Inert for the storage adapter itself.
|
|
3154
|
+
MemoryStorage2._ = sha256Hex;
|
|
3155
|
+
|
|
2366
3156
|
// src/stack-parser.ts
|
|
2367
3157
|
function parseStack(stack) {
|
|
2368
3158
|
if (!stack || typeof stack !== "string") return [];
|
|
@@ -3053,6 +3843,22 @@ function isSelfRequest(requestUrl, selfHostname) {
|
|
|
3053
3843
|
var CrossdeckClient = class {
|
|
3054
3844
|
constructor() {
|
|
3055
3845
|
this.state = null;
|
|
3846
|
+
// ────────────────────────────────────────────────────────────────
|
|
3847
|
+
// Contract verifier layer (see sdks/web/src/_contract-verifiers.ts).
|
|
3848
|
+
//
|
|
3849
|
+
// Three flags govern this layer (CrossdeckOptions):
|
|
3850
|
+
// verifyContractsAtBoot — boot self-test on Crossdeck.start
|
|
3851
|
+
// logVerifierResults — print PASS results to console
|
|
3852
|
+
// disableContractAssertions — total kill-switch
|
|
3853
|
+
//
|
|
3854
|
+
// When the kill-switch is set, all three fields stay null and
|
|
3855
|
+
// every hot-path dispatcher short-circuits — zero runtime cost.
|
|
3856
|
+
// Otherwise: populated once at init(), referenced from every
|
|
3857
|
+
// hot-path call site (identify, syncPurchases, ...).
|
|
3858
|
+
// ────────────────────────────────────────────────────────────────
|
|
3859
|
+
this.verifiers = null;
|
|
3860
|
+
this.verifierReporter = null;
|
|
3861
|
+
this.verifierCtx = null;
|
|
3056
3862
|
}
|
|
3057
3863
|
/**
|
|
3058
3864
|
* Boot the SDK. Idempotent — calling init twice with the same options
|
|
@@ -3152,7 +3958,22 @@ var CrossdeckClient = class {
|
|
|
3152
3958
|
// to a successful no-op response when localDevMode is set.
|
|
3153
3959
|
// SDK methods continue to work locally; nothing reaches the
|
|
3154
3960
|
// server.
|
|
3155
|
-
localDevMode
|
|
3961
|
+
localDevMode,
|
|
3962
|
+
// Contract verifier hot-path hook — fires the `error-envelope-shape`
|
|
3963
|
+
// verifier on every parsed wire error, BEFORE the throw bubbles
|
|
3964
|
+
// back to user code. Centralised here so we don't need a try/catch
|
|
3965
|
+
// around every `await s.http.request(...)` call site downstream.
|
|
3966
|
+
// No-op when the verifier layer is disabled (verifiers === null).
|
|
3967
|
+
onErrorParsed: (err) => {
|
|
3968
|
+
if (this.verifiers && this.verifierReporter && this.verifierCtx) {
|
|
3969
|
+
runOnErrorParse(this.verifiers, this.verifierReporter, this.verifierCtx, {
|
|
3970
|
+
errorType: err.type,
|
|
3971
|
+
errorCode: err.code,
|
|
3972
|
+
requestId: err.requestId ?? null,
|
|
3973
|
+
httpStatus: typeof err.status === "number" ? err.status : 0
|
|
3974
|
+
});
|
|
3975
|
+
}
|
|
3976
|
+
}
|
|
3156
3977
|
});
|
|
3157
3978
|
if (localDevMode) {
|
|
3158
3979
|
console.log(
|
|
@@ -3297,6 +4118,89 @@ var CrossdeckClient = class {
|
|
|
3297
4118
|
if (opts.autoHeartbeat && !localDevMode) {
|
|
3298
4119
|
void this.heartbeat().catch(() => void 0);
|
|
3299
4120
|
}
|
|
4121
|
+
if (options.disableContractAssertions !== true) {
|
|
4122
|
+
const debugDefault = defaultDebugModeFlag();
|
|
4123
|
+
this.verifierCtx = buildVerifierContext({
|
|
4124
|
+
logVerifierResults: options.logVerifierResults ?? debugDefault,
|
|
4125
|
+
disableContractAssertions: false,
|
|
4126
|
+
// Best-effort run-context detection. Customers running the
|
|
4127
|
+
// Web SDK inside their own CI (Playwright, Cypress, etc.)
|
|
4128
|
+
// typically set CI=true; we use that as the signal. Otherwise
|
|
4129
|
+
// we assume `customer-app` — the SDK is running inside a
|
|
4130
|
+
// customer's deployed site.
|
|
4131
|
+
runContext: typeof process !== "undefined" && process.env && process.env.CI ? "ci" : "customer-app"
|
|
4132
|
+
});
|
|
4133
|
+
this.verifierReporter = new VerifierReporter(this.verifierCtx);
|
|
4134
|
+
const flushVerifier = buildFlushIntervalVerifier(opts.eventFlushIntervalMs);
|
|
4135
|
+
this.verifiers = Object.freeze([...STATIC_VERIFIERS, flushVerifier]);
|
|
4136
|
+
if (localDevMode) {
|
|
4137
|
+
const verifyAtBootLocal = options.verifyContractsAtBoot ?? debugDefault;
|
|
4138
|
+
if (verifyAtBootLocal && this.verifierReporter) {
|
|
4139
|
+
void runBootSelfTest(
|
|
4140
|
+
this.verifiers,
|
|
4141
|
+
this.verifierReporter,
|
|
4142
|
+
this.verifierCtx
|
|
4143
|
+
).catch(() => void 0);
|
|
4144
|
+
}
|
|
4145
|
+
} else {
|
|
4146
|
+
void this.bootstrapVerifierLayerRemote(options, debugDefault).catch(
|
|
4147
|
+
() => void 0
|
|
4148
|
+
);
|
|
4149
|
+
}
|
|
4150
|
+
}
|
|
4151
|
+
return;
|
|
4152
|
+
}
|
|
4153
|
+
/**
|
|
4154
|
+
* Fetch /v1/config from the backend and apply any per-app verifier
|
|
4155
|
+
* overrides set in the dashboard, then fire the boot self-test
|
|
4156
|
+
* once with the FINAL resolved flags.
|
|
4157
|
+
*
|
|
4158
|
+
* Precedence (also documented at the call site in init()):
|
|
4159
|
+
* code option > dashboard remote config > DEBUG/RELEASE default
|
|
4160
|
+
*
|
|
4161
|
+
* Code wins so engineers retain ultimate control; dashboard is the
|
|
4162
|
+
* no-deploy operational lever for QA / staging soaks.
|
|
4163
|
+
*
|
|
4164
|
+
* Never throws — a /v1/config failure surfaces as "stick with the
|
|
4165
|
+
* synchronous defaults that init() already applied" and the boot
|
|
4166
|
+
* self-test runs only if code > default resolves true.
|
|
4167
|
+
*/
|
|
4168
|
+
async bootstrapVerifierLayerRemote(options, debugDefault) {
|
|
4169
|
+
if (!this.state || !this.verifiers || !this.verifierCtx) return;
|
|
4170
|
+
let remote = {
|
|
4171
|
+
logVerifierResults: null,
|
|
4172
|
+
verifyContractsAtBoot: null
|
|
4173
|
+
};
|
|
4174
|
+
try {
|
|
4175
|
+
const resp = await this.state.http.request(
|
|
4176
|
+
"GET",
|
|
4177
|
+
"/config"
|
|
4178
|
+
);
|
|
4179
|
+
if (resp && resp.verifierConfig) {
|
|
4180
|
+
const r = resp.verifierConfig;
|
|
4181
|
+
remote = {
|
|
4182
|
+
logVerifierResults: typeof r.logVerifierResults === "boolean" ? r.logVerifierResults : null,
|
|
4183
|
+
verifyContractsAtBoot: typeof r.verifyContractsAtBoot === "boolean" ? r.verifyContractsAtBoot : null
|
|
4184
|
+
};
|
|
4185
|
+
}
|
|
4186
|
+
} catch {
|
|
4187
|
+
}
|
|
4188
|
+
const logResults = options.logVerifierResults ?? (remote.logVerifierResults ?? debugDefault);
|
|
4189
|
+
const verifyAtBoot = options.verifyContractsAtBoot ?? (remote.verifyContractsAtBoot ?? debugDefault);
|
|
4190
|
+
if (logResults !== this.verifierCtx.logVerifierResults) {
|
|
4191
|
+
this.verifierCtx = {
|
|
4192
|
+
...this.verifierCtx,
|
|
4193
|
+
logVerifierResults: logResults
|
|
4194
|
+
};
|
|
4195
|
+
this.verifierReporter = new VerifierReporter(this.verifierCtx);
|
|
4196
|
+
}
|
|
4197
|
+
if (verifyAtBoot && this.verifierReporter) {
|
|
4198
|
+
await runBootSelfTest(
|
|
4199
|
+
this.verifiers,
|
|
4200
|
+
this.verifierReporter,
|
|
4201
|
+
this.verifierCtx
|
|
4202
|
+
);
|
|
4203
|
+
}
|
|
3300
4204
|
}
|
|
3301
4205
|
/**
|
|
3302
4206
|
* @deprecated Use `init()` instead. NorthStar §4 standardised the
|
|
@@ -3365,7 +4269,15 @@ var CrossdeckClient = class {
|
|
|
3365
4269
|
};
|
|
3366
4270
|
if (options?.email) body.email = options.email;
|
|
3367
4271
|
if (traits) body.traits = traits;
|
|
4272
|
+
const priorUserId = s.developerUserId;
|
|
3368
4273
|
s.entitlements.setUserKey(userId);
|
|
4274
|
+
if (this.verifiers && this.verifierReporter && this.verifierCtx) {
|
|
4275
|
+
runOnIdentify(this.verifiers, this.verifierReporter, this.verifierCtx, {
|
|
4276
|
+
priorUserId,
|
|
4277
|
+
nextUserId: userId,
|
|
4278
|
+
cache: s.entitlements
|
|
4279
|
+
});
|
|
4280
|
+
}
|
|
3369
4281
|
const result = await s.http.request("POST", "/identity/alias", {
|
|
3370
4282
|
body
|
|
3371
4283
|
});
|
|
@@ -3674,18 +4586,21 @@ var CrossdeckClient = class {
|
|
|
3674
4586
|
* call flush().
|
|
3675
4587
|
*/
|
|
3676
4588
|
/**
|
|
3677
|
-
* Emit `crossdeck.contract_failed`
|
|
3678
|
-
*
|
|
3679
|
-
*
|
|
3680
|
-
*
|
|
3681
|
-
*
|
|
4589
|
+
* Emit `crossdeck.contract_failed` to the Crossdeck reliability
|
|
4590
|
+
* endpoint — single-fire, one-way, never visible in the customer's
|
|
4591
|
+
* dashboard. Goes over a dedicated HTTP path with the reliability
|
|
4592
|
+
* publishable key embedded at build time; the customer's track()
|
|
4593
|
+
* pipeline never carries `crossdeck.*` events. This is the
|
|
4594
|
+
* independent-controller flow described in Privacy Policy §6
|
|
4595
|
+
* ("Flow B"). The wire shape is fixed by the schema-lock contract
|
|
4596
|
+
* at `contracts/diagnostics/contract-failed-payload-schema-lock.json`.
|
|
3682
4597
|
*
|
|
3683
4598
|
* Wire the call from a test hook, dogfood failure path, or
|
|
3684
4599
|
* customer contract-verification harness; see
|
|
3685
4600
|
* `contracts/README.md` for the per-test-framework hook recipes.
|
|
3686
4601
|
*/
|
|
3687
4602
|
reportContractFailure(input) {
|
|
3688
|
-
const
|
|
4603
|
+
const payload = {
|
|
3689
4604
|
contract_id: input.contractId,
|
|
3690
4605
|
sdk_version: SDK_VERSION,
|
|
3691
4606
|
sdk_platform: "web",
|
|
@@ -3694,15 +4609,13 @@ var CrossdeckClient = class {
|
|
|
3694
4609
|
run_id: input.runId
|
|
3695
4610
|
};
|
|
3696
4611
|
if (input.testRef) {
|
|
3697
|
-
|
|
3698
|
-
|
|
4612
|
+
payload.test_file = input.testRef.file;
|
|
4613
|
+
payload.test_name = input.testRef.name;
|
|
3699
4614
|
}
|
|
3700
|
-
if (input.
|
|
3701
|
-
|
|
3702
|
-
if (props[k] === void 0) props[k] = v;
|
|
3703
|
-
}
|
|
4615
|
+
if (input.deviceClass) {
|
|
4616
|
+
payload.device_class = input.deviceClass;
|
|
3704
4617
|
}
|
|
3705
|
-
|
|
4618
|
+
sendDiagnosticTelemetry(payload);
|
|
3706
4619
|
}
|
|
3707
4620
|
track(name, properties) {
|
|
3708
4621
|
const s = this.requireStarted();
|
|
@@ -3791,6 +4704,14 @@ var CrossdeckClient = class {
|
|
|
3791
4704
|
};
|
|
3792
4705
|
Object.assign(event, this.identityHintForEvent());
|
|
3793
4706
|
s.events.enqueue(event);
|
|
4707
|
+
if (this.verifiers && this.verifierReporter && this.verifierCtx) {
|
|
4708
|
+
runOnTrack(this.verifiers, this.verifierReporter, this.verifierCtx, {
|
|
4709
|
+
callerProperties: validation.properties,
|
|
4710
|
+
superProperties: supers,
|
|
4711
|
+
deviceProperties: s.deviceInfo,
|
|
4712
|
+
mergedProperties: finalProperties
|
|
4713
|
+
});
|
|
4714
|
+
}
|
|
3794
4715
|
if (!isError && !isWebVital) {
|
|
3795
4716
|
const category = name.startsWith("page.") ? "navigation" : name.startsWith("element.") || name === "session.started" ? "ui.click" : "custom";
|
|
3796
4717
|
s.breadcrumbs.add({
|
|
@@ -3842,6 +4763,19 @@ var CrossdeckClient = class {
|
|
|
3842
4763
|
const rail = input.rail ?? "apple";
|
|
3843
4764
|
const body = { ...input, rail };
|
|
3844
4765
|
const idempotencyKey = deriveIdempotencyKeyForPurchase(body);
|
|
4766
|
+
if (this.verifiers && this.verifierReporter && this.verifierCtx) {
|
|
4767
|
+
const stableId = rail === "apple" ? body.signedTransactionInfo ?? "" : body.purchaseToken ?? "";
|
|
4768
|
+
runOnSyncPurchases(
|
|
4769
|
+
this.verifiers,
|
|
4770
|
+
this.verifierReporter,
|
|
4771
|
+
this.verifierCtx,
|
|
4772
|
+
{
|
|
4773
|
+
rail,
|
|
4774
|
+
stableIdentifier: stableId,
|
|
4775
|
+
derivedKey: idempotencyKey
|
|
4776
|
+
}
|
|
4777
|
+
);
|
|
4778
|
+
}
|
|
3845
4779
|
const result = await s.http.request("POST", "/purchases/sync", {
|
|
3846
4780
|
body,
|
|
3847
4781
|
idempotencyKey
|
|
@@ -4305,9 +5239,131 @@ function getErrorCode(code) {
|
|
|
4305
5239
|
}
|
|
4306
5240
|
|
|
4307
5241
|
// src/_contracts-bundled.ts
|
|
4308
|
-
var BUNDLED_IN = "@cross-deck/web@1.5.
|
|
4309
|
-
var SDK_VERSION2 = "1.5.
|
|
5242
|
+
var BUNDLED_IN = "@cross-deck/web@1.5.3";
|
|
5243
|
+
var SDK_VERSION2 = "1.5.3";
|
|
4310
5244
|
var BUNDLED_CONTRACTS = Object.freeze([
|
|
5245
|
+
{
|
|
5246
|
+
"id": "contract-failed-payload-schema-lock",
|
|
5247
|
+
"pillar": "diagnostics",
|
|
5248
|
+
"status": "enforced",
|
|
5249
|
+
"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.",
|
|
5250
|
+
"appliesTo": [
|
|
5251
|
+
"web",
|
|
5252
|
+
"node",
|
|
5253
|
+
"swift",
|
|
5254
|
+
"android",
|
|
5255
|
+
"react-native"
|
|
5256
|
+
],
|
|
5257
|
+
"allowedFields": {
|
|
5258
|
+
"required": [
|
|
5259
|
+
"contract_id",
|
|
5260
|
+
"sdk_version",
|
|
5261
|
+
"sdk_platform",
|
|
5262
|
+
"failure_reason",
|
|
5263
|
+
"run_context",
|
|
5264
|
+
"run_id"
|
|
5265
|
+
],
|
|
5266
|
+
"optional": [
|
|
5267
|
+
"test_file",
|
|
5268
|
+
"test_name",
|
|
5269
|
+
"device_class",
|
|
5270
|
+
"verification_phase"
|
|
5271
|
+
],
|
|
5272
|
+
"forbidden": [
|
|
5273
|
+
"anonymousId",
|
|
5274
|
+
"developerUserId",
|
|
5275
|
+
"crossdeckCustomerId",
|
|
5276
|
+
"email",
|
|
5277
|
+
"ip",
|
|
5278
|
+
"user_agent",
|
|
5279
|
+
"message",
|
|
5280
|
+
"stack",
|
|
5281
|
+
"stack_trace",
|
|
5282
|
+
"frames",
|
|
5283
|
+
"exception_message",
|
|
5284
|
+
"url",
|
|
5285
|
+
"path",
|
|
5286
|
+
"screen",
|
|
5287
|
+
"title",
|
|
5288
|
+
"label",
|
|
5289
|
+
"text",
|
|
5290
|
+
"ariaLabel",
|
|
5291
|
+
"accessibilityLabel",
|
|
5292
|
+
"contentDescription",
|
|
5293
|
+
"session_id",
|
|
5294
|
+
"sessionId"
|
|
5295
|
+
]
|
|
5296
|
+
},
|
|
5297
|
+
"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.",
|
|
5298
|
+
"codeRef": [
|
|
5299
|
+
"sdks/web/src/crossdeck.ts",
|
|
5300
|
+
"sdks/node/src/crossdeck-server.ts",
|
|
5301
|
+
"sdks/swift/Sources/Crossdeck/Crossdeck.swift",
|
|
5302
|
+
"sdks/swift/Sources/Crossdeck/_DiagnosticTelemetry.swift",
|
|
5303
|
+
"sdks/android/crossdeck/src/main/kotlin/com/crossdeck/Crossdeck.kt",
|
|
5304
|
+
"sdks/android/crossdeck/src/main/kotlin/com/crossdeck/_DiagnosticTelemetry.kt",
|
|
5305
|
+
"sdks/react-native/src/crossdeck.ts",
|
|
5306
|
+
"backend/src/api/v1-sdk-diagnostic.ts",
|
|
5307
|
+
"sdks/web/src/_diagnostic-telemetry.ts",
|
|
5308
|
+
"sdks/node/src/_diagnostic-telemetry.ts",
|
|
5309
|
+
"sdks/react-native/src/_diagnostic-telemetry.ts"
|
|
5310
|
+
],
|
|
5311
|
+
"testRef": [
|
|
5312
|
+
{
|
|
5313
|
+
"file": "sdks/web/tests/contract-failed-schema-lock.test.ts",
|
|
5314
|
+
"name": "reportContractFailure payload conforms to schema-lock"
|
|
5315
|
+
},
|
|
5316
|
+
{
|
|
5317
|
+
"file": "sdks/node/tests/contract-failed-schema-lock.test.ts",
|
|
5318
|
+
"name": "reportContractFailure payload conforms to schema-lock"
|
|
5319
|
+
},
|
|
5320
|
+
{
|
|
5321
|
+
"file": "sdks/swift/Tests/CrossdeckTests/ContractFailedSchemaLockTests.swift",
|
|
5322
|
+
"name": "test_reportContractFailure_payloadFieldsAreInAllowList"
|
|
5323
|
+
},
|
|
5324
|
+
{
|
|
5325
|
+
"file": "sdks/swift/Tests/CrossdeckTests/ContractFailedSchemaLockTests.swift",
|
|
5326
|
+
"name": "test_reportContractFailure_doesNotEnterCustomerTrackPipeline"
|
|
5327
|
+
},
|
|
5328
|
+
{
|
|
5329
|
+
"file": "sdks/android/crossdeck/src/test/kotlin/com/crossdeck/ContractFailedSchemaLockTest.kt",
|
|
5330
|
+
"name": "reportContractFailure payload conforms to schema-lock"
|
|
5331
|
+
},
|
|
5332
|
+
{
|
|
5333
|
+
"file": "sdks/android/crossdeck/src/test/kotlin/com/crossdeck/ContractFailedSchemaLockTest.kt",
|
|
5334
|
+
"name": "reportContractFailure does not enter customer track pipeline"
|
|
5335
|
+
},
|
|
5336
|
+
{
|
|
5337
|
+
"file": "sdks/react-native/tests/contract-failed-schema-lock.test.ts",
|
|
5338
|
+
"name": "reportContractFailure payload conforms to schema-lock"
|
|
5339
|
+
},
|
|
5340
|
+
{
|
|
5341
|
+
"file": "backend/tests/unit/v1-sdk-diagnostic.test.ts",
|
|
5342
|
+
"name": "forbidden fields are enumerated in the schema-lock contract"
|
|
5343
|
+
},
|
|
5344
|
+
{
|
|
5345
|
+
"file": "backend/tests/unit/v1-sdk-diagnostic.test.ts",
|
|
5346
|
+
"name": "required fields are enumerated in the schema-lock contract"
|
|
5347
|
+
},
|
|
5348
|
+
{
|
|
5349
|
+
"file": "backend/tests/unit/v1-sdk-diagnostic.test.ts",
|
|
5350
|
+
"name": "regression guard: never returns a raw IP"
|
|
5351
|
+
},
|
|
5352
|
+
{
|
|
5353
|
+
"file": "backend/tests/unit/v1-sdk-diagnostic.test.ts",
|
|
5354
|
+
"name": "verification_phase is in the optional field set"
|
|
5355
|
+
}
|
|
5356
|
+
],
|
|
5357
|
+
"registeredAt": "2026-05-27",
|
|
5358
|
+
"firstRegisteredIn": "Diagnostic telemetry single-fire + schema-lock \u2014 independent-controller flow",
|
|
5359
|
+
"privacyReferences": [
|
|
5360
|
+
"legal/privacy/index.html#sdk-diagnostic",
|
|
5361
|
+
"legal/customer-disclosure/index.html#flow-b",
|
|
5362
|
+
"legal/security/index.html#diagnostic",
|
|
5363
|
+
"legal/sdk-data/index.html#b-diagnostic"
|
|
5364
|
+
],
|
|
5365
|
+
"bundledIn": "@cross-deck/web@1.5.3"
|
|
5366
|
+
},
|
|
4311
5367
|
{
|
|
4312
5368
|
"id": "error-envelope-shape",
|
|
4313
5369
|
"pillar": "errors",
|
|
@@ -4345,7 +5401,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
|
|
|
4345
5401
|
],
|
|
4346
5402
|
"registeredAt": "2026-05-26",
|
|
4347
5403
|
"firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 8 (codifies existing contract)",
|
|
4348
|
-
"bundledIn": "@cross-deck/web@1.5.
|
|
5404
|
+
"bundledIn": "@cross-deck/web@1.5.3"
|
|
4349
5405
|
},
|
|
4350
5406
|
{
|
|
4351
5407
|
"id": "flush-interval-parity",
|
|
@@ -4390,7 +5446,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
|
|
|
4390
5446
|
],
|
|
4391
5447
|
"registeredAt": "2026-05-26",
|
|
4392
5448
|
"firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 3.3",
|
|
4393
|
-
"bundledIn": "@cross-deck/web@1.5.
|
|
5449
|
+
"bundledIn": "@cross-deck/web@1.5.3"
|
|
4394
5450
|
},
|
|
4395
5451
|
{
|
|
4396
5452
|
"id": "idempotency-key-deterministic",
|
|
@@ -4495,7 +5551,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
|
|
|
4495
5551
|
],
|
|
4496
5552
|
"registeredAt": "2026-05-26",
|
|
4497
5553
|
"firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 2.2.a + 2.2.b + 2.2.c",
|
|
4498
|
-
"bundledIn": "@cross-deck/web@1.5.
|
|
5554
|
+
"bundledIn": "@cross-deck/web@1.5.3"
|
|
4499
5555
|
},
|
|
4500
5556
|
{
|
|
4501
5557
|
"id": "init-reentry-drains-prior-queue",
|
|
@@ -4522,7 +5578,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
|
|
|
4522
5578
|
],
|
|
4523
5579
|
"registeredAt": "2026-05-26",
|
|
4524
5580
|
"firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 5.5",
|
|
4525
|
-
"bundledIn": "@cross-deck/web@1.5.
|
|
5581
|
+
"bundledIn": "@cross-deck/web@1.5.3"
|
|
4526
5582
|
},
|
|
4527
5583
|
{
|
|
4528
5584
|
"id": "per-user-cache-isolation",
|
|
@@ -4601,7 +5657,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
|
|
|
4601
5657
|
],
|
|
4602
5658
|
"registeredAt": "2026-05-26",
|
|
4603
5659
|
"firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 1.3 (web/RN) + dogfood-gap fix (swift + android)",
|
|
4604
|
-
"bundledIn": "@cross-deck/web@1.5.
|
|
5660
|
+
"bundledIn": "@cross-deck/web@1.5.3"
|
|
4605
5661
|
},
|
|
4606
5662
|
{
|
|
4607
5663
|
"id": "sdk-error-codes-catalogue",
|
|
@@ -4641,7 +5697,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
|
|
|
4641
5697
|
],
|
|
4642
5698
|
"registeredAt": "2026-05-26",
|
|
4643
5699
|
"firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 6.2",
|
|
4644
|
-
"bundledIn": "@cross-deck/web@1.5.
|
|
5700
|
+
"bundledIn": "@cross-deck/web@1.5.3"
|
|
4645
5701
|
},
|
|
4646
5702
|
{
|
|
4647
5703
|
"id": "sync-purchases-funnel-parity",
|
|
@@ -4674,7 +5730,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
|
|
|
4674
5730
|
],
|
|
4675
5731
|
"registeredAt": "2026-05-26",
|
|
4676
5732
|
"firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 3.5",
|
|
4677
|
-
"bundledIn": "@cross-deck/web@1.5.
|
|
5733
|
+
"bundledIn": "@cross-deck/web@1.5.3"
|
|
4678
5734
|
}
|
|
4679
5735
|
]);
|
|
4680
5736
|
|