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