@cross-deck/web 1.3.1 → 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 +43 -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 +483 -22
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.mjs +483 -22
- package/dist/index.mjs.map +1 -1
- package/dist/react.cjs +373 -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 +373 -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 +373 -22
- package/dist/vue.cjs.map +1 -1
- package/dist/vue.mjs +373 -22
- package/dist/vue.mjs.map +1 -1
- package/package.json +2 -2
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { P as PublicEntitlement, C as CrossdeckOptions, I as IdentifyOptions, A as AliasResult, G as GroupTraits, E as EventProperties, a as PurchaseResult, H as HeartbeatResponse, D as Diagnostics, K as KeyValueStorage } from './types-
|
|
2
|
-
export { b as AuditRail, c as AutoTrackOptions, d as EntitlementsListResponse, e as Environment, f as Platform } from './types-
|
|
1
|
+
import { P as PublicEntitlement, C as CrossdeckOptions, I as IdentifyOptions, A as AliasResult, G as GroupTraits, E as EventProperties, a as PurchaseResult, H as HeartbeatResponse, D as Diagnostics, K as KeyValueStorage } from './types-Bu3jbmdq.mjs';
|
|
2
|
+
export { b as AuditRail, c as AutoTrackOptions, d as EntitlementsListResponse, e as Environment, f as Platform } from './types-Bu3jbmdq.mjs';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Durable last-known-good cache of the customer's entitlements.
|
|
@@ -678,7 +678,7 @@ declare class MemoryStorage implements KeyValueStorage {
|
|
|
678
678
|
*
|
|
679
679
|
* Do NOT edit by hand — `node scripts/sync-sdk-versions.mjs`.
|
|
680
680
|
*/
|
|
681
|
-
declare const SDK_VERSION = "1.
|
|
681
|
+
declare const SDK_VERSION = "1.4.2";
|
|
682
682
|
declare const SDK_NAME = "@cross-deck/web";
|
|
683
683
|
|
|
684
684
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { P as PublicEntitlement, C as CrossdeckOptions, I as IdentifyOptions, A as AliasResult, G as GroupTraits, E as EventProperties, a as PurchaseResult, H as HeartbeatResponse, D as Diagnostics, K as KeyValueStorage } from './types-
|
|
2
|
-
export { b as AuditRail, c as AutoTrackOptions, d as EntitlementsListResponse, e as Environment, f as Platform } from './types-
|
|
1
|
+
import { P as PublicEntitlement, C as CrossdeckOptions, I as IdentifyOptions, A as AliasResult, G as GroupTraits, E as EventProperties, a as PurchaseResult, H as HeartbeatResponse, D as Diagnostics, K as KeyValueStorage } from './types-Bu3jbmdq.js';
|
|
2
|
+
export { b as AuditRail, c as AutoTrackOptions, d as EntitlementsListResponse, e as Environment, f as Platform } from './types-Bu3jbmdq.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Durable last-known-good cache of the customer's entitlements.
|
|
@@ -678,7 +678,7 @@ declare class MemoryStorage implements KeyValueStorage {
|
|
|
678
678
|
*
|
|
679
679
|
* Do NOT edit by hand — `node scripts/sync-sdk-versions.mjs`.
|
|
680
680
|
*/
|
|
681
|
-
declare const SDK_VERSION = "1.
|
|
681
|
+
declare const SDK_VERSION = "1.4.2";
|
|
682
682
|
declare const SDK_NAME = "@cross-deck/web";
|
|
683
683
|
|
|
684
684
|
/**
|
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.4.2";
|
|
68
68
|
var SDK_NAME = "@cross-deck/web";
|
|
69
69
|
|
|
70
70
|
// src/http.ts
|
|
@@ -314,29 +314,258 @@ 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
|
+
|
|
317
491
|
// src/entitlement-cache.ts
|
|
318
492
|
var DEFAULT_STALE_AFTER_MS = 24 * 60 * 60 * 1e3;
|
|
319
|
-
var
|
|
493
|
+
var ANON_SUFFIX = "_anon";
|
|
494
|
+
var INDEX_SUFFIX = "_index";
|
|
495
|
+
var EntitlementCache = class _EntitlementCache {
|
|
320
496
|
/**
|
|
321
|
-
* @param storage
|
|
322
|
-
*
|
|
323
|
-
*
|
|
324
|
-
*
|
|
325
|
-
* @param
|
|
326
|
-
*
|
|
327
|
-
*
|
|
497
|
+
* @param storage Device storage adapter. When omitted (tests) or
|
|
498
|
+
* a MemoryStorage (strict-consent / no-persistence
|
|
499
|
+
* mode) the cache is session-only — durability is
|
|
500
|
+
* simply absent, never wrong.
|
|
501
|
+
* @param storageKeyPrefix Prefix used to derive per-user storage keys
|
|
502
|
+
* (`<prefix>:<sha256(userId)>`). Default
|
|
503
|
+
* `crossdeck:entitlements`. The trailing user
|
|
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.
|
|
328
508
|
*/
|
|
329
|
-
constructor(storage,
|
|
509
|
+
constructor(storage, storageKeyPrefix = "crossdeck:entitlements", staleAfterMs = DEFAULT_STALE_AFTER_MS) {
|
|
330
510
|
this.all = [];
|
|
331
511
|
this.lastUpdated = 0;
|
|
332
512
|
this.lastRefreshFailedAt = 0;
|
|
333
513
|
this.listeners = /* @__PURE__ */ new Set();
|
|
334
514
|
this.listenerErrorCount = 0;
|
|
515
|
+
this.currentSuffix = ANON_SUFFIX;
|
|
335
516
|
this.storage = storage;
|
|
336
|
-
this.
|
|
517
|
+
this.storageKeyPrefix = storageKeyPrefix;
|
|
337
518
|
this.staleAfterMs = staleAfterMs;
|
|
338
519
|
this.hydrate();
|
|
339
520
|
}
|
|
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
|
+
}
|
|
340
569
|
/**
|
|
341
570
|
* Sync read — true iff the entitlement is currently granting access.
|
|
342
571
|
*
|
|
@@ -406,12 +635,13 @@ var EntitlementCache = class {
|
|
|
406
635
|
this.lastUpdated = Date.now();
|
|
407
636
|
this.lastRefreshFailedAt = 0;
|
|
408
637
|
this.persist();
|
|
638
|
+
this.recordSuffixInIndex(this.currentSuffix);
|
|
409
639
|
this.notify();
|
|
410
640
|
}
|
|
411
641
|
/**
|
|
412
|
-
* Wipe
|
|
413
|
-
*
|
|
414
|
-
*
|
|
642
|
+
* Wipe the CURRENT user's slot. Used internally when a single
|
|
643
|
+
* user's cache needs to be invalidated without affecting other
|
|
644
|
+
* persisted slots. The full-logout path is [[clearAll]].
|
|
415
645
|
*/
|
|
416
646
|
clear() {
|
|
417
647
|
this.all = [];
|
|
@@ -423,6 +653,40 @@ var EntitlementCache = class {
|
|
|
423
653
|
} catch {
|
|
424
654
|
}
|
|
425
655
|
}
|
|
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
|
+
}
|
|
426
690
|
this.notify();
|
|
427
691
|
}
|
|
428
692
|
/**
|
|
@@ -472,6 +736,47 @@ var EntitlementCache = class {
|
|
|
472
736
|
} catch {
|
|
473
737
|
}
|
|
474
738
|
}
|
|
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
|
+
}
|
|
475
780
|
notify() {
|
|
476
781
|
if (this.listeners.size === 0) return;
|
|
477
782
|
const snapshot = this.all.slice();
|
|
@@ -486,6 +791,34 @@ var EntitlementCache = class {
|
|
|
486
791
|
}
|
|
487
792
|
};
|
|
488
793
|
|
|
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
|
+
|
|
489
822
|
// src/retry-policy.ts
|
|
490
823
|
var DEFAULT_BASE = 1e3;
|
|
491
824
|
var DEFAULT_MAX = 6e4;
|
|
@@ -2748,6 +3081,10 @@ var CrossdeckClient = class {
|
|
|
2748
3081
|
this.state.errors?.uninstall();
|
|
2749
3082
|
} catch {
|
|
2750
3083
|
}
|
|
3084
|
+
try {
|
|
3085
|
+
void this.state.events.flush({ keepalive: true });
|
|
3086
|
+
} catch {
|
|
3087
|
+
}
|
|
2751
3088
|
}
|
|
2752
3089
|
if (!options.publicKey || !options.publicKey.startsWith("cd_pub_")) {
|
|
2753
3090
|
throw new CrossdeckError({
|
|
@@ -2795,7 +3132,12 @@ var CrossdeckClient = class {
|
|
|
2795
3132
|
// load still flushes if the user leaves quickly (the keepalive
|
|
2796
3133
|
// pagehide handler picks up anything that doesn't); long enough
|
|
2797
3134
|
// that bursts of clicks coalesce into one network round-trip.
|
|
2798
|
-
|
|
3135
|
+
// v1.4.0 Phase 3.3 — flush interval default parity. Pre-
|
|
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,
|
|
2799
3141
|
sdkVersion: options.sdkVersion ?? SDK_VERSION,
|
|
2800
3142
|
autoTrack,
|
|
2801
3143
|
appVersion: options.appVersion ?? null
|
|
@@ -3023,14 +3365,10 @@ var CrossdeckClient = class {
|
|
|
3023
3365
|
};
|
|
3024
3366
|
if (options?.email) body.email = options.email;
|
|
3025
3367
|
if (traits) body.traits = traits;
|
|
3368
|
+
s.entitlements.setUserKey(userId);
|
|
3026
3369
|
const result = await s.http.request("POST", "/identity/alias", {
|
|
3027
3370
|
body
|
|
3028
3371
|
});
|
|
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
|
-
}
|
|
3034
3372
|
s.identity.setCrossdeckCustomerId(result.crossdeckCustomerId);
|
|
3035
3373
|
s.developerUserId = userId;
|
|
3036
3374
|
return result;
|
|
@@ -3471,11 +3809,24 @@ var CrossdeckClient = class {
|
|
|
3471
3809
|
});
|
|
3472
3810
|
}
|
|
3473
3811
|
const rail = input.rail ?? "apple";
|
|
3812
|
+
const body = { ...input, rail };
|
|
3813
|
+
const idempotencyKey = deriveIdempotencyKeyForPurchase(body);
|
|
3474
3814
|
const result = await s.http.request("POST", "/purchases/sync", {
|
|
3475
|
-
body
|
|
3815
|
+
body,
|
|
3816
|
+
idempotencyKey
|
|
3476
3817
|
});
|
|
3477
3818
|
s.identity.setCrossdeckCustomerId(result.crossdeckCustomerId);
|
|
3478
3819
|
s.entitlements.setFromList(result.entitlements);
|
|
3820
|
+
try {
|
|
3821
|
+
const sourceProductId = result.entitlements[0]?.source.productId;
|
|
3822
|
+
const sourceSubscriptionId = result.entitlements[0]?.source.subscriptionId;
|
|
3823
|
+
const props = { rail };
|
|
3824
|
+
if (sourceProductId) props.productId = sourceProductId;
|
|
3825
|
+
if (sourceSubscriptionId) props.subscriptionId = sourceSubscriptionId;
|
|
3826
|
+
if (result.idempotent_replay) props.idempotent_replay = true;
|
|
3827
|
+
this.track("purchase.completed", props);
|
|
3828
|
+
} catch {
|
|
3829
|
+
}
|
|
3479
3830
|
s.debug.emit(
|
|
3480
3831
|
"sdk.purchase_evidence_sent",
|
|
3481
3832
|
"StoreKit transaction forwarded. Waiting for backend verification.",
|
|
@@ -3531,7 +3882,7 @@ var CrossdeckClient = class {
|
|
|
3531
3882
|
}
|
|
3532
3883
|
this.state.autoTracker?.uninstall();
|
|
3533
3884
|
this.state.identity.reset();
|
|
3534
|
-
this.state.entitlements.
|
|
3885
|
+
this.state.entitlements.clearAll();
|
|
3535
3886
|
this.state.events.reset();
|
|
3536
3887
|
this.state.superProps.clear();
|
|
3537
3888
|
this.state.breadcrumbs.clear();
|
|
@@ -3806,6 +4157,116 @@ var CROSSDECK_ERROR_CODES = Object.freeze([
|
|
|
3806
4157
|
description: "The server returned a 2xx with an unparseable body.",
|
|
3807
4158
|
resolution: "Likely a transient backend bug. Retry; if it persists, contact support with the requestId.",
|
|
3808
4159
|
retryable: true
|
|
4160
|
+
},
|
|
4161
|
+
// ----- Backend-emitted codes (v1.4.0 Phase 6.2 backfill) -----
|
|
4162
|
+
// Mirror of backend/src/api/v1-errors.ts ApiErrorCode. A developer
|
|
4163
|
+
// hitting any of these on the wire can look them up via
|
|
4164
|
+
// getErrorCode(code) for a canonical remediation step instead of
|
|
4165
|
+
// hunting through Slack history.
|
|
4166
|
+
{
|
|
4167
|
+
code: "missing_api_key",
|
|
4168
|
+
type: "authentication_error",
|
|
4169
|
+
description: "No Authorization header (or Crossdeck-Api-Key header) on the request.",
|
|
4170
|
+
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.",
|
|
4171
|
+
retryable: false
|
|
4172
|
+
},
|
|
4173
|
+
{
|
|
4174
|
+
code: "invalid_api_key",
|
|
4175
|
+
type: "authentication_error",
|
|
4176
|
+
description: "The API key is malformed, unknown, or doesn't resolve to a project.",
|
|
4177
|
+
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.",
|
|
4178
|
+
retryable: false
|
|
4179
|
+
},
|
|
4180
|
+
{
|
|
4181
|
+
code: "key_revoked",
|
|
4182
|
+
type: "authentication_error",
|
|
4183
|
+
description: "The API key was revoked in the Crossdeck dashboard.",
|
|
4184
|
+
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.",
|
|
4185
|
+
retryable: false
|
|
4186
|
+
},
|
|
4187
|
+
{
|
|
4188
|
+
code: "identity_token_invalid",
|
|
4189
|
+
type: "authentication_error",
|
|
4190
|
+
description: "The Firebase / Apple / Google ID token supplied with the request didn't verify against the dashboard's configured signers.",
|
|
4191
|
+
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.",
|
|
4192
|
+
retryable: true
|
|
4193
|
+
},
|
|
4194
|
+
{
|
|
4195
|
+
code: "origin_not_allowed",
|
|
4196
|
+
type: "permission_error",
|
|
4197
|
+
description: "The Origin header isn't in the project's Allowed origins list.",
|
|
4198
|
+
resolution: "Add the origin (e.g. https://app.example.com) under dashboard \u2192 Settings \u2192 Allowed origins. Wildcards like https://*.example.com are supported.",
|
|
4199
|
+
retryable: false
|
|
4200
|
+
},
|
|
4201
|
+
{
|
|
4202
|
+
code: "bundle_id_not_allowed",
|
|
4203
|
+
type: "permission_error",
|
|
4204
|
+
description: "The iOS bundle ID sent via X-Crossdeck-Bundle-Id isn't registered under this app's Apple identity lock.",
|
|
4205
|
+
resolution: "Add the bundle ID under dashboard \u2192 Apps \u2192 <your app> \u2192 iOS bundle IDs.",
|
|
4206
|
+
retryable: false
|
|
4207
|
+
},
|
|
4208
|
+
{
|
|
4209
|
+
code: "package_name_not_allowed",
|
|
4210
|
+
type: "permission_error",
|
|
4211
|
+
description: "The Android package name sent via X-Crossdeck-Package-Name isn't registered under this app's Android identity lock.",
|
|
4212
|
+
resolution: "Add the package name under dashboard \u2192 Apps \u2192 <your app> \u2192 Android package names.",
|
|
4213
|
+
retryable: false
|
|
4214
|
+
},
|
|
4215
|
+
{
|
|
4216
|
+
code: "env_mismatch",
|
|
4217
|
+
type: "permission_error",
|
|
4218
|
+
description: "The request env (inferred from key prefix) doesn't match the resolved app's configured env.",
|
|
4219
|
+
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.",
|
|
4220
|
+
retryable: false
|
|
4221
|
+
},
|
|
4222
|
+
{
|
|
4223
|
+
code: "idempotency_key_in_use",
|
|
4224
|
+
type: "invalid_request_error",
|
|
4225
|
+
description: "An Idempotency-Key was reused for a request with a different body (Stripe-grade contract).",
|
|
4226
|
+
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.",
|
|
4227
|
+
retryable: false
|
|
4228
|
+
},
|
|
4229
|
+
{
|
|
4230
|
+
code: "rate_limited",
|
|
4231
|
+
type: "rate_limit_error",
|
|
4232
|
+
description: "Request rate exceeded the project's per-second cap.",
|
|
4233
|
+
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.",
|
|
4234
|
+
retryable: true
|
|
4235
|
+
},
|
|
4236
|
+
{
|
|
4237
|
+
code: "internal_error",
|
|
4238
|
+
type: "internal_error",
|
|
4239
|
+
description: "Server-side issue. Safe to retry with backoff.",
|
|
4240
|
+
resolution: "The SDK retries automatically. If your code paths through to this error, contact support with the requestId from the response envelope.",
|
|
4241
|
+
retryable: true
|
|
4242
|
+
},
|
|
4243
|
+
{
|
|
4244
|
+
code: "google_not_supported",
|
|
4245
|
+
type: "invalid_request_error",
|
|
4246
|
+
description: "POST /purchases/sync with rail=google is gated until the Play Developer API reconciliation worker ships.",
|
|
4247
|
+
resolution: "Until v1.5+, Google Play purchases verify via Real-time Developer Notifications. The SDK auto-track path handles this transparently for Android consumers.",
|
|
4248
|
+
retryable: false
|
|
4249
|
+
},
|
|
4250
|
+
{
|
|
4251
|
+
code: "stripe_not_supported",
|
|
4252
|
+
type: "invalid_request_error",
|
|
4253
|
+
description: "POST /purchases/sync with rail=stripe is unsupported \u2014 Stripe Checkout's redirect flow uses platform webhooks instead.",
|
|
4254
|
+
resolution: "Wire Stripe via the standard Checkout / Customer Portal flow; Crossdeck reconciles via the platform webhook automatically. No SDK call needed.",
|
|
4255
|
+
retryable: false
|
|
4256
|
+
},
|
|
4257
|
+
{
|
|
4258
|
+
code: "missing_required_param",
|
|
4259
|
+
type: "invalid_request_error",
|
|
4260
|
+
description: "A required field is absent from the request body.",
|
|
4261
|
+
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.",
|
|
4262
|
+
retryable: false
|
|
4263
|
+
},
|
|
4264
|
+
{
|
|
4265
|
+
code: "invalid_param_value",
|
|
4266
|
+
type: "invalid_request_error",
|
|
4267
|
+
description: "A field is present but the value failed validation (wrong shape, wrong length, wrong enum value).",
|
|
4268
|
+
resolution: "Read error.message for the specific field + reason. SDK-managed call sites should never emit this \u2014 file a bug if you do.",
|
|
4269
|
+
retryable: false
|
|
3809
4270
|
}
|
|
3810
4271
|
]);
|
|
3811
4272
|
function getErrorCode(code) {
|