@cross-deck/web 1.5.1 → 1.5.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/dist/crossdeck.umd.min.js +2 -2
- package/dist/crossdeck.umd.min.js.map +1 -1
- package/dist/error-codes.json +1 -106
- package/dist/index.cjs +22 -940
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +4 -177
- package/dist/index.d.ts +4 -177
- package/dist/index.mjs +22 -939
- package/dist/index.mjs.map +1 -1
- package/dist/react.cjs +22 -404
- 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 +22 -404
- package/dist/react.mjs.map +1 -1
- package/dist/{types-Bu3jbmdq.d.mts → types-BzoKor4z.d.mts} +0 -4
- package/dist/{types-Bu3jbmdq.d.ts → types-BzoKor4z.d.ts} +0 -4
- package/dist/vue.cjs +22 -404
- package/dist/vue.cjs.map +1 -1
- package/dist/vue.mjs +22 -404
- package/dist/vue.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/contracts 2.json +0 -378
- package/dist/contracts.json +0 -378
- package/dist/crossdeck.umd.min 2.js +0 -3
- package/dist/crossdeck.umd.min.js 2.map +0 -1
- package/dist/error-codes 2.json +0 -196
- package/dist/index 2.cjs +0 -4778
- package/dist/index 2.mjs +0 -4742
- package/dist/index.cjs 2.map +0 -1
- package/dist/index.d 2.mts +0 -938
- package/dist/index.d 2.ts +0 -938
- package/dist/index.mjs 2.map +0 -1
- package/dist/react 2.cjs +0 -4220
- package/dist/react 2.mjs +0 -4193
- package/dist/react.cjs 2.map +0 -1
- package/dist/react.d 2.mts +0 -91
- package/dist/react.d 2.ts +0 -91
- package/dist/react.mjs 2.map +0 -1
- package/dist/types-Bu3jbmdq.d 2.mts +0 -314
- package/dist/types-Bu3jbmdq.d 2.ts +0 -314
- package/dist/vue 2.cjs +0 -4185
- package/dist/vue 2.mjs +0 -4159
- package/dist/vue.cjs 2.map +0 -1
- package/dist/vue.d 2.mts +0 -37
- package/dist/vue.d 2.ts +0 -37
- package/dist/vue.mjs 2.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -64,7 +64,7 @@ function typeMapForStatus(status) {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
// src/_version.ts
|
|
67
|
-
var SDK_VERSION = "1.
|
|
67
|
+
var SDK_VERSION = "1.3.0";
|
|
68
68
|
var SDK_NAME = "@cross-deck/web";
|
|
69
69
|
|
|
70
70
|
// src/http.ts
|
|
@@ -314,258 +314,29 @@ function randomChars(count) {
|
|
|
314
314
|
return out.join("");
|
|
315
315
|
}
|
|
316
316
|
|
|
317
|
-
// src/hash.ts
|
|
318
|
-
var K = new Uint32Array([
|
|
319
|
-
1116352408,
|
|
320
|
-
1899447441,
|
|
321
|
-
3049323471,
|
|
322
|
-
3921009573,
|
|
323
|
-
961987163,
|
|
324
|
-
1508970993,
|
|
325
|
-
2453635748,
|
|
326
|
-
2870763221,
|
|
327
|
-
3624381080,
|
|
328
|
-
310598401,
|
|
329
|
-
607225278,
|
|
330
|
-
1426881987,
|
|
331
|
-
1925078388,
|
|
332
|
-
2162078206,
|
|
333
|
-
2614888103,
|
|
334
|
-
3248222580,
|
|
335
|
-
3835390401,
|
|
336
|
-
4022224774,
|
|
337
|
-
264347078,
|
|
338
|
-
604807628,
|
|
339
|
-
770255983,
|
|
340
|
-
1249150122,
|
|
341
|
-
1555081692,
|
|
342
|
-
1996064986,
|
|
343
|
-
2554220882,
|
|
344
|
-
2821834349,
|
|
345
|
-
2952996808,
|
|
346
|
-
3210313671,
|
|
347
|
-
3336571891,
|
|
348
|
-
3584528711,
|
|
349
|
-
113926993,
|
|
350
|
-
338241895,
|
|
351
|
-
666307205,
|
|
352
|
-
773529912,
|
|
353
|
-
1294757372,
|
|
354
|
-
1396182291,
|
|
355
|
-
1695183700,
|
|
356
|
-
1986661051,
|
|
357
|
-
2177026350,
|
|
358
|
-
2456956037,
|
|
359
|
-
2730485921,
|
|
360
|
-
2820302411,
|
|
361
|
-
3259730800,
|
|
362
|
-
3345764771,
|
|
363
|
-
3516065817,
|
|
364
|
-
3600352804,
|
|
365
|
-
4094571909,
|
|
366
|
-
275423344,
|
|
367
|
-
430227734,
|
|
368
|
-
506948616,
|
|
369
|
-
659060556,
|
|
370
|
-
883997877,
|
|
371
|
-
958139571,
|
|
372
|
-
1322822218,
|
|
373
|
-
1537002063,
|
|
374
|
-
1747873779,
|
|
375
|
-
1955562222,
|
|
376
|
-
2024104815,
|
|
377
|
-
2227730452,
|
|
378
|
-
2361852424,
|
|
379
|
-
2428436474,
|
|
380
|
-
2756734187,
|
|
381
|
-
3204031479,
|
|
382
|
-
3329325298
|
|
383
|
-
]);
|
|
384
|
-
function utf8Bytes(input) {
|
|
385
|
-
if (typeof TextEncoder !== "undefined") {
|
|
386
|
-
return new TextEncoder().encode(input);
|
|
387
|
-
}
|
|
388
|
-
const out = [];
|
|
389
|
-
for (let i = 0; i < input.length; i++) {
|
|
390
|
-
let codePoint = input.charCodeAt(i);
|
|
391
|
-
if (codePoint >= 55296 && codePoint <= 56319 && i + 1 < input.length) {
|
|
392
|
-
const next = input.charCodeAt(i + 1);
|
|
393
|
-
if (next >= 56320 && next <= 57343) {
|
|
394
|
-
codePoint = 65536 + (codePoint - 55296 << 10) + (next - 56320);
|
|
395
|
-
i++;
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
if (codePoint < 128) {
|
|
399
|
-
out.push(codePoint);
|
|
400
|
-
} else if (codePoint < 2048) {
|
|
401
|
-
out.push(192 | codePoint >> 6);
|
|
402
|
-
out.push(128 | codePoint & 63);
|
|
403
|
-
} else if (codePoint < 65536) {
|
|
404
|
-
out.push(224 | codePoint >> 12);
|
|
405
|
-
out.push(128 | codePoint >> 6 & 63);
|
|
406
|
-
out.push(128 | codePoint & 63);
|
|
407
|
-
} else {
|
|
408
|
-
out.push(240 | codePoint >> 18);
|
|
409
|
-
out.push(128 | codePoint >> 12 & 63);
|
|
410
|
-
out.push(128 | codePoint >> 6 & 63);
|
|
411
|
-
out.push(128 | codePoint & 63);
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
return new Uint8Array(out);
|
|
415
|
-
}
|
|
416
|
-
function sha256Hex(input) {
|
|
417
|
-
const bytes = utf8Bytes(input);
|
|
418
|
-
const bitLength = bytes.length * 8;
|
|
419
|
-
const blockCount = Math.floor((bytes.length + 9 + 63) / 64);
|
|
420
|
-
const padded = new Uint8Array(blockCount * 64);
|
|
421
|
-
padded.set(bytes);
|
|
422
|
-
padded[bytes.length] = 128;
|
|
423
|
-
const high = Math.floor(bitLength / 4294967296);
|
|
424
|
-
const low = bitLength >>> 0;
|
|
425
|
-
const lenOffset = padded.length - 8;
|
|
426
|
-
padded[lenOffset + 0] = high >>> 24 & 255;
|
|
427
|
-
padded[lenOffset + 1] = high >>> 16 & 255;
|
|
428
|
-
padded[lenOffset + 2] = high >>> 8 & 255;
|
|
429
|
-
padded[lenOffset + 3] = high & 255;
|
|
430
|
-
padded[lenOffset + 4] = low >>> 24 & 255;
|
|
431
|
-
padded[lenOffset + 5] = low >>> 16 & 255;
|
|
432
|
-
padded[lenOffset + 6] = low >>> 8 & 255;
|
|
433
|
-
padded[lenOffset + 7] = low & 255;
|
|
434
|
-
const H = new Uint32Array([
|
|
435
|
-
1779033703,
|
|
436
|
-
3144134277,
|
|
437
|
-
1013904242,
|
|
438
|
-
2773480762,
|
|
439
|
-
1359893119,
|
|
440
|
-
2600822924,
|
|
441
|
-
528734635,
|
|
442
|
-
1541459225
|
|
443
|
-
]);
|
|
444
|
-
const W = new Uint32Array(64);
|
|
445
|
-
for (let block = 0; block < blockCount; block++) {
|
|
446
|
-
const offset = block * 64;
|
|
447
|
-
for (let t = 0; t < 16; t++) {
|
|
448
|
-
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;
|
|
449
|
-
}
|
|
450
|
-
for (let t = 16; t < 64; t++) {
|
|
451
|
-
const w15 = W[t - 15];
|
|
452
|
-
const w2 = W[t - 2];
|
|
453
|
-
const s0 = (w15 >>> 7 | w15 << 25) ^ (w15 >>> 18 | w15 << 14) ^ w15 >>> 3;
|
|
454
|
-
const s1 = (w2 >>> 17 | w2 << 15) ^ (w2 >>> 19 | w2 << 13) ^ w2 >>> 10;
|
|
455
|
-
W[t] = W[t - 16] + s0 + W[t - 7] + s1 >>> 0;
|
|
456
|
-
}
|
|
457
|
-
let a = H[0], b = H[1], c = H[2], d = H[3];
|
|
458
|
-
let e = H[4], f = H[5], g = H[6], h = H[7];
|
|
459
|
-
for (let t = 0; t < 64; t++) {
|
|
460
|
-
const S1 = (e >>> 6 | e << 26) ^ (e >>> 11 | e << 21) ^ (e >>> 25 | e << 7);
|
|
461
|
-
const ch = e & f ^ ~e & g;
|
|
462
|
-
const temp1 = h + S1 + ch + K[t] + W[t] >>> 0;
|
|
463
|
-
const S0 = (a >>> 2 | a << 30) ^ (a >>> 13 | a << 19) ^ (a >>> 22 | a << 10);
|
|
464
|
-
const maj = a & b ^ a & c ^ b & c;
|
|
465
|
-
const temp2 = S0 + maj >>> 0;
|
|
466
|
-
h = g;
|
|
467
|
-
g = f;
|
|
468
|
-
f = e;
|
|
469
|
-
e = d + temp1 >>> 0;
|
|
470
|
-
d = c;
|
|
471
|
-
c = b;
|
|
472
|
-
b = a;
|
|
473
|
-
a = temp1 + temp2 >>> 0;
|
|
474
|
-
}
|
|
475
|
-
H[0] = H[0] + a >>> 0;
|
|
476
|
-
H[1] = H[1] + b >>> 0;
|
|
477
|
-
H[2] = H[2] + c >>> 0;
|
|
478
|
-
H[3] = H[3] + d >>> 0;
|
|
479
|
-
H[4] = H[4] + e >>> 0;
|
|
480
|
-
H[5] = H[5] + f >>> 0;
|
|
481
|
-
H[6] = H[6] + g >>> 0;
|
|
482
|
-
H[7] = H[7] + h >>> 0;
|
|
483
|
-
}
|
|
484
|
-
let hex = "";
|
|
485
|
-
for (let i = 0; i < 8; i++) {
|
|
486
|
-
hex += H[i].toString(16).padStart(8, "0");
|
|
487
|
-
}
|
|
488
|
-
return hex;
|
|
489
|
-
}
|
|
490
|
-
|
|
491
317
|
// src/entitlement-cache.ts
|
|
492
318
|
var DEFAULT_STALE_AFTER_MS = 24 * 60 * 60 * 1e3;
|
|
493
|
-
var
|
|
494
|
-
var INDEX_SUFFIX = "_index";
|
|
495
|
-
var EntitlementCache = class _EntitlementCache {
|
|
319
|
+
var EntitlementCache = class {
|
|
496
320
|
/**
|
|
497
|
-
* @param storage
|
|
498
|
-
*
|
|
499
|
-
*
|
|
500
|
-
*
|
|
501
|
-
* @param
|
|
502
|
-
*
|
|
503
|
-
*
|
|
504
|
-
* suffix is filled at identify() / reset()
|
|
505
|
-
* time — see [[setUserKey]].
|
|
506
|
-
* @param staleAfterMs Age past which last-known-good is flagged stale
|
|
507
|
-
* even without a failed refresh. Default 24h.
|
|
321
|
+
* @param storage Device storage adapter. When omitted (tests) or
|
|
322
|
+
* a MemoryStorage (strict-consent / no-persistence
|
|
323
|
+
* mode) the cache is session-only — durability is
|
|
324
|
+
* simply absent, never wrong.
|
|
325
|
+
* @param storageKey Full key the persisted blob lives under.
|
|
326
|
+
* @param staleAfterMs Age past which last-known-good is flagged stale
|
|
327
|
+
* even without a failed refresh. Default 24h.
|
|
508
328
|
*/
|
|
509
|
-
constructor(storage,
|
|
329
|
+
constructor(storage, storageKey = "crossdeck:entitlements", staleAfterMs = DEFAULT_STALE_AFTER_MS) {
|
|
510
330
|
this.all = [];
|
|
511
331
|
this.lastUpdated = 0;
|
|
512
332
|
this.lastRefreshFailedAt = 0;
|
|
513
333
|
this.listeners = /* @__PURE__ */ new Set();
|
|
514
334
|
this.listenerErrorCount = 0;
|
|
515
|
-
this.currentSuffix = ANON_SUFFIX;
|
|
516
335
|
this.storage = storage;
|
|
517
|
-
this.
|
|
336
|
+
this.storageKey = storageKey;
|
|
518
337
|
this.staleAfterMs = staleAfterMs;
|
|
519
338
|
this.hydrate();
|
|
520
339
|
}
|
|
521
|
-
/** The full storage key the current-user blob is persisted under. */
|
|
522
|
-
get storageKey() {
|
|
523
|
-
return `${this.storageKeyPrefix}:${this.currentSuffix}`;
|
|
524
|
-
}
|
|
525
|
-
/** Key of the index blob — a JSON array of every suffix we've
|
|
526
|
-
* written. Used by clearAll() to scope a logout-wipe. */
|
|
527
|
-
get indexKey() {
|
|
528
|
-
return `${this.storageKeyPrefix}:${INDEX_SUFFIX}`;
|
|
529
|
-
}
|
|
530
|
-
/** Derive a stable suffix for a developerUserId via SHA-256. The
|
|
531
|
-
* raw userId never lands in the storage key — protects against
|
|
532
|
-
* accidentally leaking email-style identifiers through DevTools
|
|
533
|
-
* inspection. Pass `null` to switch back to the anonymous slot. */
|
|
534
|
-
static suffixForUserId(userId) {
|
|
535
|
-
if (userId == null || userId === "") return ANON_SUFFIX;
|
|
536
|
-
return sha256Hex(userId);
|
|
537
|
-
}
|
|
538
|
-
/**
|
|
539
|
-
* Switch the cache to a different user's storage slot. Bank-grade
|
|
540
|
-
* three-layer isolation:
|
|
541
|
-
* (a) Physical key separation — `<prefix>:<sha256(userId)>` so
|
|
542
|
-
* a user-switch can't physically read prior user's data
|
|
543
|
-
* even if the in-memory clear was skipped.
|
|
544
|
-
* (b) Unconditional in-memory clear — invoked whenever the
|
|
545
|
-
* active suffix changes, even on same-id re-identify.
|
|
546
|
-
* (c) Re-hydrate from the new slot — a returning user observes
|
|
547
|
-
* their last-known-good cache from storage immediately.
|
|
548
|
-
*
|
|
549
|
-
* Caller (identify() / reset()) MUST invoke this BEFORE the next
|
|
550
|
-
* setFromList() so the write lands under the right key.
|
|
551
|
-
*/
|
|
552
|
-
setUserKey(userId) {
|
|
553
|
-
const nextSuffix = _EntitlementCache.suffixForUserId(userId);
|
|
554
|
-
if (nextSuffix === this.currentSuffix) {
|
|
555
|
-
this.all = [];
|
|
556
|
-
this.lastUpdated = 0;
|
|
557
|
-
this.lastRefreshFailedAt = 0;
|
|
558
|
-
this.notify();
|
|
559
|
-
this.hydrate();
|
|
560
|
-
return;
|
|
561
|
-
}
|
|
562
|
-
this.currentSuffix = nextSuffix;
|
|
563
|
-
this.all = [];
|
|
564
|
-
this.lastUpdated = 0;
|
|
565
|
-
this.lastRefreshFailedAt = 0;
|
|
566
|
-
this.hydrate();
|
|
567
|
-
this.notify();
|
|
568
|
-
}
|
|
569
340
|
/**
|
|
570
341
|
* Sync read — true iff the entitlement is currently granting access.
|
|
571
342
|
*
|
|
@@ -635,13 +406,12 @@ var EntitlementCache = class _EntitlementCache {
|
|
|
635
406
|
this.lastUpdated = Date.now();
|
|
636
407
|
this.lastRefreshFailedAt = 0;
|
|
637
408
|
this.persist();
|
|
638
|
-
this.recordSuffixInIndex(this.currentSuffix);
|
|
639
409
|
this.notify();
|
|
640
410
|
}
|
|
641
411
|
/**
|
|
642
|
-
* Wipe
|
|
643
|
-
*
|
|
644
|
-
*
|
|
412
|
+
* Wipe — used on reset() (logout) and on an identity switch. Clears
|
|
413
|
+
* BOTH memory and durable storage so a prior user's entitlements can
|
|
414
|
+
* never leak to the next person on this device.
|
|
645
415
|
*/
|
|
646
416
|
clear() {
|
|
647
417
|
this.all = [];
|
|
@@ -653,40 +423,6 @@ var EntitlementCache = class _EntitlementCache {
|
|
|
653
423
|
} catch {
|
|
654
424
|
}
|
|
655
425
|
}
|
|
656
|
-
this.removeSuffixFromIndex(this.currentSuffix);
|
|
657
|
-
this.notify();
|
|
658
|
-
}
|
|
659
|
-
/**
|
|
660
|
-
* Logout-grade wipe — bank-grade contract: removes EVERY per-user
|
|
661
|
-
* entitlement slot the SDK has ever written on this device, then
|
|
662
|
-
* clears the index. Used by `Crossdeck.reset()` so a logout on a
|
|
663
|
-
* shared device can never leave another user's entitlements
|
|
664
|
-
* readable (layer (c) of the v1.4.0 isolation fix).
|
|
665
|
-
*
|
|
666
|
-
* After clearAll(), the cache is back to anonymous + empty.
|
|
667
|
-
*/
|
|
668
|
-
clearAll() {
|
|
669
|
-
this.all = [];
|
|
670
|
-
this.lastUpdated = 0;
|
|
671
|
-
this.lastRefreshFailedAt = 0;
|
|
672
|
-
this.currentSuffix = ANON_SUFFIX;
|
|
673
|
-
if (this.storage) {
|
|
674
|
-
const suffixes = this.readIndex();
|
|
675
|
-
for (const suffix of suffixes) {
|
|
676
|
-
try {
|
|
677
|
-
this.storage.removeItem(`${this.storageKeyPrefix}:${suffix}`);
|
|
678
|
-
} catch {
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
try {
|
|
682
|
-
this.storage.removeItem(`${this.storageKeyPrefix}:${ANON_SUFFIX}`);
|
|
683
|
-
} catch {
|
|
684
|
-
}
|
|
685
|
-
try {
|
|
686
|
-
this.storage.removeItem(this.indexKey);
|
|
687
|
-
} catch {
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
426
|
this.notify();
|
|
691
427
|
}
|
|
692
428
|
/**
|
|
@@ -736,47 +472,6 @@ var EntitlementCache = class _EntitlementCache {
|
|
|
736
472
|
} catch {
|
|
737
473
|
}
|
|
738
474
|
}
|
|
739
|
-
/** Read the index of all per-user suffixes the SDK has written. */
|
|
740
|
-
readIndex() {
|
|
741
|
-
if (!this.storage) return [];
|
|
742
|
-
try {
|
|
743
|
-
const raw = this.storage.getItem(this.indexKey);
|
|
744
|
-
if (!raw) return [];
|
|
745
|
-
const parsed = JSON.parse(raw);
|
|
746
|
-
if (Array.isArray(parsed)) {
|
|
747
|
-
return parsed.filter((x) => typeof x === "string");
|
|
748
|
-
}
|
|
749
|
-
return [];
|
|
750
|
-
} catch {
|
|
751
|
-
return [];
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
/** Add a suffix to the persisted index. Idempotent. */
|
|
755
|
-
recordSuffixInIndex(suffix) {
|
|
756
|
-
if (!this.storage) return;
|
|
757
|
-
const existing = this.readIndex();
|
|
758
|
-
if (existing.includes(suffix)) return;
|
|
759
|
-
existing.push(suffix);
|
|
760
|
-
try {
|
|
761
|
-
this.storage.setItem(this.indexKey, JSON.stringify(existing));
|
|
762
|
-
} catch {
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
/** Remove a suffix from the persisted index. No-op if absent. */
|
|
766
|
-
removeSuffixFromIndex(suffix) {
|
|
767
|
-
if (!this.storage) return;
|
|
768
|
-
const existing = this.readIndex();
|
|
769
|
-
const next = existing.filter((s) => s !== suffix);
|
|
770
|
-
if (next.length === existing.length) return;
|
|
771
|
-
try {
|
|
772
|
-
if (next.length === 0) {
|
|
773
|
-
this.storage.removeItem(this.indexKey);
|
|
774
|
-
} else {
|
|
775
|
-
this.storage.setItem(this.indexKey, JSON.stringify(next));
|
|
776
|
-
}
|
|
777
|
-
} catch {
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
475
|
notify() {
|
|
781
476
|
if (this.listeners.size === 0) return;
|
|
782
477
|
const snapshot = this.all.slice();
|
|
@@ -791,34 +486,6 @@ var EntitlementCache = class _EntitlementCache {
|
|
|
791
486
|
}
|
|
792
487
|
};
|
|
793
488
|
|
|
794
|
-
// src/idempotency-key.ts
|
|
795
|
-
function formatAsUuid(hex) {
|
|
796
|
-
return [
|
|
797
|
-
hex.slice(0, 8),
|
|
798
|
-
hex.slice(8, 12),
|
|
799
|
-
hex.slice(12, 16),
|
|
800
|
-
hex.slice(16, 20),
|
|
801
|
-
hex.slice(20, 32)
|
|
802
|
-
].join("-");
|
|
803
|
-
}
|
|
804
|
-
function deriveIdempotencyKeyForPurchase(body) {
|
|
805
|
-
let identifier;
|
|
806
|
-
if (body.rail === "apple") {
|
|
807
|
-
identifier = body.signedTransactionInfo ?? "";
|
|
808
|
-
} else if (body.rail === "google") {
|
|
809
|
-
identifier = body.purchaseToken ?? "";
|
|
810
|
-
} else {
|
|
811
|
-
identifier = "";
|
|
812
|
-
}
|
|
813
|
-
if (!identifier) {
|
|
814
|
-
throw new Error(
|
|
815
|
-
`deriveIdempotencyKeyForPurchase: no stable identifier in body (rail=${body.rail}). Apple needs signedTransactionInfo; Google needs purchaseToken.`
|
|
816
|
-
);
|
|
817
|
-
}
|
|
818
|
-
const namespaced = `crossdeck:purchases/sync:${body.rail}:${identifier}`;
|
|
819
|
-
return formatAsUuid(sha256Hex(namespaced));
|
|
820
|
-
}
|
|
821
|
-
|
|
822
489
|
// src/retry-policy.ts
|
|
823
490
|
var DEFAULT_BASE = 1e3;
|
|
824
491
|
var DEFAULT_MAX = 6e4;
|
|
@@ -3081,10 +2748,6 @@ var CrossdeckClient = class {
|
|
|
3081
2748
|
this.state.errors?.uninstall();
|
|
3082
2749
|
} catch {
|
|
3083
2750
|
}
|
|
3084
|
-
try {
|
|
3085
|
-
void this.state.events.flush({ keepalive: true });
|
|
3086
|
-
} catch {
|
|
3087
|
-
}
|
|
3088
2751
|
}
|
|
3089
2752
|
if (!options.publicKey || !options.publicKey.startsWith("cd_pub_")) {
|
|
3090
2753
|
throw new CrossdeckError({
|
|
@@ -3132,12 +2795,7 @@ var CrossdeckClient = class {
|
|
|
3132
2795
|
// load still flushes if the user leaves quickly (the keepalive
|
|
3133
2796
|
// pagehide handler picks up anything that doesn't); long enough
|
|
3134
2797
|
// that bursts of clicks coalesce into one network round-trip.
|
|
3135
|
-
|
|
3136
|
-
// v1.4.0: Web/Node 1500ms, RN/Swift/Android 5000ms. All
|
|
3137
|
-
// converged on 2000ms (the Stripe-adjacent industry norm)
|
|
3138
|
-
// so cross-platform funnels show events landing at the
|
|
3139
|
-
// same cadence on every SDK. Per-instance override stays.
|
|
3140
|
-
eventFlushIntervalMs: options.eventFlushIntervalMs ?? 2e3,
|
|
2798
|
+
eventFlushIntervalMs: options.eventFlushIntervalMs ?? 1500,
|
|
3141
2799
|
sdkVersion: options.sdkVersion ?? SDK_VERSION,
|
|
3142
2800
|
autoTrack,
|
|
3143
2801
|
appVersion: options.appVersion ?? null
|
|
@@ -3365,10 +3023,14 @@ var CrossdeckClient = class {
|
|
|
3365
3023
|
};
|
|
3366
3024
|
if (options?.email) body.email = options.email;
|
|
3367
3025
|
if (traits) body.traits = traits;
|
|
3368
|
-
s.entitlements.setUserKey(userId);
|
|
3369
3026
|
const result = await s.http.request("POST", "/identity/alias", {
|
|
3370
3027
|
body
|
|
3371
3028
|
});
|
|
3029
|
+
const priorCdcust = s.identity.crossdeckCustomerId;
|
|
3030
|
+
const cacheHasEntries = s.entitlements.list().length > 0;
|
|
3031
|
+
if (priorCdcust && result.crossdeckCustomerId && priorCdcust !== result.crossdeckCustomerId || !priorCdcust && cacheHasEntries) {
|
|
3032
|
+
s.entitlements.clear();
|
|
3033
|
+
}
|
|
3372
3034
|
s.identity.setCrossdeckCustomerId(result.crossdeckCustomerId);
|
|
3373
3035
|
s.developerUserId = userId;
|
|
3374
3036
|
return result;
|
|
@@ -3673,37 +3335,6 @@ var CrossdeckClient = class {
|
|
|
3673
3335
|
* trip happens in the background. To flush before the page unloads,
|
|
3674
3336
|
* call flush().
|
|
3675
3337
|
*/
|
|
3676
|
-
/**
|
|
3677
|
-
* Emit `crossdeck.contract_failed` with the canonical property
|
|
3678
|
-
* shape (`contract_id`, `sdk_version`, `sdk_platform`,
|
|
3679
|
-
* `failure_reason`, `run_context`, `run_id`). Goes through the
|
|
3680
|
-
* standard track() pipeline — same consent gate, same queue,
|
|
3681
|
-
* same ingest, no new endpoint.
|
|
3682
|
-
*
|
|
3683
|
-
* Wire the call from a test hook, dogfood failure path, or
|
|
3684
|
-
* customer contract-verification harness; see
|
|
3685
|
-
* `contracts/README.md` for the per-test-framework hook recipes.
|
|
3686
|
-
*/
|
|
3687
|
-
reportContractFailure(input) {
|
|
3688
|
-
const props = {
|
|
3689
|
-
contract_id: input.contractId,
|
|
3690
|
-
sdk_version: SDK_VERSION,
|
|
3691
|
-
sdk_platform: "web",
|
|
3692
|
-
failure_reason: input.failureReason,
|
|
3693
|
-
run_context: input.runContext,
|
|
3694
|
-
run_id: input.runId
|
|
3695
|
-
};
|
|
3696
|
-
if (input.testRef) {
|
|
3697
|
-
props.test_file = input.testRef.file;
|
|
3698
|
-
props.test_name = input.testRef.name;
|
|
3699
|
-
}
|
|
3700
|
-
if (input.extra) {
|
|
3701
|
-
for (const [k, v] of Object.entries(input.extra)) {
|
|
3702
|
-
if (props[k] === void 0) props[k] = v;
|
|
3703
|
-
}
|
|
3704
|
-
}
|
|
3705
|
-
this.track("crossdeck.contract_failed", props);
|
|
3706
|
-
}
|
|
3707
3338
|
track(name, properties) {
|
|
3708
3339
|
const s = this.requireStarted();
|
|
3709
3340
|
if (!name) {
|
|
@@ -3840,24 +3471,11 @@ var CrossdeckClient = class {
|
|
|
3840
3471
|
});
|
|
3841
3472
|
}
|
|
3842
3473
|
const rail = input.rail ?? "apple";
|
|
3843
|
-
const body = { ...input, rail };
|
|
3844
|
-
const idempotencyKey = deriveIdempotencyKeyForPurchase(body);
|
|
3845
3474
|
const result = await s.http.request("POST", "/purchases/sync", {
|
|
3846
|
-
body,
|
|
3847
|
-
idempotencyKey
|
|
3475
|
+
body: { ...input, rail }
|
|
3848
3476
|
});
|
|
3849
3477
|
s.identity.setCrossdeckCustomerId(result.crossdeckCustomerId);
|
|
3850
3478
|
s.entitlements.setFromList(result.entitlements);
|
|
3851
|
-
try {
|
|
3852
|
-
const sourceProductId = result.entitlements[0]?.source.productId;
|
|
3853
|
-
const sourceSubscriptionId = result.entitlements[0]?.source.subscriptionId;
|
|
3854
|
-
const props = { rail };
|
|
3855
|
-
if (sourceProductId) props.productId = sourceProductId;
|
|
3856
|
-
if (sourceSubscriptionId) props.subscriptionId = sourceSubscriptionId;
|
|
3857
|
-
if (result.idempotent_replay) props.idempotent_replay = true;
|
|
3858
|
-
this.track("purchase.completed", props);
|
|
3859
|
-
} catch {
|
|
3860
|
-
}
|
|
3861
3479
|
s.debug.emit(
|
|
3862
3480
|
"sdk.purchase_evidence_sent",
|
|
3863
3481
|
"StoreKit transaction forwarded. Waiting for backend verification.",
|
|
@@ -3913,7 +3531,7 @@ var CrossdeckClient = class {
|
|
|
3913
3531
|
}
|
|
3914
3532
|
this.state.autoTracker?.uninstall();
|
|
3915
3533
|
this.state.identity.reset();
|
|
3916
|
-
this.state.entitlements.
|
|
3534
|
+
this.state.entitlements.clear();
|
|
3917
3535
|
this.state.events.reset();
|
|
3918
3536
|
this.state.superProps.clear();
|
|
3919
3537
|
this.state.breadcrumbs.clear();
|
|
@@ -4188,550 +3806,15 @@ var CROSSDECK_ERROR_CODES = Object.freeze([
|
|
|
4188
3806
|
description: "The server returned a 2xx with an unparseable body.",
|
|
4189
3807
|
resolution: "Likely a transient backend bug. Retry; if it persists, contact support with the requestId.",
|
|
4190
3808
|
retryable: true
|
|
4191
|
-
},
|
|
4192
|
-
// ----- Backend-emitted codes (v1.4.0 Phase 6.2 backfill) -----
|
|
4193
|
-
// Mirror of backend/src/api/v1-errors.ts ApiErrorCode. A developer
|
|
4194
|
-
// hitting any of these on the wire can look them up via
|
|
4195
|
-
// getErrorCode(code) for a canonical remediation step instead of
|
|
4196
|
-
// hunting through Slack history.
|
|
4197
|
-
{
|
|
4198
|
-
code: "missing_api_key",
|
|
4199
|
-
type: "authentication_error",
|
|
4200
|
-
description: "No Authorization header (or Crossdeck-Api-Key header) on the request.",
|
|
4201
|
-
resolution: "Make sure Crossdeck.init({ publicKey }) was called with a cd_pub_\u2026 key before the request fired. Re-check your env-vars in CI / Docker if the key is empty at runtime.",
|
|
4202
|
-
retryable: false
|
|
4203
|
-
},
|
|
4204
|
-
{
|
|
4205
|
-
code: "invalid_api_key",
|
|
4206
|
-
type: "authentication_error",
|
|
4207
|
-
description: "The API key is malformed, unknown, or doesn't resolve to a project.",
|
|
4208
|
-
resolution: "Copy the key from your Crossdeck dashboard \u2192 API keys. Confirm prefix is cd_pub_test_ / cd_pub_live_ for client SDKs and cd_sk_test_ / cd_sk_live_ for the Node server SDK.",
|
|
4209
|
-
retryable: false
|
|
4210
|
-
},
|
|
4211
|
-
{
|
|
4212
|
-
code: "key_revoked",
|
|
4213
|
-
type: "authentication_error",
|
|
4214
|
-
description: "The API key was revoked in the Crossdeck dashboard.",
|
|
4215
|
-
resolution: "Mint a fresh key in the dashboard \u2192 API keys \u2192 Create new, swap it in, and redeploy. The revoked key cannot be reactivated.",
|
|
4216
|
-
retryable: false
|
|
4217
|
-
},
|
|
4218
|
-
{
|
|
4219
|
-
code: "identity_token_invalid",
|
|
4220
|
-
type: "authentication_error",
|
|
4221
|
-
description: "The Firebase / Apple / Google ID token supplied with the request didn't verify against the dashboard's configured signers.",
|
|
4222
|
-
resolution: "Refresh the token client-side (Firebase auth.currentUser.getIdToken(true)) and retry. If the failure persists, confirm the signer is registered under dashboard \u2192 Authentication \u2192 Identity sources.",
|
|
4223
|
-
retryable: true
|
|
4224
|
-
},
|
|
4225
|
-
{
|
|
4226
|
-
code: "origin_not_allowed",
|
|
4227
|
-
type: "permission_error",
|
|
4228
|
-
description: "The Origin header isn't in the project's Allowed origins list.",
|
|
4229
|
-
resolution: "Add the origin (e.g. https://app.example.com) under dashboard \u2192 Settings \u2192 Allowed origins. Wildcards like https://*.example.com are supported.",
|
|
4230
|
-
retryable: false
|
|
4231
|
-
},
|
|
4232
|
-
{
|
|
4233
|
-
code: "bundle_id_not_allowed",
|
|
4234
|
-
type: "permission_error",
|
|
4235
|
-
description: "The iOS bundle ID sent via X-Crossdeck-Bundle-Id isn't registered under this app's Apple identity lock.",
|
|
4236
|
-
resolution: "Add the bundle ID under dashboard \u2192 Apps \u2192 <your app> \u2192 iOS bundle IDs.",
|
|
4237
|
-
retryable: false
|
|
4238
|
-
},
|
|
4239
|
-
{
|
|
4240
|
-
code: "package_name_not_allowed",
|
|
4241
|
-
type: "permission_error",
|
|
4242
|
-
description: "The Android package name sent via X-Crossdeck-Package-Name isn't registered under this app's Android identity lock.",
|
|
4243
|
-
resolution: "Add the package name under dashboard \u2192 Apps \u2192 <your app> \u2192 Android package names.",
|
|
4244
|
-
retryable: false
|
|
4245
|
-
},
|
|
4246
|
-
{
|
|
4247
|
-
code: "env_mismatch",
|
|
4248
|
-
type: "permission_error",
|
|
4249
|
-
description: "The request env (inferred from key prefix) doesn't match the resolved app's configured env.",
|
|
4250
|
-
resolution: "Use a cd_pub_live_ / cd_sk_live_ key with a production app, cd_pub_test_ / cd_sk_test_ with a sandbox app. The two cannot cross.",
|
|
4251
|
-
retryable: false
|
|
4252
|
-
},
|
|
4253
|
-
{
|
|
4254
|
-
code: "idempotency_key_in_use",
|
|
4255
|
-
type: "invalid_request_error",
|
|
4256
|
-
description: "An Idempotency-Key was reused for a request with a different body (Stripe-grade contract).",
|
|
4257
|
-
resolution: "Generate a fresh key for a different transaction, or reuse the key only with the EXACT same body. The v1.4.0 SDKs derive keys deterministically from the body so this should never fire on SDK-managed calls.",
|
|
4258
|
-
retryable: false
|
|
4259
|
-
},
|
|
4260
|
-
{
|
|
4261
|
-
code: "rate_limited",
|
|
4262
|
-
type: "rate_limit_error",
|
|
4263
|
-
description: "Request rate exceeded the project's per-second cap.",
|
|
4264
|
-
resolution: "Honour the Retry-After header \u2014 the SDK does this automatically on managed retries. If you're hitting it from a custom code path, throttle to <100 req/s/key.",
|
|
4265
|
-
retryable: true
|
|
4266
|
-
},
|
|
4267
|
-
{
|
|
4268
|
-
code: "internal_error",
|
|
4269
|
-
type: "internal_error",
|
|
4270
|
-
description: "Server-side issue. Safe to retry with backoff.",
|
|
4271
|
-
resolution: "The SDK retries automatically. If your code paths through to this error, contact support with the requestId from the response envelope.",
|
|
4272
|
-
retryable: true
|
|
4273
|
-
},
|
|
4274
|
-
{
|
|
4275
|
-
code: "google_not_supported",
|
|
4276
|
-
type: "invalid_request_error",
|
|
4277
|
-
description: "POST /purchases/sync with rail=google is gated until the Play Developer API reconciliation worker ships.",
|
|
4278
|
-
resolution: "Until v1.5+, Google Play purchases verify via Real-time Developer Notifications. The SDK auto-track path handles this transparently for Android consumers.",
|
|
4279
|
-
retryable: false
|
|
4280
|
-
},
|
|
4281
|
-
{
|
|
4282
|
-
code: "stripe_not_supported",
|
|
4283
|
-
type: "invalid_request_error",
|
|
4284
|
-
description: "POST /purchases/sync with rail=stripe is unsupported \u2014 Stripe Checkout's redirect flow uses platform webhooks instead.",
|
|
4285
|
-
resolution: "Wire Stripe via the standard Checkout / Customer Portal flow; Crossdeck reconciles via the platform webhook automatically. No SDK call needed.",
|
|
4286
|
-
retryable: false
|
|
4287
|
-
},
|
|
4288
|
-
{
|
|
4289
|
-
code: "missing_required_param",
|
|
4290
|
-
type: "invalid_request_error",
|
|
4291
|
-
description: "A required field is absent from the request body.",
|
|
4292
|
-
resolution: "Inspect the error.message \u2014 the missing field name is included verbatim. Refer to the SDK's TypeScript types for the canonical request shape.",
|
|
4293
|
-
retryable: false
|
|
4294
|
-
},
|
|
4295
|
-
{
|
|
4296
|
-
code: "invalid_param_value",
|
|
4297
|
-
type: "invalid_request_error",
|
|
4298
|
-
description: "A field is present but the value failed validation (wrong shape, wrong length, wrong enum value).",
|
|
4299
|
-
resolution: "Read error.message for the specific field + reason. SDK-managed call sites should never emit this \u2014 file a bug if you do.",
|
|
4300
|
-
retryable: false
|
|
4301
3809
|
}
|
|
4302
3810
|
]);
|
|
4303
3811
|
function getErrorCode(code) {
|
|
4304
3812
|
return CROSSDECK_ERROR_CODES.find((e) => e.code === code);
|
|
4305
3813
|
}
|
|
4306
|
-
|
|
4307
|
-
// src/_contracts-bundled.ts
|
|
4308
|
-
var BUNDLED_IN = "@cross-deck/web@1.5.0";
|
|
4309
|
-
var SDK_VERSION2 = "1.5.0";
|
|
4310
|
-
var BUNDLED_CONTRACTS = Object.freeze([
|
|
4311
|
-
{
|
|
4312
|
-
"id": "error-envelope-shape",
|
|
4313
|
-
"pillar": "errors",
|
|
4314
|
-
"status": "enforced",
|
|
4315
|
-
"claim": "Every v1 REST endpoint returns errors in a Stripe-shape envelope: `{ error: { type, code, message, request_id } }` where `type` is one of authentication_error / permission_error / invalid_request_error / rate_limit_error / internal_error (the wire vocabulary in backend/src/api/v1-errors.ts ApiErrorType). HTTP status parity: invalid_request_error \u2192 400, authentication_error \u2192 401, permission_error \u2192 403, rate_limit_error \u2192 429, internal_error \u2192 500. SDK-side clients parse this shape via `crossdeckErrorFromResponse` (Web/Node/RN) / `crossdeckErrorFrom(response:)` (Swift) / `crossdeckErrorFromResponse` (Android) and surface the request_id verbatim so support traces are end-to-end joinable. Firebase callable endpoints (managed-keys / dashboard auth) use the Firebase HttpsError envelope instead \u2014 this contract applies to REST /v1/* only.",
|
|
4316
|
-
"appliesTo": [
|
|
4317
|
-
"web",
|
|
4318
|
-
"node",
|
|
4319
|
-
"react-native",
|
|
4320
|
-
"swift",
|
|
4321
|
-
"android",
|
|
4322
|
-
"backend"
|
|
4323
|
-
],
|
|
4324
|
-
"codeRef": [
|
|
4325
|
-
"backend/src/api/v1-errors.ts",
|
|
4326
|
-
"sdks/web/src/errors.ts",
|
|
4327
|
-
"sdks/node/src/errors.ts",
|
|
4328
|
-
"sdks/react-native/src/errors.ts",
|
|
4329
|
-
"sdks/swift/Sources/Crossdeck/Errors.swift",
|
|
4330
|
-
"sdks/android/crossdeck/src/main/kotlin/com/crossdeck/Errors.kt"
|
|
4331
|
-
],
|
|
4332
|
-
"testRef": [
|
|
4333
|
-
{
|
|
4334
|
-
"file": "sdks/swift/Tests/CrossdeckTests/ErrorsTests.swift",
|
|
4335
|
-
"name": "test_errorEnvelope_fallsBackOnGarbageBody"
|
|
4336
|
-
},
|
|
4337
|
-
{
|
|
4338
|
-
"file": "sdks/swift/Tests/CrossdeckTests/ErrorsTests.swift",
|
|
4339
|
-
"name": "test_errorEnvelope_reads_XRequestId_fallback"
|
|
4340
|
-
},
|
|
4341
|
-
{
|
|
4342
|
-
"file": "sdks/android/crossdeck/src/test/kotlin/com/crossdeck/ErrorTypeWireVocabTest.kt",
|
|
4343
|
-
"name": "backend 500 response parses to INTERNAL_ERROR"
|
|
4344
|
-
}
|
|
4345
|
-
],
|
|
4346
|
-
"registeredAt": "2026-05-26",
|
|
4347
|
-
"firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 8 (codifies existing contract)",
|
|
4348
|
-
"bundledIn": "@cross-deck/web@1.5.0"
|
|
4349
|
-
},
|
|
4350
|
-
{
|
|
4351
|
-
"id": "flush-interval-parity",
|
|
4352
|
-
"pillar": "analytics",
|
|
4353
|
-
"status": "enforced",
|
|
4354
|
-
"claim": "Every Crossdeck SDK defaults its event-queue flush interval to 2000ms \u2014 the Stripe-adjacent industry norm. Pre-v1.4.0 the defaults disagreed (Web/Node 1500ms; RN/Swift/Android 5000ms), so cross-platform funnels saw events landing at different cadences. Per-instance override stays \u2014 call sites can still tune it freely.",
|
|
4355
|
-
"appliesTo": [
|
|
4356
|
-
"web",
|
|
4357
|
-
"node",
|
|
4358
|
-
"react-native",
|
|
4359
|
-
"swift",
|
|
4360
|
-
"android"
|
|
4361
|
-
],
|
|
4362
|
-
"codeRef": [
|
|
4363
|
-
"sdks/web/src/crossdeck.ts",
|
|
4364
|
-
"sdks/node/src/crossdeck-server.ts",
|
|
4365
|
-
"sdks/react-native/src/crossdeck.ts",
|
|
4366
|
-
"sdks/swift/Sources/Crossdeck/EventQueue.swift",
|
|
4367
|
-
"sdks/android/crossdeck/src/main/kotlin/com/crossdeck/EventQueue.kt"
|
|
4368
|
-
],
|
|
4369
|
-
"testRef": [
|
|
4370
|
-
{
|
|
4371
|
-
"file": "sdks/swift/Sources/Crossdeck/EventQueue.swift",
|
|
4372
|
-
"name": "flushIntervalMs: Int = 2_000"
|
|
4373
|
-
},
|
|
4374
|
-
{
|
|
4375
|
-
"file": "sdks/android/crossdeck/src/main/kotlin/com/crossdeck/EventQueue.kt",
|
|
4376
|
-
"name": "flushIntervalMs: Long = 2_000L"
|
|
4377
|
-
},
|
|
4378
|
-
{
|
|
4379
|
-
"file": "sdks/web/src/crossdeck.ts",
|
|
4380
|
-
"name": "options.eventFlushIntervalMs ?? 2000"
|
|
4381
|
-
},
|
|
4382
|
-
{
|
|
4383
|
-
"file": "sdks/node/src/crossdeck-server.ts",
|
|
4384
|
-
"name": "options.eventFlushIntervalMs ?? 2000"
|
|
4385
|
-
},
|
|
4386
|
-
{
|
|
4387
|
-
"file": "sdks/react-native/src/crossdeck.ts",
|
|
4388
|
-
"name": "options.eventFlushIntervalMs ?? 2000"
|
|
4389
|
-
}
|
|
4390
|
-
],
|
|
4391
|
-
"registeredAt": "2026-05-26",
|
|
4392
|
-
"firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 3.3",
|
|
4393
|
-
"bundledIn": "@cross-deck/web@1.5.0"
|
|
4394
|
-
},
|
|
4395
|
-
{
|
|
4396
|
-
"id": "idempotency-key-deterministic",
|
|
4397
|
-
"pillar": "revenue",
|
|
4398
|
-
"status": "enforced",
|
|
4399
|
-
"claim": "syncPurchases() on every SDK derives a deterministic Idempotency-Key from the request body (UUID-shaped SHA-256 of `crossdeck:purchases/sync:<rail>:<jws|token>`). Same input -> same key. Backend short-circuits same-key-same-body retries by returning the cached response (status + body) with `idempotent_replay: true` flag in the body AND `Idempotent-Replayed: true` response header. Same-key-different-body returns 400 `idempotency_key_in_use`. 24-hour TTL matches Stripe. Cache only stores 2xx responses \u2014 4xx/5xx pass through so callers can fix bugs and retry. Helper returns nil/throws on missing identifier (no silent random fallback). Cross-SDK parity is CI-pinned: deriveForPurchase('apple', 'eyJ.jws.sig') MUST equal 'a66b1640-efaf-bb4d-1261-6650033bf111' on every SDK.",
|
|
4400
|
-
"appliesTo": [
|
|
4401
|
-
"web",
|
|
4402
|
-
"node",
|
|
4403
|
-
"react-native",
|
|
4404
|
-
"swift",
|
|
4405
|
-
"android",
|
|
4406
|
-
"backend"
|
|
4407
|
-
],
|
|
4408
|
-
"codeRef": [
|
|
4409
|
-
"sdks/web/src/idempotency-key.ts",
|
|
4410
|
-
"sdks/web/src/crossdeck.ts",
|
|
4411
|
-
"sdks/react-native/src/idempotency-key.ts",
|
|
4412
|
-
"sdks/react-native/src/crossdeck.ts",
|
|
4413
|
-
"sdks/node/src/idempotency-key.ts",
|
|
4414
|
-
"sdks/node/src/crossdeck-server.ts",
|
|
4415
|
-
"sdks/swift/Sources/Crossdeck/IdempotencyKey.swift",
|
|
4416
|
-
"sdks/swift/Sources/Crossdeck/Crossdeck.swift",
|
|
4417
|
-
"sdks/android/crossdeck/src/main/kotlin/com/crossdeck/IdempotencyKey.kt",
|
|
4418
|
-
"sdks/android/crossdeck/src/main/kotlin/com/crossdeck/Crossdeck.kt",
|
|
4419
|
-
"backend/src/lib/idempotency-response-cache.ts",
|
|
4420
|
-
"backend/src/api/v1-purchases.ts"
|
|
4421
|
-
],
|
|
4422
|
-
"testRef": [
|
|
4423
|
-
{
|
|
4424
|
-
"file": "sdks/web/tests/idempotency-key.test.ts",
|
|
4425
|
-
"name": "cross-SDK oracle \u2014 apple JWS pins canonical vector"
|
|
4426
|
-
},
|
|
4427
|
-
{
|
|
4428
|
-
"file": "sdks/web/tests/idempotency-key.test.ts",
|
|
4429
|
-
"name": "is deterministic: same body twice -> identical key"
|
|
4430
|
-
},
|
|
4431
|
-
{
|
|
4432
|
-
"file": "sdks/web/tests/idempotency-key.test.ts",
|
|
4433
|
-
"name": "same identifier under different rails -> different keys"
|
|
4434
|
-
},
|
|
4435
|
-
{
|
|
4436
|
-
"file": "sdks/web/tests/idempotency-key.test.ts",
|
|
4437
|
-
"name": "never silently falls back to a random key on missing identifier"
|
|
4438
|
-
},
|
|
4439
|
-
{
|
|
4440
|
-
"file": "sdks/react-native/tests/idempotency-key.test.ts",
|
|
4441
|
-
"name": "is deterministic"
|
|
4442
|
-
},
|
|
4443
|
-
{
|
|
4444
|
-
"file": "sdks/react-native/tests/idempotency-key.test.ts",
|
|
4445
|
-
"name": "cross-SDK oracle \u2014 apple JWS pins canonical vector"
|
|
4446
|
-
},
|
|
4447
|
-
{
|
|
4448
|
-
"file": "sdks/node/tests/idempotency-key.test.ts",
|
|
4449
|
-
"name": "is deterministic"
|
|
4450
|
-
},
|
|
4451
|
-
{
|
|
4452
|
-
"file": "sdks/node/tests/idempotency-key.test.ts",
|
|
4453
|
-
"name": "rail namespacing prevents cross-rail collisions"
|
|
4454
|
-
},
|
|
4455
|
-
{
|
|
4456
|
-
"file": "sdks/node/tests/idempotency-key.test.ts",
|
|
4457
|
-
"name": "apple JWS produces the canonical pinned UUID across all 5 SDKs"
|
|
4458
|
-
},
|
|
4459
|
-
{
|
|
4460
|
-
"file": "backend/tests/unit/idempotency-response-cache.test.ts",
|
|
4461
|
-
"name": "is deterministic for the same input"
|
|
4462
|
-
},
|
|
4463
|
-
{
|
|
4464
|
-
"file": "backend/tests/unit/idempotency-response-cache.test.ts",
|
|
4465
|
-
"name": "injects idempotent_replay: true into a JSON object body"
|
|
4466
|
-
},
|
|
4467
|
-
{
|
|
4468
|
-
"file": "backend/tests/unit/idempotency-response-cache.test.ts",
|
|
4469
|
-
"name": "matches Stripe's 24-hour idempotency window"
|
|
4470
|
-
},
|
|
4471
|
-
{
|
|
4472
|
-
"file": "sdks/swift/Tests/CrossdeckTests/IdempotencyKeyTests.swift",
|
|
4473
|
-
"name": "test_crossSdkOracle_appleJWS"
|
|
4474
|
-
},
|
|
4475
|
-
{
|
|
4476
|
-
"file": "sdks/swift/Tests/CrossdeckTests/IdempotencyKeyTests.swift",
|
|
4477
|
-
"name": "test_railNamespacing_preventsCrossRailCollisions"
|
|
4478
|
-
},
|
|
4479
|
-
{
|
|
4480
|
-
"file": "sdks/swift/Tests/CrossdeckTests/IdempotencyKeyTests.swift",
|
|
4481
|
-
"name": "test_missingIdentifier_returnsNil"
|
|
4482
|
-
},
|
|
4483
|
-
{
|
|
4484
|
-
"file": "sdks/android/crossdeck/src/test/kotlin/com/crossdeck/IdempotencyKeyTest.kt",
|
|
4485
|
-
"name": "cross-SDK oracle for apple JWS"
|
|
4486
|
-
},
|
|
4487
|
-
{
|
|
4488
|
-
"file": "sdks/android/crossdeck/src/test/kotlin/com/crossdeck/IdempotencyKeyTest.kt",
|
|
4489
|
-
"name": "rail namespacing prevents cross-rail collisions"
|
|
4490
|
-
},
|
|
4491
|
-
{
|
|
4492
|
-
"file": "sdks/android/crossdeck/src/test/kotlin/com/crossdeck/IdempotencyKeyTest.kt",
|
|
4493
|
-
"name": "missing identifier returns null - never silent random fallback"
|
|
4494
|
-
}
|
|
4495
|
-
],
|
|
4496
|
-
"registeredAt": "2026-05-26",
|
|
4497
|
-
"firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 2.2.a + 2.2.b + 2.2.c",
|
|
4498
|
-
"bundledIn": "@cross-deck/web@1.5.0"
|
|
4499
|
-
},
|
|
4500
|
-
{
|
|
4501
|
-
"id": "init-reentry-drains-prior-queue",
|
|
4502
|
-
"pillar": "lifecycle",
|
|
4503
|
-
"status": "enforced",
|
|
4504
|
-
"claim": "Web + RN init() re-entry drains the prior EventQueue's pending setTimeout BEFORE replacing this.state. Pre-v1.4.0 the teardown handled autoTracker/webVitals/errors/unloadFlush but NOT events, so the prior queue's timer would fire AFTER the state swap \u2014 sending old-init events against new-init http + identity references (cross-identity leak during HMR / config swap / multi-tenant SDK shells). The teardown CANNOT call persistent.clear() \u2014 the durable queue belongs to the SDK lifetime, not the init() lifetime, and a survived crash mid-flush re-hydrates on the next init.",
|
|
4505
|
-
"appliesTo": [
|
|
4506
|
-
"web",
|
|
4507
|
-
"react-native"
|
|
4508
|
-
],
|
|
4509
|
-
"codeRef": [
|
|
4510
|
-
"sdks/web/src/crossdeck.ts",
|
|
4511
|
-
"sdks/react-native/src/crossdeck.ts"
|
|
4512
|
-
],
|
|
4513
|
-
"testRef": [
|
|
4514
|
-
{
|
|
4515
|
-
"file": "sdks/web/tests/init-reentry.test.ts",
|
|
4516
|
-
"name": "re-init drains the prior queue's pending timer before swapping state"
|
|
4517
|
-
},
|
|
4518
|
-
{
|
|
4519
|
-
"file": "sdks/web/tests/init-reentry.test.ts",
|
|
4520
|
-
"name": "re-init does NOT wipe the durable event store"
|
|
4521
|
-
}
|
|
4522
|
-
],
|
|
4523
|
-
"registeredAt": "2026-05-26",
|
|
4524
|
-
"firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 5.5",
|
|
4525
|
-
"bundledIn": "@cross-deck/web@1.5.0"
|
|
4526
|
-
},
|
|
4527
|
-
{
|
|
4528
|
-
"id": "per-user-cache-isolation",
|
|
4529
|
-
"pillar": "entitlements",
|
|
4530
|
-
"status": "enforced",
|
|
4531
|
-
"claim": "Every identify(userId) switches the entitlement cache to a physically separate per-user storage slot \u2014 `crossdeck:entitlements:<sha256(userId)>` \u2014 and unconditionally wipes the in-memory snapshot. A user-switch on a shared device CANNOT cross-read a prior user's cached entitlements, even if the in-memory clear is somehow skipped, because the storage keys are physically separate. reset() wipes every per-user slot via the persisted index.",
|
|
4532
|
-
"appliesTo": [
|
|
4533
|
-
"web",
|
|
4534
|
-
"react-native",
|
|
4535
|
-
"swift",
|
|
4536
|
-
"android"
|
|
4537
|
-
],
|
|
4538
|
-
"codeRef": [
|
|
4539
|
-
"sdks/web/src/entitlement-cache.ts",
|
|
4540
|
-
"sdks/web/src/hash.ts",
|
|
4541
|
-
"sdks/web/src/crossdeck.ts",
|
|
4542
|
-
"sdks/react-native/src/entitlement-cache.ts",
|
|
4543
|
-
"sdks/react-native/src/hash.ts",
|
|
4544
|
-
"sdks/react-native/src/crossdeck.ts",
|
|
4545
|
-
"sdks/swift/Sources/Crossdeck/EntitlementCache.swift",
|
|
4546
|
-
"sdks/swift/Sources/Crossdeck/IdempotencyKey.swift",
|
|
4547
|
-
"sdks/swift/Sources/Crossdeck/Crossdeck.swift",
|
|
4548
|
-
"sdks/android/crossdeck/src/main/kotlin/com/crossdeck/EntitlementCache.kt",
|
|
4549
|
-
"sdks/android/crossdeck/src/main/kotlin/com/crossdeck/IdempotencyKey.kt",
|
|
4550
|
-
"sdks/android/crossdeck/src/main/kotlin/com/crossdeck/Crossdeck.kt"
|
|
4551
|
-
],
|
|
4552
|
-
"testRef": [
|
|
4553
|
-
{
|
|
4554
|
-
"file": "sdks/web/tests/entitlement-cache-isolation.test.ts",
|
|
4555
|
-
"name": "identify(B) makes A's entitlements unreachable from in-memory"
|
|
4556
|
-
},
|
|
4557
|
-
{
|
|
4558
|
-
"file": "sdks/web/tests/entitlement-cache-isolation.test.ts",
|
|
4559
|
-
"name": "clearAll() removes every per-user storage key plus the index"
|
|
4560
|
-
},
|
|
4561
|
-
{
|
|
4562
|
-
"file": "sdks/web/tests/entitlement-cache-isolation.test.ts",
|
|
4563
|
-
"name": "a second cache instance reading A's storage suffix CANNOT see B's data"
|
|
4564
|
-
},
|
|
4565
|
-
{
|
|
4566
|
-
"file": "sdks/react-native/tests/entitlement-cache-isolation.test.ts",
|
|
4567
|
-
"name": "identify(B) makes A's entitlements unreachable from in-memory"
|
|
4568
|
-
},
|
|
4569
|
-
{
|
|
4570
|
-
"file": "sdks/react-native/tests/entitlement-cache-isolation.test.ts",
|
|
4571
|
-
"name": "removes every per-user storage key plus the index"
|
|
4572
|
-
},
|
|
4573
|
-
{
|
|
4574
|
-
"file": "sdks/swift/Tests/CrossdeckTests/EntitlementCacheIsolationTests.swift",
|
|
4575
|
-
"name": "test_identifyB_makesAEntitlementsUnreachable"
|
|
4576
|
-
},
|
|
4577
|
-
{
|
|
4578
|
-
"file": "sdks/swift/Tests/CrossdeckTests/EntitlementCacheIsolationTests.swift",
|
|
4579
|
-
"name": "test_identifiedWritesLandUnderPerUserSha256Key"
|
|
4580
|
-
},
|
|
4581
|
-
{
|
|
4582
|
-
"file": "sdks/swift/Tests/CrossdeckTests/EntitlementCacheIsolationTests.swift",
|
|
4583
|
-
"name": "test_clearAll_removesEveryPerUserStorageKeyPlusIndex"
|
|
4584
|
-
},
|
|
4585
|
-
{
|
|
4586
|
-
"file": "sdks/android/crossdeck/src/test/kotlin/com/crossdeck/EntitlementCacheIsolationTest.kt",
|
|
4587
|
-
"name": "identified writes land under per-user sha256 key"
|
|
4588
|
-
},
|
|
4589
|
-
{
|
|
4590
|
-
"file": "sdks/android/crossdeck/src/test/kotlin/com/crossdeck/EntitlementCacheIsolationTest.kt",
|
|
4591
|
-
"name": "identify B makes A entitlements unreachable from in-memory"
|
|
4592
|
-
},
|
|
4593
|
-
{
|
|
4594
|
-
"file": "sdks/android/crossdeck/src/test/kotlin/com/crossdeck/EntitlementCacheIsolationTest.kt",
|
|
4595
|
-
"name": "clearAll removes every per-user storage key plus the index"
|
|
4596
|
-
},
|
|
4597
|
-
{
|
|
4598
|
-
"file": "sdks/android/crossdeck/src/test/kotlin/com/crossdeck/EntitlementCacheIsolationTest.kt",
|
|
4599
|
-
"name": "a fresh cache bound to A's key CANNOT read B's blob"
|
|
4600
|
-
}
|
|
4601
|
-
],
|
|
4602
|
-
"registeredAt": "2026-05-26",
|
|
4603
|
-
"firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 1.3 (web/RN) + dogfood-gap fix (swift + android)",
|
|
4604
|
-
"bundledIn": "@cross-deck/web@1.5.0"
|
|
4605
|
-
},
|
|
4606
|
-
{
|
|
4607
|
-
"id": "sdk-error-codes-catalogue",
|
|
4608
|
-
"pillar": "errors",
|
|
4609
|
-
"status": "enforced",
|
|
4610
|
-
"claim": "Web + Node SDK error-codes catalogues include EVERY backend-emitted ApiErrorCode with a description + resolution. Pre-v1.4.0 the catalogues documented codes the SDK threw ITSELF but ZERO backend codes \u2014 a developer hitting `invalid_api_key` / `origin_not_allowed` / `bundle_id_not_allowed` / `env_mismatch` / `idempotency_key_in_use` etc. got `undefined` from getErrorCode() and had to hunt for guidance. v1.4.0 backfills the catalogue from backend/src/api/v1-errors.ts so every wire code has a canonical 'what does this mean / what should I do' answer Stripe-style.",
|
|
4611
|
-
"appliesTo": [
|
|
4612
|
-
"web",
|
|
4613
|
-
"node"
|
|
4614
|
-
],
|
|
4615
|
-
"codeRef": [
|
|
4616
|
-
"sdks/web/src/error-codes.ts",
|
|
4617
|
-
"sdks/node/src/error-codes.ts",
|
|
4618
|
-
"backend/src/api/v1-errors.ts"
|
|
4619
|
-
],
|
|
4620
|
-
"testRef": [
|
|
4621
|
-
{
|
|
4622
|
-
"file": "sdks/web/tests/error-codes-backfill.test.ts",
|
|
4623
|
-
"name": "includes backend code"
|
|
4624
|
-
},
|
|
4625
|
-
{
|
|
4626
|
-
"file": "sdks/web/tests/error-codes-backfill.test.ts",
|
|
4627
|
-
"name": "invalid_api_key resolution points at the dashboard"
|
|
4628
|
-
},
|
|
4629
|
-
{
|
|
4630
|
-
"file": "sdks/web/tests/error-codes-backfill.test.ts",
|
|
4631
|
-
"name": "idempotency_key_in_use resolution mentions Stripe-grade contract"
|
|
4632
|
-
},
|
|
4633
|
-
{
|
|
4634
|
-
"file": "sdks/web/tests/error-codes-backfill.test.ts",
|
|
4635
|
-
"name": "identity-lock codes carry permission_error type"
|
|
4636
|
-
},
|
|
4637
|
-
{
|
|
4638
|
-
"file": "sdks/web/tests/error-codes-backfill.test.ts",
|
|
4639
|
-
"name": "no entry has an empty description or resolution"
|
|
4640
|
-
}
|
|
4641
|
-
],
|
|
4642
|
-
"registeredAt": "2026-05-26",
|
|
4643
|
-
"firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 6.2",
|
|
4644
|
-
"bundledIn": "@cross-deck/web@1.5.0"
|
|
4645
|
-
},
|
|
4646
|
-
{
|
|
4647
|
-
"id": "sync-purchases-funnel-parity",
|
|
4648
|
-
"pillar": "analytics",
|
|
4649
|
-
"status": "enforced",
|
|
4650
|
-
"claim": "Manual syncPurchases() emits a `purchase.completed` analytics event on success across ALL SDKs (Web / Node / RN / Swift / Android). Pre-v1.4.0 only Swift/Android auto-track emitted it \u2014 Web/Node/RN manual calls + Swift/Android manual calls fired ZERO analytics. Schema mirrors the auto-track event name + rail/productId/subscriptionId so cross-platform funnels reconcile on every payment path. When the backend short-circuits via the v1.4.0 idempotency cache, the event also carries `idempotent_replay: true`.",
|
|
4651
|
-
"appliesTo": [
|
|
4652
|
-
"web",
|
|
4653
|
-
"node",
|
|
4654
|
-
"react-native",
|
|
4655
|
-
"swift",
|
|
4656
|
-
"android"
|
|
4657
|
-
],
|
|
4658
|
-
"codeRef": [
|
|
4659
|
-
"sdks/web/src/crossdeck.ts",
|
|
4660
|
-
"sdks/node/src/crossdeck-server.ts",
|
|
4661
|
-
"sdks/react-native/src/crossdeck.ts",
|
|
4662
|
-
"sdks/swift/Sources/Crossdeck/Crossdeck.swift",
|
|
4663
|
-
"sdks/android/crossdeck/src/main/kotlin/com/crossdeck/Crossdeck.kt"
|
|
4664
|
-
],
|
|
4665
|
-
"testRef": [
|
|
4666
|
-
{
|
|
4667
|
-
"file": "sdks/web/tests/sync-purchases-funnel.test.ts",
|
|
4668
|
-
"name": "emits purchase.completed after a successful sync"
|
|
4669
|
-
},
|
|
4670
|
-
{
|
|
4671
|
-
"file": "sdks/web/tests/sync-purchases-funnel.test.ts",
|
|
4672
|
-
"name": "carries idempotent_replay=true when backend replied from cache"
|
|
4673
|
-
}
|
|
4674
|
-
],
|
|
4675
|
-
"registeredAt": "2026-05-26",
|
|
4676
|
-
"firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 3.5",
|
|
4677
|
-
"bundledIn": "@cross-deck/web@1.5.0"
|
|
4678
|
-
}
|
|
4679
|
-
]);
|
|
4680
|
-
|
|
4681
|
-
// src/contracts.ts
|
|
4682
|
-
var CrossdeckContracts = {
|
|
4683
|
-
/** Every contract that applies to this SDK and is currently enforced. */
|
|
4684
|
-
all() {
|
|
4685
|
-
return BUNDLED_CONTRACTS.filter((c) => c.status === "enforced");
|
|
4686
|
-
},
|
|
4687
|
-
/**
|
|
4688
|
-
* Every contract bundled with this SDK release, including
|
|
4689
|
-
* `proposed` and `retired` entries. Use `all()` for the
|
|
4690
|
-
* enforced-only view.
|
|
4691
|
-
*/
|
|
4692
|
-
allIncludingHistorical() {
|
|
4693
|
-
return BUNDLED_CONTRACTS;
|
|
4694
|
-
},
|
|
4695
|
-
/** Look up a contract by its stable `id`. */
|
|
4696
|
-
byId(id) {
|
|
4697
|
-
return BUNDLED_CONTRACTS.find((c) => c.id === id);
|
|
4698
|
-
},
|
|
4699
|
-
/** Every enforced contract within a pillar. */
|
|
4700
|
-
byPillar(pillar) {
|
|
4701
|
-
return BUNDLED_CONTRACTS.filter(
|
|
4702
|
-
(c) => c.pillar === pillar && c.status === "enforced"
|
|
4703
|
-
);
|
|
4704
|
-
},
|
|
4705
|
-
/** Filter by lifecycle status. */
|
|
4706
|
-
withStatus(status) {
|
|
4707
|
-
return BUNDLED_CONTRACTS.filter((c) => c.status === status);
|
|
4708
|
-
},
|
|
4709
|
-
/** Semver of the SDK release these contracts were bundled with. */
|
|
4710
|
-
sdkVersion: SDK_VERSION2,
|
|
4711
|
-
/** Fully-qualified bundle identifier — e.g. `@cross-deck/web@1.4.2`. */
|
|
4712
|
-
bundledIn: BUNDLED_IN,
|
|
4713
|
-
/**
|
|
4714
|
-
* Resolve a failing test back to the contract it exercises.
|
|
4715
|
-
* Used by test-framework hooks (Vitest `afterEach`, XCTest
|
|
4716
|
-
* observation, JUnit `TestWatcher`) to find the contract id of
|
|
4717
|
-
* a failed contract test so `reportContractFailure(...)` can
|
|
4718
|
-
* stamp the right `contract_id` on the emitted event.
|
|
4719
|
-
*
|
|
4720
|
-
* Match is on `testRef.name` (case-sensitive, exact). Returns
|
|
4721
|
-
* the first contract whose `testRef` list contains a matching
|
|
4722
|
-
* entry, regardless of pillar or status.
|
|
4723
|
-
*/
|
|
4724
|
-
findByTestName(name) {
|
|
4725
|
-
return BUNDLED_CONTRACTS.find(
|
|
4726
|
-
(c) => c.testRef.some((ref) => ref.name === name)
|
|
4727
|
-
);
|
|
4728
|
-
}
|
|
4729
|
-
};
|
|
4730
3814
|
export {
|
|
4731
3815
|
CROSSDECK_ERROR_CODES,
|
|
4732
3816
|
Crossdeck,
|
|
4733
3817
|
CrossdeckClient,
|
|
4734
|
-
CrossdeckContracts,
|
|
4735
3818
|
CrossdeckError,
|
|
4736
3819
|
DEFAULT_BASE_URL,
|
|
4737
3820
|
MemoryStorage,
|