@cross-deck/web 1.3.1 → 1.5.0
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 +43 -1
- package/README.md +13 -1
- package/dist/contracts.json +378 -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 +940 -22
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +177 -4
- package/dist/index.d.ts +177 -4
- package/dist/index.mjs +939 -22
- package/dist/index.mjs.map +1 -1
- package/dist/react.cjs +404 -22
- 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 +404 -22
- 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 +404 -22
- package/dist/vue.cjs.map +1 -1
- package/dist/vue.mjs +404 -22
- package/dist/vue.mjs.map +1 -1
- package/package.json +4 -3
|
@@ -42,6 +42,10 @@ interface PurchaseResult {
|
|
|
42
42
|
crossdeckCustomerId: string;
|
|
43
43
|
env: Environment;
|
|
44
44
|
entitlements: PublicEntitlement[];
|
|
45
|
+
/** True when the response came from the backend's idempotency
|
|
46
|
+
* cache instead of fresh processing. Backend also returns
|
|
47
|
+
* `Idempotent-Replayed: true` as a response header (v1.4.0). */
|
|
48
|
+
idempotent_replay?: boolean;
|
|
45
49
|
}
|
|
46
50
|
interface HeartbeatResponse {
|
|
47
51
|
object: "heartbeat";
|
|
@@ -42,6 +42,10 @@ interface PurchaseResult {
|
|
|
42
42
|
crossdeckCustomerId: string;
|
|
43
43
|
env: Environment;
|
|
44
44
|
entitlements: PublicEntitlement[];
|
|
45
|
+
/** True when the response came from the backend's idempotency
|
|
46
|
+
* cache instead of fresh processing. Backend also returns
|
|
47
|
+
* `Idempotent-Replayed: true` as a response header (v1.4.0). */
|
|
48
|
+
idempotent_replay?: boolean;
|
|
45
49
|
}
|
|
46
50
|
interface HeartbeatResponse {
|
|
47
51
|
object: "heartbeat";
|
package/dist/vue.cjs
CHANGED
|
@@ -92,7 +92,7 @@ function typeMapForStatus(status) {
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
// src/_version.ts
|
|
95
|
-
var SDK_VERSION = "1.
|
|
95
|
+
var SDK_VERSION = "1.4.2";
|
|
96
96
|
var SDK_NAME = "@cross-deck/web";
|
|
97
97
|
|
|
98
98
|
// src/http.ts
|
|
@@ -342,29 +342,258 @@ function randomChars(count) {
|
|
|
342
342
|
return out.join("");
|
|
343
343
|
}
|
|
344
344
|
|
|
345
|
+
// src/hash.ts
|
|
346
|
+
var K = new Uint32Array([
|
|
347
|
+
1116352408,
|
|
348
|
+
1899447441,
|
|
349
|
+
3049323471,
|
|
350
|
+
3921009573,
|
|
351
|
+
961987163,
|
|
352
|
+
1508970993,
|
|
353
|
+
2453635748,
|
|
354
|
+
2870763221,
|
|
355
|
+
3624381080,
|
|
356
|
+
310598401,
|
|
357
|
+
607225278,
|
|
358
|
+
1426881987,
|
|
359
|
+
1925078388,
|
|
360
|
+
2162078206,
|
|
361
|
+
2614888103,
|
|
362
|
+
3248222580,
|
|
363
|
+
3835390401,
|
|
364
|
+
4022224774,
|
|
365
|
+
264347078,
|
|
366
|
+
604807628,
|
|
367
|
+
770255983,
|
|
368
|
+
1249150122,
|
|
369
|
+
1555081692,
|
|
370
|
+
1996064986,
|
|
371
|
+
2554220882,
|
|
372
|
+
2821834349,
|
|
373
|
+
2952996808,
|
|
374
|
+
3210313671,
|
|
375
|
+
3336571891,
|
|
376
|
+
3584528711,
|
|
377
|
+
113926993,
|
|
378
|
+
338241895,
|
|
379
|
+
666307205,
|
|
380
|
+
773529912,
|
|
381
|
+
1294757372,
|
|
382
|
+
1396182291,
|
|
383
|
+
1695183700,
|
|
384
|
+
1986661051,
|
|
385
|
+
2177026350,
|
|
386
|
+
2456956037,
|
|
387
|
+
2730485921,
|
|
388
|
+
2820302411,
|
|
389
|
+
3259730800,
|
|
390
|
+
3345764771,
|
|
391
|
+
3516065817,
|
|
392
|
+
3600352804,
|
|
393
|
+
4094571909,
|
|
394
|
+
275423344,
|
|
395
|
+
430227734,
|
|
396
|
+
506948616,
|
|
397
|
+
659060556,
|
|
398
|
+
883997877,
|
|
399
|
+
958139571,
|
|
400
|
+
1322822218,
|
|
401
|
+
1537002063,
|
|
402
|
+
1747873779,
|
|
403
|
+
1955562222,
|
|
404
|
+
2024104815,
|
|
405
|
+
2227730452,
|
|
406
|
+
2361852424,
|
|
407
|
+
2428436474,
|
|
408
|
+
2756734187,
|
|
409
|
+
3204031479,
|
|
410
|
+
3329325298
|
|
411
|
+
]);
|
|
412
|
+
function utf8Bytes(input) {
|
|
413
|
+
if (typeof TextEncoder !== "undefined") {
|
|
414
|
+
return new TextEncoder().encode(input);
|
|
415
|
+
}
|
|
416
|
+
const out = [];
|
|
417
|
+
for (let i = 0; i < input.length; i++) {
|
|
418
|
+
let codePoint = input.charCodeAt(i);
|
|
419
|
+
if (codePoint >= 55296 && codePoint <= 56319 && i + 1 < input.length) {
|
|
420
|
+
const next = input.charCodeAt(i + 1);
|
|
421
|
+
if (next >= 56320 && next <= 57343) {
|
|
422
|
+
codePoint = 65536 + (codePoint - 55296 << 10) + (next - 56320);
|
|
423
|
+
i++;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
if (codePoint < 128) {
|
|
427
|
+
out.push(codePoint);
|
|
428
|
+
} else if (codePoint < 2048) {
|
|
429
|
+
out.push(192 | codePoint >> 6);
|
|
430
|
+
out.push(128 | codePoint & 63);
|
|
431
|
+
} else if (codePoint < 65536) {
|
|
432
|
+
out.push(224 | codePoint >> 12);
|
|
433
|
+
out.push(128 | codePoint >> 6 & 63);
|
|
434
|
+
out.push(128 | codePoint & 63);
|
|
435
|
+
} else {
|
|
436
|
+
out.push(240 | codePoint >> 18);
|
|
437
|
+
out.push(128 | codePoint >> 12 & 63);
|
|
438
|
+
out.push(128 | codePoint >> 6 & 63);
|
|
439
|
+
out.push(128 | codePoint & 63);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
return new Uint8Array(out);
|
|
443
|
+
}
|
|
444
|
+
function sha256Hex(input) {
|
|
445
|
+
const bytes = utf8Bytes(input);
|
|
446
|
+
const bitLength = bytes.length * 8;
|
|
447
|
+
const blockCount = Math.floor((bytes.length + 9 + 63) / 64);
|
|
448
|
+
const padded = new Uint8Array(blockCount * 64);
|
|
449
|
+
padded.set(bytes);
|
|
450
|
+
padded[bytes.length] = 128;
|
|
451
|
+
const high = Math.floor(bitLength / 4294967296);
|
|
452
|
+
const low = bitLength >>> 0;
|
|
453
|
+
const lenOffset = padded.length - 8;
|
|
454
|
+
padded[lenOffset + 0] = high >>> 24 & 255;
|
|
455
|
+
padded[lenOffset + 1] = high >>> 16 & 255;
|
|
456
|
+
padded[lenOffset + 2] = high >>> 8 & 255;
|
|
457
|
+
padded[lenOffset + 3] = high & 255;
|
|
458
|
+
padded[lenOffset + 4] = low >>> 24 & 255;
|
|
459
|
+
padded[lenOffset + 5] = low >>> 16 & 255;
|
|
460
|
+
padded[lenOffset + 6] = low >>> 8 & 255;
|
|
461
|
+
padded[lenOffset + 7] = low & 255;
|
|
462
|
+
const H = new Uint32Array([
|
|
463
|
+
1779033703,
|
|
464
|
+
3144134277,
|
|
465
|
+
1013904242,
|
|
466
|
+
2773480762,
|
|
467
|
+
1359893119,
|
|
468
|
+
2600822924,
|
|
469
|
+
528734635,
|
|
470
|
+
1541459225
|
|
471
|
+
]);
|
|
472
|
+
const W = new Uint32Array(64);
|
|
473
|
+
for (let block = 0; block < blockCount; block++) {
|
|
474
|
+
const offset = block * 64;
|
|
475
|
+
for (let t = 0; t < 16; t++) {
|
|
476
|
+
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;
|
|
477
|
+
}
|
|
478
|
+
for (let t = 16; t < 64; t++) {
|
|
479
|
+
const w15 = W[t - 15];
|
|
480
|
+
const w2 = W[t - 2];
|
|
481
|
+
const s0 = (w15 >>> 7 | w15 << 25) ^ (w15 >>> 18 | w15 << 14) ^ w15 >>> 3;
|
|
482
|
+
const s1 = (w2 >>> 17 | w2 << 15) ^ (w2 >>> 19 | w2 << 13) ^ w2 >>> 10;
|
|
483
|
+
W[t] = W[t - 16] + s0 + W[t - 7] + s1 >>> 0;
|
|
484
|
+
}
|
|
485
|
+
let a = H[0], b = H[1], c = H[2], d = H[3];
|
|
486
|
+
let e = H[4], f = H[5], g = H[6], h = H[7];
|
|
487
|
+
for (let t = 0; t < 64; t++) {
|
|
488
|
+
const S1 = (e >>> 6 | e << 26) ^ (e >>> 11 | e << 21) ^ (e >>> 25 | e << 7);
|
|
489
|
+
const ch = e & f ^ ~e & g;
|
|
490
|
+
const temp1 = h + S1 + ch + K[t] + W[t] >>> 0;
|
|
491
|
+
const S0 = (a >>> 2 | a << 30) ^ (a >>> 13 | a << 19) ^ (a >>> 22 | a << 10);
|
|
492
|
+
const maj = a & b ^ a & c ^ b & c;
|
|
493
|
+
const temp2 = S0 + maj >>> 0;
|
|
494
|
+
h = g;
|
|
495
|
+
g = f;
|
|
496
|
+
f = e;
|
|
497
|
+
e = d + temp1 >>> 0;
|
|
498
|
+
d = c;
|
|
499
|
+
c = b;
|
|
500
|
+
b = a;
|
|
501
|
+
a = temp1 + temp2 >>> 0;
|
|
502
|
+
}
|
|
503
|
+
H[0] = H[0] + a >>> 0;
|
|
504
|
+
H[1] = H[1] + b >>> 0;
|
|
505
|
+
H[2] = H[2] + c >>> 0;
|
|
506
|
+
H[3] = H[3] + d >>> 0;
|
|
507
|
+
H[4] = H[4] + e >>> 0;
|
|
508
|
+
H[5] = H[5] + f >>> 0;
|
|
509
|
+
H[6] = H[6] + g >>> 0;
|
|
510
|
+
H[7] = H[7] + h >>> 0;
|
|
511
|
+
}
|
|
512
|
+
let hex = "";
|
|
513
|
+
for (let i = 0; i < 8; i++) {
|
|
514
|
+
hex += H[i].toString(16).padStart(8, "0");
|
|
515
|
+
}
|
|
516
|
+
return hex;
|
|
517
|
+
}
|
|
518
|
+
|
|
345
519
|
// src/entitlement-cache.ts
|
|
346
520
|
var DEFAULT_STALE_AFTER_MS = 24 * 60 * 60 * 1e3;
|
|
347
|
-
var
|
|
521
|
+
var ANON_SUFFIX = "_anon";
|
|
522
|
+
var INDEX_SUFFIX = "_index";
|
|
523
|
+
var EntitlementCache = class _EntitlementCache {
|
|
348
524
|
/**
|
|
349
|
-
* @param storage
|
|
350
|
-
*
|
|
351
|
-
*
|
|
352
|
-
*
|
|
353
|
-
* @param
|
|
354
|
-
*
|
|
355
|
-
*
|
|
525
|
+
* @param storage Device storage adapter. When omitted (tests) or
|
|
526
|
+
* a MemoryStorage (strict-consent / no-persistence
|
|
527
|
+
* mode) the cache is session-only — durability is
|
|
528
|
+
* simply absent, never wrong.
|
|
529
|
+
* @param storageKeyPrefix Prefix used to derive per-user storage keys
|
|
530
|
+
* (`<prefix>:<sha256(userId)>`). Default
|
|
531
|
+
* `crossdeck:entitlements`. The trailing user
|
|
532
|
+
* suffix is filled at identify() / reset()
|
|
533
|
+
* time — see [[setUserKey]].
|
|
534
|
+
* @param staleAfterMs Age past which last-known-good is flagged stale
|
|
535
|
+
* even without a failed refresh. Default 24h.
|
|
356
536
|
*/
|
|
357
|
-
constructor(storage,
|
|
537
|
+
constructor(storage, storageKeyPrefix = "crossdeck:entitlements", staleAfterMs = DEFAULT_STALE_AFTER_MS) {
|
|
358
538
|
this.all = [];
|
|
359
539
|
this.lastUpdated = 0;
|
|
360
540
|
this.lastRefreshFailedAt = 0;
|
|
361
541
|
this.listeners = /* @__PURE__ */ new Set();
|
|
362
542
|
this.listenerErrorCount = 0;
|
|
543
|
+
this.currentSuffix = ANON_SUFFIX;
|
|
363
544
|
this.storage = storage;
|
|
364
|
-
this.
|
|
545
|
+
this.storageKeyPrefix = storageKeyPrefix;
|
|
365
546
|
this.staleAfterMs = staleAfterMs;
|
|
366
547
|
this.hydrate();
|
|
367
548
|
}
|
|
549
|
+
/** The full storage key the current-user blob is persisted under. */
|
|
550
|
+
get storageKey() {
|
|
551
|
+
return `${this.storageKeyPrefix}:${this.currentSuffix}`;
|
|
552
|
+
}
|
|
553
|
+
/** Key of the index blob — a JSON array of every suffix we've
|
|
554
|
+
* written. Used by clearAll() to scope a logout-wipe. */
|
|
555
|
+
get indexKey() {
|
|
556
|
+
return `${this.storageKeyPrefix}:${INDEX_SUFFIX}`;
|
|
557
|
+
}
|
|
558
|
+
/** Derive a stable suffix for a developerUserId via SHA-256. The
|
|
559
|
+
* raw userId never lands in the storage key — protects against
|
|
560
|
+
* accidentally leaking email-style identifiers through DevTools
|
|
561
|
+
* inspection. Pass `null` to switch back to the anonymous slot. */
|
|
562
|
+
static suffixForUserId(userId) {
|
|
563
|
+
if (userId == null || userId === "") return ANON_SUFFIX;
|
|
564
|
+
return sha256Hex(userId);
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Switch the cache to a different user's storage slot. Bank-grade
|
|
568
|
+
* three-layer isolation:
|
|
569
|
+
* (a) Physical key separation — `<prefix>:<sha256(userId)>` so
|
|
570
|
+
* a user-switch can't physically read prior user's data
|
|
571
|
+
* even if the in-memory clear was skipped.
|
|
572
|
+
* (b) Unconditional in-memory clear — invoked whenever the
|
|
573
|
+
* active suffix changes, even on same-id re-identify.
|
|
574
|
+
* (c) Re-hydrate from the new slot — a returning user observes
|
|
575
|
+
* their last-known-good cache from storage immediately.
|
|
576
|
+
*
|
|
577
|
+
* Caller (identify() / reset()) MUST invoke this BEFORE the next
|
|
578
|
+
* setFromList() so the write lands under the right key.
|
|
579
|
+
*/
|
|
580
|
+
setUserKey(userId) {
|
|
581
|
+
const nextSuffix = _EntitlementCache.suffixForUserId(userId);
|
|
582
|
+
if (nextSuffix === this.currentSuffix) {
|
|
583
|
+
this.all = [];
|
|
584
|
+
this.lastUpdated = 0;
|
|
585
|
+
this.lastRefreshFailedAt = 0;
|
|
586
|
+
this.notify();
|
|
587
|
+
this.hydrate();
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
this.currentSuffix = nextSuffix;
|
|
591
|
+
this.all = [];
|
|
592
|
+
this.lastUpdated = 0;
|
|
593
|
+
this.lastRefreshFailedAt = 0;
|
|
594
|
+
this.hydrate();
|
|
595
|
+
this.notify();
|
|
596
|
+
}
|
|
368
597
|
/**
|
|
369
598
|
* Sync read — true iff the entitlement is currently granting access.
|
|
370
599
|
*
|
|
@@ -434,12 +663,13 @@ var EntitlementCache = class {
|
|
|
434
663
|
this.lastUpdated = Date.now();
|
|
435
664
|
this.lastRefreshFailedAt = 0;
|
|
436
665
|
this.persist();
|
|
666
|
+
this.recordSuffixInIndex(this.currentSuffix);
|
|
437
667
|
this.notify();
|
|
438
668
|
}
|
|
439
669
|
/**
|
|
440
|
-
* Wipe
|
|
441
|
-
*
|
|
442
|
-
*
|
|
670
|
+
* Wipe the CURRENT user's slot. Used internally when a single
|
|
671
|
+
* user's cache needs to be invalidated without affecting other
|
|
672
|
+
* persisted slots. The full-logout path is [[clearAll]].
|
|
443
673
|
*/
|
|
444
674
|
clear() {
|
|
445
675
|
this.all = [];
|
|
@@ -451,6 +681,40 @@ var EntitlementCache = class {
|
|
|
451
681
|
} catch {
|
|
452
682
|
}
|
|
453
683
|
}
|
|
684
|
+
this.removeSuffixFromIndex(this.currentSuffix);
|
|
685
|
+
this.notify();
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* Logout-grade wipe — bank-grade contract: removes EVERY per-user
|
|
689
|
+
* entitlement slot the SDK has ever written on this device, then
|
|
690
|
+
* clears the index. Used by `Crossdeck.reset()` so a logout on a
|
|
691
|
+
* shared device can never leave another user's entitlements
|
|
692
|
+
* readable (layer (c) of the v1.4.0 isolation fix).
|
|
693
|
+
*
|
|
694
|
+
* After clearAll(), the cache is back to anonymous + empty.
|
|
695
|
+
*/
|
|
696
|
+
clearAll() {
|
|
697
|
+
this.all = [];
|
|
698
|
+
this.lastUpdated = 0;
|
|
699
|
+
this.lastRefreshFailedAt = 0;
|
|
700
|
+
this.currentSuffix = ANON_SUFFIX;
|
|
701
|
+
if (this.storage) {
|
|
702
|
+
const suffixes = this.readIndex();
|
|
703
|
+
for (const suffix of suffixes) {
|
|
704
|
+
try {
|
|
705
|
+
this.storage.removeItem(`${this.storageKeyPrefix}:${suffix}`);
|
|
706
|
+
} catch {
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
try {
|
|
710
|
+
this.storage.removeItem(`${this.storageKeyPrefix}:${ANON_SUFFIX}`);
|
|
711
|
+
} catch {
|
|
712
|
+
}
|
|
713
|
+
try {
|
|
714
|
+
this.storage.removeItem(this.indexKey);
|
|
715
|
+
} catch {
|
|
716
|
+
}
|
|
717
|
+
}
|
|
454
718
|
this.notify();
|
|
455
719
|
}
|
|
456
720
|
/**
|
|
@@ -500,6 +764,47 @@ var EntitlementCache = class {
|
|
|
500
764
|
} catch {
|
|
501
765
|
}
|
|
502
766
|
}
|
|
767
|
+
/** Read the index of all per-user suffixes the SDK has written. */
|
|
768
|
+
readIndex() {
|
|
769
|
+
if (!this.storage) return [];
|
|
770
|
+
try {
|
|
771
|
+
const raw = this.storage.getItem(this.indexKey);
|
|
772
|
+
if (!raw) return [];
|
|
773
|
+
const parsed = JSON.parse(raw);
|
|
774
|
+
if (Array.isArray(parsed)) {
|
|
775
|
+
return parsed.filter((x) => typeof x === "string");
|
|
776
|
+
}
|
|
777
|
+
return [];
|
|
778
|
+
} catch {
|
|
779
|
+
return [];
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
/** Add a suffix to the persisted index. Idempotent. */
|
|
783
|
+
recordSuffixInIndex(suffix) {
|
|
784
|
+
if (!this.storage) return;
|
|
785
|
+
const existing = this.readIndex();
|
|
786
|
+
if (existing.includes(suffix)) return;
|
|
787
|
+
existing.push(suffix);
|
|
788
|
+
try {
|
|
789
|
+
this.storage.setItem(this.indexKey, JSON.stringify(existing));
|
|
790
|
+
} catch {
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
/** Remove a suffix from the persisted index. No-op if absent. */
|
|
794
|
+
removeSuffixFromIndex(suffix) {
|
|
795
|
+
if (!this.storage) return;
|
|
796
|
+
const existing = this.readIndex();
|
|
797
|
+
const next = existing.filter((s) => s !== suffix);
|
|
798
|
+
if (next.length === existing.length) return;
|
|
799
|
+
try {
|
|
800
|
+
if (next.length === 0) {
|
|
801
|
+
this.storage.removeItem(this.indexKey);
|
|
802
|
+
} else {
|
|
803
|
+
this.storage.setItem(this.indexKey, JSON.stringify(next));
|
|
804
|
+
}
|
|
805
|
+
} catch {
|
|
806
|
+
}
|
|
807
|
+
}
|
|
503
808
|
notify() {
|
|
504
809
|
if (this.listeners.size === 0) return;
|
|
505
810
|
const snapshot = this.all.slice();
|
|
@@ -514,6 +819,34 @@ var EntitlementCache = class {
|
|
|
514
819
|
}
|
|
515
820
|
};
|
|
516
821
|
|
|
822
|
+
// src/idempotency-key.ts
|
|
823
|
+
function formatAsUuid(hex) {
|
|
824
|
+
return [
|
|
825
|
+
hex.slice(0, 8),
|
|
826
|
+
hex.slice(8, 12),
|
|
827
|
+
hex.slice(12, 16),
|
|
828
|
+
hex.slice(16, 20),
|
|
829
|
+
hex.slice(20, 32)
|
|
830
|
+
].join("-");
|
|
831
|
+
}
|
|
832
|
+
function deriveIdempotencyKeyForPurchase(body) {
|
|
833
|
+
let identifier;
|
|
834
|
+
if (body.rail === "apple") {
|
|
835
|
+
identifier = body.signedTransactionInfo ?? "";
|
|
836
|
+
} else if (body.rail === "google") {
|
|
837
|
+
identifier = body.purchaseToken ?? "";
|
|
838
|
+
} else {
|
|
839
|
+
identifier = "";
|
|
840
|
+
}
|
|
841
|
+
if (!identifier) {
|
|
842
|
+
throw new Error(
|
|
843
|
+
`deriveIdempotencyKeyForPurchase: no stable identifier in body (rail=${body.rail}). Apple needs signedTransactionInfo; Google needs purchaseToken.`
|
|
844
|
+
);
|
|
845
|
+
}
|
|
846
|
+
const namespaced = `crossdeck:purchases/sync:${body.rail}:${identifier}`;
|
|
847
|
+
return formatAsUuid(sha256Hex(namespaced));
|
|
848
|
+
}
|
|
849
|
+
|
|
517
850
|
// src/retry-policy.ts
|
|
518
851
|
var DEFAULT_BASE = 1e3;
|
|
519
852
|
var DEFAULT_MAX = 6e4;
|
|
@@ -2776,6 +3109,10 @@ var CrossdeckClient = class {
|
|
|
2776
3109
|
this.state.errors?.uninstall();
|
|
2777
3110
|
} catch {
|
|
2778
3111
|
}
|
|
3112
|
+
try {
|
|
3113
|
+
void this.state.events.flush({ keepalive: true });
|
|
3114
|
+
} catch {
|
|
3115
|
+
}
|
|
2779
3116
|
}
|
|
2780
3117
|
if (!options.publicKey || !options.publicKey.startsWith("cd_pub_")) {
|
|
2781
3118
|
throw new CrossdeckError({
|
|
@@ -2823,7 +3160,12 @@ var CrossdeckClient = class {
|
|
|
2823
3160
|
// load still flushes if the user leaves quickly (the keepalive
|
|
2824
3161
|
// pagehide handler picks up anything that doesn't); long enough
|
|
2825
3162
|
// that bursts of clicks coalesce into one network round-trip.
|
|
2826
|
-
|
|
3163
|
+
// v1.4.0 Phase 3.3 — flush interval default parity. Pre-
|
|
3164
|
+
// v1.4.0: Web/Node 1500ms, RN/Swift/Android 5000ms. All
|
|
3165
|
+
// converged on 2000ms (the Stripe-adjacent industry norm)
|
|
3166
|
+
// so cross-platform funnels show events landing at the
|
|
3167
|
+
// same cadence on every SDK. Per-instance override stays.
|
|
3168
|
+
eventFlushIntervalMs: options.eventFlushIntervalMs ?? 2e3,
|
|
2827
3169
|
sdkVersion: options.sdkVersion ?? SDK_VERSION,
|
|
2828
3170
|
autoTrack,
|
|
2829
3171
|
appVersion: options.appVersion ?? null
|
|
@@ -3051,14 +3393,10 @@ var CrossdeckClient = class {
|
|
|
3051
3393
|
};
|
|
3052
3394
|
if (options?.email) body.email = options.email;
|
|
3053
3395
|
if (traits) body.traits = traits;
|
|
3396
|
+
s.entitlements.setUserKey(userId);
|
|
3054
3397
|
const result = await s.http.request("POST", "/identity/alias", {
|
|
3055
3398
|
body
|
|
3056
3399
|
});
|
|
3057
|
-
const priorCdcust = s.identity.crossdeckCustomerId;
|
|
3058
|
-
const cacheHasEntries = s.entitlements.list().length > 0;
|
|
3059
|
-
if (priorCdcust && result.crossdeckCustomerId && priorCdcust !== result.crossdeckCustomerId || !priorCdcust && cacheHasEntries) {
|
|
3060
|
-
s.entitlements.clear();
|
|
3061
|
-
}
|
|
3062
3400
|
s.identity.setCrossdeckCustomerId(result.crossdeckCustomerId);
|
|
3063
3401
|
s.developerUserId = userId;
|
|
3064
3402
|
return result;
|
|
@@ -3363,6 +3701,37 @@ var CrossdeckClient = class {
|
|
|
3363
3701
|
* trip happens in the background. To flush before the page unloads,
|
|
3364
3702
|
* call flush().
|
|
3365
3703
|
*/
|
|
3704
|
+
/**
|
|
3705
|
+
* Emit `crossdeck.contract_failed` with the canonical property
|
|
3706
|
+
* shape (`contract_id`, `sdk_version`, `sdk_platform`,
|
|
3707
|
+
* `failure_reason`, `run_context`, `run_id`). Goes through the
|
|
3708
|
+
* standard track() pipeline — same consent gate, same queue,
|
|
3709
|
+
* same ingest, no new endpoint.
|
|
3710
|
+
*
|
|
3711
|
+
* Wire the call from a test hook, dogfood failure path, or
|
|
3712
|
+
* customer contract-verification harness; see
|
|
3713
|
+
* `contracts/README.md` for the per-test-framework hook recipes.
|
|
3714
|
+
*/
|
|
3715
|
+
reportContractFailure(input) {
|
|
3716
|
+
const props = {
|
|
3717
|
+
contract_id: input.contractId,
|
|
3718
|
+
sdk_version: SDK_VERSION,
|
|
3719
|
+
sdk_platform: "web",
|
|
3720
|
+
failure_reason: input.failureReason,
|
|
3721
|
+
run_context: input.runContext,
|
|
3722
|
+
run_id: input.runId
|
|
3723
|
+
};
|
|
3724
|
+
if (input.testRef) {
|
|
3725
|
+
props.test_file = input.testRef.file;
|
|
3726
|
+
props.test_name = input.testRef.name;
|
|
3727
|
+
}
|
|
3728
|
+
if (input.extra) {
|
|
3729
|
+
for (const [k, v] of Object.entries(input.extra)) {
|
|
3730
|
+
if (props[k] === void 0) props[k] = v;
|
|
3731
|
+
}
|
|
3732
|
+
}
|
|
3733
|
+
this.track("crossdeck.contract_failed", props);
|
|
3734
|
+
}
|
|
3366
3735
|
track(name, properties) {
|
|
3367
3736
|
const s = this.requireStarted();
|
|
3368
3737
|
if (!name) {
|
|
@@ -3499,11 +3868,24 @@ var CrossdeckClient = class {
|
|
|
3499
3868
|
});
|
|
3500
3869
|
}
|
|
3501
3870
|
const rail = input.rail ?? "apple";
|
|
3871
|
+
const body = { ...input, rail };
|
|
3872
|
+
const idempotencyKey = deriveIdempotencyKeyForPurchase(body);
|
|
3502
3873
|
const result = await s.http.request("POST", "/purchases/sync", {
|
|
3503
|
-
body
|
|
3874
|
+
body,
|
|
3875
|
+
idempotencyKey
|
|
3504
3876
|
});
|
|
3505
3877
|
s.identity.setCrossdeckCustomerId(result.crossdeckCustomerId);
|
|
3506
3878
|
s.entitlements.setFromList(result.entitlements);
|
|
3879
|
+
try {
|
|
3880
|
+
const sourceProductId = result.entitlements[0]?.source.productId;
|
|
3881
|
+
const sourceSubscriptionId = result.entitlements[0]?.source.subscriptionId;
|
|
3882
|
+
const props = { rail };
|
|
3883
|
+
if (sourceProductId) props.productId = sourceProductId;
|
|
3884
|
+
if (sourceSubscriptionId) props.subscriptionId = sourceSubscriptionId;
|
|
3885
|
+
if (result.idempotent_replay) props.idempotent_replay = true;
|
|
3886
|
+
this.track("purchase.completed", props);
|
|
3887
|
+
} catch {
|
|
3888
|
+
}
|
|
3507
3889
|
s.debug.emit(
|
|
3508
3890
|
"sdk.purchase_evidence_sent",
|
|
3509
3891
|
"StoreKit transaction forwarded. Waiting for backend verification.",
|
|
@@ -3559,7 +3941,7 @@ var CrossdeckClient = class {
|
|
|
3559
3941
|
}
|
|
3560
3942
|
this.state.autoTracker?.uninstall();
|
|
3561
3943
|
this.state.identity.reset();
|
|
3562
|
-
this.state.entitlements.
|
|
3944
|
+
this.state.entitlements.clearAll();
|
|
3563
3945
|
this.state.events.reset();
|
|
3564
3946
|
this.state.superProps.clear();
|
|
3565
3947
|
this.state.breadcrumbs.clear();
|