@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/vue.mjs
CHANGED
|
@@ -66,9 +66,11 @@ function typeMapForStatus(status) {
|
|
|
66
66
|
return "internal_error";
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
// src/
|
|
69
|
+
// src/_version.ts
|
|
70
|
+
var SDK_VERSION = "1.4.2";
|
|
70
71
|
var SDK_NAME = "@cross-deck/web";
|
|
71
|
-
|
|
72
|
+
|
|
73
|
+
// src/http.ts
|
|
72
74
|
var DEFAULT_BASE_URL = "https://api.cross-deck.com/v1";
|
|
73
75
|
var DEFAULT_TIMEOUT_MS = 15e3;
|
|
74
76
|
var HttpClient = class {
|
|
@@ -315,29 +317,258 @@ function randomChars(count) {
|
|
|
315
317
|
return out.join("");
|
|
316
318
|
}
|
|
317
319
|
|
|
320
|
+
// src/hash.ts
|
|
321
|
+
var K = new Uint32Array([
|
|
322
|
+
1116352408,
|
|
323
|
+
1899447441,
|
|
324
|
+
3049323471,
|
|
325
|
+
3921009573,
|
|
326
|
+
961987163,
|
|
327
|
+
1508970993,
|
|
328
|
+
2453635748,
|
|
329
|
+
2870763221,
|
|
330
|
+
3624381080,
|
|
331
|
+
310598401,
|
|
332
|
+
607225278,
|
|
333
|
+
1426881987,
|
|
334
|
+
1925078388,
|
|
335
|
+
2162078206,
|
|
336
|
+
2614888103,
|
|
337
|
+
3248222580,
|
|
338
|
+
3835390401,
|
|
339
|
+
4022224774,
|
|
340
|
+
264347078,
|
|
341
|
+
604807628,
|
|
342
|
+
770255983,
|
|
343
|
+
1249150122,
|
|
344
|
+
1555081692,
|
|
345
|
+
1996064986,
|
|
346
|
+
2554220882,
|
|
347
|
+
2821834349,
|
|
348
|
+
2952996808,
|
|
349
|
+
3210313671,
|
|
350
|
+
3336571891,
|
|
351
|
+
3584528711,
|
|
352
|
+
113926993,
|
|
353
|
+
338241895,
|
|
354
|
+
666307205,
|
|
355
|
+
773529912,
|
|
356
|
+
1294757372,
|
|
357
|
+
1396182291,
|
|
358
|
+
1695183700,
|
|
359
|
+
1986661051,
|
|
360
|
+
2177026350,
|
|
361
|
+
2456956037,
|
|
362
|
+
2730485921,
|
|
363
|
+
2820302411,
|
|
364
|
+
3259730800,
|
|
365
|
+
3345764771,
|
|
366
|
+
3516065817,
|
|
367
|
+
3600352804,
|
|
368
|
+
4094571909,
|
|
369
|
+
275423344,
|
|
370
|
+
430227734,
|
|
371
|
+
506948616,
|
|
372
|
+
659060556,
|
|
373
|
+
883997877,
|
|
374
|
+
958139571,
|
|
375
|
+
1322822218,
|
|
376
|
+
1537002063,
|
|
377
|
+
1747873779,
|
|
378
|
+
1955562222,
|
|
379
|
+
2024104815,
|
|
380
|
+
2227730452,
|
|
381
|
+
2361852424,
|
|
382
|
+
2428436474,
|
|
383
|
+
2756734187,
|
|
384
|
+
3204031479,
|
|
385
|
+
3329325298
|
|
386
|
+
]);
|
|
387
|
+
function utf8Bytes(input) {
|
|
388
|
+
if (typeof TextEncoder !== "undefined") {
|
|
389
|
+
return new TextEncoder().encode(input);
|
|
390
|
+
}
|
|
391
|
+
const out = [];
|
|
392
|
+
for (let i = 0; i < input.length; i++) {
|
|
393
|
+
let codePoint = input.charCodeAt(i);
|
|
394
|
+
if (codePoint >= 55296 && codePoint <= 56319 && i + 1 < input.length) {
|
|
395
|
+
const next = input.charCodeAt(i + 1);
|
|
396
|
+
if (next >= 56320 && next <= 57343) {
|
|
397
|
+
codePoint = 65536 + (codePoint - 55296 << 10) + (next - 56320);
|
|
398
|
+
i++;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
if (codePoint < 128) {
|
|
402
|
+
out.push(codePoint);
|
|
403
|
+
} else if (codePoint < 2048) {
|
|
404
|
+
out.push(192 | codePoint >> 6);
|
|
405
|
+
out.push(128 | codePoint & 63);
|
|
406
|
+
} else if (codePoint < 65536) {
|
|
407
|
+
out.push(224 | codePoint >> 12);
|
|
408
|
+
out.push(128 | codePoint >> 6 & 63);
|
|
409
|
+
out.push(128 | codePoint & 63);
|
|
410
|
+
} else {
|
|
411
|
+
out.push(240 | codePoint >> 18);
|
|
412
|
+
out.push(128 | codePoint >> 12 & 63);
|
|
413
|
+
out.push(128 | codePoint >> 6 & 63);
|
|
414
|
+
out.push(128 | codePoint & 63);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return new Uint8Array(out);
|
|
418
|
+
}
|
|
419
|
+
function sha256Hex(input) {
|
|
420
|
+
const bytes = utf8Bytes(input);
|
|
421
|
+
const bitLength = bytes.length * 8;
|
|
422
|
+
const blockCount = Math.floor((bytes.length + 9 + 63) / 64);
|
|
423
|
+
const padded = new Uint8Array(blockCount * 64);
|
|
424
|
+
padded.set(bytes);
|
|
425
|
+
padded[bytes.length] = 128;
|
|
426
|
+
const high = Math.floor(bitLength / 4294967296);
|
|
427
|
+
const low = bitLength >>> 0;
|
|
428
|
+
const lenOffset = padded.length - 8;
|
|
429
|
+
padded[lenOffset + 0] = high >>> 24 & 255;
|
|
430
|
+
padded[lenOffset + 1] = high >>> 16 & 255;
|
|
431
|
+
padded[lenOffset + 2] = high >>> 8 & 255;
|
|
432
|
+
padded[lenOffset + 3] = high & 255;
|
|
433
|
+
padded[lenOffset + 4] = low >>> 24 & 255;
|
|
434
|
+
padded[lenOffset + 5] = low >>> 16 & 255;
|
|
435
|
+
padded[lenOffset + 6] = low >>> 8 & 255;
|
|
436
|
+
padded[lenOffset + 7] = low & 255;
|
|
437
|
+
const H = new Uint32Array([
|
|
438
|
+
1779033703,
|
|
439
|
+
3144134277,
|
|
440
|
+
1013904242,
|
|
441
|
+
2773480762,
|
|
442
|
+
1359893119,
|
|
443
|
+
2600822924,
|
|
444
|
+
528734635,
|
|
445
|
+
1541459225
|
|
446
|
+
]);
|
|
447
|
+
const W = new Uint32Array(64);
|
|
448
|
+
for (let block = 0; block < blockCount; block++) {
|
|
449
|
+
const offset = block * 64;
|
|
450
|
+
for (let t = 0; t < 16; t++) {
|
|
451
|
+
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;
|
|
452
|
+
}
|
|
453
|
+
for (let t = 16; t < 64; t++) {
|
|
454
|
+
const w15 = W[t - 15];
|
|
455
|
+
const w2 = W[t - 2];
|
|
456
|
+
const s0 = (w15 >>> 7 | w15 << 25) ^ (w15 >>> 18 | w15 << 14) ^ w15 >>> 3;
|
|
457
|
+
const s1 = (w2 >>> 17 | w2 << 15) ^ (w2 >>> 19 | w2 << 13) ^ w2 >>> 10;
|
|
458
|
+
W[t] = W[t - 16] + s0 + W[t - 7] + s1 >>> 0;
|
|
459
|
+
}
|
|
460
|
+
let a = H[0], b = H[1], c = H[2], d = H[3];
|
|
461
|
+
let e = H[4], f = H[5], g = H[6], h = H[7];
|
|
462
|
+
for (let t = 0; t < 64; t++) {
|
|
463
|
+
const S1 = (e >>> 6 | e << 26) ^ (e >>> 11 | e << 21) ^ (e >>> 25 | e << 7);
|
|
464
|
+
const ch = e & f ^ ~e & g;
|
|
465
|
+
const temp1 = h + S1 + ch + K[t] + W[t] >>> 0;
|
|
466
|
+
const S0 = (a >>> 2 | a << 30) ^ (a >>> 13 | a << 19) ^ (a >>> 22 | a << 10);
|
|
467
|
+
const maj = a & b ^ a & c ^ b & c;
|
|
468
|
+
const temp2 = S0 + maj >>> 0;
|
|
469
|
+
h = g;
|
|
470
|
+
g = f;
|
|
471
|
+
f = e;
|
|
472
|
+
e = d + temp1 >>> 0;
|
|
473
|
+
d = c;
|
|
474
|
+
c = b;
|
|
475
|
+
b = a;
|
|
476
|
+
a = temp1 + temp2 >>> 0;
|
|
477
|
+
}
|
|
478
|
+
H[0] = H[0] + a >>> 0;
|
|
479
|
+
H[1] = H[1] + b >>> 0;
|
|
480
|
+
H[2] = H[2] + c >>> 0;
|
|
481
|
+
H[3] = H[3] + d >>> 0;
|
|
482
|
+
H[4] = H[4] + e >>> 0;
|
|
483
|
+
H[5] = H[5] + f >>> 0;
|
|
484
|
+
H[6] = H[6] + g >>> 0;
|
|
485
|
+
H[7] = H[7] + h >>> 0;
|
|
486
|
+
}
|
|
487
|
+
let hex = "";
|
|
488
|
+
for (let i = 0; i < 8; i++) {
|
|
489
|
+
hex += H[i].toString(16).padStart(8, "0");
|
|
490
|
+
}
|
|
491
|
+
return hex;
|
|
492
|
+
}
|
|
493
|
+
|
|
318
494
|
// src/entitlement-cache.ts
|
|
319
495
|
var DEFAULT_STALE_AFTER_MS = 24 * 60 * 60 * 1e3;
|
|
320
|
-
var
|
|
496
|
+
var ANON_SUFFIX = "_anon";
|
|
497
|
+
var INDEX_SUFFIX = "_index";
|
|
498
|
+
var EntitlementCache = class _EntitlementCache {
|
|
321
499
|
/**
|
|
322
|
-
* @param storage
|
|
323
|
-
*
|
|
324
|
-
*
|
|
325
|
-
*
|
|
326
|
-
* @param
|
|
327
|
-
*
|
|
328
|
-
*
|
|
500
|
+
* @param storage Device storage adapter. When omitted (tests) or
|
|
501
|
+
* a MemoryStorage (strict-consent / no-persistence
|
|
502
|
+
* mode) the cache is session-only — durability is
|
|
503
|
+
* simply absent, never wrong.
|
|
504
|
+
* @param storageKeyPrefix Prefix used to derive per-user storage keys
|
|
505
|
+
* (`<prefix>:<sha256(userId)>`). Default
|
|
506
|
+
* `crossdeck:entitlements`. The trailing user
|
|
507
|
+
* suffix is filled at identify() / reset()
|
|
508
|
+
* time — see [[setUserKey]].
|
|
509
|
+
* @param staleAfterMs Age past which last-known-good is flagged stale
|
|
510
|
+
* even without a failed refresh. Default 24h.
|
|
329
511
|
*/
|
|
330
|
-
constructor(storage,
|
|
512
|
+
constructor(storage, storageKeyPrefix = "crossdeck:entitlements", staleAfterMs = DEFAULT_STALE_AFTER_MS) {
|
|
331
513
|
this.all = [];
|
|
332
514
|
this.lastUpdated = 0;
|
|
333
515
|
this.lastRefreshFailedAt = 0;
|
|
334
516
|
this.listeners = /* @__PURE__ */ new Set();
|
|
335
517
|
this.listenerErrorCount = 0;
|
|
518
|
+
this.currentSuffix = ANON_SUFFIX;
|
|
336
519
|
this.storage = storage;
|
|
337
|
-
this.
|
|
520
|
+
this.storageKeyPrefix = storageKeyPrefix;
|
|
338
521
|
this.staleAfterMs = staleAfterMs;
|
|
339
522
|
this.hydrate();
|
|
340
523
|
}
|
|
524
|
+
/** The full storage key the current-user blob is persisted under. */
|
|
525
|
+
get storageKey() {
|
|
526
|
+
return `${this.storageKeyPrefix}:${this.currentSuffix}`;
|
|
527
|
+
}
|
|
528
|
+
/** Key of the index blob — a JSON array of every suffix we've
|
|
529
|
+
* written. Used by clearAll() to scope a logout-wipe. */
|
|
530
|
+
get indexKey() {
|
|
531
|
+
return `${this.storageKeyPrefix}:${INDEX_SUFFIX}`;
|
|
532
|
+
}
|
|
533
|
+
/** Derive a stable suffix for a developerUserId via SHA-256. The
|
|
534
|
+
* raw userId never lands in the storage key — protects against
|
|
535
|
+
* accidentally leaking email-style identifiers through DevTools
|
|
536
|
+
* inspection. Pass `null` to switch back to the anonymous slot. */
|
|
537
|
+
static suffixForUserId(userId) {
|
|
538
|
+
if (userId == null || userId === "") return ANON_SUFFIX;
|
|
539
|
+
return sha256Hex(userId);
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* Switch the cache to a different user's storage slot. Bank-grade
|
|
543
|
+
* three-layer isolation:
|
|
544
|
+
* (a) Physical key separation — `<prefix>:<sha256(userId)>` so
|
|
545
|
+
* a user-switch can't physically read prior user's data
|
|
546
|
+
* even if the in-memory clear was skipped.
|
|
547
|
+
* (b) Unconditional in-memory clear — invoked whenever the
|
|
548
|
+
* active suffix changes, even on same-id re-identify.
|
|
549
|
+
* (c) Re-hydrate from the new slot — a returning user observes
|
|
550
|
+
* their last-known-good cache from storage immediately.
|
|
551
|
+
*
|
|
552
|
+
* Caller (identify() / reset()) MUST invoke this BEFORE the next
|
|
553
|
+
* setFromList() so the write lands under the right key.
|
|
554
|
+
*/
|
|
555
|
+
setUserKey(userId) {
|
|
556
|
+
const nextSuffix = _EntitlementCache.suffixForUserId(userId);
|
|
557
|
+
if (nextSuffix === this.currentSuffix) {
|
|
558
|
+
this.all = [];
|
|
559
|
+
this.lastUpdated = 0;
|
|
560
|
+
this.lastRefreshFailedAt = 0;
|
|
561
|
+
this.notify();
|
|
562
|
+
this.hydrate();
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
this.currentSuffix = nextSuffix;
|
|
566
|
+
this.all = [];
|
|
567
|
+
this.lastUpdated = 0;
|
|
568
|
+
this.lastRefreshFailedAt = 0;
|
|
569
|
+
this.hydrate();
|
|
570
|
+
this.notify();
|
|
571
|
+
}
|
|
341
572
|
/**
|
|
342
573
|
* Sync read — true iff the entitlement is currently granting access.
|
|
343
574
|
*
|
|
@@ -407,12 +638,13 @@ var EntitlementCache = class {
|
|
|
407
638
|
this.lastUpdated = Date.now();
|
|
408
639
|
this.lastRefreshFailedAt = 0;
|
|
409
640
|
this.persist();
|
|
641
|
+
this.recordSuffixInIndex(this.currentSuffix);
|
|
410
642
|
this.notify();
|
|
411
643
|
}
|
|
412
644
|
/**
|
|
413
|
-
* Wipe
|
|
414
|
-
*
|
|
415
|
-
*
|
|
645
|
+
* Wipe the CURRENT user's slot. Used internally when a single
|
|
646
|
+
* user's cache needs to be invalidated without affecting other
|
|
647
|
+
* persisted slots. The full-logout path is [[clearAll]].
|
|
416
648
|
*/
|
|
417
649
|
clear() {
|
|
418
650
|
this.all = [];
|
|
@@ -424,6 +656,40 @@ var EntitlementCache = class {
|
|
|
424
656
|
} catch {
|
|
425
657
|
}
|
|
426
658
|
}
|
|
659
|
+
this.removeSuffixFromIndex(this.currentSuffix);
|
|
660
|
+
this.notify();
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Logout-grade wipe — bank-grade contract: removes EVERY per-user
|
|
664
|
+
* entitlement slot the SDK has ever written on this device, then
|
|
665
|
+
* clears the index. Used by `Crossdeck.reset()` so a logout on a
|
|
666
|
+
* shared device can never leave another user's entitlements
|
|
667
|
+
* readable (layer (c) of the v1.4.0 isolation fix).
|
|
668
|
+
*
|
|
669
|
+
* After clearAll(), the cache is back to anonymous + empty.
|
|
670
|
+
*/
|
|
671
|
+
clearAll() {
|
|
672
|
+
this.all = [];
|
|
673
|
+
this.lastUpdated = 0;
|
|
674
|
+
this.lastRefreshFailedAt = 0;
|
|
675
|
+
this.currentSuffix = ANON_SUFFIX;
|
|
676
|
+
if (this.storage) {
|
|
677
|
+
const suffixes = this.readIndex();
|
|
678
|
+
for (const suffix of suffixes) {
|
|
679
|
+
try {
|
|
680
|
+
this.storage.removeItem(`${this.storageKeyPrefix}:${suffix}`);
|
|
681
|
+
} catch {
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
try {
|
|
685
|
+
this.storage.removeItem(`${this.storageKeyPrefix}:${ANON_SUFFIX}`);
|
|
686
|
+
} catch {
|
|
687
|
+
}
|
|
688
|
+
try {
|
|
689
|
+
this.storage.removeItem(this.indexKey);
|
|
690
|
+
} catch {
|
|
691
|
+
}
|
|
692
|
+
}
|
|
427
693
|
this.notify();
|
|
428
694
|
}
|
|
429
695
|
/**
|
|
@@ -473,6 +739,47 @@ var EntitlementCache = class {
|
|
|
473
739
|
} catch {
|
|
474
740
|
}
|
|
475
741
|
}
|
|
742
|
+
/** Read the index of all per-user suffixes the SDK has written. */
|
|
743
|
+
readIndex() {
|
|
744
|
+
if (!this.storage) return [];
|
|
745
|
+
try {
|
|
746
|
+
const raw = this.storage.getItem(this.indexKey);
|
|
747
|
+
if (!raw) return [];
|
|
748
|
+
const parsed = JSON.parse(raw);
|
|
749
|
+
if (Array.isArray(parsed)) {
|
|
750
|
+
return parsed.filter((x) => typeof x === "string");
|
|
751
|
+
}
|
|
752
|
+
return [];
|
|
753
|
+
} catch {
|
|
754
|
+
return [];
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
/** Add a suffix to the persisted index. Idempotent. */
|
|
758
|
+
recordSuffixInIndex(suffix) {
|
|
759
|
+
if (!this.storage) return;
|
|
760
|
+
const existing = this.readIndex();
|
|
761
|
+
if (existing.includes(suffix)) return;
|
|
762
|
+
existing.push(suffix);
|
|
763
|
+
try {
|
|
764
|
+
this.storage.setItem(this.indexKey, JSON.stringify(existing));
|
|
765
|
+
} catch {
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
/** Remove a suffix from the persisted index. No-op if absent. */
|
|
769
|
+
removeSuffixFromIndex(suffix) {
|
|
770
|
+
if (!this.storage) return;
|
|
771
|
+
const existing = this.readIndex();
|
|
772
|
+
const next = existing.filter((s) => s !== suffix);
|
|
773
|
+
if (next.length === existing.length) return;
|
|
774
|
+
try {
|
|
775
|
+
if (next.length === 0) {
|
|
776
|
+
this.storage.removeItem(this.indexKey);
|
|
777
|
+
} else {
|
|
778
|
+
this.storage.setItem(this.indexKey, JSON.stringify(next));
|
|
779
|
+
}
|
|
780
|
+
} catch {
|
|
781
|
+
}
|
|
782
|
+
}
|
|
476
783
|
notify() {
|
|
477
784
|
if (this.listeners.size === 0) return;
|
|
478
785
|
const snapshot = this.all.slice();
|
|
@@ -487,6 +794,34 @@ var EntitlementCache = class {
|
|
|
487
794
|
}
|
|
488
795
|
};
|
|
489
796
|
|
|
797
|
+
// src/idempotency-key.ts
|
|
798
|
+
function formatAsUuid(hex) {
|
|
799
|
+
return [
|
|
800
|
+
hex.slice(0, 8),
|
|
801
|
+
hex.slice(8, 12),
|
|
802
|
+
hex.slice(12, 16),
|
|
803
|
+
hex.slice(16, 20),
|
|
804
|
+
hex.slice(20, 32)
|
|
805
|
+
].join("-");
|
|
806
|
+
}
|
|
807
|
+
function deriveIdempotencyKeyForPurchase(body) {
|
|
808
|
+
let identifier;
|
|
809
|
+
if (body.rail === "apple") {
|
|
810
|
+
identifier = body.signedTransactionInfo ?? "";
|
|
811
|
+
} else if (body.rail === "google") {
|
|
812
|
+
identifier = body.purchaseToken ?? "";
|
|
813
|
+
} else {
|
|
814
|
+
identifier = "";
|
|
815
|
+
}
|
|
816
|
+
if (!identifier) {
|
|
817
|
+
throw new Error(
|
|
818
|
+
`deriveIdempotencyKeyForPurchase: no stable identifier in body (rail=${body.rail}). Apple needs signedTransactionInfo; Google needs purchaseToken.`
|
|
819
|
+
);
|
|
820
|
+
}
|
|
821
|
+
const namespaced = `crossdeck:purchases/sync:${body.rail}:${identifier}`;
|
|
822
|
+
return formatAsUuid(sha256Hex(namespaced));
|
|
823
|
+
}
|
|
824
|
+
|
|
490
825
|
// src/retry-policy.ts
|
|
491
826
|
var DEFAULT_BASE = 1e3;
|
|
492
827
|
var DEFAULT_MAX = 6e4;
|
|
@@ -499,8 +834,10 @@ function computeNextDelay(attempts, retryAfterMs, options = {}, random = Math.ra
|
|
|
499
834
|
const safeAttempts = Math.min(attempts, 30);
|
|
500
835
|
const ceiling = Math.min(max, base * Math.pow(factor, safeAttempts));
|
|
501
836
|
const jittered = ceiling * random();
|
|
502
|
-
if (retryAfterMs !== void 0
|
|
503
|
-
|
|
837
|
+
if (retryAfterMs !== void 0) {
|
|
838
|
+
const ABSOLUTE_MAX_MS = 24 * 60 * 60 * 1e3;
|
|
839
|
+
const honoured = Math.min(ABSOLUTE_MAX_MS, retryAfterMs);
|
|
840
|
+
if (honoured > jittered) return honoured;
|
|
504
841
|
}
|
|
505
842
|
return Math.max(0, Math.round(jittered));
|
|
506
843
|
}
|
|
@@ -535,6 +872,27 @@ var EventQueue = class {
|
|
|
535
872
|
constructor(cfg) {
|
|
536
873
|
this.cfg = cfg;
|
|
537
874
|
this.buffer = [];
|
|
875
|
+
/**
|
|
876
|
+
* In-flight events that have been spliced from `buffer` for the
|
|
877
|
+
* current batch but haven't yet been confirmed (success or permanent
|
|
878
|
+
* failure). On a retry-driven re-flush we re-use this slot alongside
|
|
879
|
+
* `pendingBatchId` so the Stripe-style Idempotency-Key is preserved
|
|
880
|
+
* across retries of the SAME logical batch.
|
|
881
|
+
*
|
|
882
|
+
* Pre-fix the splice cleared the buffer AND we immediately
|
|
883
|
+
* `persistent.save(empty)` BEFORE awaiting the network call — a
|
|
884
|
+
* crash mid-flight wiped the persisted blob and the batch was lost
|
|
885
|
+
* with no replay on next boot. Now the persisted blob always carries
|
|
886
|
+
* `[...pendingBatch, ...buffer]` so the in-flight events survive
|
|
887
|
+
* until the server confirms them.
|
|
888
|
+
*
|
|
889
|
+
* Pre-fix every retry attempt also minted a NEW batchId via
|
|
890
|
+
* `splice + mintBatchId`, defeating the backend's
|
|
891
|
+
* `Idempotency-Key` dedup. Reuse via this slot brings web in
|
|
892
|
+
* lockstep with node (which already had the field).
|
|
893
|
+
*/
|
|
894
|
+
this.pendingBatch = null;
|
|
895
|
+
this.pendingBatchId = null;
|
|
538
896
|
this.dropped = 0;
|
|
539
897
|
this.inFlight = 0;
|
|
540
898
|
this.lastFlushAt = 0;
|
|
@@ -567,7 +925,7 @@ var EventQueue = class {
|
|
|
567
925
|
this.cfg.onDrop?.(overflow);
|
|
568
926
|
}
|
|
569
927
|
this.cfg.onBufferChange?.(this.buffer.length);
|
|
570
|
-
this.
|
|
928
|
+
this.persistAll();
|
|
571
929
|
if (this.buffer.length >= this.cfg.batchSize) {
|
|
572
930
|
void this.flush();
|
|
573
931
|
} else {
|
|
@@ -576,22 +934,44 @@ var EventQueue = class {
|
|
|
576
934
|
}
|
|
577
935
|
/**
|
|
578
936
|
* Flush the buffer to /v1/events. Resolves when the network call
|
|
579
|
-
* completes (success or failure). On
|
|
580
|
-
*
|
|
937
|
+
* completes (success or failure). On a retryable failure the batch
|
|
938
|
+
* stays in `pendingBatch` for the next scheduled retry — the SAME
|
|
939
|
+
* batch with the SAME `Idempotency-Key` is re-sent (Stripe pattern).
|
|
940
|
+
*
|
|
941
|
+
* Three terminal states from one call:
|
|
942
|
+
* - 2xx success: pendingBatch cleared, persisted state collapses to
|
|
943
|
+
* just `buffer` (any new events that arrived during in-flight).
|
|
944
|
+
* - 4xx permanent (except 408/429): pendingBatch DROPPED, `dropped`
|
|
945
|
+
* counter increments, a `permanent_failure` signal fires. The
|
|
946
|
+
* server is telling us our request is malformed / our key is
|
|
947
|
+
* revoked / we lack permission — retrying with the same key
|
|
948
|
+
* forever just grows the queue while the customer thinks events
|
|
949
|
+
* are landing.
|
|
950
|
+
* - 5xx / network / 408 / 429: pendingBatch + batchId stay; backoff
|
|
951
|
+
* schedules a retry; the next `flush()` re-uses both.
|
|
581
952
|
*
|
|
582
953
|
* `options.keepalive` marks the underlying fetch as keepalive so the
|
|
583
|
-
* browser keeps the request alive past page unload. Use
|
|
584
|
-
*
|
|
954
|
+
* browser keeps the request alive past page unload. Use for terminal
|
|
955
|
+
* flushes (pagehide / visibilitychange→hidden / beforeunload).
|
|
585
956
|
*/
|
|
586
957
|
async flush(options = {}) {
|
|
587
|
-
|
|
958
|
+
let batch;
|
|
959
|
+
let batchId;
|
|
960
|
+
if (this.pendingBatch !== null && this.pendingBatchId !== null) {
|
|
961
|
+
batch = this.pendingBatch;
|
|
962
|
+
batchId = this.pendingBatchId;
|
|
963
|
+
} else {
|
|
964
|
+
if (this.buffer.length === 0) return null;
|
|
965
|
+
batch = this.buffer.splice(0);
|
|
966
|
+
batchId = this.mintBatchId();
|
|
967
|
+
this.pendingBatch = batch;
|
|
968
|
+
this.pendingBatchId = batchId;
|
|
969
|
+
this.inFlight += batch.length;
|
|
970
|
+
this.cfg.onBufferChange?.(this.buffer.length);
|
|
971
|
+
this.persistAll();
|
|
972
|
+
}
|
|
588
973
|
this.cancelTimerIfSet();
|
|
589
974
|
this.nextRetryAt = null;
|
|
590
|
-
const batch = this.buffer.splice(0);
|
|
591
|
-
const batchId = this.mintBatchId();
|
|
592
|
-
this.inFlight += batch.length;
|
|
593
|
-
this.persistent?.save(this.buffer);
|
|
594
|
-
this.cfg.onBufferChange?.(this.buffer.length);
|
|
595
975
|
try {
|
|
596
976
|
const env = this.cfg.envelope();
|
|
597
977
|
const result = await this.cfg.http.request("POST", "/events", {
|
|
@@ -610,20 +990,33 @@ var EventQueue = class {
|
|
|
610
990
|
this.lastFlushAt = Date.now();
|
|
611
991
|
this.lastError = null;
|
|
612
992
|
this.inFlight -= batch.length;
|
|
993
|
+
this.pendingBatch = null;
|
|
994
|
+
this.pendingBatchId = null;
|
|
613
995
|
this.retry.recordSuccess();
|
|
614
|
-
this.
|
|
996
|
+
this.persistAll();
|
|
615
997
|
if (!this.firstFlushFired) {
|
|
616
998
|
this.firstFlushFired = true;
|
|
617
999
|
this.cfg.onFirstFlushSuccess?.();
|
|
618
1000
|
}
|
|
619
1001
|
return result;
|
|
620
1002
|
} catch (err) {
|
|
621
|
-
this.buffer.unshift(...batch);
|
|
622
|
-
this.inFlight -= batch.length;
|
|
623
1003
|
const message = err instanceof Error ? err.message : String(err);
|
|
624
1004
|
this.lastError = message;
|
|
625
|
-
|
|
626
|
-
|
|
1005
|
+
if (isPermanent4xx(err)) {
|
|
1006
|
+
const droppedCount = batch.length;
|
|
1007
|
+
this.pendingBatch = null;
|
|
1008
|
+
this.pendingBatchId = null;
|
|
1009
|
+
this.inFlight -= droppedCount;
|
|
1010
|
+
this.dropped += droppedCount;
|
|
1011
|
+
this.persistAll();
|
|
1012
|
+
this.cfg.onDrop?.(droppedCount);
|
|
1013
|
+
this.cfg.onPermanentFailure?.({
|
|
1014
|
+
status: err.status ?? 0,
|
|
1015
|
+
droppedCount,
|
|
1016
|
+
lastError: message
|
|
1017
|
+
});
|
|
1018
|
+
return null;
|
|
1019
|
+
}
|
|
627
1020
|
const retryAfterMs = extractRetryAfterMs(err);
|
|
628
1021
|
const delay = this.retry.nextDelay(retryAfterMs);
|
|
629
1022
|
this.scheduleRetry(delay);
|
|
@@ -641,6 +1034,8 @@ var EventQueue = class {
|
|
|
641
1034
|
this.cancelTimerIfSet();
|
|
642
1035
|
this.nextRetryAt = null;
|
|
643
1036
|
this.buffer = [];
|
|
1037
|
+
this.pendingBatch = null;
|
|
1038
|
+
this.pendingBatchId = null;
|
|
644
1039
|
this.dropped = 0;
|
|
645
1040
|
this.inFlight = 0;
|
|
646
1041
|
this.lastError = null;
|
|
@@ -650,6 +1045,10 @@ var EventQueue = class {
|
|
|
650
1045
|
}
|
|
651
1046
|
getStats() {
|
|
652
1047
|
return {
|
|
1048
|
+
// `buffered` counts events waiting for their FIRST flush. The
|
|
1049
|
+
// in-flight pendingBatch (retrying) is tracked separately via
|
|
1050
|
+
// `inFlight` — surfacing both lets diagnostics show "we have
|
|
1051
|
+
// events stuck retrying" distinct from "new events arriving".
|
|
653
1052
|
buffered: this.buffer.length,
|
|
654
1053
|
dropped: this.dropped,
|
|
655
1054
|
inFlight: this.inFlight,
|
|
@@ -659,6 +1058,29 @@ var EventQueue = class {
|
|
|
659
1058
|
nextRetryAt: this.nextRetryAt
|
|
660
1059
|
};
|
|
661
1060
|
}
|
|
1061
|
+
/**
|
|
1062
|
+
* The Idempotency-Key of the in-flight pending batch (if any).
|
|
1063
|
+
* Exposed for testing the Stripe-style retry-reuse contract.
|
|
1064
|
+
* Production callers don't need this.
|
|
1065
|
+
*/
|
|
1066
|
+
get pendingIdempotencyKey() {
|
|
1067
|
+
return this.pendingBatchId;
|
|
1068
|
+
}
|
|
1069
|
+
/**
|
|
1070
|
+
* Persist `[...pendingBatch, ...buffer]` — the full set of
|
|
1071
|
+
* not-yet-confirmed events. The next boot rehydrates this exact set
|
|
1072
|
+
* into `buffer` and replays. The server dedups via eventId
|
|
1073
|
+
* (ReplacingMergeTree on the warehouse side), so re-sending an event
|
|
1074
|
+
* that may have already landed is safe.
|
|
1075
|
+
*/
|
|
1076
|
+
persistAll() {
|
|
1077
|
+
if (!this.persistent) return;
|
|
1078
|
+
if (this.pendingBatch === null) {
|
|
1079
|
+
this.persistent.save(this.buffer);
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
this.persistent.save([...this.pendingBatch, ...this.buffer]);
|
|
1083
|
+
}
|
|
662
1084
|
// ---------- internal scheduling ----------
|
|
663
1085
|
scheduleIdleFlush() {
|
|
664
1086
|
this.cancelTimerIfSet();
|
|
@@ -692,6 +1114,14 @@ function extractRetryAfterMs(err) {
|
|
|
692
1114
|
}
|
|
693
1115
|
return void 0;
|
|
694
1116
|
}
|
|
1117
|
+
function isPermanent4xx(err) {
|
|
1118
|
+
if (!err || typeof err !== "object") return false;
|
|
1119
|
+
const status = err.status;
|
|
1120
|
+
if (typeof status !== "number" || !Number.isFinite(status)) return false;
|
|
1121
|
+
if (status < 400 || status >= 500) return false;
|
|
1122
|
+
if (status === 408 || status === 429) return false;
|
|
1123
|
+
return true;
|
|
1124
|
+
}
|
|
695
1125
|
function defaultScheduler(fn, ms) {
|
|
696
1126
|
const id = setTimeout(fn, ms);
|
|
697
1127
|
if (typeof id.unref === "function") {
|
|
@@ -1033,6 +1463,7 @@ var AutoTracker = class {
|
|
|
1033
1463
|
/** Exposed for tests + consumers that want to reset the session manually. */
|
|
1034
1464
|
resetSession() {
|
|
1035
1465
|
if (this.session && !this.session.endedSent) this.emitSessionEnd();
|
|
1466
|
+
this.pageviewId = null;
|
|
1036
1467
|
this.session = this.startNewSession();
|
|
1037
1468
|
this.emitSessionStart();
|
|
1038
1469
|
}
|
|
@@ -1069,6 +1500,7 @@ var AutoTracker = class {
|
|
|
1069
1500
|
const hiddenFor = this.session.hiddenAt ? Date.now() - this.session.hiddenAt : 0;
|
|
1070
1501
|
if (hiddenFor >= SESSION_RESUME_THRESHOLD_MS) {
|
|
1071
1502
|
this.emitSessionEnd();
|
|
1503
|
+
this.pageviewId = null;
|
|
1072
1504
|
this.session = this.startNewSession();
|
|
1073
1505
|
this.emitSessionStart();
|
|
1074
1506
|
} else {
|
|
@@ -1442,7 +1874,7 @@ function validateEventProperties(input, options = {}) {
|
|
|
1442
1874
|
const maxStringLength = options.maxStringLength ?? DEFAULT_MAX_STRING;
|
|
1443
1875
|
const maxBatchPropertyBytes = options.maxBatchPropertyBytes ?? DEFAULT_MAX_BYTES;
|
|
1444
1876
|
const maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH;
|
|
1445
|
-
const seen = /* @__PURE__ */ new
|
|
1877
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1446
1878
|
const visit = (value, key, depth) => {
|
|
1447
1879
|
if (depth > maxDepth) {
|
|
1448
1880
|
warnings.push({ kind: "depth_exceeded", key });
|
|
@@ -1530,6 +1962,7 @@ function validateEventProperties(input, options = {}) {
|
|
|
1530
1962
|
const result = visit(value[i], `${key}[${i}]`, depth + 1);
|
|
1531
1963
|
if (result.keep) out.push(result.value);
|
|
1532
1964
|
}
|
|
1965
|
+
seen.delete(value);
|
|
1533
1966
|
return { keep: true, value: out };
|
|
1534
1967
|
}
|
|
1535
1968
|
if (t === "object") {
|
|
@@ -1544,6 +1977,7 @@ function validateEventProperties(input, options = {}) {
|
|
|
1544
1977
|
const result = visit(obj[k], `${key}.${k}`, depth + 1);
|
|
1545
1978
|
if (result.keep) out[k] = result.value;
|
|
1546
1979
|
}
|
|
1980
|
+
seen.delete(obj);
|
|
1547
1981
|
return { keep: true, value: out };
|
|
1548
1982
|
}
|
|
1549
1983
|
warnings.push({ kind: "non_serialisable", key });
|
|
@@ -1886,35 +2320,27 @@ var ConsentManager = class {
|
|
|
1886
2320
|
};
|
|
1887
2321
|
var EMAIL_PATTERN = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g;
|
|
1888
2322
|
var CARD_PATTERN = /\b\d(?:[ -]?\d){12,18}\b/g;
|
|
1889
|
-
var REPLACEMENT_EMAIL = "
|
|
1890
|
-
var REPLACEMENT_CARD = "
|
|
2323
|
+
var REPLACEMENT_EMAIL = "<email>";
|
|
2324
|
+
var REPLACEMENT_CARD = "<card>";
|
|
1891
2325
|
function scrubPii(value) {
|
|
1892
2326
|
if (!value) return value;
|
|
1893
|
-
|
|
1894
|
-
if (EMAIL_PATTERN.test(out)) {
|
|
1895
|
-
out = out.replace(EMAIL_PATTERN, REPLACEMENT_EMAIL);
|
|
1896
|
-
}
|
|
1897
|
-
EMAIL_PATTERN.lastIndex = 0;
|
|
1898
|
-
if (CARD_PATTERN.test(out)) {
|
|
1899
|
-
out = out.replace(CARD_PATTERN, REPLACEMENT_CARD);
|
|
1900
|
-
}
|
|
1901
|
-
CARD_PATTERN.lastIndex = 0;
|
|
1902
|
-
return out;
|
|
2327
|
+
return value.replace(EMAIL_PATTERN, REPLACEMENT_EMAIL).replace(CARD_PATTERN, REPLACEMENT_CARD);
|
|
1903
2328
|
}
|
|
1904
2329
|
function scrubPiiFromProperties(properties) {
|
|
1905
2330
|
const out = {};
|
|
1906
2331
|
for (const k of Object.keys(properties)) {
|
|
1907
|
-
|
|
1908
|
-
if (typeof v === "string") {
|
|
1909
|
-
out[k] = scrubPii(v);
|
|
1910
|
-
} else if (Array.isArray(v)) {
|
|
1911
|
-
out[k] = v.map((item) => typeof item === "string" ? scrubPii(item) : item);
|
|
1912
|
-
} else {
|
|
1913
|
-
out[k] = v;
|
|
1914
|
-
}
|
|
2332
|
+
out[k] = scrubValue(properties[k]);
|
|
1915
2333
|
}
|
|
1916
2334
|
return out;
|
|
1917
2335
|
}
|
|
2336
|
+
function scrubValue(v) {
|
|
2337
|
+
if (typeof v === "string") return scrubPii(v);
|
|
2338
|
+
if (Array.isArray(v)) return v.map(scrubValue);
|
|
2339
|
+
if (v && typeof v === "object" && v.constructor === Object) {
|
|
2340
|
+
return scrubPiiFromProperties(v);
|
|
2341
|
+
}
|
|
2342
|
+
return v;
|
|
2343
|
+
}
|
|
1918
2344
|
|
|
1919
2345
|
// src/breadcrumbs.ts
|
|
1920
2346
|
var BreadcrumbBuffer = class {
|
|
@@ -2204,16 +2630,18 @@ var ErrorTracker = class {
|
|
|
2204
2630
|
const url = typeof input === "string" ? input : input?.url ?? "";
|
|
2205
2631
|
const method = (init.method || "GET").toUpperCase();
|
|
2206
2632
|
const start = Date.now();
|
|
2207
|
-
this.opts.
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2633
|
+
if (!isSelfRequest(url, this.opts.selfHostname)) {
|
|
2634
|
+
this.opts.breadcrumbs.add({
|
|
2635
|
+
timestamp: start,
|
|
2636
|
+
category: "http",
|
|
2637
|
+
message: `${method} ${url}`,
|
|
2638
|
+
data: { url, method }
|
|
2639
|
+
});
|
|
2640
|
+
}
|
|
2213
2641
|
try {
|
|
2214
2642
|
const response = await origFetch(...args);
|
|
2215
2643
|
if (response.status >= 500 && this.opts.isConsented()) {
|
|
2216
|
-
if (!url.
|
|
2644
|
+
if (!isSelfRequest(url, this.opts.selfHostname)) {
|
|
2217
2645
|
this.captureHttp({
|
|
2218
2646
|
url,
|
|
2219
2647
|
method,
|
|
@@ -2262,7 +2690,7 @@ var ErrorTracker = class {
|
|
|
2262
2690
|
try {
|
|
2263
2691
|
if (xhr.status >= 500 && tracker.opts.isConsented()) {
|
|
2264
2692
|
const url = xhr._cdUrl ?? "";
|
|
2265
|
-
if (!url.
|
|
2693
|
+
if (!isSelfRequest(url, tracker.opts.selfHostname)) {
|
|
2266
2694
|
tracker.captureHttp({
|
|
2267
2695
|
url,
|
|
2268
2696
|
method: (xhr._cdMethod ?? "GET").toUpperCase(),
|
|
@@ -2422,9 +2850,10 @@ var ErrorTracker = class {
|
|
|
2422
2850
|
if (!this.passesSample(err)) return;
|
|
2423
2851
|
if (!this.passesRateLimit(err)) return;
|
|
2424
2852
|
let finalErr = err;
|
|
2425
|
-
|
|
2853
|
+
const hook = this.opts.beforeSend?.();
|
|
2854
|
+
if (hook) {
|
|
2426
2855
|
try {
|
|
2427
|
-
finalErr =
|
|
2856
|
+
finalErr = hook(err);
|
|
2428
2857
|
} catch {
|
|
2429
2858
|
finalErr = err;
|
|
2430
2859
|
}
|
|
@@ -2606,6 +3035,22 @@ function safeClone(v) {
|
|
|
2606
3035
|
function safeStringify2(v) {
|
|
2607
3036
|
return coerceErrorPayload(v).message;
|
|
2608
3037
|
}
|
|
3038
|
+
function extractSelfHostname(baseUrl) {
|
|
3039
|
+
if (!baseUrl || typeof baseUrl !== "string") return null;
|
|
3040
|
+
try {
|
|
3041
|
+
return new URL(baseUrl).hostname.toLowerCase();
|
|
3042
|
+
} catch {
|
|
3043
|
+
return null;
|
|
3044
|
+
}
|
|
3045
|
+
}
|
|
3046
|
+
function isSelfRequest(requestUrl, selfHostname) {
|
|
3047
|
+
if (!selfHostname || !requestUrl) return false;
|
|
3048
|
+
try {
|
|
3049
|
+
return new URL(requestUrl).hostname.toLowerCase() === selfHostname;
|
|
3050
|
+
} catch {
|
|
3051
|
+
return false;
|
|
3052
|
+
}
|
|
3053
|
+
}
|
|
2609
3054
|
|
|
2610
3055
|
// src/crossdeck.ts
|
|
2611
3056
|
var CrossdeckClient = class {
|
|
@@ -2622,6 +3067,28 @@ var CrossdeckClient = class {
|
|
|
2622
3067
|
* mismatched env fails fast at boot rather than at first event-flush.
|
|
2623
3068
|
*/
|
|
2624
3069
|
init(options) {
|
|
3070
|
+
if (this.state) {
|
|
3071
|
+
try {
|
|
3072
|
+
this.state.uninstallUnloadFlush?.();
|
|
3073
|
+
} catch {
|
|
3074
|
+
}
|
|
3075
|
+
try {
|
|
3076
|
+
this.state.autoTracker?.uninstall();
|
|
3077
|
+
} catch {
|
|
3078
|
+
}
|
|
3079
|
+
try {
|
|
3080
|
+
this.state.webVitals?.uninstall();
|
|
3081
|
+
} catch {
|
|
3082
|
+
}
|
|
3083
|
+
try {
|
|
3084
|
+
this.state.errors?.uninstall();
|
|
3085
|
+
} catch {
|
|
3086
|
+
}
|
|
3087
|
+
try {
|
|
3088
|
+
void this.state.events.flush({ keepalive: true });
|
|
3089
|
+
} catch {
|
|
3090
|
+
}
|
|
3091
|
+
}
|
|
2625
3092
|
if (!options.publicKey || !options.publicKey.startsWith("cd_pub_")) {
|
|
2626
3093
|
throw new CrossdeckError({
|
|
2627
3094
|
type: "configuration_error",
|
|
@@ -2668,7 +3135,12 @@ var CrossdeckClient = class {
|
|
|
2668
3135
|
// load still flushes if the user leaves quickly (the keepalive
|
|
2669
3136
|
// pagehide handler picks up anything that doesn't); long enough
|
|
2670
3137
|
// that bursts of clicks coalesce into one network round-trip.
|
|
2671
|
-
|
|
3138
|
+
// v1.4.0 Phase 3.3 — flush interval default parity. Pre-
|
|
3139
|
+
// v1.4.0: Web/Node 1500ms, RN/Swift/Android 5000ms. All
|
|
3140
|
+
// converged on 2000ms (the Stripe-adjacent industry norm)
|
|
3141
|
+
// so cross-platform funnels show events landing at the
|
|
3142
|
+
// same cadence on every SDK. Per-instance override stays.
|
|
3143
|
+
eventFlushIntervalMs: options.eventFlushIntervalMs ?? 2e3,
|
|
2672
3144
|
sdkVersion: options.sdkVersion ?? SDK_VERSION,
|
|
2673
3145
|
autoTrack,
|
|
2674
3146
|
appVersion: options.appVersion ?? null
|
|
@@ -2729,6 +3201,15 @@ var CrossdeckClient = class {
|
|
|
2729
3201
|
`Event flush failed (${info.lastError}). Retrying in ${info.delayMs}ms (attempt ${info.consecutiveFailures}).`,
|
|
2730
3202
|
{ ...info }
|
|
2731
3203
|
);
|
|
3204
|
+
},
|
|
3205
|
+
onPermanentFailure: (info) => {
|
|
3206
|
+
const headline = `[crossdeck] Event batch DROPPED (status ${info.status}): ${info.lastError}. ${info.droppedCount} event(s) lost \u2014 check your publishable key + app config.`;
|
|
3207
|
+
console.error(headline);
|
|
3208
|
+
debug.emit(
|
|
3209
|
+
"sdk.flush_permanent_failure",
|
|
3210
|
+
headline,
|
|
3211
|
+
{ ...info }
|
|
3212
|
+
);
|
|
2732
3213
|
}
|
|
2733
3214
|
});
|
|
2734
3215
|
const deviceInfo = autoTrack.deviceInfo ? collectDeviceInfo({ appVersion: opts.appVersion ?? void 0 }) : opts.appVersion ? { appVersion: opts.appVersion } : {};
|
|
@@ -2795,8 +3276,20 @@ var CrossdeckClient = class {
|
|
|
2795
3276
|
report: (err) => this.reportError(err),
|
|
2796
3277
|
getContext: () => ({ ...this.state.errorContext }),
|
|
2797
3278
|
getTags: () => ({ ...this.state.errorTags }),
|
|
2798
|
-
|
|
2799
|
-
|
|
3279
|
+
// GETTER, not a captured value — `setErrorBeforeSend()` mutates
|
|
3280
|
+
// `state.errorBeforeSend` after init() and the tracker MUST
|
|
3281
|
+
// pick up the new hook on the next error. The pre-fix shape
|
|
3282
|
+
// (`beforeSend: this.state!.errorBeforeSend`) snapshotted
|
|
3283
|
+
// `null` at construction and made the customer's PII scrubber
|
|
3284
|
+
// silently inert. See error-capture.ts:ErrorTrackerOptions.beforeSend.
|
|
3285
|
+
beforeSend: () => this.state.errorBeforeSend,
|
|
3286
|
+
isConsented: () => this.state.consent.errors,
|
|
3287
|
+
// Derived from the configured baseUrl at init() time. Used by
|
|
3288
|
+
// the fetch / XHR wrappers to skip captureHttp on Crossdeck's
|
|
3289
|
+
// own requests — pre-fix the skip was hardcoded to
|
|
3290
|
+
// `api.cross-deck.com` and broke for customers on staging /
|
|
3291
|
+
// regional / self-hosted base URLs (recursive capture loop).
|
|
3292
|
+
selfHostname: extractSelfHostname(opts.baseUrl)
|
|
2800
3293
|
});
|
|
2801
3294
|
this.state.errors = tracker;
|
|
2802
3295
|
tracker.install();
|
|
@@ -2875,13 +3368,10 @@ var CrossdeckClient = class {
|
|
|
2875
3368
|
};
|
|
2876
3369
|
if (options?.email) body.email = options.email;
|
|
2877
3370
|
if (traits) body.traits = traits;
|
|
3371
|
+
s.entitlements.setUserKey(userId);
|
|
2878
3372
|
const result = await s.http.request("POST", "/identity/alias", {
|
|
2879
3373
|
body
|
|
2880
3374
|
});
|
|
2881
|
-
const priorCdcust = s.identity.crossdeckCustomerId;
|
|
2882
|
-
if (priorCdcust && result.crossdeckCustomerId && priorCdcust !== result.crossdeckCustomerId) {
|
|
2883
|
-
s.entitlements.clear();
|
|
2884
|
-
}
|
|
2885
3375
|
s.identity.setCrossdeckCustomerId(result.crossdeckCustomerId);
|
|
2886
3376
|
s.developerUserId = userId;
|
|
2887
3377
|
return result;
|
|
@@ -3321,11 +3811,25 @@ var CrossdeckClient = class {
|
|
|
3321
3811
|
message: "syncPurchases requires a signedTransactionInfo string from StoreKit 2."
|
|
3322
3812
|
});
|
|
3323
3813
|
}
|
|
3814
|
+
const rail = input.rail ?? "apple";
|
|
3815
|
+
const body = { ...input, rail };
|
|
3816
|
+
const idempotencyKey = deriveIdempotencyKeyForPurchase(body);
|
|
3324
3817
|
const result = await s.http.request("POST", "/purchases/sync", {
|
|
3325
|
-
body
|
|
3818
|
+
body,
|
|
3819
|
+
idempotencyKey
|
|
3326
3820
|
});
|
|
3327
3821
|
s.identity.setCrossdeckCustomerId(result.crossdeckCustomerId);
|
|
3328
3822
|
s.entitlements.setFromList(result.entitlements);
|
|
3823
|
+
try {
|
|
3824
|
+
const sourceProductId = result.entitlements[0]?.source.productId;
|
|
3825
|
+
const sourceSubscriptionId = result.entitlements[0]?.source.subscriptionId;
|
|
3826
|
+
const props = { rail };
|
|
3827
|
+
if (sourceProductId) props.productId = sourceProductId;
|
|
3828
|
+
if (sourceSubscriptionId) props.subscriptionId = sourceSubscriptionId;
|
|
3829
|
+
if (result.idempotent_replay) props.idempotent_replay = true;
|
|
3830
|
+
this.track("purchase.completed", props);
|
|
3831
|
+
} catch {
|
|
3832
|
+
}
|
|
3329
3833
|
s.debug.emit(
|
|
3330
3834
|
"sdk.purchase_evidence_sent",
|
|
3331
3835
|
"StoreKit transaction forwarded. Waiting for backend verification.",
|
|
@@ -3381,13 +3885,15 @@ var CrossdeckClient = class {
|
|
|
3381
3885
|
}
|
|
3382
3886
|
this.state.autoTracker?.uninstall();
|
|
3383
3887
|
this.state.identity.reset();
|
|
3384
|
-
this.state.entitlements.
|
|
3888
|
+
this.state.entitlements.clearAll();
|
|
3385
3889
|
this.state.events.reset();
|
|
3386
3890
|
this.state.superProps.clear();
|
|
3387
3891
|
this.state.breadcrumbs.clear();
|
|
3388
3892
|
this.state.errorContext = {};
|
|
3389
3893
|
this.state.errorTags = {};
|
|
3390
3894
|
this.state.developerUserId = null;
|
|
3895
|
+
this.state.lastServerTime = null;
|
|
3896
|
+
this.state.lastClientTime = null;
|
|
3391
3897
|
if (this.state.autoTracker) {
|
|
3392
3898
|
const tracker = new AutoTracker(
|
|
3393
3899
|
this.state.options.autoTrack,
|
|
@@ -3516,7 +4022,9 @@ function isLocalHostname() {
|
|
|
3516
4022
|
const hostname = w?.location?.hostname;
|
|
3517
4023
|
if (!hostname) return false;
|
|
3518
4024
|
if (hostname === "localhost" || hostname === "127.0.0.1") return true;
|
|
4025
|
+
if (hostname === "0.0.0.0") return true;
|
|
3519
4026
|
if (hostname === "::1" || hostname === "[::1]") return true;
|
|
4027
|
+
if (/^\[?fe80::/i.test(hostname)) return true;
|
|
3520
4028
|
if (hostname.endsWith(".local")) return true;
|
|
3521
4029
|
if (/^10\./.test(hostname)) return true;
|
|
3522
4030
|
if (/^192\.168\./.test(hostname)) return true;
|