@cross-deck/node 1.1.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,44 @@ All notable changes to `@cross-deck/node` will be documented here. The
4
4
  format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [1.2.0] — 2026-05-18
8
+
9
+ ### Added
10
+
11
+ - **Pluggable durable entitlement store (`entitlementStore`).** A new
12
+ constructor option taking an async `EntitlementStore` (a `load` /
13
+ `save` pair) — back it with Redis, your own database, or a KV. Every
14
+ successful `getEntitlements()` persists the result to it, and on a
15
+ network failure the SDK falls back to the stored snapshot. This is
16
+ what gives serverless deployments (Cloud Run / Lambda) cold-start
17
+ durability that an in-memory cache alone cannot. `EntitlementStore`
18
+ and `StoredEntitlements` are exported.
19
+ - **Staleness fields in `diagnostics()`.** `entitlements.staleCustomers`,
20
+ `isStale`, `durableStore`, and `coldStartDurable` — so serving
21
+ last-known-good through a Crossdeck outage is observable, not silent.
22
+ - **`sdk.no_durable_store` debug signal**, emitted once on a serverless
23
+ runtime with no `entitlementStore` configured, alongside a
24
+ `durability` fact on the boot telemetry event — so the cold-start gap
25
+ is measurable rather than a surprise in production.
26
+
27
+ ### Changed
28
+
29
+ - **The entitlement cache is now durable last-known-good.**
30
+ `isEntitled()` and `list()` no longer expire to `false` / `[]` when
31
+ `entitlementCacheTtlMs` elapses — they keep serving the last
32
+ successfully-fetched entitlements. The TTL is now a refresh hint, not
33
+ an invalidation. Each entitlement is still honoured against its own
34
+ `validUntil`. A brief Crossdeck outage can no longer fail a paying
35
+ customer down to free 60 seconds after a warm.
36
+
37
+ ## [1.1.1] — 2026-05-14
38
+
39
+ ### Changed
40
+
41
+ - Ported the "never silently surface an `Unknown` error" hardening to
42
+ `@cross-deck/node` — a captured error with no usable type or message
43
+ is now labelled precisely instead of collapsing to `Unknown error`.
44
+
7
45
  ## [1.1.0] — 2026-05-13
8
46
 
9
47
  ### Added
@@ -1,4 +1,4 @@
1
- import { p as CrossdeckServer } from '../crossdeck-server-BXQaFjVx.mjs';
1
+ import { p as CrossdeckServer } from '../crossdeck-server-BZVZEuS-.mjs';
2
2
  import 'node:events';
3
3
 
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { p as CrossdeckServer } from '../crossdeck-server-BXQaFjVx.js';
1
+ import { p as CrossdeckServer } from '../crossdeck-server-BZVZEuS-.js';
2
2
  import 'node:events';
3
3
 
4
4
  /**
@@ -221,6 +221,22 @@ interface RuntimeInfo {
221
221
  platformRelease: string;
222
222
  hostname: string;
223
223
  host: RuntimeHost;
224
+ /**
225
+ * Whether the host is a scale-to-zero / per-request-instance platform
226
+ * where a cold start begins with empty process memory.
227
+ *
228
+ * `true` for FaaS + serverless-container platforms (Lambda, Cloud
229
+ * Run, Firebase Functions v1/v2, Vercel, Netlify, Azure Functions,
230
+ * App Engine). `false` for long-lived process hosts (Heroku, Render,
231
+ * Railway, Fly, Kubernetes, plain Node) where the process — and thus
232
+ * the in-memory entitlement cache — persists across requests.
233
+ *
234
+ * The entitlement-cache durability layer reads this: a serverless
235
+ * host with no `entitlementStore` has no cold-start durability, and
236
+ * the SDK surfaces that explicitly (debug warning + a `durability`
237
+ * fact on the boot telemetry event).
238
+ */
239
+ isServerless: boolean;
224
240
  region: string | null;
225
241
  serviceName: string | null;
226
242
  serviceVersion: string | null;
@@ -236,7 +252,7 @@ interface RuntimeInfo {
236
252
  }
237
253
 
238
254
  declare const SDK_NAME = "@cross-deck/node";
239
- declare const SDK_VERSION = "1.1.0";
255
+ declare const SDK_VERSION = "1.2.0";
240
256
  declare const DEFAULT_BASE_URL = "https://api.cross-deck.com/v1";
241
257
  declare const DEFAULT_TIMEOUT_MS = 15000;
242
258
  /**
@@ -310,6 +326,63 @@ interface EntitlementsListResponse {
310
326
  crossdeckCustomerId: string;
311
327
  env: Environment;
312
328
  }
329
+ /**
330
+ * Snapshot of one customer's last-known-good entitlements, as written
331
+ * to / read from a durable `EntitlementStore`. Versioned for forward-
332
+ * compat — a future SDK can refuse a blob whose `v` it doesn't know.
333
+ *
334
+ * Carries enough to fully reconstruct an `EntitlementsListResponse` on
335
+ * a cold start (the durable read path in `getEntitlements()` rebuilds
336
+ * the response from this), plus `savedAt` so staleness is measurable
337
+ * after a process restart.
338
+ */
339
+ interface StoredEntitlements {
340
+ v: 1;
341
+ /** Canonical Crossdeck customer ID this snapshot belongs to. */
342
+ crossdeckCustomerId: string;
343
+ /** The entitlement set exactly as the server last returned it. */
344
+ entitlements: PublicEntitlement[];
345
+ env: Environment;
346
+ /** Epoch ms of the successful server fetch that produced this snapshot. */
347
+ savedAt: number;
348
+ }
349
+ /**
350
+ * Pluggable async durable store for last-known-good entitlements.
351
+ *
352
+ * The Node SDK's entitlement cache is an in-memory per-customer `Map`.
353
+ * On serverless (Cloud Run / AWS Lambda) a cold start is an empty Map,
354
+ * and a brief Crossdeck outage during that window would otherwise read
355
+ * a paying customer as un-entitled. An `EntitlementStore` is the
356
+ * developer-supplied durability layer — Redis, their primary DB, a KV —
357
+ * that survives both a cold start and an outage.
358
+ *
359
+ * Contract:
360
+ * - `load` returns the most recent snapshot for a customer, or `null`
361
+ * if none exists. It MUST NOT throw for a missing key — return
362
+ * `null`. (The SDK additionally guards every call in a try/catch,
363
+ * but a well-behaved store returns `null`.)
364
+ * - `save` persists a snapshot. Called only after a SUCCESSFUL server
365
+ * fetch, so the store never holds anything but server-confirmed
366
+ * truth.
367
+ * - Both are awaited inside `getEntitlements()` (already async). They
368
+ * are NEVER called from the synchronous `isEntitled()` — that stays
369
+ * a pure in-memory `Map` read with zero I/O.
370
+ * - The SDK swallows store errors: a failed `save` never fails a
371
+ * successful fetch, a failed `load` degrades to "no durable copy".
372
+ * A broken store weakens durability; it never breaks the SDK.
373
+ *
374
+ * The `key` passed to `load` / `save` is whatever identity string the
375
+ * caller used — a canonical `crossdeckCustomerId`, or a developer
376
+ * `userId` / `anonymousId` hint. The SDK saves a snapshot under every
377
+ * identity it knows for a customer so a cold-start `load` succeeds even
378
+ * before the in-memory alias map is populated.
379
+ */
380
+ interface EntitlementStore {
381
+ /** Resolve a customer's last-known-good snapshot, or `null` if none. */
382
+ load(key: string): Promise<StoredEntitlements | null>;
383
+ /** Persist a customer's last-known-good snapshot. */
384
+ save(key: string, value: StoredEntitlements): Promise<void>;
385
+ }
313
386
  interface AliasResult {
314
387
  object: "alias_result";
315
388
  crossdeckCustomerId: string;
@@ -462,8 +535,46 @@ interface CrossdeckServerOptions {
462
535
  *
463
536
  * Pass `0` to disable caching (every `isEntitled` requires a fresh
464
537
  * `getEntitlements()` call to populate the cache — useful for tests).
538
+ *
539
+ * NOTE: the TTL is a REFRESH HINT, not an invalidation. Once a
540
+ * customer is warm, `isEntitled()` keeps serving last-known-good past
541
+ * the TTL — a brief Crossdeck outage can never flip a paying customer
542
+ * to `false`. The TTL only tells `needsRefresh()` when a re-fetch is
543
+ * due, and (with no failed refresh) when the cache is flagged stale.
544
+ * Each entitlement's own `validUntil` is still honoured at read time.
465
545
  */
466
546
  entitlementCacheTtlMs?: number;
547
+ /**
548
+ * Age (ms) past which last-known-good entitlement data is flagged
549
+ * STALE in `diagnostics()` even with no failed refresh. Default 24h.
550
+ *
551
+ * Staleness never changes what `isEntitled()` returns — the cache
552
+ * keeps serving last-known-good. This window only makes "we have been
553
+ * serving an un-refreshed answer for a long time" observable, so an
554
+ * event-based revoke (chargeback / refund — which has no `validUntil`)
555
+ * riding out a long outage is visible instead of silent.
556
+ */
557
+ entitlementStaleAfterMs?: number;
558
+ /**
559
+ * Durable last-known-good store for entitlements. Optional.
560
+ *
561
+ * The entitlement cache is in-memory. On serverless (Cloud Run /
562
+ * Lambda) every cold start begins with an empty cache — and if
563
+ * Crossdeck is briefly unreachable during that window, a paying
564
+ * customer would read as un-entitled. Wiring an `EntitlementStore`
565
+ * (Redis / your DB / a KV) closes that gap: every successful
566
+ * `getEntitlements()` persists the result, and a network failure
567
+ * falls back to the stored snapshot instead of throwing.
568
+ *
569
+ * Without a store on a serverless host the SDK has NO cold-start
570
+ * durability — that is unavoidable and the SDK says so explicitly
571
+ * (a `debug.emit` warning plus a `durability` fact on the boot
572
+ * telemetry event). It is not hidden.
573
+ *
574
+ * `isEntitled()` stays synchronous regardless — the store is only
575
+ * ever touched from the already-async `getEntitlements()`.
576
+ */
577
+ entitlementStore?: EntitlementStore;
467
578
  /**
468
579
  * Service name for runtime enrichment. Attached to every event + error
469
580
  * as `properties.serviceName`. Default: env-detected via
@@ -674,6 +785,32 @@ interface Diagnostics {
674
785
  ttlMs: number;
675
786
  /** Cumulative count of listener invocations that threw. Swallowed inside the cache; surfaced here. */
676
787
  listenerErrors: number;
788
+ /**
789
+ * Number of cached customers currently flagged STALE — their most
790
+ * recent refresh attempt failed, or their data has aged past
791
+ * `entitlementStaleAfterMs`. The cache keeps serving last-known-good
792
+ * for them; this count makes "serving through an outage" observable.
793
+ */
794
+ staleCustomers: number;
795
+ /**
796
+ * Whether ANY cached customer is stale. Quick boolean for health
797
+ * checks / alerting without inspecting `staleCustomers`.
798
+ */
799
+ isStale: boolean;
800
+ /**
801
+ * Most recent failed-refresh timestamp across all customers (epoch
802
+ * ms), or 0 if every customer's last refresh succeeded.
803
+ */
804
+ lastRefreshFailedAt: number;
805
+ /**
806
+ * Durable-store posture. `durableStore` is true iff an
807
+ * `EntitlementStore` is configured. `coldStartDurable` is true iff
808
+ * the SDK has cold-start durability — which on a serverless host
809
+ * requires a store, and on a long-lived host is inherently true
810
+ * (the process, hence the in-memory cache, survives).
811
+ */
812
+ durableStore: boolean;
813
+ coldStartDurable: boolean;
677
814
  };
678
815
  events: {
679
816
  buffered: number;
@@ -844,8 +981,8 @@ interface GroupMembership {
844
981
  }
845
982
 
846
983
  /**
847
- * Per-customer entitlement cache with TTL — the third Crossdeck USP
848
- * on the server.
984
+ * Per-customer durable last-known-good entitlement cache — the third
985
+ * Crossdeck USP on the server.
849
986
  *
850
987
  * Why this exists: server-side gating code looks like
851
988
  *
@@ -856,29 +993,68 @@ interface GroupMembership {
856
993
  * per request, every request, for every customer. The cache makes
857
994
  * `isEntitled()` a `Map.get()` after the first warm.
858
995
  *
859
- * Differences from `@cross-deck/web/src/entitlement-cache.ts`:
996
+ * Durability contract (mirrors `@cross-deck/web/src/entitlement-cache.ts`,
997
+ * adapted for a multi-tenant server):
998
+ * - This cache is NOT a second source of truth. Crossdeck remains the
999
+ * only source; this is the SDK's local copy of what the server last
1000
+ * told us — a cache that does not forget during a network partition.
1001
+ * - Only a SUCCESSFUL fetch replaces a customer's entry (via
1002
+ * `setForCustomer`). A failed refresh never reaches it, so an outage
1003
+ * can never fail a paying customer down to free.
1004
+ * - **The TTL is a REFRESH HINT, not an invalidation.** `isEntitled()`
1005
+ * and `list()` keep serving last-known-good after `ttlMs` elapses —
1006
+ * they do NOT return `false` / `[]` because the entry aged. The TTL
1007
+ * only drives `needsRefresh()` ("a re-fetch is due") and, with no
1008
+ * failed refresh, the stale flag. This is the central fix: on
1009
+ * serverless a paying customer must not be locked out 60s after a
1010
+ * warm just because Crossdeck was briefly unreachable.
1011
+ * - Staleness alone never returns false. Each entitlement is honoured
1012
+ * against its OWN `validUntil` instead — a time-based trial expiry
1013
+ * still applies even mid-partition; a still-valid Pro entitlement
1014
+ * rides the outage out.
1015
+ * - Staleness is VISIBLE, not silent. `validUntil` covers time-based
1016
+ * expiry; it does NOT cover an event-based revoke (chargeback,
1017
+ * refund, fraud) — that has no `validUntil`, so the cache would keep
1018
+ * serving a revoked customer through an outage. Serving them is the
1019
+ * right trade (don't lock real payers out), but unbounded-and-
1020
+ * invisible is the bug. So once a refresh ATTEMPT fails
1021
+ * (`markRefreshFailed`) or the data ages past `staleAfterMs`, the
1022
+ * customer is flagged stale — `isStale()` / `staleCustomerCount` are
1023
+ * surfaced in `diagnostics()`. It keeps serving last-known-good; the
1024
+ * staleness is just no longer hidden.
1025
+ *
1026
+ * Cold-start durability lives one layer up: `getEntitlements()` in
1027
+ * `crossdeck-server.ts` persists every successful fetch to an optional
1028
+ * `EntitlementStore` and, on a network failure, loads last-known-good
1029
+ * back from it and into this cache. This cache stays a pure in-memory
1030
+ * structure with NO I/O — `isEntitled()` is and remains synchronous.
1031
+ *
1032
+ * Differences from the web SDK's cache:
860
1033
  * - **Per-customer**, not singleton. Web SDK has one user per browser
861
1034
  * tab; Node SDK has many users hitting one server. The cache is
862
- * keyed by `crossdeckCustomerId`.
863
- * - **TTL-bounded**. Each customer's entry expires after `ttlMs`
864
- * (default 60_000) and the next read returns `false` until
865
- * `getEntitlements()` refreshes. Stripe + Mixpanel ship the same
866
- * pattern server-side.
1035
+ * keyed by `crossdeckCustomerId`. Staleness, freshness and the
1036
+ * failed-refresh marker are therefore all per-customer too.
1037
+ * - **No synchronous storage hydration** in the constructor. The web
1038
+ * SDK hydrates from `localStorage` on boot; the Node durable store
1039
+ * is async, so hydration happens lazily inside `getEntitlements()`.
1040
+ * - **LRU-bounded** by `maxCustomers` — a long-running multi-tenant
1041
+ * server would otherwise leak Map entries forever.
867
1042
  * - **Subscriber API unchanged** — `subscribe(listener)` fires after
868
- * any mutation (set / clear / per-customer expiry-driven eviction
869
- * is NOT considered a mutation, by design — listeners shouldn't
870
- * re-render just because a TTL elapsed).
871
- *
872
- * The cache holds only ACTIVE entitlements — `setForCustomer` filters.
873
- * `isEntitled()` returns `false` when:
874
- * - the customer has no cached entry
875
- * - the entry has expired
876
- * - the requested key isn't in the active set
1043
+ * any mutation (set / clear). Passive LRU eviction and a TTL
1044
+ * elapsing are NOT mutations, by design.
877
1045
  */
878
1046
 
879
1047
  type EntitlementsListener = (customerId: string, entitlements: PublicEntitlement[]) => void;
880
1048
  interface EntitlementCacheOptions {
881
- /** TTL in ms. Default 60_000 (60s). 0 disables caching (every read is cold). */
1049
+ /**
1050
+ * Refresh-hint TTL in ms. Default 60_000 (60s).
1051
+ *
1052
+ * After `ttlMs` a customer's entry is "refresh due" — `needsRefresh()`
1053
+ * returns true and the caller should re-fetch. It is NOT an expiry:
1054
+ * `isEntitled()` keeps serving last-known-good past it. `0` makes
1055
+ * every entry immediately refresh-due (useful for tests) but STILL
1056
+ * does not invalidate — last-known-good is served regardless.
1057
+ */
882
1058
  ttlMs?: number;
883
1059
  /**
884
1060
  * Maximum number of customers cached at once. Long-running multi-tenant
@@ -889,6 +1065,16 @@ interface EntitlementCacheOptions {
889
1065
  * (passive eviction is not a mutation by design).
890
1066
  */
891
1067
  maxCustomers?: number;
1068
+ /**
1069
+ * Age (ms) past which a customer's last-known-good data is flagged
1070
+ * STALE even with no failed refresh. Default 24h.
1071
+ *
1072
+ * Staleness never changes what `isEntitled()` returns; it only makes a
1073
+ * long un-refreshed window observable via `isStale()` / diagnostics —
1074
+ * so an event-based revoke (no `validUntil`) riding out an outage is
1075
+ * visible instead of silent.
1076
+ */
1077
+ staleAfterMs?: number;
892
1078
  }
893
1079
 
894
1080
  /**
@@ -995,6 +1181,16 @@ declare class CrossdeckServer extends EventEmitter {
995
1181
  private readonly flushOnExit;
996
1182
  private readonly superProps;
997
1183
  private readonly entitlementCache;
1184
+ /**
1185
+ * Optional developer-supplied durable store for last-known-good
1186
+ * entitlements (Redis / their DB / a KV). `undefined` when not
1187
+ * configured — the SDK then has no cold-start durability on
1188
+ * serverless, which it states explicitly at boot.
1189
+ *
1190
+ * Touched ONLY from the async `getEntitlements()` — never from the
1191
+ * synchronous `isEntitled()`.
1192
+ */
1193
+ private readonly entitlementStore;
998
1194
  private readonly debug;
999
1195
  /**
1000
1196
  * Alias map — `developerUserId` / `anonymousId` → canonical
@@ -1014,9 +1210,58 @@ declare class CrossdeckServer extends EventEmitter {
1014
1210
  private errorTags;
1015
1211
  private errorBeforeSend;
1016
1212
  constructor(options: CrossdeckServerOptions);
1213
+ /**
1214
+ * Emit the one-time `sdk.boot` telemetry event and, when the runtime
1215
+ * is serverless with no `entitlementStore`, the honest "no cold-start
1216
+ * durability" warning.
1217
+ *
1218
+ * Why a `track()` event and not the heartbeat: `GET /v1/sdk/heartbeat`
1219
+ * carries no request body, so it cannot transport a structured
1220
+ * `durability` fact. The event pipeline can — every `track()` event
1221
+ * lands as an aggregatable document the backend can query, so
1222
+ * Crossdeck can compute fleet-wide "% serverless-with-no-durable-
1223
+ * store" from `sdk.boot` events (denominator = all `sdk.boot`,
1224
+ * numerator = those with `durability.coldStartDurable === false`).
1225
+ * The event rides the existing batched + retried + idempotent queue
1226
+ * and is drained by flush-on-exit, so it survives a serverless
1227
+ * teardown — it is NOT a local-only debug log.
1228
+ *
1229
+ * `isServerless` AND no store is the gap: a cold start begins with an
1230
+ * empty in-memory cache and a brief Crossdeck outage in that window
1231
+ * would read a paying customer as un-entitled. That gap is
1232
+ * unavoidable without a store — so the SDK STATES it (a
1233
+ * `sdk.no_durable_store` debug warning) rather than hiding it.
1234
+ *
1235
+ * Called once, from the deferred boot block — so it inherits the
1236
+ * `testMode` / `bootHeartbeat:false` opt-outs and never fires before
1237
+ * the constructor returns.
1238
+ */
1239
+ private emitBootTelemetry;
1017
1240
  identify(userId: string, anonymousId: string, options?: IdentifyOptions & RequestOptions): Promise<AliasResult>;
1018
1241
  aliasIdentity(input: AliasIdentityInput, options?: RequestOptions): Promise<AliasResult>;
1019
1242
  forget(hints: IdentityHints, options?: RequestOptions): Promise<ForgetResult>;
1243
+ /**
1244
+ * Fetch a customer's entitlements from Crossdeck and warm the cache.
1245
+ *
1246
+ * Durability — this is where last-known-good lives, NOT in the
1247
+ * synchronous `isEntitled()`:
1248
+ * - On a SUCCESSFUL fetch: the entitlement cache is populated and,
1249
+ * if an `entitlementStore` is configured, the result is persisted
1250
+ * to it (`await store.save(...)`). The cache + store now hold
1251
+ * server-confirmed truth.
1252
+ * - On a network FAILURE: the cache is marked refresh-failed for the
1253
+ * customer (so `diagnostics()` shows the staleness), then — if a
1254
+ * store is configured — last-known-good is loaded back from it
1255
+ * (`await store.load(...)`). If the store yields a snapshot, the
1256
+ * cache is populated from it and that snapshot is RETURNED as a
1257
+ * normal `EntitlementsListResponse` — a cold-start / outage no
1258
+ * longer fails a paying customer. If there is no store, or the
1259
+ * store is empty, the network error is rethrown unchanged so the
1260
+ * caller still sees the failure.
1261
+ *
1262
+ * The store is touched only here, inside the `await` that already
1263
+ * existed. `isEntitled()` remains a pure synchronous `Map` read.
1264
+ */
1020
1265
  getEntitlements(hints: IdentityHints, options?: RequestOptions): Promise<EntitlementsListResponse>;
1021
1266
  getCustomerEntitlements(customerId: string, options?: RequestOptions): Promise<EntitlementsListResponse>;
1022
1267
  /**
@@ -1389,6 +1634,44 @@ declare class CrossdeckServer extends EventEmitter {
1389
1634
  * with the entitlement cache's max-customers cap.
1390
1635
  */
1391
1636
  private populateEntitlementCache;
1637
+ /**
1638
+ * Persist a successful entitlements fetch to the durable store, if
1639
+ * one is configured. No-op when there is no store.
1640
+ *
1641
+ * Saved under EVERY identity the caller might later look up by — the
1642
+ * canonical `crossdeckCustomerId` plus any `userId` / `anonymousId`
1643
+ * hint. The Node cache resolves a hint to a canonical ID via an
1644
+ * in-memory alias map; on a cold start that map is empty, so a
1645
+ * failure-path `load()` must be able to hit the store with the raw
1646
+ * hint the caller passed. Saving under all keys makes that work.
1647
+ *
1648
+ * Best-effort: a store `save()` that throws is swallowed (logged in
1649
+ * debug) — it weakens durability for that customer but must never
1650
+ * fail an otherwise-successful `getEntitlements()`.
1651
+ */
1652
+ private saveEntitlementsToStore;
1653
+ /**
1654
+ * Load last-known-good entitlements from the durable store on a
1655
+ * network-failure path. Returns the first snapshot found across the
1656
+ * caller's identity keys, or `null` if there is no store / no stored
1657
+ * snapshot / every read failed.
1658
+ *
1659
+ * Tries the canonical `customerId` hint first, then `userId`, then
1660
+ * `anonymousId` — the order callers most commonly key by. A corrupt
1661
+ * or wrong-shaped blob is treated as a miss (the store is developer-
1662
+ * supplied; the SDK validates rather than trusts).
1663
+ */
1664
+ private loadEntitlementsFromStore;
1665
+ /**
1666
+ * Resolve the customer ID to stamp a failed-refresh marker against.
1667
+ *
1668
+ * Prefers a canonical ID the cache already knows (so the marker lands
1669
+ * on the existing warm entry), then falls back to whatever raw hint
1670
+ * the caller supplied — on a true cold-start failure there is no
1671
+ * cache entry yet, and marking under the hint still makes "we tried
1672
+ * for this customer and Crossdeck was down" observable.
1673
+ */
1674
+ private resolveFailedRefreshCustomerId;
1392
1675
  private touchAlias;
1393
1676
  /**
1394
1677
  * Resolve any hint shape (canonical customerId / userId hint /
@@ -1411,4 +1694,4 @@ declare class CrossdeckServer extends EventEmitter {
1411
1694
  private normalizeIngestEvent;
1412
1695
  }
1413
1696
 
1414
- export { type StackFrame as $, type AliasIdentityInput as A, type Breadcrumb as B, CROSSDECK_API_VERSION as C, DEFAULT_BASE_URL as D, type EntitlementCacheOptions as E, type EventProperties as F, type ForgetResult as G, type GrantDuration as H, type GrantEntitlementInput as I, type GroupMembership as J, type HeartbeatResponse as K, type HttpRequestInfo as L, type HttpResponseInfo as M, type HttpRetriesConfig as N, type IdentifyOptions as O, type IdentityHints as P, type IngestOptions as Q, type IngestResponse as R, type PublicEntitlement as S, type PurchaseResult as T, type RequestOptions as U, type RevokeEntitlementInput as V, type RuntimeHost as W, type RuntimeInfo as X, SDK_NAME as Y, SDK_VERSION as Z, type ServerEvent as _, type AliasResult as a, type SyncPurchaseInput as a0, makeCrossdeckError as a1, type AuditDecision as b, type AuditEntry as c, type BreadcrumbCategory as d, type BreadcrumbLevel as e, type CapturedError as f, CrossdeckAuthenticationError as g, CrossdeckConfigurationError as h, CrossdeckError as i, type CrossdeckErrorPayload as j, type CrossdeckErrorType as k, CrossdeckInternalError as l, CrossdeckNetworkError as m, CrossdeckPermissionError as n, CrossdeckRateLimitError as o, CrossdeckServer as p, type CrossdeckServerOptions as q, CrossdeckValidationError as r, DEFAULT_TIMEOUT_MS as s, type Diagnostics as t, type EntitlementMutationResult as u, type EntitlementsListResponse as v, type EntitlementsListener as w, type Environment as x, type ErrorCaptureConfig as y, type ErrorLevel as z };
1697
+ export { type ServerEvent as $, type AliasIdentityInput as A, type Breadcrumb as B, CROSSDECK_API_VERSION as C, DEFAULT_BASE_URL as D, type EntitlementCacheOptions as E, type ErrorLevel as F, type EventProperties as G, type ForgetResult as H, type GrantDuration as I, type GrantEntitlementInput as J, type GroupMembership as K, type HeartbeatResponse as L, type HttpRequestInfo as M, type HttpResponseInfo as N, type HttpRetriesConfig as O, type IdentifyOptions as P, type IdentityHints as Q, type IngestOptions as R, type IngestResponse as S, type PublicEntitlement as T, type PurchaseResult as U, type RequestOptions as V, type RevokeEntitlementInput as W, type RuntimeHost as X, type RuntimeInfo as Y, SDK_NAME as Z, SDK_VERSION as _, type AliasResult as a, type StackFrame as a0, type StoredEntitlements as a1, type SyncPurchaseInput as a2, makeCrossdeckError as a3, type AuditDecision as b, type AuditEntry as c, type BreadcrumbCategory as d, type BreadcrumbLevel as e, type CapturedError as f, CrossdeckAuthenticationError as g, CrossdeckConfigurationError as h, CrossdeckError as i, type CrossdeckErrorPayload as j, type CrossdeckErrorType as k, CrossdeckInternalError as l, CrossdeckNetworkError as m, CrossdeckPermissionError as n, CrossdeckRateLimitError as o, CrossdeckServer as p, type CrossdeckServerOptions as q, CrossdeckValidationError as r, DEFAULT_TIMEOUT_MS as s, type Diagnostics as t, type EntitlementMutationResult as u, type EntitlementStore as v, type EntitlementsListResponse as w, type EntitlementsListener as x, type Environment as y, type ErrorCaptureConfig as z };