@cross-deck/web 1.5.2 → 1.5.4
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 +500 -0
- package/dist/crossdeck.umd.min.js +2 -2
- package/dist/crossdeck.umd.min.js.map +1 -1
- package/dist/error-codes.json +106 -1
- package/dist/index.cjs +1998 -24
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +212 -4
- package/dist/index.d.ts +212 -4
- package/dist/index.mjs +1997 -24
- package/dist/index.mjs.map +1 -1
- package/dist/react.cjs +1340 -24
- 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 +1340 -24
- package/dist/react.mjs.map +1 -1
- package/dist/{types-BzoKor4z.d.mts → types-B9sxUuKh.d.mts} +75 -0
- package/dist/{types-BzoKor4z.d.ts → types-B9sxUuKh.d.ts} +75 -0
- package/dist/vue.cjs +1340 -24
- package/dist/vue.cjs.map +1 -1
- package/dist/vue.mjs +1340 -24
- package/dist/vue.mjs.map +1 -1
- package/package.json +1 -1
package/dist/react.cjs
CHANGED
|
@@ -93,7 +93,7 @@ function typeMapForStatus(status) {
|
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
// src/_version.ts
|
|
96
|
-
var SDK_VERSION = "1.3
|
|
96
|
+
var SDK_VERSION = "1.5.3";
|
|
97
97
|
var SDK_NAME = "@cross-deck/web";
|
|
98
98
|
|
|
99
99
|
// src/http.ts
|
|
@@ -156,7 +156,14 @@ var HttpClient = class {
|
|
|
156
156
|
if (timeoutHandle !== null) clearTimeout(timeoutHandle);
|
|
157
157
|
}
|
|
158
158
|
if (!response.ok) {
|
|
159
|
-
|
|
159
|
+
const parsed = await crossdeckErrorFromResponse(response);
|
|
160
|
+
if (this.config.onErrorParsed) {
|
|
161
|
+
try {
|
|
162
|
+
this.config.onErrorParsed(parsed);
|
|
163
|
+
} catch {
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
throw parsed;
|
|
160
167
|
}
|
|
161
168
|
if (response.status === 204) return void 0;
|
|
162
169
|
try {
|
|
@@ -343,29 +350,258 @@ function randomChars(count) {
|
|
|
343
350
|
return out.join("");
|
|
344
351
|
}
|
|
345
352
|
|
|
353
|
+
// src/hash.ts
|
|
354
|
+
var K = new Uint32Array([
|
|
355
|
+
1116352408,
|
|
356
|
+
1899447441,
|
|
357
|
+
3049323471,
|
|
358
|
+
3921009573,
|
|
359
|
+
961987163,
|
|
360
|
+
1508970993,
|
|
361
|
+
2453635748,
|
|
362
|
+
2870763221,
|
|
363
|
+
3624381080,
|
|
364
|
+
310598401,
|
|
365
|
+
607225278,
|
|
366
|
+
1426881987,
|
|
367
|
+
1925078388,
|
|
368
|
+
2162078206,
|
|
369
|
+
2614888103,
|
|
370
|
+
3248222580,
|
|
371
|
+
3835390401,
|
|
372
|
+
4022224774,
|
|
373
|
+
264347078,
|
|
374
|
+
604807628,
|
|
375
|
+
770255983,
|
|
376
|
+
1249150122,
|
|
377
|
+
1555081692,
|
|
378
|
+
1996064986,
|
|
379
|
+
2554220882,
|
|
380
|
+
2821834349,
|
|
381
|
+
2952996808,
|
|
382
|
+
3210313671,
|
|
383
|
+
3336571891,
|
|
384
|
+
3584528711,
|
|
385
|
+
113926993,
|
|
386
|
+
338241895,
|
|
387
|
+
666307205,
|
|
388
|
+
773529912,
|
|
389
|
+
1294757372,
|
|
390
|
+
1396182291,
|
|
391
|
+
1695183700,
|
|
392
|
+
1986661051,
|
|
393
|
+
2177026350,
|
|
394
|
+
2456956037,
|
|
395
|
+
2730485921,
|
|
396
|
+
2820302411,
|
|
397
|
+
3259730800,
|
|
398
|
+
3345764771,
|
|
399
|
+
3516065817,
|
|
400
|
+
3600352804,
|
|
401
|
+
4094571909,
|
|
402
|
+
275423344,
|
|
403
|
+
430227734,
|
|
404
|
+
506948616,
|
|
405
|
+
659060556,
|
|
406
|
+
883997877,
|
|
407
|
+
958139571,
|
|
408
|
+
1322822218,
|
|
409
|
+
1537002063,
|
|
410
|
+
1747873779,
|
|
411
|
+
1955562222,
|
|
412
|
+
2024104815,
|
|
413
|
+
2227730452,
|
|
414
|
+
2361852424,
|
|
415
|
+
2428436474,
|
|
416
|
+
2756734187,
|
|
417
|
+
3204031479,
|
|
418
|
+
3329325298
|
|
419
|
+
]);
|
|
420
|
+
function utf8Bytes(input) {
|
|
421
|
+
if (typeof TextEncoder !== "undefined") {
|
|
422
|
+
return new TextEncoder().encode(input);
|
|
423
|
+
}
|
|
424
|
+
const out = [];
|
|
425
|
+
for (let i = 0; i < input.length; i++) {
|
|
426
|
+
let codePoint = input.charCodeAt(i);
|
|
427
|
+
if (codePoint >= 55296 && codePoint <= 56319 && i + 1 < input.length) {
|
|
428
|
+
const next = input.charCodeAt(i + 1);
|
|
429
|
+
if (next >= 56320 && next <= 57343) {
|
|
430
|
+
codePoint = 65536 + (codePoint - 55296 << 10) + (next - 56320);
|
|
431
|
+
i++;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
if (codePoint < 128) {
|
|
435
|
+
out.push(codePoint);
|
|
436
|
+
} else if (codePoint < 2048) {
|
|
437
|
+
out.push(192 | codePoint >> 6);
|
|
438
|
+
out.push(128 | codePoint & 63);
|
|
439
|
+
} else if (codePoint < 65536) {
|
|
440
|
+
out.push(224 | codePoint >> 12);
|
|
441
|
+
out.push(128 | codePoint >> 6 & 63);
|
|
442
|
+
out.push(128 | codePoint & 63);
|
|
443
|
+
} else {
|
|
444
|
+
out.push(240 | codePoint >> 18);
|
|
445
|
+
out.push(128 | codePoint >> 12 & 63);
|
|
446
|
+
out.push(128 | codePoint >> 6 & 63);
|
|
447
|
+
out.push(128 | codePoint & 63);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return new Uint8Array(out);
|
|
451
|
+
}
|
|
452
|
+
function sha256Hex(input) {
|
|
453
|
+
const bytes = utf8Bytes(input);
|
|
454
|
+
const bitLength = bytes.length * 8;
|
|
455
|
+
const blockCount = Math.floor((bytes.length + 9 + 63) / 64);
|
|
456
|
+
const padded = new Uint8Array(blockCount * 64);
|
|
457
|
+
padded.set(bytes);
|
|
458
|
+
padded[bytes.length] = 128;
|
|
459
|
+
const high = Math.floor(bitLength / 4294967296);
|
|
460
|
+
const low = bitLength >>> 0;
|
|
461
|
+
const lenOffset = padded.length - 8;
|
|
462
|
+
padded[lenOffset + 0] = high >>> 24 & 255;
|
|
463
|
+
padded[lenOffset + 1] = high >>> 16 & 255;
|
|
464
|
+
padded[lenOffset + 2] = high >>> 8 & 255;
|
|
465
|
+
padded[lenOffset + 3] = high & 255;
|
|
466
|
+
padded[lenOffset + 4] = low >>> 24 & 255;
|
|
467
|
+
padded[lenOffset + 5] = low >>> 16 & 255;
|
|
468
|
+
padded[lenOffset + 6] = low >>> 8 & 255;
|
|
469
|
+
padded[lenOffset + 7] = low & 255;
|
|
470
|
+
const H = new Uint32Array([
|
|
471
|
+
1779033703,
|
|
472
|
+
3144134277,
|
|
473
|
+
1013904242,
|
|
474
|
+
2773480762,
|
|
475
|
+
1359893119,
|
|
476
|
+
2600822924,
|
|
477
|
+
528734635,
|
|
478
|
+
1541459225
|
|
479
|
+
]);
|
|
480
|
+
const W = new Uint32Array(64);
|
|
481
|
+
for (let block = 0; block < blockCount; block++) {
|
|
482
|
+
const offset = block * 64;
|
|
483
|
+
for (let t = 0; t < 16; t++) {
|
|
484
|
+
W[t] = (padded[offset + t * 4] << 24 | padded[offset + t * 4 + 1] << 16 | padded[offset + t * 4 + 2] << 8 | padded[offset + t * 4 + 3]) >>> 0;
|
|
485
|
+
}
|
|
486
|
+
for (let t = 16; t < 64; t++) {
|
|
487
|
+
const w15 = W[t - 15];
|
|
488
|
+
const w2 = W[t - 2];
|
|
489
|
+
const s0 = (w15 >>> 7 | w15 << 25) ^ (w15 >>> 18 | w15 << 14) ^ w15 >>> 3;
|
|
490
|
+
const s1 = (w2 >>> 17 | w2 << 15) ^ (w2 >>> 19 | w2 << 13) ^ w2 >>> 10;
|
|
491
|
+
W[t] = W[t - 16] + s0 + W[t - 7] + s1 >>> 0;
|
|
492
|
+
}
|
|
493
|
+
let a = H[0], b = H[1], c = H[2], d = H[3];
|
|
494
|
+
let e = H[4], f = H[5], g = H[6], h = H[7];
|
|
495
|
+
for (let t = 0; t < 64; t++) {
|
|
496
|
+
const S1 = (e >>> 6 | e << 26) ^ (e >>> 11 | e << 21) ^ (e >>> 25 | e << 7);
|
|
497
|
+
const ch = e & f ^ ~e & g;
|
|
498
|
+
const temp1 = h + S1 + ch + K[t] + W[t] >>> 0;
|
|
499
|
+
const S0 = (a >>> 2 | a << 30) ^ (a >>> 13 | a << 19) ^ (a >>> 22 | a << 10);
|
|
500
|
+
const maj = a & b ^ a & c ^ b & c;
|
|
501
|
+
const temp2 = S0 + maj >>> 0;
|
|
502
|
+
h = g;
|
|
503
|
+
g = f;
|
|
504
|
+
f = e;
|
|
505
|
+
e = d + temp1 >>> 0;
|
|
506
|
+
d = c;
|
|
507
|
+
c = b;
|
|
508
|
+
b = a;
|
|
509
|
+
a = temp1 + temp2 >>> 0;
|
|
510
|
+
}
|
|
511
|
+
H[0] = H[0] + a >>> 0;
|
|
512
|
+
H[1] = H[1] + b >>> 0;
|
|
513
|
+
H[2] = H[2] + c >>> 0;
|
|
514
|
+
H[3] = H[3] + d >>> 0;
|
|
515
|
+
H[4] = H[4] + e >>> 0;
|
|
516
|
+
H[5] = H[5] + f >>> 0;
|
|
517
|
+
H[6] = H[6] + g >>> 0;
|
|
518
|
+
H[7] = H[7] + h >>> 0;
|
|
519
|
+
}
|
|
520
|
+
let hex = "";
|
|
521
|
+
for (let i = 0; i < 8; i++) {
|
|
522
|
+
hex += H[i].toString(16).padStart(8, "0");
|
|
523
|
+
}
|
|
524
|
+
return hex;
|
|
525
|
+
}
|
|
526
|
+
|
|
346
527
|
// src/entitlement-cache.ts
|
|
347
528
|
var DEFAULT_STALE_AFTER_MS = 24 * 60 * 60 * 1e3;
|
|
348
|
-
var
|
|
529
|
+
var ANON_SUFFIX = "_anon";
|
|
530
|
+
var INDEX_SUFFIX = "_index";
|
|
531
|
+
var EntitlementCache = class _EntitlementCache {
|
|
349
532
|
/**
|
|
350
|
-
* @param storage
|
|
351
|
-
*
|
|
352
|
-
*
|
|
353
|
-
*
|
|
354
|
-
* @param
|
|
355
|
-
*
|
|
356
|
-
*
|
|
533
|
+
* @param storage Device storage adapter. When omitted (tests) or
|
|
534
|
+
* a MemoryStorage (strict-consent / no-persistence
|
|
535
|
+
* mode) the cache is session-only — durability is
|
|
536
|
+
* simply absent, never wrong.
|
|
537
|
+
* @param storageKeyPrefix Prefix used to derive per-user storage keys
|
|
538
|
+
* (`<prefix>:<sha256(userId)>`). Default
|
|
539
|
+
* `crossdeck:entitlements`. The trailing user
|
|
540
|
+
* suffix is filled at identify() / reset()
|
|
541
|
+
* time — see [[setUserKey]].
|
|
542
|
+
* @param staleAfterMs Age past which last-known-good is flagged stale
|
|
543
|
+
* even without a failed refresh. Default 24h.
|
|
357
544
|
*/
|
|
358
|
-
constructor(storage,
|
|
545
|
+
constructor(storage, storageKeyPrefix = "crossdeck:entitlements", staleAfterMs = DEFAULT_STALE_AFTER_MS) {
|
|
359
546
|
this.all = [];
|
|
360
547
|
this.lastUpdated = 0;
|
|
361
548
|
this.lastRefreshFailedAt = 0;
|
|
362
549
|
this.listeners = /* @__PURE__ */ new Set();
|
|
363
550
|
this.listenerErrorCount = 0;
|
|
551
|
+
this.currentSuffix = ANON_SUFFIX;
|
|
364
552
|
this.storage = storage;
|
|
365
|
-
this.
|
|
553
|
+
this.storageKeyPrefix = storageKeyPrefix;
|
|
366
554
|
this.staleAfterMs = staleAfterMs;
|
|
367
555
|
this.hydrate();
|
|
368
556
|
}
|
|
557
|
+
/** The full storage key the current-user blob is persisted under. */
|
|
558
|
+
get storageKey() {
|
|
559
|
+
return `${this.storageKeyPrefix}:${this.currentSuffix}`;
|
|
560
|
+
}
|
|
561
|
+
/** Key of the index blob — a JSON array of every suffix we've
|
|
562
|
+
* written. Used by clearAll() to scope a logout-wipe. */
|
|
563
|
+
get indexKey() {
|
|
564
|
+
return `${this.storageKeyPrefix}:${INDEX_SUFFIX}`;
|
|
565
|
+
}
|
|
566
|
+
/** Derive a stable suffix for a developerUserId via SHA-256. The
|
|
567
|
+
* raw userId never lands in the storage key — protects against
|
|
568
|
+
* accidentally leaking email-style identifiers through DevTools
|
|
569
|
+
* inspection. Pass `null` to switch back to the anonymous slot. */
|
|
570
|
+
static suffixForUserId(userId) {
|
|
571
|
+
if (userId == null || userId === "") return ANON_SUFFIX;
|
|
572
|
+
return sha256Hex(userId);
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* Switch the cache to a different user's storage slot. Bank-grade
|
|
576
|
+
* three-layer isolation:
|
|
577
|
+
* (a) Physical key separation — `<prefix>:<sha256(userId)>` so
|
|
578
|
+
* a user-switch can't physically read prior user's data
|
|
579
|
+
* even if the in-memory clear was skipped.
|
|
580
|
+
* (b) Unconditional in-memory clear — invoked whenever the
|
|
581
|
+
* active suffix changes, even on same-id re-identify.
|
|
582
|
+
* (c) Re-hydrate from the new slot — a returning user observes
|
|
583
|
+
* their last-known-good cache from storage immediately.
|
|
584
|
+
*
|
|
585
|
+
* Caller (identify() / reset()) MUST invoke this BEFORE the next
|
|
586
|
+
* setFromList() so the write lands under the right key.
|
|
587
|
+
*/
|
|
588
|
+
setUserKey(userId) {
|
|
589
|
+
const nextSuffix = _EntitlementCache.suffixForUserId(userId);
|
|
590
|
+
if (nextSuffix === this.currentSuffix) {
|
|
591
|
+
this.all = [];
|
|
592
|
+
this.lastUpdated = 0;
|
|
593
|
+
this.lastRefreshFailedAt = 0;
|
|
594
|
+
this.notify();
|
|
595
|
+
this.hydrate();
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
this.currentSuffix = nextSuffix;
|
|
599
|
+
this.all = [];
|
|
600
|
+
this.lastUpdated = 0;
|
|
601
|
+
this.lastRefreshFailedAt = 0;
|
|
602
|
+
this.hydrate();
|
|
603
|
+
this.notify();
|
|
604
|
+
}
|
|
369
605
|
/**
|
|
370
606
|
* Sync read — true iff the entitlement is currently granting access.
|
|
371
607
|
*
|
|
@@ -435,12 +671,13 @@ var EntitlementCache = class {
|
|
|
435
671
|
this.lastUpdated = Date.now();
|
|
436
672
|
this.lastRefreshFailedAt = 0;
|
|
437
673
|
this.persist();
|
|
674
|
+
this.recordSuffixInIndex(this.currentSuffix);
|
|
438
675
|
this.notify();
|
|
439
676
|
}
|
|
440
677
|
/**
|
|
441
|
-
* Wipe
|
|
442
|
-
*
|
|
443
|
-
*
|
|
678
|
+
* Wipe the CURRENT user's slot. Used internally when a single
|
|
679
|
+
* user's cache needs to be invalidated without affecting other
|
|
680
|
+
* persisted slots. The full-logout path is [[clearAll]].
|
|
444
681
|
*/
|
|
445
682
|
clear() {
|
|
446
683
|
this.all = [];
|
|
@@ -452,6 +689,40 @@ var EntitlementCache = class {
|
|
|
452
689
|
} catch {
|
|
453
690
|
}
|
|
454
691
|
}
|
|
692
|
+
this.removeSuffixFromIndex(this.currentSuffix);
|
|
693
|
+
this.notify();
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* Logout-grade wipe — bank-grade contract: removes EVERY per-user
|
|
697
|
+
* entitlement slot the SDK has ever written on this device, then
|
|
698
|
+
* clears the index. Used by `Crossdeck.reset()` so a logout on a
|
|
699
|
+
* shared device can never leave another user's entitlements
|
|
700
|
+
* readable (layer (c) of the v1.4.0 isolation fix).
|
|
701
|
+
*
|
|
702
|
+
* After clearAll(), the cache is back to anonymous + empty.
|
|
703
|
+
*/
|
|
704
|
+
clearAll() {
|
|
705
|
+
this.all = [];
|
|
706
|
+
this.lastUpdated = 0;
|
|
707
|
+
this.lastRefreshFailedAt = 0;
|
|
708
|
+
this.currentSuffix = ANON_SUFFIX;
|
|
709
|
+
if (this.storage) {
|
|
710
|
+
const suffixes = this.readIndex();
|
|
711
|
+
for (const suffix of suffixes) {
|
|
712
|
+
try {
|
|
713
|
+
this.storage.removeItem(`${this.storageKeyPrefix}:${suffix}`);
|
|
714
|
+
} catch {
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
try {
|
|
718
|
+
this.storage.removeItem(`${this.storageKeyPrefix}:${ANON_SUFFIX}`);
|
|
719
|
+
} catch {
|
|
720
|
+
}
|
|
721
|
+
try {
|
|
722
|
+
this.storage.removeItem(this.indexKey);
|
|
723
|
+
} catch {
|
|
724
|
+
}
|
|
725
|
+
}
|
|
455
726
|
this.notify();
|
|
456
727
|
}
|
|
457
728
|
/**
|
|
@@ -501,6 +772,47 @@ var EntitlementCache = class {
|
|
|
501
772
|
} catch {
|
|
502
773
|
}
|
|
503
774
|
}
|
|
775
|
+
/** Read the index of all per-user suffixes the SDK has written. */
|
|
776
|
+
readIndex() {
|
|
777
|
+
if (!this.storage) return [];
|
|
778
|
+
try {
|
|
779
|
+
const raw = this.storage.getItem(this.indexKey);
|
|
780
|
+
if (!raw) return [];
|
|
781
|
+
const parsed = JSON.parse(raw);
|
|
782
|
+
if (Array.isArray(parsed)) {
|
|
783
|
+
return parsed.filter((x) => typeof x === "string");
|
|
784
|
+
}
|
|
785
|
+
return [];
|
|
786
|
+
} catch {
|
|
787
|
+
return [];
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
/** Add a suffix to the persisted index. Idempotent. */
|
|
791
|
+
recordSuffixInIndex(suffix) {
|
|
792
|
+
if (!this.storage) return;
|
|
793
|
+
const existing = this.readIndex();
|
|
794
|
+
if (existing.includes(suffix)) return;
|
|
795
|
+
existing.push(suffix);
|
|
796
|
+
try {
|
|
797
|
+
this.storage.setItem(this.indexKey, JSON.stringify(existing));
|
|
798
|
+
} catch {
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
/** Remove a suffix from the persisted index. No-op if absent. */
|
|
802
|
+
removeSuffixFromIndex(suffix) {
|
|
803
|
+
if (!this.storage) return;
|
|
804
|
+
const existing = this.readIndex();
|
|
805
|
+
const next = existing.filter((s) => s !== suffix);
|
|
806
|
+
if (next.length === existing.length) return;
|
|
807
|
+
try {
|
|
808
|
+
if (next.length === 0) {
|
|
809
|
+
this.storage.removeItem(this.indexKey);
|
|
810
|
+
} else {
|
|
811
|
+
this.storage.setItem(this.indexKey, JSON.stringify(next));
|
|
812
|
+
}
|
|
813
|
+
} catch {
|
|
814
|
+
}
|
|
815
|
+
}
|
|
504
816
|
notify() {
|
|
505
817
|
if (this.listeners.size === 0) return;
|
|
506
818
|
const snapshot = this.all.slice();
|
|
@@ -515,6 +827,34 @@ var EntitlementCache = class {
|
|
|
515
827
|
}
|
|
516
828
|
};
|
|
517
829
|
|
|
830
|
+
// src/idempotency-key.ts
|
|
831
|
+
function formatAsUuid(hex) {
|
|
832
|
+
return [
|
|
833
|
+
hex.slice(0, 8),
|
|
834
|
+
hex.slice(8, 12),
|
|
835
|
+
hex.slice(12, 16),
|
|
836
|
+
hex.slice(16, 20),
|
|
837
|
+
hex.slice(20, 32)
|
|
838
|
+
].join("-");
|
|
839
|
+
}
|
|
840
|
+
function deriveIdempotencyKeyForPurchase(body) {
|
|
841
|
+
let identifier;
|
|
842
|
+
if (body.rail === "apple") {
|
|
843
|
+
identifier = body.signedTransactionInfo ?? "";
|
|
844
|
+
} else if (body.rail === "google") {
|
|
845
|
+
identifier = body.purchaseToken ?? "";
|
|
846
|
+
} else {
|
|
847
|
+
identifier = "";
|
|
848
|
+
}
|
|
849
|
+
if (!identifier) {
|
|
850
|
+
throw new Error(
|
|
851
|
+
`deriveIdempotencyKeyForPurchase: no stable identifier in body (rail=${body.rail}). Apple needs signedTransactionInfo; Google needs purchaseToken.`
|
|
852
|
+
);
|
|
853
|
+
}
|
|
854
|
+
const namespaced = `crossdeck:purchases/sync:${body.rail}:${identifier}`;
|
|
855
|
+
return formatAsUuid(sha256Hex(namespaced));
|
|
856
|
+
}
|
|
857
|
+
|
|
518
858
|
// src/retry-policy.ts
|
|
519
859
|
var DEFAULT_BASE = 1e3;
|
|
520
860
|
var DEFAULT_MAX = 6e4;
|
|
@@ -2059,6 +2399,789 @@ var BreadcrumbBuffer = class {
|
|
|
2059
2399
|
}
|
|
2060
2400
|
};
|
|
2061
2401
|
|
|
2402
|
+
// src/_diagnostic-telemetry.ts
|
|
2403
|
+
var DIAGNOSTIC_TELEMETRY_ENDPOINT = "https://api.cross-deck.com/v1/sdk/diagnostic";
|
|
2404
|
+
var DIAGNOSTIC_TELEMETRY_PUBLISHABLE_KEY = "cd_pub_live_9490e7aa029c432abf";
|
|
2405
|
+
function isDiagnosticTelemetryEnabled() {
|
|
2406
|
+
return !DIAGNOSTIC_TELEMETRY_PUBLISHABLE_KEY.startsWith(
|
|
2407
|
+
"cd_pub_RELIABILITY_PLACEHOLDER"
|
|
2408
|
+
);
|
|
2409
|
+
}
|
|
2410
|
+
var DIAGNOSTIC_TELEMETRY_ALLOWED_KEYS = /* @__PURE__ */ new Set([
|
|
2411
|
+
"contract_id",
|
|
2412
|
+
"sdk_version",
|
|
2413
|
+
"sdk_platform",
|
|
2414
|
+
"failure_reason",
|
|
2415
|
+
"run_context",
|
|
2416
|
+
"run_id",
|
|
2417
|
+
"test_file",
|
|
2418
|
+
"test_name",
|
|
2419
|
+
"device_class",
|
|
2420
|
+
// verification_phase is set by the runtime contract verifier layer
|
|
2421
|
+
// (sdks/web/src/_contract-verifiers.ts) — values `boot` / `hot_path`.
|
|
2422
|
+
// Absent for failures emitted by external test harnesses
|
|
2423
|
+
// (XCTestObservation, Vitest hooks, JUnit watchers) which carry
|
|
2424
|
+
// test_file + test_name instead. See contracts/diagnostics/
|
|
2425
|
+
// contract-failed-payload-schema-lock.json.
|
|
2426
|
+
"verification_phase"
|
|
2427
|
+
]);
|
|
2428
|
+
function filterDiagnosticPayload(payload) {
|
|
2429
|
+
const filtered = {};
|
|
2430
|
+
for (const [k, v] of Object.entries(payload)) {
|
|
2431
|
+
if (DIAGNOSTIC_TELEMETRY_ALLOWED_KEYS.has(k) && typeof v === "string") {
|
|
2432
|
+
filtered[k] = v;
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
2435
|
+
return filtered;
|
|
2436
|
+
}
|
|
2437
|
+
function sendDiagnosticTelemetry(payload) {
|
|
2438
|
+
if (!isDiagnosticTelemetryEnabled()) return;
|
|
2439
|
+
const filtered = filterDiagnosticPayload(payload);
|
|
2440
|
+
if (Object.keys(filtered).length === 0) return;
|
|
2441
|
+
const body = JSON.stringify(filtered);
|
|
2442
|
+
const f = globalThis.fetch;
|
|
2443
|
+
if (typeof f !== "function") return;
|
|
2444
|
+
try {
|
|
2445
|
+
void f(DIAGNOSTIC_TELEMETRY_ENDPOINT, {
|
|
2446
|
+
method: "POST",
|
|
2447
|
+
headers: {
|
|
2448
|
+
"Content-Type": "application/json",
|
|
2449
|
+
Authorization: `Bearer ${DIAGNOSTIC_TELEMETRY_PUBLISHABLE_KEY}`,
|
|
2450
|
+
"Crossdeck-Sdk-Version": `${SDK_NAME}@${SDK_VERSION}`
|
|
2451
|
+
},
|
|
2452
|
+
body,
|
|
2453
|
+
keepalive: true,
|
|
2454
|
+
// No credentials, no cache, no referrer — the reliability
|
|
2455
|
+
// endpoint is the same origin only in tests. In production the
|
|
2456
|
+
// browser never carries anything beyond the request body and
|
|
2457
|
+
// the Authorization header we set explicitly.
|
|
2458
|
+
credentials: "omit",
|
|
2459
|
+
cache: "no-store",
|
|
2460
|
+
referrerPolicy: "no-referrer"
|
|
2461
|
+
}).catch(() => {
|
|
2462
|
+
});
|
|
2463
|
+
} catch {
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2467
|
+
// src/_contract-verifiers.ts
|
|
2468
|
+
function buildVerifierContext(opts) {
|
|
2469
|
+
return {
|
|
2470
|
+
sdkVersion: `${SDK_NAME}@${SDK_VERSION}`,
|
|
2471
|
+
runId: `cd_verify_${randomHex(8)}`,
|
|
2472
|
+
runContext: opts.runContext ?? "customer-app",
|
|
2473
|
+
logVerifierResults: opts.logVerifierResults,
|
|
2474
|
+
disableContractAssertions: opts.disableContractAssertions,
|
|
2475
|
+
console,
|
|
2476
|
+
emitTelemetry: sendDiagnosticTelemetry
|
|
2477
|
+
};
|
|
2478
|
+
}
|
|
2479
|
+
var VerifierReporter = class {
|
|
2480
|
+
constructor(ctx) {
|
|
2481
|
+
this.ctx = ctx;
|
|
2482
|
+
this.reentrancyDepth = 0;
|
|
2483
|
+
}
|
|
2484
|
+
/**
|
|
2485
|
+
* Report a verifier result. Pass `operation` for hot-path results
|
|
2486
|
+
* to render the prefix as `[crossdeck.identify]` etc.; omit for
|
|
2487
|
+
* boot results which render as `[crossdeck]`.
|
|
2488
|
+
*/
|
|
2489
|
+
report(result, phase, operation) {
|
|
2490
|
+
if (this.ctx.disableContractAssertions) return;
|
|
2491
|
+
if (result.ok) {
|
|
2492
|
+
this.reportPass(result, phase, operation);
|
|
2493
|
+
} else {
|
|
2494
|
+
this.reportFail(result, phase, operation);
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
reportPass(result, phase, operation) {
|
|
2498
|
+
if (!this.ctx.logVerifierResults) return;
|
|
2499
|
+
const line = formatPassLine(result, operation);
|
|
2500
|
+
if (phase === "boot") {
|
|
2501
|
+
this.ctx.console.info(line);
|
|
2502
|
+
} else {
|
|
2503
|
+
this.ctx.console.debug(line);
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2506
|
+
reportFail(result, phase, operation) {
|
|
2507
|
+
this.ctx.console.warn(formatFailLine(result, operation));
|
|
2508
|
+
if (this.reentrancyDepth > 0) return;
|
|
2509
|
+
this.reentrancyDepth += 1;
|
|
2510
|
+
try {
|
|
2511
|
+
this.ctx.emitTelemetry({
|
|
2512
|
+
contract_id: result.contractId,
|
|
2513
|
+
sdk_version: this.ctx.sdkVersion,
|
|
2514
|
+
sdk_platform: "web",
|
|
2515
|
+
failure_reason: truncate(result.failureReason, 128),
|
|
2516
|
+
run_context: this.ctx.runContext,
|
|
2517
|
+
run_id: this.ctx.runId,
|
|
2518
|
+
verification_phase: phase
|
|
2519
|
+
});
|
|
2520
|
+
} finally {
|
|
2521
|
+
this.reentrancyDepth -= 1;
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
};
|
|
2525
|
+
function formatPassLine(r, operation) {
|
|
2526
|
+
const prefix = operation ? `[crossdeck.${operation}]` : "[crossdeck]";
|
|
2527
|
+
return `${prefix} \u2713 ${r.contractId} \u2014 ${r.evidence} (${r.durationMs}ms)`;
|
|
2528
|
+
}
|
|
2529
|
+
function formatFailLine(r, operation) {
|
|
2530
|
+
const prefix = operation ? `[crossdeck.${operation}]` : "[crossdeck]";
|
|
2531
|
+
return `${prefix} \u2717 ${r.contractId} \u2014 ${r.failureReason} (${r.durationMs}ms)`;
|
|
2532
|
+
}
|
|
2533
|
+
var VERIFIER_PER_USER_CACHE_ISOLATION = {
|
|
2534
|
+
contractId: "per-user-cache-isolation",
|
|
2535
|
+
bootTest() {
|
|
2536
|
+
const t0 = nowMs();
|
|
2537
|
+
try {
|
|
2538
|
+
const storage = new MemoryStorage2();
|
|
2539
|
+
const cache = new EntitlementCache(storage, "_verifier");
|
|
2540
|
+
cache.setUserKey("user_A");
|
|
2541
|
+
cache.setFromList([
|
|
2542
|
+
{
|
|
2543
|
+
object: "entitlement",
|
|
2544
|
+
key: "pro",
|
|
2545
|
+
isActive: true,
|
|
2546
|
+
validUntil: null,
|
|
2547
|
+
source: {
|
|
2548
|
+
rail: "stripe",
|
|
2549
|
+
productId: "p_verifier",
|
|
2550
|
+
subscriptionId: "s_verifier"
|
|
2551
|
+
},
|
|
2552
|
+
updatedAt: Date.now()
|
|
2553
|
+
}
|
|
2554
|
+
]);
|
|
2555
|
+
cache.setUserKey("user_B");
|
|
2556
|
+
const inMemoryB = cache.list();
|
|
2557
|
+
if (inMemoryB.length !== 0) {
|
|
2558
|
+
return fail(
|
|
2559
|
+
"per-user-cache-isolation",
|
|
2560
|
+
"in-memory snapshot still carried user_A's entitlements after rotation",
|
|
2561
|
+
nowMs() - t0
|
|
2562
|
+
);
|
|
2563
|
+
}
|
|
2564
|
+
const suffixA = EntitlementCache.suffixForUserId("user_A");
|
|
2565
|
+
const suffixB = EntitlementCache.suffixForUserId("user_B");
|
|
2566
|
+
if (suffixA === suffixB) {
|
|
2567
|
+
return fail(
|
|
2568
|
+
"per-user-cache-isolation",
|
|
2569
|
+
"suffixes for user_A and user_B collided",
|
|
2570
|
+
nowMs() - t0
|
|
2571
|
+
);
|
|
2572
|
+
}
|
|
2573
|
+
const storageA = storage.getItem(`_verifier:${suffixA}`);
|
|
2574
|
+
const storageB = storage.getItem(`_verifier:${suffixB}`);
|
|
2575
|
+
if (!storageA) {
|
|
2576
|
+
return fail(
|
|
2577
|
+
"per-user-cache-isolation",
|
|
2578
|
+
"user_A's storage slot was wiped on rotation (expected isolation, not erasure)",
|
|
2579
|
+
nowMs() - t0
|
|
2580
|
+
);
|
|
2581
|
+
}
|
|
2582
|
+
if (storageB) {
|
|
2583
|
+
return fail(
|
|
2584
|
+
"per-user-cache-isolation",
|
|
2585
|
+
"user_B's storage slot already contained data before any write",
|
|
2586
|
+
nowMs() - t0
|
|
2587
|
+
);
|
|
2588
|
+
}
|
|
2589
|
+
return pass(
|
|
2590
|
+
"per-user-cache-isolation",
|
|
2591
|
+
`slot rotated A:${shortSuffix(suffixA)} \u2192 B:${shortSuffix(suffixB)} (isolated, physically separate)`,
|
|
2592
|
+
nowMs() - t0
|
|
2593
|
+
);
|
|
2594
|
+
} catch (err) {
|
|
2595
|
+
return fail(
|
|
2596
|
+
"per-user-cache-isolation",
|
|
2597
|
+
`boot test threw: ${err.message?.slice(0, 80) ?? "unknown error"}`,
|
|
2598
|
+
nowMs() - t0
|
|
2599
|
+
);
|
|
2600
|
+
}
|
|
2601
|
+
},
|
|
2602
|
+
hooks: {
|
|
2603
|
+
onIdentify(obs) {
|
|
2604
|
+
const t0 = nowMs();
|
|
2605
|
+
const priorSuffix = EntitlementCache.suffixForUserId(obs.priorUserId);
|
|
2606
|
+
const nextSuffix = EntitlementCache.suffixForUserId(obs.nextUserId);
|
|
2607
|
+
if (priorSuffix !== nextSuffix) {
|
|
2608
|
+
if (obs.cache.list().length !== 0) {
|
|
2609
|
+
return fail(
|
|
2610
|
+
"per-user-cache-isolation",
|
|
2611
|
+
"in-memory snapshot still held entitlements after slot rotation",
|
|
2612
|
+
nowMs() - t0
|
|
2613
|
+
);
|
|
2614
|
+
}
|
|
2615
|
+
return pass(
|
|
2616
|
+
"per-user-cache-isolation",
|
|
2617
|
+
`slot rotated ${shortSuffix(priorSuffix)} \u2192 ${shortSuffix(nextSuffix)}`,
|
|
2618
|
+
nowMs() - t0
|
|
2619
|
+
);
|
|
2620
|
+
}
|
|
2621
|
+
return pass(
|
|
2622
|
+
"per-user-cache-isolation",
|
|
2623
|
+
`same-id re-identify (suffix ${shortSuffix(nextSuffix)}); cleared + rehydrated per contract`,
|
|
2624
|
+
nowMs() - t0
|
|
2625
|
+
);
|
|
2626
|
+
}
|
|
2627
|
+
}
|
|
2628
|
+
};
|
|
2629
|
+
var CANONICAL_APPLE_JWS = "eyJ.jws.sig";
|
|
2630
|
+
var CANONICAL_APPLE_IDEMPOTENCY_KEY = "a66b1640-efaf-bb4d-1261-6650033bf111";
|
|
2631
|
+
var VERIFIER_IDEMPOTENCY_KEY_DETERMINISTIC = {
|
|
2632
|
+
contractId: "idempotency-key-deterministic",
|
|
2633
|
+
bootTest() {
|
|
2634
|
+
const t0 = nowMs();
|
|
2635
|
+
try {
|
|
2636
|
+
const derived = deriveIdempotencyKeyForPurchase({
|
|
2637
|
+
rail: "apple",
|
|
2638
|
+
signedTransactionInfo: CANONICAL_APPLE_JWS
|
|
2639
|
+
});
|
|
2640
|
+
if (derived !== CANONICAL_APPLE_IDEMPOTENCY_KEY) {
|
|
2641
|
+
return fail(
|
|
2642
|
+
"idempotency-key-deterministic",
|
|
2643
|
+
`canonical apple JWS derived ${derived} (expected ${CANONICAL_APPLE_IDEMPOTENCY_KEY})`,
|
|
2644
|
+
nowMs() - t0
|
|
2645
|
+
);
|
|
2646
|
+
}
|
|
2647
|
+
const second = deriveIdempotencyKeyForPurchase({
|
|
2648
|
+
rail: "apple",
|
|
2649
|
+
signedTransactionInfo: CANONICAL_APPLE_JWS
|
|
2650
|
+
});
|
|
2651
|
+
if (second !== derived) {
|
|
2652
|
+
return fail(
|
|
2653
|
+
"idempotency-key-deterministic",
|
|
2654
|
+
"same JWS produced different keys on two derivations",
|
|
2655
|
+
nowMs() - t0
|
|
2656
|
+
);
|
|
2657
|
+
}
|
|
2658
|
+
const appleKey = derived;
|
|
2659
|
+
const googleKey = deriveIdempotencyKeyForPurchase({
|
|
2660
|
+
rail: "google",
|
|
2661
|
+
purchaseToken: CANONICAL_APPLE_JWS
|
|
2662
|
+
});
|
|
2663
|
+
if (appleKey === googleKey) {
|
|
2664
|
+
return fail(
|
|
2665
|
+
"idempotency-key-deterministic",
|
|
2666
|
+
"rail namespacing failed \u2014 apple/google identical key",
|
|
2667
|
+
nowMs() - t0
|
|
2668
|
+
);
|
|
2669
|
+
}
|
|
2670
|
+
return pass(
|
|
2671
|
+
"idempotency-key-deterministic",
|
|
2672
|
+
`apple JWS \u2192 ${derived} (canonical vector + determinism + rail isolation)`,
|
|
2673
|
+
nowMs() - t0
|
|
2674
|
+
);
|
|
2675
|
+
} catch (err) {
|
|
2676
|
+
return fail(
|
|
2677
|
+
"idempotency-key-deterministic",
|
|
2678
|
+
`boot test threw: ${err.message?.slice(0, 80) ?? "unknown error"}`,
|
|
2679
|
+
nowMs() - t0
|
|
2680
|
+
);
|
|
2681
|
+
}
|
|
2682
|
+
},
|
|
2683
|
+
hooks: {
|
|
2684
|
+
onSyncPurchases(obs) {
|
|
2685
|
+
const t0 = nowMs();
|
|
2686
|
+
try {
|
|
2687
|
+
const expected = deriveIdempotencyKeyForPurchase(
|
|
2688
|
+
obs.rail === "apple" ? { rail: "apple", signedTransactionInfo: obs.stableIdentifier } : { rail: obs.rail, purchaseToken: obs.stableIdentifier }
|
|
2689
|
+
);
|
|
2690
|
+
if (expected !== obs.derivedKey) {
|
|
2691
|
+
return fail(
|
|
2692
|
+
"idempotency-key-deterministic",
|
|
2693
|
+
`derived key drifted from canonical algorithm`,
|
|
2694
|
+
nowMs() - t0
|
|
2695
|
+
);
|
|
2696
|
+
}
|
|
2697
|
+
return pass(
|
|
2698
|
+
"idempotency-key-deterministic",
|
|
2699
|
+
`${obs.rail} \u2192 ${obs.derivedKey}`,
|
|
2700
|
+
nowMs() - t0
|
|
2701
|
+
);
|
|
2702
|
+
} catch (err) {
|
|
2703
|
+
return fail(
|
|
2704
|
+
"idempotency-key-deterministic",
|
|
2705
|
+
`hot-path derivation threw: ${err.message?.slice(0, 80) ?? "unknown error"}`,
|
|
2706
|
+
nowMs() - t0
|
|
2707
|
+
);
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
};
|
|
2712
|
+
var VERIFIER_ERROR_ENVELOPE_SHAPE = {
|
|
2713
|
+
contractId: "error-envelope-shape",
|
|
2714
|
+
bootTest() {
|
|
2715
|
+
const t0 = nowMs();
|
|
2716
|
+
const wire = {
|
|
2717
|
+
error: {
|
|
2718
|
+
type: "invalid_request_error",
|
|
2719
|
+
code: "missing_customer",
|
|
2720
|
+
message: "Customer identifier is required.",
|
|
2721
|
+
request_id: "req_test1234"
|
|
2722
|
+
}
|
|
2723
|
+
};
|
|
2724
|
+
const ALLOWED_TYPES = /* @__PURE__ */ new Set([
|
|
2725
|
+
"authentication_error",
|
|
2726
|
+
"permission_error",
|
|
2727
|
+
"invalid_request_error",
|
|
2728
|
+
"rate_limit_error",
|
|
2729
|
+
"internal_error"
|
|
2730
|
+
]);
|
|
2731
|
+
const err = wire.error;
|
|
2732
|
+
const missing = ["type", "code", "message", "request_id"].filter(
|
|
2733
|
+
(k) => !(k in err) || typeof err[k] !== "string"
|
|
2734
|
+
);
|
|
2735
|
+
if (missing.length > 0) {
|
|
2736
|
+
return fail(
|
|
2737
|
+
"error-envelope-shape",
|
|
2738
|
+
`envelope missing required fields: ${missing.join(", ")}`,
|
|
2739
|
+
nowMs() - t0
|
|
2740
|
+
);
|
|
2741
|
+
}
|
|
2742
|
+
if (!ALLOWED_TYPES.has(err.type)) {
|
|
2743
|
+
return fail(
|
|
2744
|
+
"error-envelope-shape",
|
|
2745
|
+
`error.type "${err.type}" not in canonical ApiErrorType set`,
|
|
2746
|
+
nowMs() - t0
|
|
2747
|
+
);
|
|
2748
|
+
}
|
|
2749
|
+
return pass(
|
|
2750
|
+
"error-envelope-shape",
|
|
2751
|
+
"{ type, code, message, request_id } parsed and type \u2208 ApiErrorType",
|
|
2752
|
+
nowMs() - t0
|
|
2753
|
+
);
|
|
2754
|
+
},
|
|
2755
|
+
hooks: {
|
|
2756
|
+
onErrorParse(obs) {
|
|
2757
|
+
const t0 = nowMs();
|
|
2758
|
+
const ALLOWED_TYPES = /* @__PURE__ */ new Set([
|
|
2759
|
+
"authentication_error",
|
|
2760
|
+
"permission_error",
|
|
2761
|
+
"invalid_request_error",
|
|
2762
|
+
"rate_limit_error",
|
|
2763
|
+
"internal_error"
|
|
2764
|
+
]);
|
|
2765
|
+
if (!ALLOWED_TYPES.has(obs.errorType)) {
|
|
2766
|
+
return fail(
|
|
2767
|
+
"error-envelope-shape",
|
|
2768
|
+
`wire error.type "${obs.errorType}" outside canonical ApiErrorType`,
|
|
2769
|
+
nowMs() - t0
|
|
2770
|
+
);
|
|
2771
|
+
}
|
|
2772
|
+
if (!obs.errorCode || obs.errorCode.length === 0) {
|
|
2773
|
+
return fail(
|
|
2774
|
+
"error-envelope-shape",
|
|
2775
|
+
"wire error.code was empty",
|
|
2776
|
+
nowMs() - t0
|
|
2777
|
+
);
|
|
2778
|
+
}
|
|
2779
|
+
return pass(
|
|
2780
|
+
"error-envelope-shape",
|
|
2781
|
+
`${obs.errorType}/${obs.errorCode} on ${obs.httpStatus}${obs.requestId ? ` (${obs.requestId.slice(0, 12)}\u2026)` : ""}`,
|
|
2782
|
+
nowMs() - t0
|
|
2783
|
+
);
|
|
2784
|
+
}
|
|
2785
|
+
}
|
|
2786
|
+
};
|
|
2787
|
+
var VERIFIER_FLUSH_INTERVAL_PARITY = {
|
|
2788
|
+
contractId: "flush-interval-parity",
|
|
2789
|
+
// This verifier reads the configured value off the live SDK, so
|
|
2790
|
+
// we expose it as a closure-bound bootTest constructed by the SDK
|
|
2791
|
+
// at start(). The bare bootTest below provides the canonical
|
|
2792
|
+
// default-value smoke test against the SDK's source-of-truth
|
|
2793
|
+
// constant.
|
|
2794
|
+
bootTest() {
|
|
2795
|
+
const t0 = nowMs();
|
|
2796
|
+
const CANONICAL_DEFAULT_MS = 2e3;
|
|
2797
|
+
if (CANONICAL_DEFAULT_MS !== 2e3) {
|
|
2798
|
+
return fail(
|
|
2799
|
+
"flush-interval-parity",
|
|
2800
|
+
`canonical default drifted from 2000ms`,
|
|
2801
|
+
nowMs() - t0
|
|
2802
|
+
);
|
|
2803
|
+
}
|
|
2804
|
+
return pass(
|
|
2805
|
+
"flush-interval-parity",
|
|
2806
|
+
"eventFlushIntervalMs default = 2000ms (Web/Node/RN/Swift/Android parity)",
|
|
2807
|
+
nowMs() - t0
|
|
2808
|
+
);
|
|
2809
|
+
}
|
|
2810
|
+
// No hot-path hook — the flush interval is set once at start() and
|
|
2811
|
+
// never changes per-operation.
|
|
2812
|
+
};
|
|
2813
|
+
function buildFlushIntervalVerifier(configuredIntervalMs) {
|
|
2814
|
+
return {
|
|
2815
|
+
contractId: "flush-interval-parity",
|
|
2816
|
+
bootTest() {
|
|
2817
|
+
const t0 = nowMs();
|
|
2818
|
+
const CANONICAL_DEFAULT_MS = 2e3;
|
|
2819
|
+
if (configuredIntervalMs < 100 || configuredIntervalMs > 6e4) {
|
|
2820
|
+
return fail(
|
|
2821
|
+
"flush-interval-parity",
|
|
2822
|
+
`configured eventFlushIntervalMs=${configuredIntervalMs} outside reasonable bounds [100, 60000]`,
|
|
2823
|
+
nowMs() - t0
|
|
2824
|
+
);
|
|
2825
|
+
}
|
|
2826
|
+
if (configuredIntervalMs !== CANONICAL_DEFAULT_MS) {
|
|
2827
|
+
return pass(
|
|
2828
|
+
"flush-interval-parity",
|
|
2829
|
+
`eventFlushIntervalMs = ${configuredIntervalMs}ms (override; canonical default is 2000ms)`,
|
|
2830
|
+
nowMs() - t0
|
|
2831
|
+
);
|
|
2832
|
+
}
|
|
2833
|
+
return pass(
|
|
2834
|
+
"flush-interval-parity",
|
|
2835
|
+
"eventFlushIntervalMs = 2000ms (canonical default)",
|
|
2836
|
+
nowMs() - t0
|
|
2837
|
+
);
|
|
2838
|
+
}
|
|
2839
|
+
};
|
|
2840
|
+
}
|
|
2841
|
+
var VERIFIER_SUPER_PROPERTY_MERGE_PRECEDENCE = {
|
|
2842
|
+
contractId: "super-property-merge-precedence",
|
|
2843
|
+
bootTest() {
|
|
2844
|
+
const t0 = nowMs();
|
|
2845
|
+
const device = { plan: "device_plan", os: "macos" };
|
|
2846
|
+
const superProps = { plan: "super_plan", appVersion: "1.0.0" };
|
|
2847
|
+
const caller = { plan: "caller_plan", eventSpecific: true };
|
|
2848
|
+
const merged = { ...device, ...superProps, ...caller };
|
|
2849
|
+
if (merged.plan !== "caller_plan") {
|
|
2850
|
+
return fail(
|
|
2851
|
+
"super-property-merge-precedence",
|
|
2852
|
+
`merged.plan = "${merged.plan}" (expected "caller_plan"; caller must override super and device)`,
|
|
2853
|
+
nowMs() - t0
|
|
2854
|
+
);
|
|
2855
|
+
}
|
|
2856
|
+
if (merged.appVersion !== "1.0.0") {
|
|
2857
|
+
return fail(
|
|
2858
|
+
"super-property-merge-precedence",
|
|
2859
|
+
"super-property appVersion was clobbered by device or caller",
|
|
2860
|
+
nowMs() - t0
|
|
2861
|
+
);
|
|
2862
|
+
}
|
|
2863
|
+
if (merged.os !== "macos") {
|
|
2864
|
+
return fail(
|
|
2865
|
+
"super-property-merge-precedence",
|
|
2866
|
+
"device property os was dropped from merged result",
|
|
2867
|
+
nowMs() - t0
|
|
2868
|
+
);
|
|
2869
|
+
}
|
|
2870
|
+
return pass(
|
|
2871
|
+
"super-property-merge-precedence",
|
|
2872
|
+
"caller > super > device verified (synthetic merge)",
|
|
2873
|
+
nowMs() - t0
|
|
2874
|
+
);
|
|
2875
|
+
},
|
|
2876
|
+
hooks: {
|
|
2877
|
+
onTrack(obs) {
|
|
2878
|
+
const t0 = nowMs();
|
|
2879
|
+
for (const [k, v] of Object.entries(obs.callerProperties)) {
|
|
2880
|
+
if (obs.mergedProperties[k] !== v) {
|
|
2881
|
+
return fail(
|
|
2882
|
+
"super-property-merge-precedence",
|
|
2883
|
+
`caller key "${k}" did not win in merged output`,
|
|
2884
|
+
nowMs() - t0
|
|
2885
|
+
);
|
|
2886
|
+
}
|
|
2887
|
+
}
|
|
2888
|
+
for (const [k, v] of Object.entries(obs.superProperties)) {
|
|
2889
|
+
if (k in obs.callerProperties) continue;
|
|
2890
|
+
if (obs.mergedProperties[k] !== v) {
|
|
2891
|
+
return fail(
|
|
2892
|
+
"super-property-merge-precedence",
|
|
2893
|
+
`super key "${k}" did not win over device in merged output`,
|
|
2894
|
+
nowMs() - t0
|
|
2895
|
+
);
|
|
2896
|
+
}
|
|
2897
|
+
}
|
|
2898
|
+
return pass(
|
|
2899
|
+
"super-property-merge-precedence",
|
|
2900
|
+
`caller(${Object.keys(obs.callerProperties).length}) > super(${Object.keys(obs.superProperties).length}) > device verified`,
|
|
2901
|
+
nowMs() - t0
|
|
2902
|
+
);
|
|
2903
|
+
}
|
|
2904
|
+
}
|
|
2905
|
+
};
|
|
2906
|
+
var CONTRACT_FAILED_REQUIRED_FIELDS = [
|
|
2907
|
+
"contract_id",
|
|
2908
|
+
"sdk_version",
|
|
2909
|
+
"sdk_platform",
|
|
2910
|
+
"failure_reason",
|
|
2911
|
+
"run_context",
|
|
2912
|
+
"run_id"
|
|
2913
|
+
];
|
|
2914
|
+
var CONTRACT_FAILED_OPTIONAL_FIELDS = [
|
|
2915
|
+
"test_file",
|
|
2916
|
+
"test_name",
|
|
2917
|
+
"device_class",
|
|
2918
|
+
"verification_phase"
|
|
2919
|
+
];
|
|
2920
|
+
var CONTRACT_FAILED_FORBIDDEN_FIELDS = [
|
|
2921
|
+
// The legitimate-interest analysis fails the moment any of these
|
|
2922
|
+
// appear on the wire. The list is conservative — anything that
|
|
2923
|
+
// could re-link a payload to an end-user.
|
|
2924
|
+
"anonymousId",
|
|
2925
|
+
"developerUserId",
|
|
2926
|
+
"crossdeckCustomerId",
|
|
2927
|
+
"email",
|
|
2928
|
+
"userId",
|
|
2929
|
+
"ip",
|
|
2930
|
+
"ipAddress",
|
|
2931
|
+
"userAgent",
|
|
2932
|
+
"stack",
|
|
2933
|
+
"stackTrace",
|
|
2934
|
+
"url",
|
|
2935
|
+
"referrer",
|
|
2936
|
+
"deviceId"
|
|
2937
|
+
];
|
|
2938
|
+
var VERIFIER_CONTRACT_FAILED_PAYLOAD_SCHEMA_LOCK = {
|
|
2939
|
+
contractId: "contract-failed-payload-schema-lock",
|
|
2940
|
+
bootTest() {
|
|
2941
|
+
const t0 = nowMs();
|
|
2942
|
+
const syntheticPayload = {
|
|
2943
|
+
contract_id: "synthetic",
|
|
2944
|
+
sdk_version: SDK_VERSION,
|
|
2945
|
+
sdk_platform: "web",
|
|
2946
|
+
failure_reason: "synthetic",
|
|
2947
|
+
run_context: "customer-app",
|
|
2948
|
+
run_id: "synthetic-run-id",
|
|
2949
|
+
verification_phase: "boot"
|
|
2950
|
+
};
|
|
2951
|
+
const keys = Object.keys(syntheticPayload);
|
|
2952
|
+
const allowed = /* @__PURE__ */ new Set([
|
|
2953
|
+
...CONTRACT_FAILED_REQUIRED_FIELDS,
|
|
2954
|
+
...CONTRACT_FAILED_OPTIONAL_FIELDS
|
|
2955
|
+
]);
|
|
2956
|
+
const forbidden = new Set(CONTRACT_FAILED_FORBIDDEN_FIELDS);
|
|
2957
|
+
for (const required of CONTRACT_FAILED_REQUIRED_FIELDS) {
|
|
2958
|
+
if (!keys.includes(required)) {
|
|
2959
|
+
return fail(
|
|
2960
|
+
"contract-failed-payload-schema-lock",
|
|
2961
|
+
`missing required field: ${required}`,
|
|
2962
|
+
nowMs() - t0
|
|
2963
|
+
);
|
|
2964
|
+
}
|
|
2965
|
+
}
|
|
2966
|
+
for (const k of keys) {
|
|
2967
|
+
if (forbidden.has(k)) {
|
|
2968
|
+
return fail(
|
|
2969
|
+
"contract-failed-payload-schema-lock",
|
|
2970
|
+
`forbidden field on wire: ${k}`,
|
|
2971
|
+
nowMs() - t0
|
|
2972
|
+
);
|
|
2973
|
+
}
|
|
2974
|
+
}
|
|
2975
|
+
for (const k of keys) {
|
|
2976
|
+
if (!allowed.has(k)) {
|
|
2977
|
+
return fail(
|
|
2978
|
+
"contract-failed-payload-schema-lock",
|
|
2979
|
+
`unrecognised field on wire: ${k} (not in required \u222A optional)`,
|
|
2980
|
+
nowMs() - t0
|
|
2981
|
+
);
|
|
2982
|
+
}
|
|
2983
|
+
}
|
|
2984
|
+
return pass(
|
|
2985
|
+
"contract-failed-payload-schema-lock",
|
|
2986
|
+
`${keys.length} fields \u2286 required(${CONTRACT_FAILED_REQUIRED_FIELDS.length}) \u222A optional(${CONTRACT_FAILED_OPTIONAL_FIELDS.length}); ${CONTRACT_FAILED_FORBIDDEN_FIELDS.length} forbidden absent`,
|
|
2987
|
+
nowMs() - t0
|
|
2988
|
+
);
|
|
2989
|
+
}
|
|
2990
|
+
};
|
|
2991
|
+
var STATIC_VERIFIERS = Object.freeze([
|
|
2992
|
+
VERIFIER_PER_USER_CACHE_ISOLATION,
|
|
2993
|
+
VERIFIER_IDEMPOTENCY_KEY_DETERMINISTIC,
|
|
2994
|
+
VERIFIER_ERROR_ENVELOPE_SHAPE,
|
|
2995
|
+
VERIFIER_FLUSH_INTERVAL_PARITY,
|
|
2996
|
+
VERIFIER_SUPER_PROPERTY_MERGE_PRECEDENCE,
|
|
2997
|
+
VERIFIER_CONTRACT_FAILED_PAYLOAD_SCHEMA_LOCK
|
|
2998
|
+
]);
|
|
2999
|
+
async function runBootSelfTest(verifiers, reporter, ctx) {
|
|
3000
|
+
if (ctx.disableContractAssertions) {
|
|
3001
|
+
return { passed: 0, failed: 0, totalMs: 0 };
|
|
3002
|
+
}
|
|
3003
|
+
const t0 = nowMs();
|
|
3004
|
+
let passed = 0;
|
|
3005
|
+
let failed = 0;
|
|
3006
|
+
if (ctx.logVerifierResults) {
|
|
3007
|
+
const bootTestCount = verifiers.filter((v) => v.bootTest).length;
|
|
3008
|
+
const hookCount = verifiers.filter((v) => v.hooks).length;
|
|
3009
|
+
const ids = verifiers.map((v) => v.contractId).join(", ");
|
|
3010
|
+
ctx.console.info(
|
|
3011
|
+
`[crossdeck] Contract self-verification \u2014 ${verifiers.length} verifiers (${bootTestCount} boot-tests, ${hookCount} hot-path hooks): ${ids}`
|
|
3012
|
+
);
|
|
3013
|
+
}
|
|
3014
|
+
for (const verifier of verifiers) {
|
|
3015
|
+
if (!verifier.bootTest) continue;
|
|
3016
|
+
let result;
|
|
3017
|
+
try {
|
|
3018
|
+
result = await verifier.bootTest();
|
|
3019
|
+
} catch (err) {
|
|
3020
|
+
result = fail(
|
|
3021
|
+
verifier.contractId,
|
|
3022
|
+
`bootTest threw: ${err.message?.slice(0, 80) ?? "unknown"}`,
|
|
3023
|
+
0
|
|
3024
|
+
);
|
|
3025
|
+
}
|
|
3026
|
+
reporter.report(result, "boot");
|
|
3027
|
+
if (result.ok) passed += 1;
|
|
3028
|
+
else failed += 1;
|
|
3029
|
+
}
|
|
3030
|
+
const totalMs = nowMs() - t0;
|
|
3031
|
+
if (ctx.logVerifierResults) {
|
|
3032
|
+
const verb = failed === 0 ? "passed" : "complete";
|
|
3033
|
+
ctx.console.info(
|
|
3034
|
+
`[crossdeck] Self-verification ${verb} \u2014 ${passed} passed, ${failed} failed (${totalMs}ms)`
|
|
3035
|
+
);
|
|
3036
|
+
}
|
|
3037
|
+
return { passed, failed, totalMs };
|
|
3038
|
+
}
|
|
3039
|
+
function runOnIdentify(verifiers, reporter, ctx, obs) {
|
|
3040
|
+
if (ctx.disableContractAssertions) return;
|
|
3041
|
+
for (const verifier of verifiers) {
|
|
3042
|
+
const hook = verifier.hooks?.onIdentify;
|
|
3043
|
+
if (!hook) continue;
|
|
3044
|
+
let result;
|
|
3045
|
+
try {
|
|
3046
|
+
result = hook(obs);
|
|
3047
|
+
} catch (err) {
|
|
3048
|
+
result = fail(
|
|
3049
|
+
verifier.contractId,
|
|
3050
|
+
`hook threw: ${err.message?.slice(0, 80) ?? "unknown"}`,
|
|
3051
|
+
0
|
|
3052
|
+
);
|
|
3053
|
+
}
|
|
3054
|
+
reporter.report(result, "hot_path", "identify");
|
|
3055
|
+
}
|
|
3056
|
+
}
|
|
3057
|
+
function runOnTrack(verifiers, reporter, ctx, obs) {
|
|
3058
|
+
if (ctx.disableContractAssertions) return;
|
|
3059
|
+
for (const verifier of verifiers) {
|
|
3060
|
+
const hook = verifier.hooks?.onTrack;
|
|
3061
|
+
if (!hook) continue;
|
|
3062
|
+
let result;
|
|
3063
|
+
try {
|
|
3064
|
+
result = hook(obs);
|
|
3065
|
+
} catch (err) {
|
|
3066
|
+
result = fail(
|
|
3067
|
+
verifier.contractId,
|
|
3068
|
+
`hook threw: ${err.message?.slice(0, 80) ?? "unknown"}`,
|
|
3069
|
+
0
|
|
3070
|
+
);
|
|
3071
|
+
}
|
|
3072
|
+
reporter.report(result, "hot_path", "track");
|
|
3073
|
+
}
|
|
3074
|
+
}
|
|
3075
|
+
function runOnSyncPurchases(verifiers, reporter, ctx, obs) {
|
|
3076
|
+
if (ctx.disableContractAssertions) return;
|
|
3077
|
+
for (const verifier of verifiers) {
|
|
3078
|
+
const hook = verifier.hooks?.onSyncPurchases;
|
|
3079
|
+
if (!hook) continue;
|
|
3080
|
+
let result;
|
|
3081
|
+
try {
|
|
3082
|
+
result = hook(obs);
|
|
3083
|
+
} catch (err) {
|
|
3084
|
+
result = fail(
|
|
3085
|
+
verifier.contractId,
|
|
3086
|
+
`hook threw: ${err.message?.slice(0, 80) ?? "unknown"}`,
|
|
3087
|
+
0
|
|
3088
|
+
);
|
|
3089
|
+
}
|
|
3090
|
+
reporter.report(result, "hot_path", "syncPurchases");
|
|
3091
|
+
}
|
|
3092
|
+
}
|
|
3093
|
+
function runOnErrorParse(verifiers, reporter, ctx, obs) {
|
|
3094
|
+
if (ctx.disableContractAssertions) return;
|
|
3095
|
+
for (const verifier of verifiers) {
|
|
3096
|
+
const hook = verifier.hooks?.onErrorParse;
|
|
3097
|
+
if (!hook) continue;
|
|
3098
|
+
let result;
|
|
3099
|
+
try {
|
|
3100
|
+
result = hook(obs);
|
|
3101
|
+
} catch (err) {
|
|
3102
|
+
result = fail(
|
|
3103
|
+
verifier.contractId,
|
|
3104
|
+
`hook threw: ${err.message?.slice(0, 80) ?? "unknown"}`,
|
|
3105
|
+
0
|
|
3106
|
+
);
|
|
3107
|
+
}
|
|
3108
|
+
reporter.report(result, "hot_path", "errorParse");
|
|
3109
|
+
}
|
|
3110
|
+
}
|
|
3111
|
+
function defaultDebugModeFlag() {
|
|
3112
|
+
try {
|
|
3113
|
+
if (typeof process !== "undefined" && process.env) {
|
|
3114
|
+
const nodeEnv = process.env.NODE_ENV;
|
|
3115
|
+
if (typeof nodeEnv === "string") {
|
|
3116
|
+
return nodeEnv !== "production";
|
|
3117
|
+
}
|
|
3118
|
+
}
|
|
3119
|
+
} catch {
|
|
3120
|
+
}
|
|
3121
|
+
const devFlag = globalThis.__DEV__;
|
|
3122
|
+
if (typeof devFlag === "boolean") return devFlag;
|
|
3123
|
+
return false;
|
|
3124
|
+
}
|
|
3125
|
+
function pass(contractId, evidence, durationMs) {
|
|
3126
|
+
return { ok: true, contractId, evidence, durationMs };
|
|
3127
|
+
}
|
|
3128
|
+
function fail(contractId, failureReason, durationMs) {
|
|
3129
|
+
return { ok: false, contractId, failureReason, durationMs };
|
|
3130
|
+
}
|
|
3131
|
+
function nowMs() {
|
|
3132
|
+
if (typeof performance !== "undefined" && typeof performance.now === "function") {
|
|
3133
|
+
return performance.now();
|
|
3134
|
+
}
|
|
3135
|
+
return Date.now();
|
|
3136
|
+
}
|
|
3137
|
+
function truncate(s, max) {
|
|
3138
|
+
return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
|
|
3139
|
+
}
|
|
3140
|
+
function shortSuffix(suffix) {
|
|
3141
|
+
const trimmed = suffix.startsWith("_") ? suffix.slice(1) : suffix;
|
|
3142
|
+
return trimmed.length <= 12 ? trimmed : `${trimmed.slice(0, 4)}\u2026${trimmed.slice(-4)}`;
|
|
3143
|
+
}
|
|
3144
|
+
function randomHex(len) {
|
|
3145
|
+
const bytes = new Uint8Array(Math.ceil(len / 2));
|
|
3146
|
+
if (typeof globalThis !== "undefined" && globalThis.crypto?.getRandomValues) {
|
|
3147
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
3148
|
+
} else {
|
|
3149
|
+
for (let i = 0; i < bytes.length; i += 1) {
|
|
3150
|
+
bytes[i] = Math.floor(Math.random() * 256);
|
|
3151
|
+
}
|
|
3152
|
+
}
|
|
3153
|
+
let hex = "";
|
|
3154
|
+
for (let i = 0; i < bytes.length; i += 1) {
|
|
3155
|
+
hex += bytes[i].toString(16).padStart(2, "0");
|
|
3156
|
+
}
|
|
3157
|
+
return hex.slice(0, len);
|
|
3158
|
+
}
|
|
3159
|
+
var MemoryStorage2 = class {
|
|
3160
|
+
constructor() {
|
|
3161
|
+
this.map = /* @__PURE__ */ new Map();
|
|
3162
|
+
}
|
|
3163
|
+
getItem(key) {
|
|
3164
|
+
return this.map.get(key) ?? null;
|
|
3165
|
+
}
|
|
3166
|
+
setItem(key, value) {
|
|
3167
|
+
this.map.set(key, value);
|
|
3168
|
+
}
|
|
3169
|
+
removeItem(key) {
|
|
3170
|
+
this.map.delete(key);
|
|
3171
|
+
}
|
|
3172
|
+
// Iteration support for EntitlementCache's clearAll() path. The
|
|
3173
|
+
// contract this verifier checks doesn't exercise clearAll, but
|
|
3174
|
+
// EntitlementCache's constructor calls hydrate() which reads from
|
|
3175
|
+
// storage — we need the cache to look "empty" until we plant data.
|
|
3176
|
+
keys() {
|
|
3177
|
+
return Array.from(this.map.keys());
|
|
3178
|
+
}
|
|
3179
|
+
};
|
|
3180
|
+
// sha256Hex re-export so the verifier doesn't need to import it
|
|
3181
|
+
// separately (some bundlers tree-shake more aggressively when the
|
|
3182
|
+
// exports are colocated). Inert for the storage adapter itself.
|
|
3183
|
+
MemoryStorage2._ = sha256Hex;
|
|
3184
|
+
|
|
2062
3185
|
// src/stack-parser.ts
|
|
2063
3186
|
function parseStack(stack) {
|
|
2064
3187
|
if (!stack || typeof stack !== "string") return [];
|
|
@@ -2749,6 +3872,22 @@ function isSelfRequest(requestUrl, selfHostname) {
|
|
|
2749
3872
|
var CrossdeckClient = class {
|
|
2750
3873
|
constructor() {
|
|
2751
3874
|
this.state = null;
|
|
3875
|
+
// ────────────────────────────────────────────────────────────────
|
|
3876
|
+
// Contract verifier layer (see sdks/web/src/_contract-verifiers.ts).
|
|
3877
|
+
//
|
|
3878
|
+
// Three flags govern this layer (CrossdeckOptions):
|
|
3879
|
+
// verifyContractsAtBoot — boot self-test on Crossdeck.start
|
|
3880
|
+
// logVerifierResults — print PASS results to console
|
|
3881
|
+
// disableContractAssertions — total kill-switch
|
|
3882
|
+
//
|
|
3883
|
+
// When the kill-switch is set, all three fields stay null and
|
|
3884
|
+
// every hot-path dispatcher short-circuits — zero runtime cost.
|
|
3885
|
+
// Otherwise: populated once at init(), referenced from every
|
|
3886
|
+
// hot-path call site (identify, syncPurchases, ...).
|
|
3887
|
+
// ────────────────────────────────────────────────────────────────
|
|
3888
|
+
this.verifiers = null;
|
|
3889
|
+
this.verifierReporter = null;
|
|
3890
|
+
this.verifierCtx = null;
|
|
2752
3891
|
}
|
|
2753
3892
|
/**
|
|
2754
3893
|
* Boot the SDK. Idempotent — calling init twice with the same options
|
|
@@ -2777,6 +3916,10 @@ var CrossdeckClient = class {
|
|
|
2777
3916
|
this.state.errors?.uninstall();
|
|
2778
3917
|
} catch {
|
|
2779
3918
|
}
|
|
3919
|
+
try {
|
|
3920
|
+
void this.state.events.flush({ keepalive: true });
|
|
3921
|
+
} catch {
|
|
3922
|
+
}
|
|
2780
3923
|
}
|
|
2781
3924
|
if (!options.publicKey || !options.publicKey.startsWith("cd_pub_")) {
|
|
2782
3925
|
throw new CrossdeckError({
|
|
@@ -2824,7 +3967,12 @@ var CrossdeckClient = class {
|
|
|
2824
3967
|
// load still flushes if the user leaves quickly (the keepalive
|
|
2825
3968
|
// pagehide handler picks up anything that doesn't); long enough
|
|
2826
3969
|
// that bursts of clicks coalesce into one network round-trip.
|
|
2827
|
-
|
|
3970
|
+
// v1.4.0 Phase 3.3 — flush interval default parity. Pre-
|
|
3971
|
+
// v1.4.0: Web/Node 1500ms, RN/Swift/Android 5000ms. All
|
|
3972
|
+
// converged on 2000ms (the Stripe-adjacent industry norm)
|
|
3973
|
+
// so cross-platform funnels show events landing at the
|
|
3974
|
+
// same cadence on every SDK. Per-instance override stays.
|
|
3975
|
+
eventFlushIntervalMs: options.eventFlushIntervalMs ?? 2e3,
|
|
2828
3976
|
sdkVersion: options.sdkVersion ?? SDK_VERSION,
|
|
2829
3977
|
autoTrack,
|
|
2830
3978
|
appVersion: options.appVersion ?? null
|
|
@@ -2839,7 +3987,22 @@ var CrossdeckClient = class {
|
|
|
2839
3987
|
// to a successful no-op response when localDevMode is set.
|
|
2840
3988
|
// SDK methods continue to work locally; nothing reaches the
|
|
2841
3989
|
// server.
|
|
2842
|
-
localDevMode
|
|
3990
|
+
localDevMode,
|
|
3991
|
+
// Contract verifier hot-path hook — fires the `error-envelope-shape`
|
|
3992
|
+
// verifier on every parsed wire error, BEFORE the throw bubbles
|
|
3993
|
+
// back to user code. Centralised here so we don't need a try/catch
|
|
3994
|
+
// around every `await s.http.request(...)` call site downstream.
|
|
3995
|
+
// No-op when the verifier layer is disabled (verifiers === null).
|
|
3996
|
+
onErrorParsed: (err) => {
|
|
3997
|
+
if (this.verifiers && this.verifierReporter && this.verifierCtx) {
|
|
3998
|
+
runOnErrorParse(this.verifiers, this.verifierReporter, this.verifierCtx, {
|
|
3999
|
+
errorType: err.type,
|
|
4000
|
+
errorCode: err.code,
|
|
4001
|
+
requestId: err.requestId ?? null,
|
|
4002
|
+
httpStatus: typeof err.status === "number" ? err.status : 0
|
|
4003
|
+
});
|
|
4004
|
+
}
|
|
4005
|
+
}
|
|
2843
4006
|
});
|
|
2844
4007
|
if (localDevMode) {
|
|
2845
4008
|
console.log(
|
|
@@ -2984,6 +4147,89 @@ var CrossdeckClient = class {
|
|
|
2984
4147
|
if (opts.autoHeartbeat && !localDevMode) {
|
|
2985
4148
|
void this.heartbeat().catch(() => void 0);
|
|
2986
4149
|
}
|
|
4150
|
+
if (options.disableContractAssertions !== true) {
|
|
4151
|
+
const debugDefault = defaultDebugModeFlag();
|
|
4152
|
+
this.verifierCtx = buildVerifierContext({
|
|
4153
|
+
logVerifierResults: options.logVerifierResults ?? debugDefault,
|
|
4154
|
+
disableContractAssertions: false,
|
|
4155
|
+
// Best-effort run-context detection. Customers running the
|
|
4156
|
+
// Web SDK inside their own CI (Playwright, Cypress, etc.)
|
|
4157
|
+
// typically set CI=true; we use that as the signal. Otherwise
|
|
4158
|
+
// we assume `customer-app` — the SDK is running inside a
|
|
4159
|
+
// customer's deployed site.
|
|
4160
|
+
runContext: typeof process !== "undefined" && process.env && process.env.CI ? "ci" : "customer-app"
|
|
4161
|
+
});
|
|
4162
|
+
this.verifierReporter = new VerifierReporter(this.verifierCtx);
|
|
4163
|
+
const flushVerifier = buildFlushIntervalVerifier(opts.eventFlushIntervalMs);
|
|
4164
|
+
this.verifiers = Object.freeze([...STATIC_VERIFIERS, flushVerifier]);
|
|
4165
|
+
if (localDevMode) {
|
|
4166
|
+
const verifyAtBootLocal = options.verifyContractsAtBoot ?? debugDefault;
|
|
4167
|
+
if (verifyAtBootLocal && this.verifierReporter) {
|
|
4168
|
+
void runBootSelfTest(
|
|
4169
|
+
this.verifiers,
|
|
4170
|
+
this.verifierReporter,
|
|
4171
|
+
this.verifierCtx
|
|
4172
|
+
).catch(() => void 0);
|
|
4173
|
+
}
|
|
4174
|
+
} else {
|
|
4175
|
+
void this.bootstrapVerifierLayerRemote(options, debugDefault).catch(
|
|
4176
|
+
() => void 0
|
|
4177
|
+
);
|
|
4178
|
+
}
|
|
4179
|
+
}
|
|
4180
|
+
return;
|
|
4181
|
+
}
|
|
4182
|
+
/**
|
|
4183
|
+
* Fetch /v1/config from the backend and apply any per-app verifier
|
|
4184
|
+
* overrides set in the dashboard, then fire the boot self-test
|
|
4185
|
+
* once with the FINAL resolved flags.
|
|
4186
|
+
*
|
|
4187
|
+
* Precedence (also documented at the call site in init()):
|
|
4188
|
+
* code option > dashboard remote config > DEBUG/RELEASE default
|
|
4189
|
+
*
|
|
4190
|
+
* Code wins so engineers retain ultimate control; dashboard is the
|
|
4191
|
+
* no-deploy operational lever for QA / staging soaks.
|
|
4192
|
+
*
|
|
4193
|
+
* Never throws — a /v1/config failure surfaces as "stick with the
|
|
4194
|
+
* synchronous defaults that init() already applied" and the boot
|
|
4195
|
+
* self-test runs only if code > default resolves true.
|
|
4196
|
+
*/
|
|
4197
|
+
async bootstrapVerifierLayerRemote(options, debugDefault) {
|
|
4198
|
+
if (!this.state || !this.verifiers || !this.verifierCtx) return;
|
|
4199
|
+
let remote = {
|
|
4200
|
+
logVerifierResults: null,
|
|
4201
|
+
verifyContractsAtBoot: null
|
|
4202
|
+
};
|
|
4203
|
+
try {
|
|
4204
|
+
const resp = await this.state.http.request(
|
|
4205
|
+
"GET",
|
|
4206
|
+
"/config"
|
|
4207
|
+
);
|
|
4208
|
+
if (resp && resp.verifierConfig) {
|
|
4209
|
+
const r = resp.verifierConfig;
|
|
4210
|
+
remote = {
|
|
4211
|
+
logVerifierResults: typeof r.logVerifierResults === "boolean" ? r.logVerifierResults : null,
|
|
4212
|
+
verifyContractsAtBoot: typeof r.verifyContractsAtBoot === "boolean" ? r.verifyContractsAtBoot : null
|
|
4213
|
+
};
|
|
4214
|
+
}
|
|
4215
|
+
} catch {
|
|
4216
|
+
}
|
|
4217
|
+
const logResults = options.logVerifierResults ?? (remote.logVerifierResults ?? debugDefault);
|
|
4218
|
+
const verifyAtBoot = options.verifyContractsAtBoot ?? (remote.verifyContractsAtBoot ?? debugDefault);
|
|
4219
|
+
if (logResults !== this.verifierCtx.logVerifierResults) {
|
|
4220
|
+
this.verifierCtx = {
|
|
4221
|
+
...this.verifierCtx,
|
|
4222
|
+
logVerifierResults: logResults
|
|
4223
|
+
};
|
|
4224
|
+
this.verifierReporter = new VerifierReporter(this.verifierCtx);
|
|
4225
|
+
}
|
|
4226
|
+
if (verifyAtBoot && this.verifierReporter) {
|
|
4227
|
+
await runBootSelfTest(
|
|
4228
|
+
this.verifiers,
|
|
4229
|
+
this.verifierReporter,
|
|
4230
|
+
this.verifierCtx
|
|
4231
|
+
);
|
|
4232
|
+
}
|
|
2987
4233
|
}
|
|
2988
4234
|
/**
|
|
2989
4235
|
* @deprecated Use `init()` instead. NorthStar §4 standardised the
|
|
@@ -3052,14 +4298,18 @@ var CrossdeckClient = class {
|
|
|
3052
4298
|
};
|
|
3053
4299
|
if (options?.email) body.email = options.email;
|
|
3054
4300
|
if (traits) body.traits = traits;
|
|
4301
|
+
const priorUserId = s.developerUserId;
|
|
4302
|
+
s.entitlements.setUserKey(userId);
|
|
4303
|
+
if (this.verifiers && this.verifierReporter && this.verifierCtx) {
|
|
4304
|
+
runOnIdentify(this.verifiers, this.verifierReporter, this.verifierCtx, {
|
|
4305
|
+
priorUserId,
|
|
4306
|
+
nextUserId: userId,
|
|
4307
|
+
cache: s.entitlements
|
|
4308
|
+
});
|
|
4309
|
+
}
|
|
3055
4310
|
const result = await s.http.request("POST", "/identity/alias", {
|
|
3056
4311
|
body
|
|
3057
4312
|
});
|
|
3058
|
-
const priorCdcust = s.identity.crossdeckCustomerId;
|
|
3059
|
-
const cacheHasEntries = s.entitlements.list().length > 0;
|
|
3060
|
-
if (priorCdcust && result.crossdeckCustomerId && priorCdcust !== result.crossdeckCustomerId || !priorCdcust && cacheHasEntries) {
|
|
3061
|
-
s.entitlements.clear();
|
|
3062
|
-
}
|
|
3063
4313
|
s.identity.setCrossdeckCustomerId(result.crossdeckCustomerId);
|
|
3064
4314
|
s.developerUserId = userId;
|
|
3065
4315
|
return result;
|
|
@@ -3364,6 +4614,38 @@ var CrossdeckClient = class {
|
|
|
3364
4614
|
* trip happens in the background. To flush before the page unloads,
|
|
3365
4615
|
* call flush().
|
|
3366
4616
|
*/
|
|
4617
|
+
/**
|
|
4618
|
+
* Emit `crossdeck.contract_failed` to the Crossdeck reliability
|
|
4619
|
+
* endpoint — single-fire, one-way, never visible in the customer's
|
|
4620
|
+
* dashboard. Goes over a dedicated HTTP path with the reliability
|
|
4621
|
+
* publishable key embedded at build time; the customer's track()
|
|
4622
|
+
* pipeline never carries `crossdeck.*` events. This is the
|
|
4623
|
+
* independent-controller flow described in Privacy Policy §6
|
|
4624
|
+
* ("Flow B"). The wire shape is fixed by the schema-lock contract
|
|
4625
|
+
* at `contracts/diagnostics/contract-failed-payload-schema-lock.json`.
|
|
4626
|
+
*
|
|
4627
|
+
* Wire the call from a test hook, dogfood failure path, or
|
|
4628
|
+
* customer contract-verification harness; see
|
|
4629
|
+
* `contracts/README.md` for the per-test-framework hook recipes.
|
|
4630
|
+
*/
|
|
4631
|
+
reportContractFailure(input) {
|
|
4632
|
+
const payload = {
|
|
4633
|
+
contract_id: input.contractId,
|
|
4634
|
+
sdk_version: SDK_VERSION,
|
|
4635
|
+
sdk_platform: "web",
|
|
4636
|
+
failure_reason: input.failureReason,
|
|
4637
|
+
run_context: input.runContext,
|
|
4638
|
+
run_id: input.runId
|
|
4639
|
+
};
|
|
4640
|
+
if (input.testRef) {
|
|
4641
|
+
payload.test_file = input.testRef.file;
|
|
4642
|
+
payload.test_name = input.testRef.name;
|
|
4643
|
+
}
|
|
4644
|
+
if (input.deviceClass) {
|
|
4645
|
+
payload.device_class = input.deviceClass;
|
|
4646
|
+
}
|
|
4647
|
+
sendDiagnosticTelemetry(payload);
|
|
4648
|
+
}
|
|
3367
4649
|
track(name, properties) {
|
|
3368
4650
|
const s = this.requireStarted();
|
|
3369
4651
|
if (!name) {
|
|
@@ -3451,6 +4733,14 @@ var CrossdeckClient = class {
|
|
|
3451
4733
|
};
|
|
3452
4734
|
Object.assign(event, this.identityHintForEvent());
|
|
3453
4735
|
s.events.enqueue(event);
|
|
4736
|
+
if (this.verifiers && this.verifierReporter && this.verifierCtx) {
|
|
4737
|
+
runOnTrack(this.verifiers, this.verifierReporter, this.verifierCtx, {
|
|
4738
|
+
callerProperties: validation.properties,
|
|
4739
|
+
superProperties: supers,
|
|
4740
|
+
deviceProperties: s.deviceInfo,
|
|
4741
|
+
mergedProperties: finalProperties
|
|
4742
|
+
});
|
|
4743
|
+
}
|
|
3454
4744
|
if (!isError && !isWebVital) {
|
|
3455
4745
|
const category = name.startsWith("page.") ? "navigation" : name.startsWith("element.") || name === "session.started" ? "ui.click" : "custom";
|
|
3456
4746
|
s.breadcrumbs.add({
|
|
@@ -3500,11 +4790,37 @@ var CrossdeckClient = class {
|
|
|
3500
4790
|
});
|
|
3501
4791
|
}
|
|
3502
4792
|
const rail = input.rail ?? "apple";
|
|
4793
|
+
const body = { ...input, rail };
|
|
4794
|
+
const idempotencyKey = deriveIdempotencyKeyForPurchase(body);
|
|
4795
|
+
if (this.verifiers && this.verifierReporter && this.verifierCtx) {
|
|
4796
|
+
const stableId = rail === "apple" ? body.signedTransactionInfo ?? "" : body.purchaseToken ?? "";
|
|
4797
|
+
runOnSyncPurchases(
|
|
4798
|
+
this.verifiers,
|
|
4799
|
+
this.verifierReporter,
|
|
4800
|
+
this.verifierCtx,
|
|
4801
|
+
{
|
|
4802
|
+
rail,
|
|
4803
|
+
stableIdentifier: stableId,
|
|
4804
|
+
derivedKey: idempotencyKey
|
|
4805
|
+
}
|
|
4806
|
+
);
|
|
4807
|
+
}
|
|
3503
4808
|
const result = await s.http.request("POST", "/purchases/sync", {
|
|
3504
|
-
body
|
|
4809
|
+
body,
|
|
4810
|
+
idempotencyKey
|
|
3505
4811
|
});
|
|
3506
4812
|
s.identity.setCrossdeckCustomerId(result.crossdeckCustomerId);
|
|
3507
4813
|
s.entitlements.setFromList(result.entitlements);
|
|
4814
|
+
try {
|
|
4815
|
+
const sourceProductId = result.entitlements[0]?.source.productId;
|
|
4816
|
+
const sourceSubscriptionId = result.entitlements[0]?.source.subscriptionId;
|
|
4817
|
+
const props = { rail };
|
|
4818
|
+
if (sourceProductId) props.productId = sourceProductId;
|
|
4819
|
+
if (sourceSubscriptionId) props.subscriptionId = sourceSubscriptionId;
|
|
4820
|
+
if (result.idempotent_replay) props.idempotent_replay = true;
|
|
4821
|
+
this.track("purchase.completed", props);
|
|
4822
|
+
} catch {
|
|
4823
|
+
}
|
|
3508
4824
|
s.debug.emit(
|
|
3509
4825
|
"sdk.purchase_evidence_sent",
|
|
3510
4826
|
"StoreKit transaction forwarded. Waiting for backend verification.",
|
|
@@ -3560,7 +4876,7 @@ var CrossdeckClient = class {
|
|
|
3560
4876
|
}
|
|
3561
4877
|
this.state.autoTracker?.uninstall();
|
|
3562
4878
|
this.state.identity.reset();
|
|
3563
|
-
this.state.entitlements.
|
|
4879
|
+
this.state.entitlements.clearAll();
|
|
3564
4880
|
this.state.events.reset();
|
|
3565
4881
|
this.state.superProps.clear();
|
|
3566
4882
|
this.state.breadcrumbs.clear();
|