@cross-deck/web 1.2.0 → 1.4.2
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/CHANGELOG.md +106 -1
- package/README.md +13 -1
- 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 +691 -73
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +22 -4
- package/dist/index.d.ts +22 -4
- package/dist/index.mjs +691 -73
- package/dist/index.mjs.map +1 -1
- package/dist/react.cjs +581 -73
- 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 +581 -73
- package/dist/react.mjs.map +1 -1
- package/dist/{types-BzoKor4z.d.mts → types-Bu3jbmdq.d.mts} +4 -0
- package/dist/{types-BzoKor4z.d.ts → types-Bu3jbmdq.d.ts} +4 -0
- package/dist/vue.cjs +581 -73
- package/dist/vue.cjs.map +1 -1
- package/dist/vue.mjs +581 -73
- package/dist/vue.mjs.map +1 -1
- package/package.json +2 -2
package/dist/react.cjs
CHANGED
|
@@ -92,9 +92,11 @@ function typeMapForStatus(status) {
|
|
|
92
92
|
return "internal_error";
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
// src/
|
|
95
|
+
// src/_version.ts
|
|
96
|
+
var SDK_VERSION = "1.4.2";
|
|
96
97
|
var SDK_NAME = "@cross-deck/web";
|
|
97
|
-
|
|
98
|
+
|
|
99
|
+
// src/http.ts
|
|
98
100
|
var DEFAULT_BASE_URL = "https://api.cross-deck.com/v1";
|
|
99
101
|
var DEFAULT_TIMEOUT_MS = 15e3;
|
|
100
102
|
var HttpClient = class {
|
|
@@ -341,29 +343,258 @@ function randomChars(count) {
|
|
|
341
343
|
return out.join("");
|
|
342
344
|
}
|
|
343
345
|
|
|
346
|
+
// src/hash.ts
|
|
347
|
+
var K = new Uint32Array([
|
|
348
|
+
1116352408,
|
|
349
|
+
1899447441,
|
|
350
|
+
3049323471,
|
|
351
|
+
3921009573,
|
|
352
|
+
961987163,
|
|
353
|
+
1508970993,
|
|
354
|
+
2453635748,
|
|
355
|
+
2870763221,
|
|
356
|
+
3624381080,
|
|
357
|
+
310598401,
|
|
358
|
+
607225278,
|
|
359
|
+
1426881987,
|
|
360
|
+
1925078388,
|
|
361
|
+
2162078206,
|
|
362
|
+
2614888103,
|
|
363
|
+
3248222580,
|
|
364
|
+
3835390401,
|
|
365
|
+
4022224774,
|
|
366
|
+
264347078,
|
|
367
|
+
604807628,
|
|
368
|
+
770255983,
|
|
369
|
+
1249150122,
|
|
370
|
+
1555081692,
|
|
371
|
+
1996064986,
|
|
372
|
+
2554220882,
|
|
373
|
+
2821834349,
|
|
374
|
+
2952996808,
|
|
375
|
+
3210313671,
|
|
376
|
+
3336571891,
|
|
377
|
+
3584528711,
|
|
378
|
+
113926993,
|
|
379
|
+
338241895,
|
|
380
|
+
666307205,
|
|
381
|
+
773529912,
|
|
382
|
+
1294757372,
|
|
383
|
+
1396182291,
|
|
384
|
+
1695183700,
|
|
385
|
+
1986661051,
|
|
386
|
+
2177026350,
|
|
387
|
+
2456956037,
|
|
388
|
+
2730485921,
|
|
389
|
+
2820302411,
|
|
390
|
+
3259730800,
|
|
391
|
+
3345764771,
|
|
392
|
+
3516065817,
|
|
393
|
+
3600352804,
|
|
394
|
+
4094571909,
|
|
395
|
+
275423344,
|
|
396
|
+
430227734,
|
|
397
|
+
506948616,
|
|
398
|
+
659060556,
|
|
399
|
+
883997877,
|
|
400
|
+
958139571,
|
|
401
|
+
1322822218,
|
|
402
|
+
1537002063,
|
|
403
|
+
1747873779,
|
|
404
|
+
1955562222,
|
|
405
|
+
2024104815,
|
|
406
|
+
2227730452,
|
|
407
|
+
2361852424,
|
|
408
|
+
2428436474,
|
|
409
|
+
2756734187,
|
|
410
|
+
3204031479,
|
|
411
|
+
3329325298
|
|
412
|
+
]);
|
|
413
|
+
function utf8Bytes(input) {
|
|
414
|
+
if (typeof TextEncoder !== "undefined") {
|
|
415
|
+
return new TextEncoder().encode(input);
|
|
416
|
+
}
|
|
417
|
+
const out = [];
|
|
418
|
+
for (let i = 0; i < input.length; i++) {
|
|
419
|
+
let codePoint = input.charCodeAt(i);
|
|
420
|
+
if (codePoint >= 55296 && codePoint <= 56319 && i + 1 < input.length) {
|
|
421
|
+
const next = input.charCodeAt(i + 1);
|
|
422
|
+
if (next >= 56320 && next <= 57343) {
|
|
423
|
+
codePoint = 65536 + (codePoint - 55296 << 10) + (next - 56320);
|
|
424
|
+
i++;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
if (codePoint < 128) {
|
|
428
|
+
out.push(codePoint);
|
|
429
|
+
} else if (codePoint < 2048) {
|
|
430
|
+
out.push(192 | codePoint >> 6);
|
|
431
|
+
out.push(128 | codePoint & 63);
|
|
432
|
+
} else if (codePoint < 65536) {
|
|
433
|
+
out.push(224 | codePoint >> 12);
|
|
434
|
+
out.push(128 | codePoint >> 6 & 63);
|
|
435
|
+
out.push(128 | codePoint & 63);
|
|
436
|
+
} else {
|
|
437
|
+
out.push(240 | codePoint >> 18);
|
|
438
|
+
out.push(128 | codePoint >> 12 & 63);
|
|
439
|
+
out.push(128 | codePoint >> 6 & 63);
|
|
440
|
+
out.push(128 | codePoint & 63);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
return new Uint8Array(out);
|
|
444
|
+
}
|
|
445
|
+
function sha256Hex(input) {
|
|
446
|
+
const bytes = utf8Bytes(input);
|
|
447
|
+
const bitLength = bytes.length * 8;
|
|
448
|
+
const blockCount = Math.floor((bytes.length + 9 + 63) / 64);
|
|
449
|
+
const padded = new Uint8Array(blockCount * 64);
|
|
450
|
+
padded.set(bytes);
|
|
451
|
+
padded[bytes.length] = 128;
|
|
452
|
+
const high = Math.floor(bitLength / 4294967296);
|
|
453
|
+
const low = bitLength >>> 0;
|
|
454
|
+
const lenOffset = padded.length - 8;
|
|
455
|
+
padded[lenOffset + 0] = high >>> 24 & 255;
|
|
456
|
+
padded[lenOffset + 1] = high >>> 16 & 255;
|
|
457
|
+
padded[lenOffset + 2] = high >>> 8 & 255;
|
|
458
|
+
padded[lenOffset + 3] = high & 255;
|
|
459
|
+
padded[lenOffset + 4] = low >>> 24 & 255;
|
|
460
|
+
padded[lenOffset + 5] = low >>> 16 & 255;
|
|
461
|
+
padded[lenOffset + 6] = low >>> 8 & 255;
|
|
462
|
+
padded[lenOffset + 7] = low & 255;
|
|
463
|
+
const H = new Uint32Array([
|
|
464
|
+
1779033703,
|
|
465
|
+
3144134277,
|
|
466
|
+
1013904242,
|
|
467
|
+
2773480762,
|
|
468
|
+
1359893119,
|
|
469
|
+
2600822924,
|
|
470
|
+
528734635,
|
|
471
|
+
1541459225
|
|
472
|
+
]);
|
|
473
|
+
const W = new Uint32Array(64);
|
|
474
|
+
for (let block = 0; block < blockCount; block++) {
|
|
475
|
+
const offset = block * 64;
|
|
476
|
+
for (let t = 0; t < 16; t++) {
|
|
477
|
+
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;
|
|
478
|
+
}
|
|
479
|
+
for (let t = 16; t < 64; t++) {
|
|
480
|
+
const w15 = W[t - 15];
|
|
481
|
+
const w2 = W[t - 2];
|
|
482
|
+
const s0 = (w15 >>> 7 | w15 << 25) ^ (w15 >>> 18 | w15 << 14) ^ w15 >>> 3;
|
|
483
|
+
const s1 = (w2 >>> 17 | w2 << 15) ^ (w2 >>> 19 | w2 << 13) ^ w2 >>> 10;
|
|
484
|
+
W[t] = W[t - 16] + s0 + W[t - 7] + s1 >>> 0;
|
|
485
|
+
}
|
|
486
|
+
let a = H[0], b = H[1], c = H[2], d = H[3];
|
|
487
|
+
let e = H[4], f = H[5], g = H[6], h = H[7];
|
|
488
|
+
for (let t = 0; t < 64; t++) {
|
|
489
|
+
const S1 = (e >>> 6 | e << 26) ^ (e >>> 11 | e << 21) ^ (e >>> 25 | e << 7);
|
|
490
|
+
const ch = e & f ^ ~e & g;
|
|
491
|
+
const temp1 = h + S1 + ch + K[t] + W[t] >>> 0;
|
|
492
|
+
const S0 = (a >>> 2 | a << 30) ^ (a >>> 13 | a << 19) ^ (a >>> 22 | a << 10);
|
|
493
|
+
const maj = a & b ^ a & c ^ b & c;
|
|
494
|
+
const temp2 = S0 + maj >>> 0;
|
|
495
|
+
h = g;
|
|
496
|
+
g = f;
|
|
497
|
+
f = e;
|
|
498
|
+
e = d + temp1 >>> 0;
|
|
499
|
+
d = c;
|
|
500
|
+
c = b;
|
|
501
|
+
b = a;
|
|
502
|
+
a = temp1 + temp2 >>> 0;
|
|
503
|
+
}
|
|
504
|
+
H[0] = H[0] + a >>> 0;
|
|
505
|
+
H[1] = H[1] + b >>> 0;
|
|
506
|
+
H[2] = H[2] + c >>> 0;
|
|
507
|
+
H[3] = H[3] + d >>> 0;
|
|
508
|
+
H[4] = H[4] + e >>> 0;
|
|
509
|
+
H[5] = H[5] + f >>> 0;
|
|
510
|
+
H[6] = H[6] + g >>> 0;
|
|
511
|
+
H[7] = H[7] + h >>> 0;
|
|
512
|
+
}
|
|
513
|
+
let hex = "";
|
|
514
|
+
for (let i = 0; i < 8; i++) {
|
|
515
|
+
hex += H[i].toString(16).padStart(8, "0");
|
|
516
|
+
}
|
|
517
|
+
return hex;
|
|
518
|
+
}
|
|
519
|
+
|
|
344
520
|
// src/entitlement-cache.ts
|
|
345
521
|
var DEFAULT_STALE_AFTER_MS = 24 * 60 * 60 * 1e3;
|
|
346
|
-
var
|
|
522
|
+
var ANON_SUFFIX = "_anon";
|
|
523
|
+
var INDEX_SUFFIX = "_index";
|
|
524
|
+
var EntitlementCache = class _EntitlementCache {
|
|
347
525
|
/**
|
|
348
|
-
* @param storage
|
|
349
|
-
*
|
|
350
|
-
*
|
|
351
|
-
*
|
|
352
|
-
* @param
|
|
353
|
-
*
|
|
354
|
-
*
|
|
526
|
+
* @param storage Device storage adapter. When omitted (tests) or
|
|
527
|
+
* a MemoryStorage (strict-consent / no-persistence
|
|
528
|
+
* mode) the cache is session-only — durability is
|
|
529
|
+
* simply absent, never wrong.
|
|
530
|
+
* @param storageKeyPrefix Prefix used to derive per-user storage keys
|
|
531
|
+
* (`<prefix>:<sha256(userId)>`). Default
|
|
532
|
+
* `crossdeck:entitlements`. The trailing user
|
|
533
|
+
* suffix is filled at identify() / reset()
|
|
534
|
+
* time — see [[setUserKey]].
|
|
535
|
+
* @param staleAfterMs Age past which last-known-good is flagged stale
|
|
536
|
+
* even without a failed refresh. Default 24h.
|
|
355
537
|
*/
|
|
356
|
-
constructor(storage,
|
|
538
|
+
constructor(storage, storageKeyPrefix = "crossdeck:entitlements", staleAfterMs = DEFAULT_STALE_AFTER_MS) {
|
|
357
539
|
this.all = [];
|
|
358
540
|
this.lastUpdated = 0;
|
|
359
541
|
this.lastRefreshFailedAt = 0;
|
|
360
542
|
this.listeners = /* @__PURE__ */ new Set();
|
|
361
543
|
this.listenerErrorCount = 0;
|
|
544
|
+
this.currentSuffix = ANON_SUFFIX;
|
|
362
545
|
this.storage = storage;
|
|
363
|
-
this.
|
|
546
|
+
this.storageKeyPrefix = storageKeyPrefix;
|
|
364
547
|
this.staleAfterMs = staleAfterMs;
|
|
365
548
|
this.hydrate();
|
|
366
549
|
}
|
|
550
|
+
/** The full storage key the current-user blob is persisted under. */
|
|
551
|
+
get storageKey() {
|
|
552
|
+
return `${this.storageKeyPrefix}:${this.currentSuffix}`;
|
|
553
|
+
}
|
|
554
|
+
/** Key of the index blob — a JSON array of every suffix we've
|
|
555
|
+
* written. Used by clearAll() to scope a logout-wipe. */
|
|
556
|
+
get indexKey() {
|
|
557
|
+
return `${this.storageKeyPrefix}:${INDEX_SUFFIX}`;
|
|
558
|
+
}
|
|
559
|
+
/** Derive a stable suffix for a developerUserId via SHA-256. The
|
|
560
|
+
* raw userId never lands in the storage key — protects against
|
|
561
|
+
* accidentally leaking email-style identifiers through DevTools
|
|
562
|
+
* inspection. Pass `null` to switch back to the anonymous slot. */
|
|
563
|
+
static suffixForUserId(userId) {
|
|
564
|
+
if (userId == null || userId === "") return ANON_SUFFIX;
|
|
565
|
+
return sha256Hex(userId);
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* Switch the cache to a different user's storage slot. Bank-grade
|
|
569
|
+
* three-layer isolation:
|
|
570
|
+
* (a) Physical key separation — `<prefix>:<sha256(userId)>` so
|
|
571
|
+
* a user-switch can't physically read prior user's data
|
|
572
|
+
* even if the in-memory clear was skipped.
|
|
573
|
+
* (b) Unconditional in-memory clear — invoked whenever the
|
|
574
|
+
* active suffix changes, even on same-id re-identify.
|
|
575
|
+
* (c) Re-hydrate from the new slot — a returning user observes
|
|
576
|
+
* their last-known-good cache from storage immediately.
|
|
577
|
+
*
|
|
578
|
+
* Caller (identify() / reset()) MUST invoke this BEFORE the next
|
|
579
|
+
* setFromList() so the write lands under the right key.
|
|
580
|
+
*/
|
|
581
|
+
setUserKey(userId) {
|
|
582
|
+
const nextSuffix = _EntitlementCache.suffixForUserId(userId);
|
|
583
|
+
if (nextSuffix === this.currentSuffix) {
|
|
584
|
+
this.all = [];
|
|
585
|
+
this.lastUpdated = 0;
|
|
586
|
+
this.lastRefreshFailedAt = 0;
|
|
587
|
+
this.notify();
|
|
588
|
+
this.hydrate();
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
this.currentSuffix = nextSuffix;
|
|
592
|
+
this.all = [];
|
|
593
|
+
this.lastUpdated = 0;
|
|
594
|
+
this.lastRefreshFailedAt = 0;
|
|
595
|
+
this.hydrate();
|
|
596
|
+
this.notify();
|
|
597
|
+
}
|
|
367
598
|
/**
|
|
368
599
|
* Sync read — true iff the entitlement is currently granting access.
|
|
369
600
|
*
|
|
@@ -433,12 +664,13 @@ var EntitlementCache = class {
|
|
|
433
664
|
this.lastUpdated = Date.now();
|
|
434
665
|
this.lastRefreshFailedAt = 0;
|
|
435
666
|
this.persist();
|
|
667
|
+
this.recordSuffixInIndex(this.currentSuffix);
|
|
436
668
|
this.notify();
|
|
437
669
|
}
|
|
438
670
|
/**
|
|
439
|
-
* Wipe
|
|
440
|
-
*
|
|
441
|
-
*
|
|
671
|
+
* Wipe the CURRENT user's slot. Used internally when a single
|
|
672
|
+
* user's cache needs to be invalidated without affecting other
|
|
673
|
+
* persisted slots. The full-logout path is [[clearAll]].
|
|
442
674
|
*/
|
|
443
675
|
clear() {
|
|
444
676
|
this.all = [];
|
|
@@ -450,6 +682,40 @@ var EntitlementCache = class {
|
|
|
450
682
|
} catch {
|
|
451
683
|
}
|
|
452
684
|
}
|
|
685
|
+
this.removeSuffixFromIndex(this.currentSuffix);
|
|
686
|
+
this.notify();
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* Logout-grade wipe — bank-grade contract: removes EVERY per-user
|
|
690
|
+
* entitlement slot the SDK has ever written on this device, then
|
|
691
|
+
* clears the index. Used by `Crossdeck.reset()` so a logout on a
|
|
692
|
+
* shared device can never leave another user's entitlements
|
|
693
|
+
* readable (layer (c) of the v1.4.0 isolation fix).
|
|
694
|
+
*
|
|
695
|
+
* After clearAll(), the cache is back to anonymous + empty.
|
|
696
|
+
*/
|
|
697
|
+
clearAll() {
|
|
698
|
+
this.all = [];
|
|
699
|
+
this.lastUpdated = 0;
|
|
700
|
+
this.lastRefreshFailedAt = 0;
|
|
701
|
+
this.currentSuffix = ANON_SUFFIX;
|
|
702
|
+
if (this.storage) {
|
|
703
|
+
const suffixes = this.readIndex();
|
|
704
|
+
for (const suffix of suffixes) {
|
|
705
|
+
try {
|
|
706
|
+
this.storage.removeItem(`${this.storageKeyPrefix}:${suffix}`);
|
|
707
|
+
} catch {
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
try {
|
|
711
|
+
this.storage.removeItem(`${this.storageKeyPrefix}:${ANON_SUFFIX}`);
|
|
712
|
+
} catch {
|
|
713
|
+
}
|
|
714
|
+
try {
|
|
715
|
+
this.storage.removeItem(this.indexKey);
|
|
716
|
+
} catch {
|
|
717
|
+
}
|
|
718
|
+
}
|
|
453
719
|
this.notify();
|
|
454
720
|
}
|
|
455
721
|
/**
|
|
@@ -499,6 +765,47 @@ var EntitlementCache = class {
|
|
|
499
765
|
} catch {
|
|
500
766
|
}
|
|
501
767
|
}
|
|
768
|
+
/** Read the index of all per-user suffixes the SDK has written. */
|
|
769
|
+
readIndex() {
|
|
770
|
+
if (!this.storage) return [];
|
|
771
|
+
try {
|
|
772
|
+
const raw = this.storage.getItem(this.indexKey);
|
|
773
|
+
if (!raw) return [];
|
|
774
|
+
const parsed = JSON.parse(raw);
|
|
775
|
+
if (Array.isArray(parsed)) {
|
|
776
|
+
return parsed.filter((x) => typeof x === "string");
|
|
777
|
+
}
|
|
778
|
+
return [];
|
|
779
|
+
} catch {
|
|
780
|
+
return [];
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
/** Add a suffix to the persisted index. Idempotent. */
|
|
784
|
+
recordSuffixInIndex(suffix) {
|
|
785
|
+
if (!this.storage) return;
|
|
786
|
+
const existing = this.readIndex();
|
|
787
|
+
if (existing.includes(suffix)) return;
|
|
788
|
+
existing.push(suffix);
|
|
789
|
+
try {
|
|
790
|
+
this.storage.setItem(this.indexKey, JSON.stringify(existing));
|
|
791
|
+
} catch {
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
/** Remove a suffix from the persisted index. No-op if absent. */
|
|
795
|
+
removeSuffixFromIndex(suffix) {
|
|
796
|
+
if (!this.storage) return;
|
|
797
|
+
const existing = this.readIndex();
|
|
798
|
+
const next = existing.filter((s) => s !== suffix);
|
|
799
|
+
if (next.length === existing.length) return;
|
|
800
|
+
try {
|
|
801
|
+
if (next.length === 0) {
|
|
802
|
+
this.storage.removeItem(this.indexKey);
|
|
803
|
+
} else {
|
|
804
|
+
this.storage.setItem(this.indexKey, JSON.stringify(next));
|
|
805
|
+
}
|
|
806
|
+
} catch {
|
|
807
|
+
}
|
|
808
|
+
}
|
|
502
809
|
notify() {
|
|
503
810
|
if (this.listeners.size === 0) return;
|
|
504
811
|
const snapshot = this.all.slice();
|
|
@@ -513,6 +820,34 @@ var EntitlementCache = class {
|
|
|
513
820
|
}
|
|
514
821
|
};
|
|
515
822
|
|
|
823
|
+
// src/idempotency-key.ts
|
|
824
|
+
function formatAsUuid(hex) {
|
|
825
|
+
return [
|
|
826
|
+
hex.slice(0, 8),
|
|
827
|
+
hex.slice(8, 12),
|
|
828
|
+
hex.slice(12, 16),
|
|
829
|
+
hex.slice(16, 20),
|
|
830
|
+
hex.slice(20, 32)
|
|
831
|
+
].join("-");
|
|
832
|
+
}
|
|
833
|
+
function deriveIdempotencyKeyForPurchase(body) {
|
|
834
|
+
let identifier;
|
|
835
|
+
if (body.rail === "apple") {
|
|
836
|
+
identifier = body.signedTransactionInfo ?? "";
|
|
837
|
+
} else if (body.rail === "google") {
|
|
838
|
+
identifier = body.purchaseToken ?? "";
|
|
839
|
+
} else {
|
|
840
|
+
identifier = "";
|
|
841
|
+
}
|
|
842
|
+
if (!identifier) {
|
|
843
|
+
throw new Error(
|
|
844
|
+
`deriveIdempotencyKeyForPurchase: no stable identifier in body (rail=${body.rail}). Apple needs signedTransactionInfo; Google needs purchaseToken.`
|
|
845
|
+
);
|
|
846
|
+
}
|
|
847
|
+
const namespaced = `crossdeck:purchases/sync:${body.rail}:${identifier}`;
|
|
848
|
+
return formatAsUuid(sha256Hex(namespaced));
|
|
849
|
+
}
|
|
850
|
+
|
|
516
851
|
// src/retry-policy.ts
|
|
517
852
|
var DEFAULT_BASE = 1e3;
|
|
518
853
|
var DEFAULT_MAX = 6e4;
|
|
@@ -525,8 +860,10 @@ function computeNextDelay(attempts, retryAfterMs, options = {}, random = Math.ra
|
|
|
525
860
|
const safeAttempts = Math.min(attempts, 30);
|
|
526
861
|
const ceiling = Math.min(max, base * Math.pow(factor, safeAttempts));
|
|
527
862
|
const jittered = ceiling * random();
|
|
528
|
-
if (retryAfterMs !== void 0
|
|
529
|
-
|
|
863
|
+
if (retryAfterMs !== void 0) {
|
|
864
|
+
const ABSOLUTE_MAX_MS = 24 * 60 * 60 * 1e3;
|
|
865
|
+
const honoured = Math.min(ABSOLUTE_MAX_MS, retryAfterMs);
|
|
866
|
+
if (honoured > jittered) return honoured;
|
|
530
867
|
}
|
|
531
868
|
return Math.max(0, Math.round(jittered));
|
|
532
869
|
}
|
|
@@ -561,6 +898,27 @@ var EventQueue = class {
|
|
|
561
898
|
constructor(cfg) {
|
|
562
899
|
this.cfg = cfg;
|
|
563
900
|
this.buffer = [];
|
|
901
|
+
/**
|
|
902
|
+
* In-flight events that have been spliced from `buffer` for the
|
|
903
|
+
* current batch but haven't yet been confirmed (success or permanent
|
|
904
|
+
* failure). On a retry-driven re-flush we re-use this slot alongside
|
|
905
|
+
* `pendingBatchId` so the Stripe-style Idempotency-Key is preserved
|
|
906
|
+
* across retries of the SAME logical batch.
|
|
907
|
+
*
|
|
908
|
+
* Pre-fix the splice cleared the buffer AND we immediately
|
|
909
|
+
* `persistent.save(empty)` BEFORE awaiting the network call — a
|
|
910
|
+
* crash mid-flight wiped the persisted blob and the batch was lost
|
|
911
|
+
* with no replay on next boot. Now the persisted blob always carries
|
|
912
|
+
* `[...pendingBatch, ...buffer]` so the in-flight events survive
|
|
913
|
+
* until the server confirms them.
|
|
914
|
+
*
|
|
915
|
+
* Pre-fix every retry attempt also minted a NEW batchId via
|
|
916
|
+
* `splice + mintBatchId`, defeating the backend's
|
|
917
|
+
* `Idempotency-Key` dedup. Reuse via this slot brings web in
|
|
918
|
+
* lockstep with node (which already had the field).
|
|
919
|
+
*/
|
|
920
|
+
this.pendingBatch = null;
|
|
921
|
+
this.pendingBatchId = null;
|
|
564
922
|
this.dropped = 0;
|
|
565
923
|
this.inFlight = 0;
|
|
566
924
|
this.lastFlushAt = 0;
|
|
@@ -593,7 +951,7 @@ var EventQueue = class {
|
|
|
593
951
|
this.cfg.onDrop?.(overflow);
|
|
594
952
|
}
|
|
595
953
|
this.cfg.onBufferChange?.(this.buffer.length);
|
|
596
|
-
this.
|
|
954
|
+
this.persistAll();
|
|
597
955
|
if (this.buffer.length >= this.cfg.batchSize) {
|
|
598
956
|
void this.flush();
|
|
599
957
|
} else {
|
|
@@ -602,22 +960,44 @@ var EventQueue = class {
|
|
|
602
960
|
}
|
|
603
961
|
/**
|
|
604
962
|
* Flush the buffer to /v1/events. Resolves when the network call
|
|
605
|
-
* completes (success or failure). On
|
|
606
|
-
*
|
|
963
|
+
* completes (success or failure). On a retryable failure the batch
|
|
964
|
+
* stays in `pendingBatch` for the next scheduled retry — the SAME
|
|
965
|
+
* batch with the SAME `Idempotency-Key` is re-sent (Stripe pattern).
|
|
966
|
+
*
|
|
967
|
+
* Three terminal states from one call:
|
|
968
|
+
* - 2xx success: pendingBatch cleared, persisted state collapses to
|
|
969
|
+
* just `buffer` (any new events that arrived during in-flight).
|
|
970
|
+
* - 4xx permanent (except 408/429): pendingBatch DROPPED, `dropped`
|
|
971
|
+
* counter increments, a `permanent_failure` signal fires. The
|
|
972
|
+
* server is telling us our request is malformed / our key is
|
|
973
|
+
* revoked / we lack permission — retrying with the same key
|
|
974
|
+
* forever just grows the queue while the customer thinks events
|
|
975
|
+
* are landing.
|
|
976
|
+
* - 5xx / network / 408 / 429: pendingBatch + batchId stay; backoff
|
|
977
|
+
* schedules a retry; the next `flush()` re-uses both.
|
|
607
978
|
*
|
|
608
979
|
* `options.keepalive` marks the underlying fetch as keepalive so the
|
|
609
|
-
* browser keeps the request alive past page unload. Use
|
|
610
|
-
*
|
|
980
|
+
* browser keeps the request alive past page unload. Use for terminal
|
|
981
|
+
* flushes (pagehide / visibilitychange→hidden / beforeunload).
|
|
611
982
|
*/
|
|
612
983
|
async flush(options = {}) {
|
|
613
|
-
|
|
984
|
+
let batch;
|
|
985
|
+
let batchId;
|
|
986
|
+
if (this.pendingBatch !== null && this.pendingBatchId !== null) {
|
|
987
|
+
batch = this.pendingBatch;
|
|
988
|
+
batchId = this.pendingBatchId;
|
|
989
|
+
} else {
|
|
990
|
+
if (this.buffer.length === 0) return null;
|
|
991
|
+
batch = this.buffer.splice(0);
|
|
992
|
+
batchId = this.mintBatchId();
|
|
993
|
+
this.pendingBatch = batch;
|
|
994
|
+
this.pendingBatchId = batchId;
|
|
995
|
+
this.inFlight += batch.length;
|
|
996
|
+
this.cfg.onBufferChange?.(this.buffer.length);
|
|
997
|
+
this.persistAll();
|
|
998
|
+
}
|
|
614
999
|
this.cancelTimerIfSet();
|
|
615
1000
|
this.nextRetryAt = null;
|
|
616
|
-
const batch = this.buffer.splice(0);
|
|
617
|
-
const batchId = this.mintBatchId();
|
|
618
|
-
this.inFlight += batch.length;
|
|
619
|
-
this.persistent?.save(this.buffer);
|
|
620
|
-
this.cfg.onBufferChange?.(this.buffer.length);
|
|
621
1001
|
try {
|
|
622
1002
|
const env = this.cfg.envelope();
|
|
623
1003
|
const result = await this.cfg.http.request("POST", "/events", {
|
|
@@ -636,20 +1016,33 @@ var EventQueue = class {
|
|
|
636
1016
|
this.lastFlushAt = Date.now();
|
|
637
1017
|
this.lastError = null;
|
|
638
1018
|
this.inFlight -= batch.length;
|
|
1019
|
+
this.pendingBatch = null;
|
|
1020
|
+
this.pendingBatchId = null;
|
|
639
1021
|
this.retry.recordSuccess();
|
|
640
|
-
this.
|
|
1022
|
+
this.persistAll();
|
|
641
1023
|
if (!this.firstFlushFired) {
|
|
642
1024
|
this.firstFlushFired = true;
|
|
643
1025
|
this.cfg.onFirstFlushSuccess?.();
|
|
644
1026
|
}
|
|
645
1027
|
return result;
|
|
646
1028
|
} catch (err) {
|
|
647
|
-
this.buffer.unshift(...batch);
|
|
648
|
-
this.inFlight -= batch.length;
|
|
649
1029
|
const message = err instanceof Error ? err.message : String(err);
|
|
650
1030
|
this.lastError = message;
|
|
651
|
-
|
|
652
|
-
|
|
1031
|
+
if (isPermanent4xx(err)) {
|
|
1032
|
+
const droppedCount = batch.length;
|
|
1033
|
+
this.pendingBatch = null;
|
|
1034
|
+
this.pendingBatchId = null;
|
|
1035
|
+
this.inFlight -= droppedCount;
|
|
1036
|
+
this.dropped += droppedCount;
|
|
1037
|
+
this.persistAll();
|
|
1038
|
+
this.cfg.onDrop?.(droppedCount);
|
|
1039
|
+
this.cfg.onPermanentFailure?.({
|
|
1040
|
+
status: err.status ?? 0,
|
|
1041
|
+
droppedCount,
|
|
1042
|
+
lastError: message
|
|
1043
|
+
});
|
|
1044
|
+
return null;
|
|
1045
|
+
}
|
|
653
1046
|
const retryAfterMs = extractRetryAfterMs(err);
|
|
654
1047
|
const delay = this.retry.nextDelay(retryAfterMs);
|
|
655
1048
|
this.scheduleRetry(delay);
|
|
@@ -667,6 +1060,8 @@ var EventQueue = class {
|
|
|
667
1060
|
this.cancelTimerIfSet();
|
|
668
1061
|
this.nextRetryAt = null;
|
|
669
1062
|
this.buffer = [];
|
|
1063
|
+
this.pendingBatch = null;
|
|
1064
|
+
this.pendingBatchId = null;
|
|
670
1065
|
this.dropped = 0;
|
|
671
1066
|
this.inFlight = 0;
|
|
672
1067
|
this.lastError = null;
|
|
@@ -676,6 +1071,10 @@ var EventQueue = class {
|
|
|
676
1071
|
}
|
|
677
1072
|
getStats() {
|
|
678
1073
|
return {
|
|
1074
|
+
// `buffered` counts events waiting for their FIRST flush. The
|
|
1075
|
+
// in-flight pendingBatch (retrying) is tracked separately via
|
|
1076
|
+
// `inFlight` — surfacing both lets diagnostics show "we have
|
|
1077
|
+
// events stuck retrying" distinct from "new events arriving".
|
|
679
1078
|
buffered: this.buffer.length,
|
|
680
1079
|
dropped: this.dropped,
|
|
681
1080
|
inFlight: this.inFlight,
|
|
@@ -685,6 +1084,29 @@ var EventQueue = class {
|
|
|
685
1084
|
nextRetryAt: this.nextRetryAt
|
|
686
1085
|
};
|
|
687
1086
|
}
|
|
1087
|
+
/**
|
|
1088
|
+
* The Idempotency-Key of the in-flight pending batch (if any).
|
|
1089
|
+
* Exposed for testing the Stripe-style retry-reuse contract.
|
|
1090
|
+
* Production callers don't need this.
|
|
1091
|
+
*/
|
|
1092
|
+
get pendingIdempotencyKey() {
|
|
1093
|
+
return this.pendingBatchId;
|
|
1094
|
+
}
|
|
1095
|
+
/**
|
|
1096
|
+
* Persist `[...pendingBatch, ...buffer]` — the full set of
|
|
1097
|
+
* not-yet-confirmed events. The next boot rehydrates this exact set
|
|
1098
|
+
* into `buffer` and replays. The server dedups via eventId
|
|
1099
|
+
* (ReplacingMergeTree on the warehouse side), so re-sending an event
|
|
1100
|
+
* that may have already landed is safe.
|
|
1101
|
+
*/
|
|
1102
|
+
persistAll() {
|
|
1103
|
+
if (!this.persistent) return;
|
|
1104
|
+
if (this.pendingBatch === null) {
|
|
1105
|
+
this.persistent.save(this.buffer);
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
this.persistent.save([...this.pendingBatch, ...this.buffer]);
|
|
1109
|
+
}
|
|
688
1110
|
// ---------- internal scheduling ----------
|
|
689
1111
|
scheduleIdleFlush() {
|
|
690
1112
|
this.cancelTimerIfSet();
|
|
@@ -718,6 +1140,14 @@ function extractRetryAfterMs(err) {
|
|
|
718
1140
|
}
|
|
719
1141
|
return void 0;
|
|
720
1142
|
}
|
|
1143
|
+
function isPermanent4xx(err) {
|
|
1144
|
+
if (!err || typeof err !== "object") return false;
|
|
1145
|
+
const status = err.status;
|
|
1146
|
+
if (typeof status !== "number" || !Number.isFinite(status)) return false;
|
|
1147
|
+
if (status < 400 || status >= 500) return false;
|
|
1148
|
+
if (status === 408 || status === 429) return false;
|
|
1149
|
+
return true;
|
|
1150
|
+
}
|
|
721
1151
|
function defaultScheduler(fn, ms) {
|
|
722
1152
|
const id = setTimeout(fn, ms);
|
|
723
1153
|
if (typeof id.unref === "function") {
|
|
@@ -1059,6 +1489,7 @@ var AutoTracker = class {
|
|
|
1059
1489
|
/** Exposed for tests + consumers that want to reset the session manually. */
|
|
1060
1490
|
resetSession() {
|
|
1061
1491
|
if (this.session && !this.session.endedSent) this.emitSessionEnd();
|
|
1492
|
+
this.pageviewId = null;
|
|
1062
1493
|
this.session = this.startNewSession();
|
|
1063
1494
|
this.emitSessionStart();
|
|
1064
1495
|
}
|
|
@@ -1095,6 +1526,7 @@ var AutoTracker = class {
|
|
|
1095
1526
|
const hiddenFor = this.session.hiddenAt ? Date.now() - this.session.hiddenAt : 0;
|
|
1096
1527
|
if (hiddenFor >= SESSION_RESUME_THRESHOLD_MS) {
|
|
1097
1528
|
this.emitSessionEnd();
|
|
1529
|
+
this.pageviewId = null;
|
|
1098
1530
|
this.session = this.startNewSession();
|
|
1099
1531
|
this.emitSessionStart();
|
|
1100
1532
|
} else {
|
|
@@ -1468,7 +1900,7 @@ function validateEventProperties(input, options = {}) {
|
|
|
1468
1900
|
const maxStringLength = options.maxStringLength ?? DEFAULT_MAX_STRING;
|
|
1469
1901
|
const maxBatchPropertyBytes = options.maxBatchPropertyBytes ?? DEFAULT_MAX_BYTES;
|
|
1470
1902
|
const maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH;
|
|
1471
|
-
const seen = /* @__PURE__ */ new
|
|
1903
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1472
1904
|
const visit = (value, key, depth) => {
|
|
1473
1905
|
if (depth > maxDepth) {
|
|
1474
1906
|
warnings.push({ kind: "depth_exceeded", key });
|
|
@@ -1556,6 +1988,7 @@ function validateEventProperties(input, options = {}) {
|
|
|
1556
1988
|
const result = visit(value[i], `${key}[${i}]`, depth + 1);
|
|
1557
1989
|
if (result.keep) out.push(result.value);
|
|
1558
1990
|
}
|
|
1991
|
+
seen.delete(value);
|
|
1559
1992
|
return { keep: true, value: out };
|
|
1560
1993
|
}
|
|
1561
1994
|
if (t === "object") {
|
|
@@ -1570,6 +2003,7 @@ function validateEventProperties(input, options = {}) {
|
|
|
1570
2003
|
const result = visit(obj[k], `${key}.${k}`, depth + 1);
|
|
1571
2004
|
if (result.keep) out[k] = result.value;
|
|
1572
2005
|
}
|
|
2006
|
+
seen.delete(obj);
|
|
1573
2007
|
return { keep: true, value: out };
|
|
1574
2008
|
}
|
|
1575
2009
|
warnings.push({ kind: "non_serialisable", key });
|
|
@@ -1912,35 +2346,27 @@ var ConsentManager = class {
|
|
|
1912
2346
|
};
|
|
1913
2347
|
var EMAIL_PATTERN = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g;
|
|
1914
2348
|
var CARD_PATTERN = /\b\d(?:[ -]?\d){12,18}\b/g;
|
|
1915
|
-
var REPLACEMENT_EMAIL = "
|
|
1916
|
-
var REPLACEMENT_CARD = "
|
|
2349
|
+
var REPLACEMENT_EMAIL = "<email>";
|
|
2350
|
+
var REPLACEMENT_CARD = "<card>";
|
|
1917
2351
|
function scrubPii(value) {
|
|
1918
2352
|
if (!value) return value;
|
|
1919
|
-
|
|
1920
|
-
if (EMAIL_PATTERN.test(out)) {
|
|
1921
|
-
out = out.replace(EMAIL_PATTERN, REPLACEMENT_EMAIL);
|
|
1922
|
-
}
|
|
1923
|
-
EMAIL_PATTERN.lastIndex = 0;
|
|
1924
|
-
if (CARD_PATTERN.test(out)) {
|
|
1925
|
-
out = out.replace(CARD_PATTERN, REPLACEMENT_CARD);
|
|
1926
|
-
}
|
|
1927
|
-
CARD_PATTERN.lastIndex = 0;
|
|
1928
|
-
return out;
|
|
2353
|
+
return value.replace(EMAIL_PATTERN, REPLACEMENT_EMAIL).replace(CARD_PATTERN, REPLACEMENT_CARD);
|
|
1929
2354
|
}
|
|
1930
2355
|
function scrubPiiFromProperties(properties) {
|
|
1931
2356
|
const out = {};
|
|
1932
2357
|
for (const k of Object.keys(properties)) {
|
|
1933
|
-
|
|
1934
|
-
if (typeof v === "string") {
|
|
1935
|
-
out[k] = scrubPii(v);
|
|
1936
|
-
} else if (Array.isArray(v)) {
|
|
1937
|
-
out[k] = v.map((item) => typeof item === "string" ? scrubPii(item) : item);
|
|
1938
|
-
} else {
|
|
1939
|
-
out[k] = v;
|
|
1940
|
-
}
|
|
2358
|
+
out[k] = scrubValue(properties[k]);
|
|
1941
2359
|
}
|
|
1942
2360
|
return out;
|
|
1943
2361
|
}
|
|
2362
|
+
function scrubValue(v) {
|
|
2363
|
+
if (typeof v === "string") return scrubPii(v);
|
|
2364
|
+
if (Array.isArray(v)) return v.map(scrubValue);
|
|
2365
|
+
if (v && typeof v === "object" && v.constructor === Object) {
|
|
2366
|
+
return scrubPiiFromProperties(v);
|
|
2367
|
+
}
|
|
2368
|
+
return v;
|
|
2369
|
+
}
|
|
1944
2370
|
|
|
1945
2371
|
// src/breadcrumbs.ts
|
|
1946
2372
|
var BreadcrumbBuffer = class {
|
|
@@ -2230,16 +2656,18 @@ var ErrorTracker = class {
|
|
|
2230
2656
|
const url = typeof input === "string" ? input : input?.url ?? "";
|
|
2231
2657
|
const method = (init.method || "GET").toUpperCase();
|
|
2232
2658
|
const start = Date.now();
|
|
2233
|
-
this.opts.
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2659
|
+
if (!isSelfRequest(url, this.opts.selfHostname)) {
|
|
2660
|
+
this.opts.breadcrumbs.add({
|
|
2661
|
+
timestamp: start,
|
|
2662
|
+
category: "http",
|
|
2663
|
+
message: `${method} ${url}`,
|
|
2664
|
+
data: { url, method }
|
|
2665
|
+
});
|
|
2666
|
+
}
|
|
2239
2667
|
try {
|
|
2240
2668
|
const response = await origFetch(...args);
|
|
2241
2669
|
if (response.status >= 500 && this.opts.isConsented()) {
|
|
2242
|
-
if (!url.
|
|
2670
|
+
if (!isSelfRequest(url, this.opts.selfHostname)) {
|
|
2243
2671
|
this.captureHttp({
|
|
2244
2672
|
url,
|
|
2245
2673
|
method,
|
|
@@ -2288,7 +2716,7 @@ var ErrorTracker = class {
|
|
|
2288
2716
|
try {
|
|
2289
2717
|
if (xhr.status >= 500 && tracker.opts.isConsented()) {
|
|
2290
2718
|
const url = xhr._cdUrl ?? "";
|
|
2291
|
-
if (!url.
|
|
2719
|
+
if (!isSelfRequest(url, tracker.opts.selfHostname)) {
|
|
2292
2720
|
tracker.captureHttp({
|
|
2293
2721
|
url,
|
|
2294
2722
|
method: (xhr._cdMethod ?? "GET").toUpperCase(),
|
|
@@ -2448,9 +2876,10 @@ var ErrorTracker = class {
|
|
|
2448
2876
|
if (!this.passesSample(err)) return;
|
|
2449
2877
|
if (!this.passesRateLimit(err)) return;
|
|
2450
2878
|
let finalErr = err;
|
|
2451
|
-
|
|
2879
|
+
const hook = this.opts.beforeSend?.();
|
|
2880
|
+
if (hook) {
|
|
2452
2881
|
try {
|
|
2453
|
-
finalErr =
|
|
2882
|
+
finalErr = hook(err);
|
|
2454
2883
|
} catch {
|
|
2455
2884
|
finalErr = err;
|
|
2456
2885
|
}
|
|
@@ -2632,6 +3061,22 @@ function safeClone(v) {
|
|
|
2632
3061
|
function safeStringify2(v) {
|
|
2633
3062
|
return coerceErrorPayload(v).message;
|
|
2634
3063
|
}
|
|
3064
|
+
function extractSelfHostname(baseUrl) {
|
|
3065
|
+
if (!baseUrl || typeof baseUrl !== "string") return null;
|
|
3066
|
+
try {
|
|
3067
|
+
return new URL(baseUrl).hostname.toLowerCase();
|
|
3068
|
+
} catch {
|
|
3069
|
+
return null;
|
|
3070
|
+
}
|
|
3071
|
+
}
|
|
3072
|
+
function isSelfRequest(requestUrl, selfHostname) {
|
|
3073
|
+
if (!selfHostname || !requestUrl) return false;
|
|
3074
|
+
try {
|
|
3075
|
+
return new URL(requestUrl).hostname.toLowerCase() === selfHostname;
|
|
3076
|
+
} catch {
|
|
3077
|
+
return false;
|
|
3078
|
+
}
|
|
3079
|
+
}
|
|
2635
3080
|
|
|
2636
3081
|
// src/crossdeck.ts
|
|
2637
3082
|
var CrossdeckClient = class {
|
|
@@ -2648,6 +3093,28 @@ var CrossdeckClient = class {
|
|
|
2648
3093
|
* mismatched env fails fast at boot rather than at first event-flush.
|
|
2649
3094
|
*/
|
|
2650
3095
|
init(options) {
|
|
3096
|
+
if (this.state) {
|
|
3097
|
+
try {
|
|
3098
|
+
this.state.uninstallUnloadFlush?.();
|
|
3099
|
+
} catch {
|
|
3100
|
+
}
|
|
3101
|
+
try {
|
|
3102
|
+
this.state.autoTracker?.uninstall();
|
|
3103
|
+
} catch {
|
|
3104
|
+
}
|
|
3105
|
+
try {
|
|
3106
|
+
this.state.webVitals?.uninstall();
|
|
3107
|
+
} catch {
|
|
3108
|
+
}
|
|
3109
|
+
try {
|
|
3110
|
+
this.state.errors?.uninstall();
|
|
3111
|
+
} catch {
|
|
3112
|
+
}
|
|
3113
|
+
try {
|
|
3114
|
+
void this.state.events.flush({ keepalive: true });
|
|
3115
|
+
} catch {
|
|
3116
|
+
}
|
|
3117
|
+
}
|
|
2651
3118
|
if (!options.publicKey || !options.publicKey.startsWith("cd_pub_")) {
|
|
2652
3119
|
throw new CrossdeckError({
|
|
2653
3120
|
type: "configuration_error",
|
|
@@ -2694,7 +3161,12 @@ var CrossdeckClient = class {
|
|
|
2694
3161
|
// load still flushes if the user leaves quickly (the keepalive
|
|
2695
3162
|
// pagehide handler picks up anything that doesn't); long enough
|
|
2696
3163
|
// that bursts of clicks coalesce into one network round-trip.
|
|
2697
|
-
|
|
3164
|
+
// v1.4.0 Phase 3.3 — flush interval default parity. Pre-
|
|
3165
|
+
// v1.4.0: Web/Node 1500ms, RN/Swift/Android 5000ms. All
|
|
3166
|
+
// converged on 2000ms (the Stripe-adjacent industry norm)
|
|
3167
|
+
// so cross-platform funnels show events landing at the
|
|
3168
|
+
// same cadence on every SDK. Per-instance override stays.
|
|
3169
|
+
eventFlushIntervalMs: options.eventFlushIntervalMs ?? 2e3,
|
|
2698
3170
|
sdkVersion: options.sdkVersion ?? SDK_VERSION,
|
|
2699
3171
|
autoTrack,
|
|
2700
3172
|
appVersion: options.appVersion ?? null
|
|
@@ -2755,6 +3227,15 @@ var CrossdeckClient = class {
|
|
|
2755
3227
|
`Event flush failed (${info.lastError}). Retrying in ${info.delayMs}ms (attempt ${info.consecutiveFailures}).`,
|
|
2756
3228
|
{ ...info }
|
|
2757
3229
|
);
|
|
3230
|
+
},
|
|
3231
|
+
onPermanentFailure: (info) => {
|
|
3232
|
+
const headline = `[crossdeck] Event batch DROPPED (status ${info.status}): ${info.lastError}. ${info.droppedCount} event(s) lost \u2014 check your publishable key + app config.`;
|
|
3233
|
+
console.error(headline);
|
|
3234
|
+
debug.emit(
|
|
3235
|
+
"sdk.flush_permanent_failure",
|
|
3236
|
+
headline,
|
|
3237
|
+
{ ...info }
|
|
3238
|
+
);
|
|
2758
3239
|
}
|
|
2759
3240
|
});
|
|
2760
3241
|
const deviceInfo = autoTrack.deviceInfo ? collectDeviceInfo({ appVersion: opts.appVersion ?? void 0 }) : opts.appVersion ? { appVersion: opts.appVersion } : {};
|
|
@@ -2821,8 +3302,20 @@ var CrossdeckClient = class {
|
|
|
2821
3302
|
report: (err) => this.reportError(err),
|
|
2822
3303
|
getContext: () => ({ ...this.state.errorContext }),
|
|
2823
3304
|
getTags: () => ({ ...this.state.errorTags }),
|
|
2824
|
-
|
|
2825
|
-
|
|
3305
|
+
// GETTER, not a captured value — `setErrorBeforeSend()` mutates
|
|
3306
|
+
// `state.errorBeforeSend` after init() and the tracker MUST
|
|
3307
|
+
// pick up the new hook on the next error. The pre-fix shape
|
|
3308
|
+
// (`beforeSend: this.state!.errorBeforeSend`) snapshotted
|
|
3309
|
+
// `null` at construction and made the customer's PII scrubber
|
|
3310
|
+
// silently inert. See error-capture.ts:ErrorTrackerOptions.beforeSend.
|
|
3311
|
+
beforeSend: () => this.state.errorBeforeSend,
|
|
3312
|
+
isConsented: () => this.state.consent.errors,
|
|
3313
|
+
// Derived from the configured baseUrl at init() time. Used by
|
|
3314
|
+
// the fetch / XHR wrappers to skip captureHttp on Crossdeck's
|
|
3315
|
+
// own requests — pre-fix the skip was hardcoded to
|
|
3316
|
+
// `api.cross-deck.com` and broke for customers on staging /
|
|
3317
|
+
// regional / self-hosted base URLs (recursive capture loop).
|
|
3318
|
+
selfHostname: extractSelfHostname(opts.baseUrl)
|
|
2826
3319
|
});
|
|
2827
3320
|
this.state.errors = tracker;
|
|
2828
3321
|
tracker.install();
|
|
@@ -2901,13 +3394,10 @@ var CrossdeckClient = class {
|
|
|
2901
3394
|
};
|
|
2902
3395
|
if (options?.email) body.email = options.email;
|
|
2903
3396
|
if (traits) body.traits = traits;
|
|
3397
|
+
s.entitlements.setUserKey(userId);
|
|
2904
3398
|
const result = await s.http.request("POST", "/identity/alias", {
|
|
2905
3399
|
body
|
|
2906
3400
|
});
|
|
2907
|
-
const priorCdcust = s.identity.crossdeckCustomerId;
|
|
2908
|
-
if (priorCdcust && result.crossdeckCustomerId && priorCdcust !== result.crossdeckCustomerId) {
|
|
2909
|
-
s.entitlements.clear();
|
|
2910
|
-
}
|
|
2911
3401
|
s.identity.setCrossdeckCustomerId(result.crossdeckCustomerId);
|
|
2912
3402
|
s.developerUserId = userId;
|
|
2913
3403
|
return result;
|
|
@@ -3347,11 +3837,25 @@ var CrossdeckClient = class {
|
|
|
3347
3837
|
message: "syncPurchases requires a signedTransactionInfo string from StoreKit 2."
|
|
3348
3838
|
});
|
|
3349
3839
|
}
|
|
3840
|
+
const rail = input.rail ?? "apple";
|
|
3841
|
+
const body = { ...input, rail };
|
|
3842
|
+
const idempotencyKey = deriveIdempotencyKeyForPurchase(body);
|
|
3350
3843
|
const result = await s.http.request("POST", "/purchases/sync", {
|
|
3351
|
-
body
|
|
3844
|
+
body,
|
|
3845
|
+
idempotencyKey
|
|
3352
3846
|
});
|
|
3353
3847
|
s.identity.setCrossdeckCustomerId(result.crossdeckCustomerId);
|
|
3354
3848
|
s.entitlements.setFromList(result.entitlements);
|
|
3849
|
+
try {
|
|
3850
|
+
const sourceProductId = result.entitlements[0]?.source.productId;
|
|
3851
|
+
const sourceSubscriptionId = result.entitlements[0]?.source.subscriptionId;
|
|
3852
|
+
const props = { rail };
|
|
3853
|
+
if (sourceProductId) props.productId = sourceProductId;
|
|
3854
|
+
if (sourceSubscriptionId) props.subscriptionId = sourceSubscriptionId;
|
|
3855
|
+
if (result.idempotent_replay) props.idempotent_replay = true;
|
|
3856
|
+
this.track("purchase.completed", props);
|
|
3857
|
+
} catch {
|
|
3858
|
+
}
|
|
3355
3859
|
s.debug.emit(
|
|
3356
3860
|
"sdk.purchase_evidence_sent",
|
|
3357
3861
|
"StoreKit transaction forwarded. Waiting for backend verification.",
|
|
@@ -3407,13 +3911,15 @@ var CrossdeckClient = class {
|
|
|
3407
3911
|
}
|
|
3408
3912
|
this.state.autoTracker?.uninstall();
|
|
3409
3913
|
this.state.identity.reset();
|
|
3410
|
-
this.state.entitlements.
|
|
3914
|
+
this.state.entitlements.clearAll();
|
|
3411
3915
|
this.state.events.reset();
|
|
3412
3916
|
this.state.superProps.clear();
|
|
3413
3917
|
this.state.breadcrumbs.clear();
|
|
3414
3918
|
this.state.errorContext = {};
|
|
3415
3919
|
this.state.errorTags = {};
|
|
3416
3920
|
this.state.developerUserId = null;
|
|
3921
|
+
this.state.lastServerTime = null;
|
|
3922
|
+
this.state.lastClientTime = null;
|
|
3417
3923
|
if (this.state.autoTracker) {
|
|
3418
3924
|
const tracker = new AutoTracker(
|
|
3419
3925
|
this.state.options.autoTrack,
|
|
@@ -3542,7 +4048,9 @@ function isLocalHostname() {
|
|
|
3542
4048
|
const hostname = w?.location?.hostname;
|
|
3543
4049
|
if (!hostname) return false;
|
|
3544
4050
|
if (hostname === "localhost" || hostname === "127.0.0.1") return true;
|
|
4051
|
+
if (hostname === "0.0.0.0") return true;
|
|
3545
4052
|
if (hostname === "::1" || hostname === "[::1]") return true;
|
|
4053
|
+
if (/^\[?fe80::/i.test(hostname)) return true;
|
|
3546
4054
|
if (hostname.endsWith(".local")) return true;
|
|
3547
4055
|
if (/^10\./.test(hostname)) return true;
|
|
3548
4056
|
if (/^192\.168\./.test(hostname)) return true;
|