@cross-deck/web 1.0.1 → 1.1.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 +18 -0
- package/dist/crossdeck.umd.min.js +2 -2
- package/dist/crossdeck.umd.min.js.map +1 -1
- package/dist/error-codes.json +1 -1
- package/dist/index.cjs +387 -91
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +58 -36
- package/dist/index.d.ts +58 -36
- package/dist/index.mjs +387 -91
- package/dist/index.mjs.map +1 -1
- package/dist/react.cjs +387 -91
- package/dist/react.cjs.map +1 -1
- package/dist/react.mjs +387 -91
- package/dist/react.mjs.map +1 -1
- package/dist/vue.cjs +387 -91
- package/dist/vue.cjs.map +1 -1
- package/dist/vue.mjs +387 -91
- package/dist/vue.mjs.map +1 -1
- package/package.json +1 -1
package/dist/react.mjs
CHANGED
|
@@ -68,7 +68,7 @@ function typeMapForStatus(status) {
|
|
|
68
68
|
|
|
69
69
|
// src/http.ts
|
|
70
70
|
var SDK_NAME = "@cross-deck/web";
|
|
71
|
-
var SDK_VERSION = "1.
|
|
71
|
+
var SDK_VERSION = "1.1.0";
|
|
72
72
|
var DEFAULT_BASE_URL = "https://api.cross-deck.com/v1";
|
|
73
73
|
var DEFAULT_TIMEOUT_MS = 15e3;
|
|
74
74
|
var HttpClient = class {
|
|
@@ -316,70 +316,120 @@ function randomChars(count) {
|
|
|
316
316
|
}
|
|
317
317
|
|
|
318
318
|
// src/entitlement-cache.ts
|
|
319
|
+
var DEFAULT_STALE_AFTER_MS = 24 * 60 * 60 * 1e3;
|
|
319
320
|
var EntitlementCache = class {
|
|
320
|
-
|
|
321
|
-
|
|
321
|
+
/**
|
|
322
|
+
* @param storage Device storage adapter. When omitted (tests) or
|
|
323
|
+
* a MemoryStorage (strict-consent / no-persistence
|
|
324
|
+
* mode) the cache is session-only — durability is
|
|
325
|
+
* simply absent, never wrong.
|
|
326
|
+
* @param storageKey Full key the persisted blob lives under.
|
|
327
|
+
* @param staleAfterMs Age past which last-known-good is flagged stale
|
|
328
|
+
* even without a failed refresh. Default 24h.
|
|
329
|
+
*/
|
|
330
|
+
constructor(storage, storageKey = "crossdeck:entitlements", staleAfterMs = DEFAULT_STALE_AFTER_MS) {
|
|
322
331
|
this.all = [];
|
|
323
332
|
this.lastUpdated = 0;
|
|
333
|
+
this.lastRefreshFailedAt = 0;
|
|
324
334
|
this.listeners = /* @__PURE__ */ new Set();
|
|
325
335
|
this.listenerErrorCount = 0;
|
|
336
|
+
this.storage = storage;
|
|
337
|
+
this.storageKey = storageKey;
|
|
338
|
+
this.staleAfterMs = staleAfterMs;
|
|
339
|
+
this.hydrate();
|
|
326
340
|
}
|
|
327
|
-
/**
|
|
341
|
+
/**
|
|
342
|
+
* Sync read — true iff the entitlement is currently granting access.
|
|
343
|
+
*
|
|
344
|
+
* Served from last-known-good: a stale cache (server unreachable since
|
|
345
|
+
* the last successful fetch) still answers true for a still-valid
|
|
346
|
+
* entitlement. The ONLY thing that turns it false is the entitlement's
|
|
347
|
+
* own expiry (validUntil) — never overall cache staleness.
|
|
348
|
+
*/
|
|
328
349
|
isEntitled(key) {
|
|
329
|
-
|
|
350
|
+
const nowSec = Date.now() / 1e3;
|
|
351
|
+
return this.all.some(
|
|
352
|
+
(e) => e.key === key && e.isActive && (e.validUntil == null || e.validUntil > nowSec)
|
|
353
|
+
);
|
|
330
354
|
}
|
|
331
355
|
/** Full snapshot for callers that need source / validUntil details. */
|
|
332
356
|
list() {
|
|
333
357
|
return this.all.slice();
|
|
334
358
|
}
|
|
335
|
-
/** When the cache was last refreshed. 0 means "never". */
|
|
359
|
+
/** When the cache was last refreshed from the server. 0 means "never". */
|
|
336
360
|
get freshness() {
|
|
337
361
|
return this.lastUpdated;
|
|
338
362
|
}
|
|
339
363
|
/**
|
|
340
|
-
*
|
|
341
|
-
*
|
|
342
|
-
*
|
|
343
|
-
*
|
|
364
|
+
* Whether the cache is knowingly serving older-than-trustworthy data.
|
|
365
|
+
*
|
|
366
|
+
* True when the most recent refresh ATTEMPT failed (Crossdeck
|
|
367
|
+
* unreachable since the last success — the outage case, distinct from
|
|
368
|
+
* a benign idle tab that simply hasn't re-fetched), OR when
|
|
369
|
+
* last-known-good has aged past staleAfterMs.
|
|
370
|
+
*
|
|
371
|
+
* isStale never changes what isEntitled() returns — the cache still
|
|
372
|
+
* serves last-known-good. It exists so the staleness is observable
|
|
373
|
+
* (diagnostics()) instead of an unbounded silent window where a
|
|
374
|
+
* revoked customer holds access with nobody able to see it.
|
|
344
375
|
*/
|
|
376
|
+
get isStale() {
|
|
377
|
+
if (this.lastRefreshFailedAt > this.lastUpdated) return true;
|
|
378
|
+
return this.lastUpdated > 0 && Date.now() - this.lastUpdated > this.staleAfterMs;
|
|
379
|
+
}
|
|
380
|
+
/** Epoch ms of the last failed refresh attempt. 0 if none since the last success. */
|
|
381
|
+
get refreshFailedAt() {
|
|
382
|
+
return this.lastRefreshFailedAt;
|
|
383
|
+
}
|
|
345
384
|
get listenerErrors() {
|
|
346
385
|
return this.listenerErrorCount;
|
|
347
386
|
}
|
|
348
387
|
/**
|
|
349
|
-
*
|
|
350
|
-
*
|
|
351
|
-
*
|
|
388
|
+
* Record that a refresh attempt failed (Crossdeck unreachable / a
|
|
389
|
+
* transient error). The SDK's getEntitlements() calls this in its
|
|
390
|
+
* catch path. It does NOT touch the cached entitlements — last-known-
|
|
391
|
+
* good keeps serving — it only flips isStale so the staleness shows
|
|
392
|
+
* up in diagnostics() rather than being silent.
|
|
393
|
+
*/
|
|
394
|
+
markRefreshFailed() {
|
|
395
|
+
this.lastRefreshFailedAt = Date.now();
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Replace the cache with a fresh server response and persist it to
|
|
399
|
+
* device storage so it survives a reload / app restart.
|
|
352
400
|
*
|
|
353
|
-
*
|
|
401
|
+
* Called ONLY after a successful server read — a failed fetch throws
|
|
402
|
+
* before it reaches here, so last-known-good is preserved through an
|
|
403
|
+
* outage. A success also clears the stale flag.
|
|
354
404
|
*/
|
|
355
405
|
setFromList(entitlements) {
|
|
356
406
|
this.all = entitlements.slice();
|
|
357
|
-
this.active = new Set(entitlements.filter((e) => e.isActive).map((e) => e.key));
|
|
358
407
|
this.lastUpdated = Date.now();
|
|
408
|
+
this.lastRefreshFailedAt = 0;
|
|
409
|
+
this.persist();
|
|
359
410
|
this.notify();
|
|
360
411
|
}
|
|
361
412
|
/**
|
|
362
|
-
* Wipe — used on reset() (logout)
|
|
363
|
-
*
|
|
364
|
-
*
|
|
365
|
-
* Fires listeners so React/SwiftUI/etc bindings re-render to the
|
|
366
|
-
* logged-out state immediately.
|
|
413
|
+
* Wipe — used on reset() (logout) and on an identity switch. Clears
|
|
414
|
+
* BOTH memory and durable storage so a prior user's entitlements can
|
|
415
|
+
* never leak to the next person on this device.
|
|
367
416
|
*/
|
|
368
417
|
clear() {
|
|
369
|
-
this.active.clear();
|
|
370
418
|
this.all = [];
|
|
371
419
|
this.lastUpdated = 0;
|
|
420
|
+
this.lastRefreshFailedAt = 0;
|
|
421
|
+
if (this.storage) {
|
|
422
|
+
try {
|
|
423
|
+
this.storage.removeItem(this.storageKey);
|
|
424
|
+
} catch {
|
|
425
|
+
}
|
|
426
|
+
}
|
|
372
427
|
this.notify();
|
|
373
428
|
}
|
|
374
429
|
/**
|
|
375
|
-
* Subscribe to cache mutations. Returns an unsubscribe
|
|
376
|
-
*
|
|
377
|
-
*
|
|
378
|
-
* current snapshot. Throwing inside a listener is non-fatal — the
|
|
379
|
-
* error is swallowed and subsequent listeners still run.
|
|
380
|
-
*
|
|
381
|
-
* Used by `@cross-deck/web/react`'s `useEntitlement` hook to
|
|
382
|
-
* trigger re-renders when entitlements change.
|
|
430
|
+
* Subscribe to cache mutations. Returns an idempotent unsubscribe fn.
|
|
431
|
+
* The listener fires AFTER setFromList() or clear() with the current
|
|
432
|
+
* snapshot. Used by `@cross-deck/web/react`'s `useEntitlement` hook.
|
|
383
433
|
*/
|
|
384
434
|
subscribe(listener) {
|
|
385
435
|
this.listeners.add(listener);
|
|
@@ -390,6 +440,39 @@ var EntitlementCache = class {
|
|
|
390
440
|
this.listeners.delete(listener);
|
|
391
441
|
};
|
|
392
442
|
}
|
|
443
|
+
// ----- Durable persistence -----
|
|
444
|
+
/**
|
|
445
|
+
* Load last-known-good from device storage. Runs once in the
|
|
446
|
+
* constructor, synchronously, so isEntitled() is correct from boot.
|
|
447
|
+
* Any corrupt / unparseable blob degrades silently to an empty cache —
|
|
448
|
+
* boot must never throw.
|
|
449
|
+
*/
|
|
450
|
+
hydrate() {
|
|
451
|
+
if (!this.storage) return;
|
|
452
|
+
try {
|
|
453
|
+
const raw = this.storage.getItem(this.storageKey);
|
|
454
|
+
if (!raw) return;
|
|
455
|
+
const parsed = JSON.parse(raw);
|
|
456
|
+
if (parsed && parsed.v === 1 && Array.isArray(parsed.entitlements)) {
|
|
457
|
+
this.all = parsed.entitlements;
|
|
458
|
+
this.lastUpdated = typeof parsed.lastUpdated === "number" ? parsed.lastUpdated : 0;
|
|
459
|
+
}
|
|
460
|
+
} catch {
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
/** Write last-known-good to device storage. Best-effort. */
|
|
464
|
+
persist() {
|
|
465
|
+
if (!this.storage) return;
|
|
466
|
+
try {
|
|
467
|
+
const blob = {
|
|
468
|
+
v: 1,
|
|
469
|
+
entitlements: this.all,
|
|
470
|
+
lastUpdated: this.lastUpdated
|
|
471
|
+
};
|
|
472
|
+
this.storage.setItem(this.storageKey, JSON.stringify(blob));
|
|
473
|
+
} catch {
|
|
474
|
+
}
|
|
475
|
+
}
|
|
393
476
|
notify() {
|
|
394
477
|
if (this.listeners.size === 0) return;
|
|
395
478
|
const snapshot = this.all.slice();
|
|
@@ -1186,11 +1269,50 @@ function isInsidePasswordField(el) {
|
|
|
1186
1269
|
return false;
|
|
1187
1270
|
}
|
|
1188
1271
|
function extractText(el) {
|
|
1272
|
+
const clean = (s) => s.replace(/\s+/g, " ").trim();
|
|
1273
|
+
const explicit = el.getAttribute("data-cd-track") || el.getAttribute("data-track") || el.getAttribute("data-testid");
|
|
1274
|
+
if (explicit) {
|
|
1275
|
+
const t = clean(explicit);
|
|
1276
|
+
if (t) return t;
|
|
1277
|
+
}
|
|
1189
1278
|
const aria = el.getAttribute("aria-label");
|
|
1190
|
-
if (aria)
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1279
|
+
if (aria) {
|
|
1280
|
+
const t = clean(aria);
|
|
1281
|
+
if (t) return t;
|
|
1282
|
+
}
|
|
1283
|
+
const labelledBy = el.getAttribute("aria-labelledby");
|
|
1284
|
+
if (labelledBy && typeof el.ownerDocument?.getElementById === "function") {
|
|
1285
|
+
const parts = [];
|
|
1286
|
+
for (const id of labelledBy.split(/\s+/)) {
|
|
1287
|
+
const ref = el.ownerDocument.getElementById(id);
|
|
1288
|
+
const t = ref?.textContent ? clean(ref.textContent) : "";
|
|
1289
|
+
if (t) parts.push(t);
|
|
1290
|
+
}
|
|
1291
|
+
if (parts.length > 0) return parts.join(" ");
|
|
1292
|
+
}
|
|
1293
|
+
if (el instanceof HTMLInputElement && el.value) {
|
|
1294
|
+
const t = clean(el.value);
|
|
1295
|
+
if (t) return t;
|
|
1296
|
+
}
|
|
1297
|
+
const text = clean(el.textContent || "");
|
|
1298
|
+
if (text) return text;
|
|
1299
|
+
const title = el.getAttribute("title");
|
|
1300
|
+
if (title) {
|
|
1301
|
+
const t = clean(title);
|
|
1302
|
+
if (t) return t;
|
|
1303
|
+
}
|
|
1304
|
+
const img = el.querySelector("img[alt]");
|
|
1305
|
+
if (img) {
|
|
1306
|
+
const alt = img.getAttribute("alt") ?? "";
|
|
1307
|
+
const t = clean(alt);
|
|
1308
|
+
if (t) return t;
|
|
1309
|
+
}
|
|
1310
|
+
const svgTitle = el.querySelector("svg title");
|
|
1311
|
+
if (svgTitle?.textContent) {
|
|
1312
|
+
const t = clean(svgTitle.textContent);
|
|
1313
|
+
if (t) return t;
|
|
1314
|
+
}
|
|
1315
|
+
return "";
|
|
1194
1316
|
}
|
|
1195
1317
|
function trimText(s, cap) {
|
|
1196
1318
|
if (s.length <= cap) return s;
|
|
@@ -1895,13 +2017,22 @@ function isInAppFrame(filename) {
|
|
|
1895
2017
|
if (/\/crossdeck\.umd\.min\.js$/.test(filename)) return false;
|
|
1896
2018
|
return true;
|
|
1897
2019
|
}
|
|
1898
|
-
function fingerprintError(message, frames) {
|
|
2020
|
+
function fingerprintError(message, frames, location) {
|
|
1899
2021
|
const inAppFrames = frames.filter((f) => f.in_app).slice(0, 3);
|
|
1900
|
-
const
|
|
2022
|
+
const parts = [
|
|
1901
2023
|
(message || "").slice(0, 200),
|
|
1902
2024
|
...inAppFrames.map((f) => `${f.function}@${f.filename}:${f.lineno}`)
|
|
1903
|
-
]
|
|
1904
|
-
|
|
2025
|
+
];
|
|
2026
|
+
if (inAppFrames.length === 0 && location) {
|
|
2027
|
+
const loc = [
|
|
2028
|
+
location.errorType ?? "",
|
|
2029
|
+
location.filename ?? "",
|
|
2030
|
+
location.lineno ?? "",
|
|
2031
|
+
location.colno ?? ""
|
|
2032
|
+
].join(":");
|
|
2033
|
+
if (loc !== ":::") parts.push(loc);
|
|
2034
|
+
}
|
|
2035
|
+
return djb2Hex(parts.join("|"));
|
|
1905
2036
|
}
|
|
1906
2037
|
function djb2Hex(input) {
|
|
1907
2038
|
let h = 5381;
|
|
@@ -1923,11 +2054,17 @@ var DEFAULT_ERROR_CAPTURE = {
|
|
|
1923
2054
|
// Classic browser noise. These aren't application bugs.
|
|
1924
2055
|
"ResizeObserver loop limit exceeded",
|
|
1925
2056
|
"ResizeObserver loop completed with undelivered notifications",
|
|
1926
|
-
"Non-Error promise rejection captured"
|
|
1927
|
-
//
|
|
1928
|
-
//
|
|
1929
|
-
"
|
|
1930
|
-
"
|
|
2057
|
+
"Non-Error promise rejection captured"
|
|
2058
|
+
// NOTE: We deliberately do NOT drop cross-origin "Script error."
|
|
2059
|
+
// events here. They used to be silently filtered as noise, but
|
|
2060
|
+
// silent drops are the opposite of "developer sleeps well at
|
|
2061
|
+
// night". We now capture them with a clear label and the
|
|
2062
|
+
// `cross_origin` tag so the dashboard surfaces them as a
|
|
2063
|
+
// distinct, actionable category (the fix is always the same —
|
|
2064
|
+
// add `crossorigin="anonymous"` to the script tag + CORS
|
|
2065
|
+
// headers on the script's origin). Apps that genuinely want
|
|
2066
|
+
// them muted can re-add "Script error" to ignoreErrors via
|
|
2067
|
+
// init config.
|
|
1931
2068
|
],
|
|
1932
2069
|
allowUrls: [],
|
|
1933
2070
|
denyUrls: [
|
|
@@ -2167,61 +2304,84 @@ var ErrorTracker = class {
|
|
|
2167
2304
|
// ============================================================
|
|
2168
2305
|
buildFromErrorEvent(event) {
|
|
2169
2306
|
const err = event.error;
|
|
2170
|
-
const
|
|
2307
|
+
const filename = event.filename || null;
|
|
2308
|
+
const lineno = typeof event.lineno === "number" && event.lineno > 0 ? event.lineno : null;
|
|
2309
|
+
const colno = typeof event.colno === "number" && event.colno > 0 ? event.colno : null;
|
|
2310
|
+
const isCrossOriginStripped = err == null && !filename && lineno == null && (event.message === "Script error." || event.message === "Script error" || !event.message);
|
|
2311
|
+
if (isCrossOriginStripped) {
|
|
2312
|
+
const message2 = "Cross-origin script error (browser hid details \u2014 script needs crossorigin attribute + CORS headers)";
|
|
2313
|
+
return {
|
|
2314
|
+
timestamp: Date.now(),
|
|
2315
|
+
kind: "error.unhandled",
|
|
2316
|
+
level: "error",
|
|
2317
|
+
message: message2,
|
|
2318
|
+
errorType: "ScriptError",
|
|
2319
|
+
frames: [],
|
|
2320
|
+
rawStack: null,
|
|
2321
|
+
filename: null,
|
|
2322
|
+
lineno: null,
|
|
2323
|
+
colno: null,
|
|
2324
|
+
// No location to fingerprint by — all of these will share one
|
|
2325
|
+
// group, which is correct: developer fixes them all with the
|
|
2326
|
+
// same CORS change.
|
|
2327
|
+
fingerprint: fingerprintError(message2, []),
|
|
2328
|
+
breadcrumbs: this.opts.breadcrumbs.snapshot(),
|
|
2329
|
+
context: this.opts.getContext(),
|
|
2330
|
+
tags: { ...this.opts.getTags(), cross_origin: "true" }
|
|
2331
|
+
};
|
|
2332
|
+
}
|
|
2333
|
+
const payload = coerceErrorPayload(err);
|
|
2334
|
+
const message = (payload.message || event.message || "Unknown error").slice(0, 1024);
|
|
2171
2335
|
const stack = err instanceof Error ? err.stack ?? null : null;
|
|
2172
2336
|
const frames = parseStack(stack);
|
|
2337
|
+
const errorType = payload.errorType ?? null;
|
|
2338
|
+
const context = payload.extras ? { ...this.opts.getContext(), __error_extras: payload.extras } : this.opts.getContext();
|
|
2173
2339
|
return {
|
|
2174
2340
|
timestamp: Date.now(),
|
|
2175
2341
|
kind: "error.unhandled",
|
|
2176
2342
|
level: "error",
|
|
2177
|
-
message
|
|
2178
|
-
errorType
|
|
2343
|
+
message,
|
|
2344
|
+
errorType,
|
|
2179
2345
|
frames,
|
|
2180
2346
|
rawStack: stack,
|
|
2181
|
-
filename
|
|
2182
|
-
lineno
|
|
2183
|
-
colno
|
|
2184
|
-
|
|
2347
|
+
filename,
|
|
2348
|
+
lineno,
|
|
2349
|
+
colno,
|
|
2350
|
+
// Location fallback ensures distinct call sites stay separate
|
|
2351
|
+
// even when the message is generic ("Unknown error",
|
|
2352
|
+
// "[object Object]") and there are no parseable frames.
|
|
2353
|
+
fingerprint: fingerprintError(message, frames, {
|
|
2354
|
+
filename,
|
|
2355
|
+
lineno,
|
|
2356
|
+
colno,
|
|
2357
|
+
errorType
|
|
2358
|
+
}),
|
|
2185
2359
|
breadcrumbs: this.opts.breadcrumbs.snapshot(),
|
|
2186
|
-
context
|
|
2360
|
+
context,
|
|
2187
2361
|
tags: this.opts.getTags()
|
|
2188
2362
|
};
|
|
2189
2363
|
}
|
|
2190
2364
|
buildFromUnknown(err, kind, level) {
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
message: String(err.message).slice(0, 1024),
|
|
2198
|
-
errorType: err.name,
|
|
2199
|
-
frames,
|
|
2200
|
-
rawStack: err.stack ?? null,
|
|
2201
|
-
filename: null,
|
|
2202
|
-
lineno: null,
|
|
2203
|
-
colno: null,
|
|
2204
|
-
fingerprint: fingerprintError(err.message, frames),
|
|
2205
|
-
breadcrumbs: this.opts.breadcrumbs.snapshot(),
|
|
2206
|
-
context: this.opts.getContext(),
|
|
2207
|
-
tags: this.opts.getTags()
|
|
2208
|
-
};
|
|
2209
|
-
}
|
|
2210
|
-
const message = safeStringify2(err).slice(0, 1024);
|
|
2365
|
+
const payload = coerceErrorPayload(err);
|
|
2366
|
+
const message = (payload.message || "Unknown error").slice(0, 1024);
|
|
2367
|
+
const stack = err instanceof Error ? err.stack ?? null : null;
|
|
2368
|
+
const frames = parseStack(stack);
|
|
2369
|
+
const errorType = payload.errorType ?? null;
|
|
2370
|
+
const context = payload.extras ? { ...this.opts.getContext(), __error_extras: payload.extras } : this.opts.getContext();
|
|
2211
2371
|
return {
|
|
2212
2372
|
timestamp: Date.now(),
|
|
2213
2373
|
kind,
|
|
2214
2374
|
level,
|
|
2215
2375
|
message,
|
|
2216
|
-
errorType
|
|
2217
|
-
frames
|
|
2218
|
-
rawStack:
|
|
2376
|
+
errorType,
|
|
2377
|
+
frames,
|
|
2378
|
+
rawStack: stack,
|
|
2219
2379
|
filename: null,
|
|
2220
2380
|
lineno: null,
|
|
2221
2381
|
colno: null,
|
|
2222
|
-
fingerprint: fingerprintError(message,
|
|
2382
|
+
fingerprint: fingerprintError(message, frames, { errorType }),
|
|
2223
2383
|
breadcrumbs: this.opts.breadcrumbs.snapshot(),
|
|
2224
|
-
context
|
|
2384
|
+
context,
|
|
2225
2385
|
tags: this.opts.getTags()
|
|
2226
2386
|
};
|
|
2227
2387
|
}
|
|
@@ -2239,7 +2399,10 @@ var ErrorTracker = class {
|
|
|
2239
2399
|
filename: info.url,
|
|
2240
2400
|
lineno: null,
|
|
2241
2401
|
colno: null,
|
|
2242
|
-
fingerprint: fingerprintError(`HTTP ${info.status} ${info.method}`, []
|
|
2402
|
+
fingerprint: fingerprintError(`HTTP ${info.status} ${info.method}`, [], {
|
|
2403
|
+
filename: info.url,
|
|
2404
|
+
errorType: "HTTPError"
|
|
2405
|
+
}),
|
|
2243
2406
|
breadcrumbs: this.opts.breadcrumbs.snapshot(),
|
|
2244
2407
|
context: this.opts.getContext(),
|
|
2245
2408
|
tags: this.opts.getTags(),
|
|
@@ -2318,16 +2481,131 @@ var ErrorTracker = class {
|
|
|
2318
2481
|
return true;
|
|
2319
2482
|
}
|
|
2320
2483
|
};
|
|
2321
|
-
function
|
|
2322
|
-
if (v
|
|
2323
|
-
if (
|
|
2324
|
-
if (typeof v === "
|
|
2484
|
+
function coerceErrorPayload(v) {
|
|
2485
|
+
if (v === null) return { message: "(thrown: null)", errorType: null, extras: null };
|
|
2486
|
+
if (v === void 0) return { message: "(thrown: undefined)", errorType: null, extras: null };
|
|
2487
|
+
if (typeof v === "string") {
|
|
2488
|
+
return { message: v, errorType: null, extras: null };
|
|
2489
|
+
}
|
|
2490
|
+
if (typeof v === "number" || typeof v === "boolean" || typeof v === "bigint") {
|
|
2491
|
+
return { message: String(v), errorType: typeof v, extras: null };
|
|
2492
|
+
}
|
|
2493
|
+
if (typeof v === "symbol") {
|
|
2494
|
+
return { message: v.toString(), errorType: "symbol", extras: null };
|
|
2495
|
+
}
|
|
2496
|
+
if (typeof v === "function") {
|
|
2497
|
+
return { message: `(thrown function: ${v.name || "anonymous"})`, errorType: "function", extras: null };
|
|
2498
|
+
}
|
|
2499
|
+
if (v instanceof Error) {
|
|
2500
|
+
const errorType = v.name || v.constructor?.name || "Error";
|
|
2501
|
+
const message = typeof v.message === "string" && v.message.length > 0 ? v.message : safeToString(v) || errorType;
|
|
2502
|
+
const extras = {};
|
|
2503
|
+
const causeChain = collectCauseChain(v);
|
|
2504
|
+
if (causeChain.length > 0) extras.cause = causeChain;
|
|
2505
|
+
for (const key of ["code", "status", "statusCode", "errno", "response", "data", "detail", "details"]) {
|
|
2506
|
+
const val = v[key];
|
|
2507
|
+
if (val !== void 0 && typeof val !== "function") {
|
|
2508
|
+
extras[key] = safeClone(val);
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2511
|
+
for (const key of Object.keys(v)) {
|
|
2512
|
+
if (key === "message" || key === "stack" || key === "name" || key === "cause") continue;
|
|
2513
|
+
if (key in extras) continue;
|
|
2514
|
+
const val = v[key];
|
|
2515
|
+
if (typeof val === "function") continue;
|
|
2516
|
+
extras[key] = safeClone(val);
|
|
2517
|
+
}
|
|
2518
|
+
return {
|
|
2519
|
+
message,
|
|
2520
|
+
errorType,
|
|
2521
|
+
extras: Object.keys(extras).length > 0 ? extras : null
|
|
2522
|
+
};
|
|
2523
|
+
}
|
|
2524
|
+
if (typeof Response !== "undefined" && v instanceof Response) {
|
|
2525
|
+
return {
|
|
2526
|
+
message: `HTTP ${v.status} ${v.statusText || ""}${v.url ? ` ${v.url}` : ""}`.trim(),
|
|
2527
|
+
errorType: "Response",
|
|
2528
|
+
extras: { status: v.status, statusText: v.statusText, url: v.url, type: v.type }
|
|
2529
|
+
};
|
|
2530
|
+
}
|
|
2531
|
+
if (typeof v === "object") {
|
|
2532
|
+
const obj = v;
|
|
2533
|
+
const ctorName = obj.constructor && typeof obj.constructor === "function" && obj.constructor.name || null;
|
|
2534
|
+
const ownMessage = typeof obj.message === "string" && obj.message ? obj.message : null;
|
|
2535
|
+
const ownName = typeof obj.name === "string" && obj.name ? obj.name : null;
|
|
2536
|
+
let jsonForm = null;
|
|
2537
|
+
try {
|
|
2538
|
+
const serialised = JSON.stringify(obj);
|
|
2539
|
+
jsonForm = serialised === "{}" ? null : serialised;
|
|
2540
|
+
} catch {
|
|
2541
|
+
jsonForm = null;
|
|
2542
|
+
}
|
|
2543
|
+
const fallbackString = safeToString(obj);
|
|
2544
|
+
const message = ownMessage ?? jsonForm ?? (fallbackString && fallbackString !== "[object Object]" ? fallbackString : null) ?? (ctorName ? `(thrown ${ctorName} with no message)` : "(thrown object with no message)");
|
|
2545
|
+
const errorType = ownName ?? ctorName ?? null;
|
|
2546
|
+
const extras = {};
|
|
2547
|
+
let count = 0;
|
|
2548
|
+
for (const key of Object.keys(obj)) {
|
|
2549
|
+
if (count >= 20) break;
|
|
2550
|
+
if (key === "message" || key === "name") continue;
|
|
2551
|
+
const val = obj[key];
|
|
2552
|
+
if (typeof val === "function") continue;
|
|
2553
|
+
extras[key] = safeClone(val);
|
|
2554
|
+
count++;
|
|
2555
|
+
}
|
|
2556
|
+
return {
|
|
2557
|
+
message,
|
|
2558
|
+
errorType,
|
|
2559
|
+
extras: Object.keys(extras).length > 0 ? extras : null
|
|
2560
|
+
};
|
|
2561
|
+
}
|
|
2562
|
+
return { message: safeToString(v) || "(unstringifiable thrown value)", errorType: null, extras: null };
|
|
2563
|
+
}
|
|
2564
|
+
function collectCauseChain(err) {
|
|
2565
|
+
const out = [];
|
|
2566
|
+
let cur = err.cause;
|
|
2567
|
+
let depth = 0;
|
|
2568
|
+
while (cur != null && depth < 5) {
|
|
2569
|
+
if (cur instanceof Error) {
|
|
2570
|
+
out.push({ name: cur.name || "Error", message: cur.message || "" });
|
|
2571
|
+
cur = cur.cause;
|
|
2572
|
+
} else {
|
|
2573
|
+
out.push({ name: "non-Error", message: safeToString(cur) });
|
|
2574
|
+
cur = null;
|
|
2575
|
+
}
|
|
2576
|
+
depth++;
|
|
2577
|
+
}
|
|
2578
|
+
return out;
|
|
2579
|
+
}
|
|
2580
|
+
function safeToString(v) {
|
|
2325
2581
|
try {
|
|
2326
|
-
|
|
2582
|
+
const s = Object.prototype.toString.call(v);
|
|
2583
|
+
if (s !== "[object Object]") return s;
|
|
2584
|
+
const own = v?.toString;
|
|
2585
|
+
if (typeof own === "function" && own !== Object.prototype.toString) {
|
|
2586
|
+
const r = own.call(v);
|
|
2587
|
+
if (typeof r === "string") return r;
|
|
2588
|
+
}
|
|
2589
|
+
return s;
|
|
2327
2590
|
} catch {
|
|
2328
|
-
return
|
|
2591
|
+
return "(throwing toString)";
|
|
2329
2592
|
}
|
|
2330
2593
|
}
|
|
2594
|
+
function safeClone(v) {
|
|
2595
|
+
if (v == null) return v;
|
|
2596
|
+
const t = typeof v;
|
|
2597
|
+
if (t === "string" || t === "number" || t === "boolean") return v;
|
|
2598
|
+
if (t === "bigint") return String(v);
|
|
2599
|
+
try {
|
|
2600
|
+
const s = JSON.stringify(v);
|
|
2601
|
+
return s === void 0 ? safeToString(v) : JSON.parse(s);
|
|
2602
|
+
} catch {
|
|
2603
|
+
return safeToString(v);
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2606
|
+
function safeStringify2(v) {
|
|
2607
|
+
return coerceErrorPayload(v).message;
|
|
2608
|
+
}
|
|
2331
2609
|
|
|
2332
2610
|
// src/crossdeck.ts
|
|
2333
2611
|
var CrossdeckClient = class {
|
|
@@ -2417,7 +2695,10 @@ var CrossdeckClient = class {
|
|
|
2417
2695
|
typeof globalThis.document !== "undefined";
|
|
2418
2696
|
const cookieStore = useCookieRedundancy ? new CookieStorage() : void 0;
|
|
2419
2697
|
const identity = new IdentityStore(effectiveStorage, opts.storagePrefix, cookieStore);
|
|
2420
|
-
const entitlements = new EntitlementCache(
|
|
2698
|
+
const entitlements = new EntitlementCache(
|
|
2699
|
+
effectiveStorage,
|
|
2700
|
+
opts.storagePrefix + "entitlements"
|
|
2701
|
+
);
|
|
2421
2702
|
const persistentEvents = persistIdentity ? new PersistentEventStore({ storage: effectiveStorage, prefix: opts.storagePrefix }) : null;
|
|
2422
2703
|
if (persistentEvents) {
|
|
2423
2704
|
debug.emit(
|
|
@@ -2597,6 +2878,10 @@ var CrossdeckClient = class {
|
|
|
2597
2878
|
const result = await s.http.request("POST", "/identity/alias", {
|
|
2598
2879
|
body
|
|
2599
2880
|
});
|
|
2881
|
+
const priorCdcust = s.identity.crossdeckCustomerId;
|
|
2882
|
+
if (priorCdcust && result.crossdeckCustomerId && priorCdcust !== result.crossdeckCustomerId) {
|
|
2883
|
+
s.entitlements.clear();
|
|
2884
|
+
}
|
|
2600
2885
|
s.identity.setCrossdeckCustomerId(result.crossdeckCustomerId);
|
|
2601
2886
|
s.developerUserId = userId;
|
|
2602
2887
|
return result;
|
|
@@ -2831,11 +3116,17 @@ var CrossdeckClient = class {
|
|
|
2831
3116
|
async getEntitlements() {
|
|
2832
3117
|
const s = this.requireStarted();
|
|
2833
3118
|
const query = this.identityQueryParams();
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
3119
|
+
let result;
|
|
3120
|
+
try {
|
|
3121
|
+
result = await s.http.request(
|
|
3122
|
+
"GET",
|
|
3123
|
+
"/entitlements",
|
|
3124
|
+
{ query }
|
|
3125
|
+
);
|
|
3126
|
+
} catch (err) {
|
|
3127
|
+
s.entitlements.markRefreshFailed();
|
|
3128
|
+
throw err;
|
|
3129
|
+
}
|
|
2839
3130
|
if (result.crossdeckCustomerId) {
|
|
2840
3131
|
s.identity.setCrossdeckCustomerId(result.crossdeckCustomerId);
|
|
2841
3132
|
}
|
|
@@ -2843,8 +3134,12 @@ var CrossdeckClient = class {
|
|
|
2843
3134
|
return result.data;
|
|
2844
3135
|
}
|
|
2845
3136
|
/**
|
|
2846
|
-
* Synchronous read from the local cache
|
|
2847
|
-
*
|
|
3137
|
+
* Synchronous read from the durable local cache — answers from
|
|
3138
|
+
* last-known-good. The cache hydrates from device storage on boot and
|
|
3139
|
+
* survives a Crossdeck outage, so a returning paying customer reads
|
|
3140
|
+
* true even before the session's first network round-trip. Returns
|
|
3141
|
+
* false only for a genuinely new install that has never completed a
|
|
3142
|
+
* getEntitlements(), or for an entitlement past its own validUntil.
|
|
2848
3143
|
*/
|
|
2849
3144
|
isEntitled(key) {
|
|
2850
3145
|
const s = this.requireStarted();
|
|
@@ -3120,7 +3415,7 @@ var CrossdeckClient = class {
|
|
|
3120
3415
|
sdkVersion: null,
|
|
3121
3416
|
baseUrl: null,
|
|
3122
3417
|
clock: { lastServerTime: null, lastClientTime: null, skewMs: null },
|
|
3123
|
-
entitlements: { count: 0, lastUpdated: 0, listenerErrors: 0 },
|
|
3418
|
+
entitlements: { count: 0, lastUpdated: 0, stale: false, listenerErrors: 0 },
|
|
3124
3419
|
events: {
|
|
3125
3420
|
buffered: 0,
|
|
3126
3421
|
dropped: 0,
|
|
@@ -3149,6 +3444,7 @@ var CrossdeckClient = class {
|
|
|
3149
3444
|
entitlements: {
|
|
3150
3445
|
count: s.entitlements.list().length,
|
|
3151
3446
|
lastUpdated: s.entitlements.freshness,
|
|
3447
|
+
stale: s.entitlements.isStale,
|
|
3152
3448
|
listenerErrors: s.entitlements.listenerErrors
|
|
3153
3449
|
},
|
|
3154
3450
|
events: s.events.getStats()
|