@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/dist/index.cjs CHANGED
@@ -98,7 +98,7 @@ function typeMapForStatus(status) {
98
98
  }
99
99
 
100
100
  // src/_version.ts
101
- var SDK_VERSION = "1.3.0";
101
+ var SDK_VERSION = "1.4.2";
102
102
  var SDK_NAME = "@cross-deck/web";
103
103
 
104
104
  // src/http.ts
@@ -348,29 +348,258 @@ function randomChars(count) {
348
348
  return out.join("");
349
349
  }
350
350
 
351
+ // src/hash.ts
352
+ var K = new Uint32Array([
353
+ 1116352408,
354
+ 1899447441,
355
+ 3049323471,
356
+ 3921009573,
357
+ 961987163,
358
+ 1508970993,
359
+ 2453635748,
360
+ 2870763221,
361
+ 3624381080,
362
+ 310598401,
363
+ 607225278,
364
+ 1426881987,
365
+ 1925078388,
366
+ 2162078206,
367
+ 2614888103,
368
+ 3248222580,
369
+ 3835390401,
370
+ 4022224774,
371
+ 264347078,
372
+ 604807628,
373
+ 770255983,
374
+ 1249150122,
375
+ 1555081692,
376
+ 1996064986,
377
+ 2554220882,
378
+ 2821834349,
379
+ 2952996808,
380
+ 3210313671,
381
+ 3336571891,
382
+ 3584528711,
383
+ 113926993,
384
+ 338241895,
385
+ 666307205,
386
+ 773529912,
387
+ 1294757372,
388
+ 1396182291,
389
+ 1695183700,
390
+ 1986661051,
391
+ 2177026350,
392
+ 2456956037,
393
+ 2730485921,
394
+ 2820302411,
395
+ 3259730800,
396
+ 3345764771,
397
+ 3516065817,
398
+ 3600352804,
399
+ 4094571909,
400
+ 275423344,
401
+ 430227734,
402
+ 506948616,
403
+ 659060556,
404
+ 883997877,
405
+ 958139571,
406
+ 1322822218,
407
+ 1537002063,
408
+ 1747873779,
409
+ 1955562222,
410
+ 2024104815,
411
+ 2227730452,
412
+ 2361852424,
413
+ 2428436474,
414
+ 2756734187,
415
+ 3204031479,
416
+ 3329325298
417
+ ]);
418
+ function utf8Bytes(input) {
419
+ if (typeof TextEncoder !== "undefined") {
420
+ return new TextEncoder().encode(input);
421
+ }
422
+ const out = [];
423
+ for (let i = 0; i < input.length; i++) {
424
+ let codePoint = input.charCodeAt(i);
425
+ if (codePoint >= 55296 && codePoint <= 56319 && i + 1 < input.length) {
426
+ const next = input.charCodeAt(i + 1);
427
+ if (next >= 56320 && next <= 57343) {
428
+ codePoint = 65536 + (codePoint - 55296 << 10) + (next - 56320);
429
+ i++;
430
+ }
431
+ }
432
+ if (codePoint < 128) {
433
+ out.push(codePoint);
434
+ } else if (codePoint < 2048) {
435
+ out.push(192 | codePoint >> 6);
436
+ out.push(128 | codePoint & 63);
437
+ } else if (codePoint < 65536) {
438
+ out.push(224 | codePoint >> 12);
439
+ out.push(128 | codePoint >> 6 & 63);
440
+ out.push(128 | codePoint & 63);
441
+ } else {
442
+ out.push(240 | codePoint >> 18);
443
+ out.push(128 | codePoint >> 12 & 63);
444
+ out.push(128 | codePoint >> 6 & 63);
445
+ out.push(128 | codePoint & 63);
446
+ }
447
+ }
448
+ return new Uint8Array(out);
449
+ }
450
+ function sha256Hex(input) {
451
+ const bytes = utf8Bytes(input);
452
+ const bitLength = bytes.length * 8;
453
+ const blockCount = Math.floor((bytes.length + 9 + 63) / 64);
454
+ const padded = new Uint8Array(blockCount * 64);
455
+ padded.set(bytes);
456
+ padded[bytes.length] = 128;
457
+ const high = Math.floor(bitLength / 4294967296);
458
+ const low = bitLength >>> 0;
459
+ const lenOffset = padded.length - 8;
460
+ padded[lenOffset + 0] = high >>> 24 & 255;
461
+ padded[lenOffset + 1] = high >>> 16 & 255;
462
+ padded[lenOffset + 2] = high >>> 8 & 255;
463
+ padded[lenOffset + 3] = high & 255;
464
+ padded[lenOffset + 4] = low >>> 24 & 255;
465
+ padded[lenOffset + 5] = low >>> 16 & 255;
466
+ padded[lenOffset + 6] = low >>> 8 & 255;
467
+ padded[lenOffset + 7] = low & 255;
468
+ const H = new Uint32Array([
469
+ 1779033703,
470
+ 3144134277,
471
+ 1013904242,
472
+ 2773480762,
473
+ 1359893119,
474
+ 2600822924,
475
+ 528734635,
476
+ 1541459225
477
+ ]);
478
+ const W = new Uint32Array(64);
479
+ for (let block = 0; block < blockCount; block++) {
480
+ const offset = block * 64;
481
+ for (let t = 0; t < 16; t++) {
482
+ 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;
483
+ }
484
+ for (let t = 16; t < 64; t++) {
485
+ const w15 = W[t - 15];
486
+ const w2 = W[t - 2];
487
+ const s0 = (w15 >>> 7 | w15 << 25) ^ (w15 >>> 18 | w15 << 14) ^ w15 >>> 3;
488
+ const s1 = (w2 >>> 17 | w2 << 15) ^ (w2 >>> 19 | w2 << 13) ^ w2 >>> 10;
489
+ W[t] = W[t - 16] + s0 + W[t - 7] + s1 >>> 0;
490
+ }
491
+ let a = H[0], b = H[1], c = H[2], d = H[3];
492
+ let e = H[4], f = H[5], g = H[6], h = H[7];
493
+ for (let t = 0; t < 64; t++) {
494
+ const S1 = (e >>> 6 | e << 26) ^ (e >>> 11 | e << 21) ^ (e >>> 25 | e << 7);
495
+ const ch = e & f ^ ~e & g;
496
+ const temp1 = h + S1 + ch + K[t] + W[t] >>> 0;
497
+ const S0 = (a >>> 2 | a << 30) ^ (a >>> 13 | a << 19) ^ (a >>> 22 | a << 10);
498
+ const maj = a & b ^ a & c ^ b & c;
499
+ const temp2 = S0 + maj >>> 0;
500
+ h = g;
501
+ g = f;
502
+ f = e;
503
+ e = d + temp1 >>> 0;
504
+ d = c;
505
+ c = b;
506
+ b = a;
507
+ a = temp1 + temp2 >>> 0;
508
+ }
509
+ H[0] = H[0] + a >>> 0;
510
+ H[1] = H[1] + b >>> 0;
511
+ H[2] = H[2] + c >>> 0;
512
+ H[3] = H[3] + d >>> 0;
513
+ H[4] = H[4] + e >>> 0;
514
+ H[5] = H[5] + f >>> 0;
515
+ H[6] = H[6] + g >>> 0;
516
+ H[7] = H[7] + h >>> 0;
517
+ }
518
+ let hex = "";
519
+ for (let i = 0; i < 8; i++) {
520
+ hex += H[i].toString(16).padStart(8, "0");
521
+ }
522
+ return hex;
523
+ }
524
+
351
525
  // src/entitlement-cache.ts
352
526
  var DEFAULT_STALE_AFTER_MS = 24 * 60 * 60 * 1e3;
353
- var EntitlementCache = class {
527
+ var ANON_SUFFIX = "_anon";
528
+ var INDEX_SUFFIX = "_index";
529
+ var EntitlementCache = class _EntitlementCache {
354
530
  /**
355
- * @param storage Device storage adapter. When omitted (tests) or
356
- * a MemoryStorage (strict-consent / no-persistence
357
- * mode) the cache is session-only — durability is
358
- * simply absent, never wrong.
359
- * @param storageKey Full key the persisted blob lives under.
360
- * @param staleAfterMs Age past which last-known-good is flagged stale
361
- * even without a failed refresh. Default 24h.
531
+ * @param storage Device storage adapter. When omitted (tests) or
532
+ * a MemoryStorage (strict-consent / no-persistence
533
+ * mode) the cache is session-only — durability is
534
+ * simply absent, never wrong.
535
+ * @param storageKeyPrefix Prefix used to derive per-user storage keys
536
+ * (`<prefix>:<sha256(userId)>`). Default
537
+ * `crossdeck:entitlements`. The trailing user
538
+ * suffix is filled at identify() / reset()
539
+ * time — see [[setUserKey]].
540
+ * @param staleAfterMs Age past which last-known-good is flagged stale
541
+ * even without a failed refresh. Default 24h.
362
542
  */
363
- constructor(storage, storageKey = "crossdeck:entitlements", staleAfterMs = DEFAULT_STALE_AFTER_MS) {
543
+ constructor(storage, storageKeyPrefix = "crossdeck:entitlements", staleAfterMs = DEFAULT_STALE_AFTER_MS) {
364
544
  this.all = [];
365
545
  this.lastUpdated = 0;
366
546
  this.lastRefreshFailedAt = 0;
367
547
  this.listeners = /* @__PURE__ */ new Set();
368
548
  this.listenerErrorCount = 0;
549
+ this.currentSuffix = ANON_SUFFIX;
369
550
  this.storage = storage;
370
- this.storageKey = storageKey;
551
+ this.storageKeyPrefix = storageKeyPrefix;
371
552
  this.staleAfterMs = staleAfterMs;
372
553
  this.hydrate();
373
554
  }
555
+ /** The full storage key the current-user blob is persisted under. */
556
+ get storageKey() {
557
+ return `${this.storageKeyPrefix}:${this.currentSuffix}`;
558
+ }
559
+ /** Key of the index blob — a JSON array of every suffix we've
560
+ * written. Used by clearAll() to scope a logout-wipe. */
561
+ get indexKey() {
562
+ return `${this.storageKeyPrefix}:${INDEX_SUFFIX}`;
563
+ }
564
+ /** Derive a stable suffix for a developerUserId via SHA-256. The
565
+ * raw userId never lands in the storage key — protects against
566
+ * accidentally leaking email-style identifiers through DevTools
567
+ * inspection. Pass `null` to switch back to the anonymous slot. */
568
+ static suffixForUserId(userId) {
569
+ if (userId == null || userId === "") return ANON_SUFFIX;
570
+ return sha256Hex(userId);
571
+ }
572
+ /**
573
+ * Switch the cache to a different user's storage slot. Bank-grade
574
+ * three-layer isolation:
575
+ * (a) Physical key separation — `<prefix>:<sha256(userId)>` so
576
+ * a user-switch can't physically read prior user's data
577
+ * even if the in-memory clear was skipped.
578
+ * (b) Unconditional in-memory clear — invoked whenever the
579
+ * active suffix changes, even on same-id re-identify.
580
+ * (c) Re-hydrate from the new slot — a returning user observes
581
+ * their last-known-good cache from storage immediately.
582
+ *
583
+ * Caller (identify() / reset()) MUST invoke this BEFORE the next
584
+ * setFromList() so the write lands under the right key.
585
+ */
586
+ setUserKey(userId) {
587
+ const nextSuffix = _EntitlementCache.suffixForUserId(userId);
588
+ if (nextSuffix === this.currentSuffix) {
589
+ this.all = [];
590
+ this.lastUpdated = 0;
591
+ this.lastRefreshFailedAt = 0;
592
+ this.notify();
593
+ this.hydrate();
594
+ return;
595
+ }
596
+ this.currentSuffix = nextSuffix;
597
+ this.all = [];
598
+ this.lastUpdated = 0;
599
+ this.lastRefreshFailedAt = 0;
600
+ this.hydrate();
601
+ this.notify();
602
+ }
374
603
  /**
375
604
  * Sync read — true iff the entitlement is currently granting access.
376
605
  *
@@ -440,12 +669,13 @@ var EntitlementCache = class {
440
669
  this.lastUpdated = Date.now();
441
670
  this.lastRefreshFailedAt = 0;
442
671
  this.persist();
672
+ this.recordSuffixInIndex(this.currentSuffix);
443
673
  this.notify();
444
674
  }
445
675
  /**
446
- * Wipe used on reset() (logout) and on an identity switch. Clears
447
- * BOTH memory and durable storage so a prior user's entitlements can
448
- * never leak to the next person on this device.
676
+ * Wipe the CURRENT user's slot. Used internally when a single
677
+ * user's cache needs to be invalidated without affecting other
678
+ * persisted slots. The full-logout path is [[clearAll]].
449
679
  */
450
680
  clear() {
451
681
  this.all = [];
@@ -457,6 +687,40 @@ var EntitlementCache = class {
457
687
  } catch {
458
688
  }
459
689
  }
690
+ this.removeSuffixFromIndex(this.currentSuffix);
691
+ this.notify();
692
+ }
693
+ /**
694
+ * Logout-grade wipe — bank-grade contract: removes EVERY per-user
695
+ * entitlement slot the SDK has ever written on this device, then
696
+ * clears the index. Used by `Crossdeck.reset()` so a logout on a
697
+ * shared device can never leave another user's entitlements
698
+ * readable (layer (c) of the v1.4.0 isolation fix).
699
+ *
700
+ * After clearAll(), the cache is back to anonymous + empty.
701
+ */
702
+ clearAll() {
703
+ this.all = [];
704
+ this.lastUpdated = 0;
705
+ this.lastRefreshFailedAt = 0;
706
+ this.currentSuffix = ANON_SUFFIX;
707
+ if (this.storage) {
708
+ const suffixes = this.readIndex();
709
+ for (const suffix of suffixes) {
710
+ try {
711
+ this.storage.removeItem(`${this.storageKeyPrefix}:${suffix}`);
712
+ } catch {
713
+ }
714
+ }
715
+ try {
716
+ this.storage.removeItem(`${this.storageKeyPrefix}:${ANON_SUFFIX}`);
717
+ } catch {
718
+ }
719
+ try {
720
+ this.storage.removeItem(this.indexKey);
721
+ } catch {
722
+ }
723
+ }
460
724
  this.notify();
461
725
  }
462
726
  /**
@@ -506,6 +770,47 @@ var EntitlementCache = class {
506
770
  } catch {
507
771
  }
508
772
  }
773
+ /** Read the index of all per-user suffixes the SDK has written. */
774
+ readIndex() {
775
+ if (!this.storage) return [];
776
+ try {
777
+ const raw = this.storage.getItem(this.indexKey);
778
+ if (!raw) return [];
779
+ const parsed = JSON.parse(raw);
780
+ if (Array.isArray(parsed)) {
781
+ return parsed.filter((x) => typeof x === "string");
782
+ }
783
+ return [];
784
+ } catch {
785
+ return [];
786
+ }
787
+ }
788
+ /** Add a suffix to the persisted index. Idempotent. */
789
+ recordSuffixInIndex(suffix) {
790
+ if (!this.storage) return;
791
+ const existing = this.readIndex();
792
+ if (existing.includes(suffix)) return;
793
+ existing.push(suffix);
794
+ try {
795
+ this.storage.setItem(this.indexKey, JSON.stringify(existing));
796
+ } catch {
797
+ }
798
+ }
799
+ /** Remove a suffix from the persisted index. No-op if absent. */
800
+ removeSuffixFromIndex(suffix) {
801
+ if (!this.storage) return;
802
+ const existing = this.readIndex();
803
+ const next = existing.filter((s) => s !== suffix);
804
+ if (next.length === existing.length) return;
805
+ try {
806
+ if (next.length === 0) {
807
+ this.storage.removeItem(this.indexKey);
808
+ } else {
809
+ this.storage.setItem(this.indexKey, JSON.stringify(next));
810
+ }
811
+ } catch {
812
+ }
813
+ }
509
814
  notify() {
510
815
  if (this.listeners.size === 0) return;
511
816
  const snapshot = this.all.slice();
@@ -520,6 +825,34 @@ var EntitlementCache = class {
520
825
  }
521
826
  };
522
827
 
828
+ // src/idempotency-key.ts
829
+ function formatAsUuid(hex) {
830
+ return [
831
+ hex.slice(0, 8),
832
+ hex.slice(8, 12),
833
+ hex.slice(12, 16),
834
+ hex.slice(16, 20),
835
+ hex.slice(20, 32)
836
+ ].join("-");
837
+ }
838
+ function deriveIdempotencyKeyForPurchase(body) {
839
+ let identifier;
840
+ if (body.rail === "apple") {
841
+ identifier = body.signedTransactionInfo ?? "";
842
+ } else if (body.rail === "google") {
843
+ identifier = body.purchaseToken ?? "";
844
+ } else {
845
+ identifier = "";
846
+ }
847
+ if (!identifier) {
848
+ throw new Error(
849
+ `deriveIdempotencyKeyForPurchase: no stable identifier in body (rail=${body.rail}). Apple needs signedTransactionInfo; Google needs purchaseToken.`
850
+ );
851
+ }
852
+ const namespaced = `crossdeck:purchases/sync:${body.rail}:${identifier}`;
853
+ return formatAsUuid(sha256Hex(namespaced));
854
+ }
855
+
523
856
  // src/retry-policy.ts
524
857
  var DEFAULT_BASE = 1e3;
525
858
  var DEFAULT_MAX = 6e4;
@@ -2782,6 +3115,10 @@ var CrossdeckClient = class {
2782
3115
  this.state.errors?.uninstall();
2783
3116
  } catch {
2784
3117
  }
3118
+ try {
3119
+ void this.state.events.flush({ keepalive: true });
3120
+ } catch {
3121
+ }
2785
3122
  }
2786
3123
  if (!options.publicKey || !options.publicKey.startsWith("cd_pub_")) {
2787
3124
  throw new CrossdeckError({
@@ -2829,7 +3166,12 @@ var CrossdeckClient = class {
2829
3166
  // load still flushes if the user leaves quickly (the keepalive
2830
3167
  // pagehide handler picks up anything that doesn't); long enough
2831
3168
  // that bursts of clicks coalesce into one network round-trip.
2832
- eventFlushIntervalMs: options.eventFlushIntervalMs ?? 1500,
3169
+ // v1.4.0 Phase 3.3 — flush interval default parity. Pre-
3170
+ // v1.4.0: Web/Node 1500ms, RN/Swift/Android 5000ms. All
3171
+ // converged on 2000ms (the Stripe-adjacent industry norm)
3172
+ // so cross-platform funnels show events landing at the
3173
+ // same cadence on every SDK. Per-instance override stays.
3174
+ eventFlushIntervalMs: options.eventFlushIntervalMs ?? 2e3,
2833
3175
  sdkVersion: options.sdkVersion ?? SDK_VERSION,
2834
3176
  autoTrack,
2835
3177
  appVersion: options.appVersion ?? null
@@ -3057,14 +3399,10 @@ var CrossdeckClient = class {
3057
3399
  };
3058
3400
  if (options?.email) body.email = options.email;
3059
3401
  if (traits) body.traits = traits;
3402
+ s.entitlements.setUserKey(userId);
3060
3403
  const result = await s.http.request("POST", "/identity/alias", {
3061
3404
  body
3062
3405
  });
3063
- const priorCdcust = s.identity.crossdeckCustomerId;
3064
- const cacheHasEntries = s.entitlements.list().length > 0;
3065
- if (priorCdcust && result.crossdeckCustomerId && priorCdcust !== result.crossdeckCustomerId || !priorCdcust && cacheHasEntries) {
3066
- s.entitlements.clear();
3067
- }
3068
3406
  s.identity.setCrossdeckCustomerId(result.crossdeckCustomerId);
3069
3407
  s.developerUserId = userId;
3070
3408
  return result;
@@ -3505,11 +3843,24 @@ var CrossdeckClient = class {
3505
3843
  });
3506
3844
  }
3507
3845
  const rail = input.rail ?? "apple";
3846
+ const body = { ...input, rail };
3847
+ const idempotencyKey = deriveIdempotencyKeyForPurchase(body);
3508
3848
  const result = await s.http.request("POST", "/purchases/sync", {
3509
- body: { ...input, rail }
3849
+ body,
3850
+ idempotencyKey
3510
3851
  });
3511
3852
  s.identity.setCrossdeckCustomerId(result.crossdeckCustomerId);
3512
3853
  s.entitlements.setFromList(result.entitlements);
3854
+ try {
3855
+ const sourceProductId = result.entitlements[0]?.source.productId;
3856
+ const sourceSubscriptionId = result.entitlements[0]?.source.subscriptionId;
3857
+ const props = { rail };
3858
+ if (sourceProductId) props.productId = sourceProductId;
3859
+ if (sourceSubscriptionId) props.subscriptionId = sourceSubscriptionId;
3860
+ if (result.idempotent_replay) props.idempotent_replay = true;
3861
+ this.track("purchase.completed", props);
3862
+ } catch {
3863
+ }
3513
3864
  s.debug.emit(
3514
3865
  "sdk.purchase_evidence_sent",
3515
3866
  "StoreKit transaction forwarded. Waiting for backend verification.",
@@ -3565,7 +3916,7 @@ var CrossdeckClient = class {
3565
3916
  }
3566
3917
  this.state.autoTracker?.uninstall();
3567
3918
  this.state.identity.reset();
3568
- this.state.entitlements.clear();
3919
+ this.state.entitlements.clearAll();
3569
3920
  this.state.events.reset();
3570
3921
  this.state.superProps.clear();
3571
3922
  this.state.breadcrumbs.clear();
@@ -3840,6 +4191,116 @@ var CROSSDECK_ERROR_CODES = Object.freeze([
3840
4191
  description: "The server returned a 2xx with an unparseable body.",
3841
4192
  resolution: "Likely a transient backend bug. Retry; if it persists, contact support with the requestId.",
3842
4193
  retryable: true
4194
+ },
4195
+ // ----- Backend-emitted codes (v1.4.0 Phase 6.2 backfill) -----
4196
+ // Mirror of backend/src/api/v1-errors.ts ApiErrorCode. A developer
4197
+ // hitting any of these on the wire can look them up via
4198
+ // getErrorCode(code) for a canonical remediation step instead of
4199
+ // hunting through Slack history.
4200
+ {
4201
+ code: "missing_api_key",
4202
+ type: "authentication_error",
4203
+ description: "No Authorization header (or Crossdeck-Api-Key header) on the request.",
4204
+ 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.",
4205
+ retryable: false
4206
+ },
4207
+ {
4208
+ code: "invalid_api_key",
4209
+ type: "authentication_error",
4210
+ description: "The API key is malformed, unknown, or doesn't resolve to a project.",
4211
+ 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.",
4212
+ retryable: false
4213
+ },
4214
+ {
4215
+ code: "key_revoked",
4216
+ type: "authentication_error",
4217
+ description: "The API key was revoked in the Crossdeck dashboard.",
4218
+ 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.",
4219
+ retryable: false
4220
+ },
4221
+ {
4222
+ code: "identity_token_invalid",
4223
+ type: "authentication_error",
4224
+ description: "The Firebase / Apple / Google ID token supplied with the request didn't verify against the dashboard's configured signers.",
4225
+ 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.",
4226
+ retryable: true
4227
+ },
4228
+ {
4229
+ code: "origin_not_allowed",
4230
+ type: "permission_error",
4231
+ description: "The Origin header isn't in the project's Allowed origins list.",
4232
+ resolution: "Add the origin (e.g. https://app.example.com) under dashboard \u2192 Settings \u2192 Allowed origins. Wildcards like https://*.example.com are supported.",
4233
+ retryable: false
4234
+ },
4235
+ {
4236
+ code: "bundle_id_not_allowed",
4237
+ type: "permission_error",
4238
+ description: "The iOS bundle ID sent via X-Crossdeck-Bundle-Id isn't registered under this app's Apple identity lock.",
4239
+ resolution: "Add the bundle ID under dashboard \u2192 Apps \u2192 <your app> \u2192 iOS bundle IDs.",
4240
+ retryable: false
4241
+ },
4242
+ {
4243
+ code: "package_name_not_allowed",
4244
+ type: "permission_error",
4245
+ description: "The Android package name sent via X-Crossdeck-Package-Name isn't registered under this app's Android identity lock.",
4246
+ resolution: "Add the package name under dashboard \u2192 Apps \u2192 <your app> \u2192 Android package names.",
4247
+ retryable: false
4248
+ },
4249
+ {
4250
+ code: "env_mismatch",
4251
+ type: "permission_error",
4252
+ description: "The request env (inferred from key prefix) doesn't match the resolved app's configured env.",
4253
+ 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.",
4254
+ retryable: false
4255
+ },
4256
+ {
4257
+ code: "idempotency_key_in_use",
4258
+ type: "invalid_request_error",
4259
+ description: "An Idempotency-Key was reused for a request with a different body (Stripe-grade contract).",
4260
+ 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.",
4261
+ retryable: false
4262
+ },
4263
+ {
4264
+ code: "rate_limited",
4265
+ type: "rate_limit_error",
4266
+ description: "Request rate exceeded the project's per-second cap.",
4267
+ 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.",
4268
+ retryable: true
4269
+ },
4270
+ {
4271
+ code: "internal_error",
4272
+ type: "internal_error",
4273
+ description: "Server-side issue. Safe to retry with backoff.",
4274
+ resolution: "The SDK retries automatically. If your code paths through to this error, contact support with the requestId from the response envelope.",
4275
+ retryable: true
4276
+ },
4277
+ {
4278
+ code: "google_not_supported",
4279
+ type: "invalid_request_error",
4280
+ description: "POST /purchases/sync with rail=google is gated until the Play Developer API reconciliation worker ships.",
4281
+ resolution: "Until v1.5+, Google Play purchases verify via Real-time Developer Notifications. The SDK auto-track path handles this transparently for Android consumers.",
4282
+ retryable: false
4283
+ },
4284
+ {
4285
+ code: "stripe_not_supported",
4286
+ type: "invalid_request_error",
4287
+ description: "POST /purchases/sync with rail=stripe is unsupported \u2014 Stripe Checkout's redirect flow uses platform webhooks instead.",
4288
+ resolution: "Wire Stripe via the standard Checkout / Customer Portal flow; Crossdeck reconciles via the platform webhook automatically. No SDK call needed.",
4289
+ retryable: false
4290
+ },
4291
+ {
4292
+ code: "missing_required_param",
4293
+ type: "invalid_request_error",
4294
+ description: "A required field is absent from the request body.",
4295
+ 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.",
4296
+ retryable: false
4297
+ },
4298
+ {
4299
+ code: "invalid_param_value",
4300
+ type: "invalid_request_error",
4301
+ description: "A field is present but the value failed validation (wrong shape, wrong length, wrong enum value).",
4302
+ resolution: "Read error.message for the specific field + reason. SDK-managed call sites should never emit this \u2014 file a bug if you do.",
4303
+ retryable: false
3843
4304
  }
3844
4305
  ]);
3845
4306
  function getErrorCode(code) {