@cross-deck/web 1.0.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 +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 +54 -338
- package/dist/index.d.ts +54 -338
- package/dist/index.mjs +387 -91
- package/dist/index.mjs.map +1 -1
- package/dist/react.cjs +422 -91
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.mts +24 -1
- package/dist/react.d.ts +24 -1
- package/dist/react.mjs +422 -92
- package/dist/react.mjs.map +1 -1
- package/dist/types-BzoKor4z.d.mts +310 -0
- package/dist/types-BzoKor4z.d.ts +310 -0
- 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/error-codes.json
CHANGED
package/dist/index.cjs
CHANGED
|
@@ -99,7 +99,7 @@ function typeMapForStatus(status) {
|
|
|
99
99
|
|
|
100
100
|
// src/http.ts
|
|
101
101
|
var SDK_NAME = "@cross-deck/web";
|
|
102
|
-
var SDK_VERSION = "1.
|
|
102
|
+
var SDK_VERSION = "1.1.0";
|
|
103
103
|
var DEFAULT_BASE_URL = "https://api.cross-deck.com/v1";
|
|
104
104
|
var DEFAULT_TIMEOUT_MS = 15e3;
|
|
105
105
|
var HttpClient = class {
|
|
@@ -347,70 +347,120 @@ function randomChars(count) {
|
|
|
347
347
|
}
|
|
348
348
|
|
|
349
349
|
// src/entitlement-cache.ts
|
|
350
|
+
var DEFAULT_STALE_AFTER_MS = 24 * 60 * 60 * 1e3;
|
|
350
351
|
var EntitlementCache = class {
|
|
351
|
-
|
|
352
|
-
|
|
352
|
+
/**
|
|
353
|
+
* @param storage Device storage adapter. When omitted (tests) or
|
|
354
|
+
* a MemoryStorage (strict-consent / no-persistence
|
|
355
|
+
* mode) the cache is session-only — durability is
|
|
356
|
+
* simply absent, never wrong.
|
|
357
|
+
* @param storageKey Full key the persisted blob lives under.
|
|
358
|
+
* @param staleAfterMs Age past which last-known-good is flagged stale
|
|
359
|
+
* even without a failed refresh. Default 24h.
|
|
360
|
+
*/
|
|
361
|
+
constructor(storage, storageKey = "crossdeck:entitlements", staleAfterMs = DEFAULT_STALE_AFTER_MS) {
|
|
353
362
|
this.all = [];
|
|
354
363
|
this.lastUpdated = 0;
|
|
364
|
+
this.lastRefreshFailedAt = 0;
|
|
355
365
|
this.listeners = /* @__PURE__ */ new Set();
|
|
356
366
|
this.listenerErrorCount = 0;
|
|
367
|
+
this.storage = storage;
|
|
368
|
+
this.storageKey = storageKey;
|
|
369
|
+
this.staleAfterMs = staleAfterMs;
|
|
370
|
+
this.hydrate();
|
|
357
371
|
}
|
|
358
|
-
/**
|
|
372
|
+
/**
|
|
373
|
+
* Sync read — true iff the entitlement is currently granting access.
|
|
374
|
+
*
|
|
375
|
+
* Served from last-known-good: a stale cache (server unreachable since
|
|
376
|
+
* the last successful fetch) still answers true for a still-valid
|
|
377
|
+
* entitlement. The ONLY thing that turns it false is the entitlement's
|
|
378
|
+
* own expiry (validUntil) — never overall cache staleness.
|
|
379
|
+
*/
|
|
359
380
|
isEntitled(key) {
|
|
360
|
-
|
|
381
|
+
const nowSec = Date.now() / 1e3;
|
|
382
|
+
return this.all.some(
|
|
383
|
+
(e) => e.key === key && e.isActive && (e.validUntil == null || e.validUntil > nowSec)
|
|
384
|
+
);
|
|
361
385
|
}
|
|
362
386
|
/** Full snapshot for callers that need source / validUntil details. */
|
|
363
387
|
list() {
|
|
364
388
|
return this.all.slice();
|
|
365
389
|
}
|
|
366
|
-
/** When the cache was last refreshed. 0 means "never". */
|
|
390
|
+
/** When the cache was last refreshed from the server. 0 means "never". */
|
|
367
391
|
get freshness() {
|
|
368
392
|
return this.lastUpdated;
|
|
369
393
|
}
|
|
370
394
|
/**
|
|
371
|
-
*
|
|
372
|
-
*
|
|
373
|
-
*
|
|
374
|
-
*
|
|
395
|
+
* Whether the cache is knowingly serving older-than-trustworthy data.
|
|
396
|
+
*
|
|
397
|
+
* True when the most recent refresh ATTEMPT failed (Crossdeck
|
|
398
|
+
* unreachable since the last success — the outage case, distinct from
|
|
399
|
+
* a benign idle tab that simply hasn't re-fetched), OR when
|
|
400
|
+
* last-known-good has aged past staleAfterMs.
|
|
401
|
+
*
|
|
402
|
+
* isStale never changes what isEntitled() returns — the cache still
|
|
403
|
+
* serves last-known-good. It exists so the staleness is observable
|
|
404
|
+
* (diagnostics()) instead of an unbounded silent window where a
|
|
405
|
+
* revoked customer holds access with nobody able to see it.
|
|
375
406
|
*/
|
|
407
|
+
get isStale() {
|
|
408
|
+
if (this.lastRefreshFailedAt > this.lastUpdated) return true;
|
|
409
|
+
return this.lastUpdated > 0 && Date.now() - this.lastUpdated > this.staleAfterMs;
|
|
410
|
+
}
|
|
411
|
+
/** Epoch ms of the last failed refresh attempt. 0 if none since the last success. */
|
|
412
|
+
get refreshFailedAt() {
|
|
413
|
+
return this.lastRefreshFailedAt;
|
|
414
|
+
}
|
|
376
415
|
get listenerErrors() {
|
|
377
416
|
return this.listenerErrorCount;
|
|
378
417
|
}
|
|
379
418
|
/**
|
|
380
|
-
*
|
|
381
|
-
*
|
|
382
|
-
*
|
|
419
|
+
* Record that a refresh attempt failed (Crossdeck unreachable / a
|
|
420
|
+
* transient error). The SDK's getEntitlements() calls this in its
|
|
421
|
+
* catch path. It does NOT touch the cached entitlements — last-known-
|
|
422
|
+
* good keeps serving — it only flips isStale so the staleness shows
|
|
423
|
+
* up in diagnostics() rather than being silent.
|
|
424
|
+
*/
|
|
425
|
+
markRefreshFailed() {
|
|
426
|
+
this.lastRefreshFailedAt = Date.now();
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Replace the cache with a fresh server response and persist it to
|
|
430
|
+
* device storage so it survives a reload / app restart.
|
|
383
431
|
*
|
|
384
|
-
*
|
|
432
|
+
* Called ONLY after a successful server read — a failed fetch throws
|
|
433
|
+
* before it reaches here, so last-known-good is preserved through an
|
|
434
|
+
* outage. A success also clears the stale flag.
|
|
385
435
|
*/
|
|
386
436
|
setFromList(entitlements) {
|
|
387
437
|
this.all = entitlements.slice();
|
|
388
|
-
this.active = new Set(entitlements.filter((e) => e.isActive).map((e) => e.key));
|
|
389
438
|
this.lastUpdated = Date.now();
|
|
439
|
+
this.lastRefreshFailedAt = 0;
|
|
440
|
+
this.persist();
|
|
390
441
|
this.notify();
|
|
391
442
|
}
|
|
392
443
|
/**
|
|
393
|
-
* Wipe — used on reset() (logout)
|
|
394
|
-
*
|
|
395
|
-
*
|
|
396
|
-
* Fires listeners so React/SwiftUI/etc bindings re-render to the
|
|
397
|
-
* logged-out state immediately.
|
|
444
|
+
* Wipe — used on reset() (logout) and on an identity switch. Clears
|
|
445
|
+
* BOTH memory and durable storage so a prior user's entitlements can
|
|
446
|
+
* never leak to the next person on this device.
|
|
398
447
|
*/
|
|
399
448
|
clear() {
|
|
400
|
-
this.active.clear();
|
|
401
449
|
this.all = [];
|
|
402
450
|
this.lastUpdated = 0;
|
|
451
|
+
this.lastRefreshFailedAt = 0;
|
|
452
|
+
if (this.storage) {
|
|
453
|
+
try {
|
|
454
|
+
this.storage.removeItem(this.storageKey);
|
|
455
|
+
} catch {
|
|
456
|
+
}
|
|
457
|
+
}
|
|
403
458
|
this.notify();
|
|
404
459
|
}
|
|
405
460
|
/**
|
|
406
|
-
* Subscribe to cache mutations. Returns an unsubscribe
|
|
407
|
-
*
|
|
408
|
-
*
|
|
409
|
-
* current snapshot. Throwing inside a listener is non-fatal — the
|
|
410
|
-
* error is swallowed and subsequent listeners still run.
|
|
411
|
-
*
|
|
412
|
-
* Used by `@cross-deck/web/react`'s `useEntitlement` hook to
|
|
413
|
-
* trigger re-renders when entitlements change.
|
|
461
|
+
* Subscribe to cache mutations. Returns an idempotent unsubscribe fn.
|
|
462
|
+
* The listener fires AFTER setFromList() or clear() with the current
|
|
463
|
+
* snapshot. Used by `@cross-deck/web/react`'s `useEntitlement` hook.
|
|
414
464
|
*/
|
|
415
465
|
subscribe(listener) {
|
|
416
466
|
this.listeners.add(listener);
|
|
@@ -421,6 +471,39 @@ var EntitlementCache = class {
|
|
|
421
471
|
this.listeners.delete(listener);
|
|
422
472
|
};
|
|
423
473
|
}
|
|
474
|
+
// ----- Durable persistence -----
|
|
475
|
+
/**
|
|
476
|
+
* Load last-known-good from device storage. Runs once in the
|
|
477
|
+
* constructor, synchronously, so isEntitled() is correct from boot.
|
|
478
|
+
* Any corrupt / unparseable blob degrades silently to an empty cache —
|
|
479
|
+
* boot must never throw.
|
|
480
|
+
*/
|
|
481
|
+
hydrate() {
|
|
482
|
+
if (!this.storage) return;
|
|
483
|
+
try {
|
|
484
|
+
const raw = this.storage.getItem(this.storageKey);
|
|
485
|
+
if (!raw) return;
|
|
486
|
+
const parsed = JSON.parse(raw);
|
|
487
|
+
if (parsed && parsed.v === 1 && Array.isArray(parsed.entitlements)) {
|
|
488
|
+
this.all = parsed.entitlements;
|
|
489
|
+
this.lastUpdated = typeof parsed.lastUpdated === "number" ? parsed.lastUpdated : 0;
|
|
490
|
+
}
|
|
491
|
+
} catch {
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
/** Write last-known-good to device storage. Best-effort. */
|
|
495
|
+
persist() {
|
|
496
|
+
if (!this.storage) return;
|
|
497
|
+
try {
|
|
498
|
+
const blob = {
|
|
499
|
+
v: 1,
|
|
500
|
+
entitlements: this.all,
|
|
501
|
+
lastUpdated: this.lastUpdated
|
|
502
|
+
};
|
|
503
|
+
this.storage.setItem(this.storageKey, JSON.stringify(blob));
|
|
504
|
+
} catch {
|
|
505
|
+
}
|
|
506
|
+
}
|
|
424
507
|
notify() {
|
|
425
508
|
if (this.listeners.size === 0) return;
|
|
426
509
|
const snapshot = this.all.slice();
|
|
@@ -1217,11 +1300,50 @@ function isInsidePasswordField(el) {
|
|
|
1217
1300
|
return false;
|
|
1218
1301
|
}
|
|
1219
1302
|
function extractText(el) {
|
|
1303
|
+
const clean = (s) => s.replace(/\s+/g, " ").trim();
|
|
1304
|
+
const explicit = el.getAttribute("data-cd-track") || el.getAttribute("data-track") || el.getAttribute("data-testid");
|
|
1305
|
+
if (explicit) {
|
|
1306
|
+
const t = clean(explicit);
|
|
1307
|
+
if (t) return t;
|
|
1308
|
+
}
|
|
1220
1309
|
const aria = el.getAttribute("aria-label");
|
|
1221
|
-
if (aria)
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1310
|
+
if (aria) {
|
|
1311
|
+
const t = clean(aria);
|
|
1312
|
+
if (t) return t;
|
|
1313
|
+
}
|
|
1314
|
+
const labelledBy = el.getAttribute("aria-labelledby");
|
|
1315
|
+
if (labelledBy && typeof el.ownerDocument?.getElementById === "function") {
|
|
1316
|
+
const parts = [];
|
|
1317
|
+
for (const id of labelledBy.split(/\s+/)) {
|
|
1318
|
+
const ref = el.ownerDocument.getElementById(id);
|
|
1319
|
+
const t = ref?.textContent ? clean(ref.textContent) : "";
|
|
1320
|
+
if (t) parts.push(t);
|
|
1321
|
+
}
|
|
1322
|
+
if (parts.length > 0) return parts.join(" ");
|
|
1323
|
+
}
|
|
1324
|
+
if (el instanceof HTMLInputElement && el.value) {
|
|
1325
|
+
const t = clean(el.value);
|
|
1326
|
+
if (t) return t;
|
|
1327
|
+
}
|
|
1328
|
+
const text = clean(el.textContent || "");
|
|
1329
|
+
if (text) return text;
|
|
1330
|
+
const title = el.getAttribute("title");
|
|
1331
|
+
if (title) {
|
|
1332
|
+
const t = clean(title);
|
|
1333
|
+
if (t) return t;
|
|
1334
|
+
}
|
|
1335
|
+
const img = el.querySelector("img[alt]");
|
|
1336
|
+
if (img) {
|
|
1337
|
+
const alt = img.getAttribute("alt") ?? "";
|
|
1338
|
+
const t = clean(alt);
|
|
1339
|
+
if (t) return t;
|
|
1340
|
+
}
|
|
1341
|
+
const svgTitle = el.querySelector("svg title");
|
|
1342
|
+
if (svgTitle?.textContent) {
|
|
1343
|
+
const t = clean(svgTitle.textContent);
|
|
1344
|
+
if (t) return t;
|
|
1345
|
+
}
|
|
1346
|
+
return "";
|
|
1225
1347
|
}
|
|
1226
1348
|
function trimText(s, cap) {
|
|
1227
1349
|
if (s.length <= cap) return s;
|
|
@@ -1926,13 +2048,22 @@ function isInAppFrame(filename) {
|
|
|
1926
2048
|
if (/\/crossdeck\.umd\.min\.js$/.test(filename)) return false;
|
|
1927
2049
|
return true;
|
|
1928
2050
|
}
|
|
1929
|
-
function fingerprintError(message, frames) {
|
|
2051
|
+
function fingerprintError(message, frames, location) {
|
|
1930
2052
|
const inAppFrames = frames.filter((f) => f.in_app).slice(0, 3);
|
|
1931
|
-
const
|
|
2053
|
+
const parts = [
|
|
1932
2054
|
(message || "").slice(0, 200),
|
|
1933
2055
|
...inAppFrames.map((f) => `${f.function}@${f.filename}:${f.lineno}`)
|
|
1934
|
-
]
|
|
1935
|
-
|
|
2056
|
+
];
|
|
2057
|
+
if (inAppFrames.length === 0 && location) {
|
|
2058
|
+
const loc = [
|
|
2059
|
+
location.errorType ?? "",
|
|
2060
|
+
location.filename ?? "",
|
|
2061
|
+
location.lineno ?? "",
|
|
2062
|
+
location.colno ?? ""
|
|
2063
|
+
].join(":");
|
|
2064
|
+
if (loc !== ":::") parts.push(loc);
|
|
2065
|
+
}
|
|
2066
|
+
return djb2Hex(parts.join("|"));
|
|
1936
2067
|
}
|
|
1937
2068
|
function djb2Hex(input) {
|
|
1938
2069
|
let h = 5381;
|
|
@@ -1954,11 +2085,17 @@ var DEFAULT_ERROR_CAPTURE = {
|
|
|
1954
2085
|
// Classic browser noise. These aren't application bugs.
|
|
1955
2086
|
"ResizeObserver loop limit exceeded",
|
|
1956
2087
|
"ResizeObserver loop completed with undelivered notifications",
|
|
1957
|
-
"Non-Error promise rejection captured"
|
|
1958
|
-
//
|
|
1959
|
-
//
|
|
1960
|
-
"
|
|
1961
|
-
"
|
|
2088
|
+
"Non-Error promise rejection captured"
|
|
2089
|
+
// NOTE: We deliberately do NOT drop cross-origin "Script error."
|
|
2090
|
+
// events here. They used to be silently filtered as noise, but
|
|
2091
|
+
// silent drops are the opposite of "developer sleeps well at
|
|
2092
|
+
// night". We now capture them with a clear label and the
|
|
2093
|
+
// `cross_origin` tag so the dashboard surfaces them as a
|
|
2094
|
+
// distinct, actionable category (the fix is always the same —
|
|
2095
|
+
// add `crossorigin="anonymous"` to the script tag + CORS
|
|
2096
|
+
// headers on the script's origin). Apps that genuinely want
|
|
2097
|
+
// them muted can re-add "Script error" to ignoreErrors via
|
|
2098
|
+
// init config.
|
|
1962
2099
|
],
|
|
1963
2100
|
allowUrls: [],
|
|
1964
2101
|
denyUrls: [
|
|
@@ -2198,61 +2335,84 @@ var ErrorTracker = class {
|
|
|
2198
2335
|
// ============================================================
|
|
2199
2336
|
buildFromErrorEvent(event) {
|
|
2200
2337
|
const err = event.error;
|
|
2201
|
-
const
|
|
2338
|
+
const filename = event.filename || null;
|
|
2339
|
+
const lineno = typeof event.lineno === "number" && event.lineno > 0 ? event.lineno : null;
|
|
2340
|
+
const colno = typeof event.colno === "number" && event.colno > 0 ? event.colno : null;
|
|
2341
|
+
const isCrossOriginStripped = err == null && !filename && lineno == null && (event.message === "Script error." || event.message === "Script error" || !event.message);
|
|
2342
|
+
if (isCrossOriginStripped) {
|
|
2343
|
+
const message2 = "Cross-origin script error (browser hid details \u2014 script needs crossorigin attribute + CORS headers)";
|
|
2344
|
+
return {
|
|
2345
|
+
timestamp: Date.now(),
|
|
2346
|
+
kind: "error.unhandled",
|
|
2347
|
+
level: "error",
|
|
2348
|
+
message: message2,
|
|
2349
|
+
errorType: "ScriptError",
|
|
2350
|
+
frames: [],
|
|
2351
|
+
rawStack: null,
|
|
2352
|
+
filename: null,
|
|
2353
|
+
lineno: null,
|
|
2354
|
+
colno: null,
|
|
2355
|
+
// No location to fingerprint by — all of these will share one
|
|
2356
|
+
// group, which is correct: developer fixes them all with the
|
|
2357
|
+
// same CORS change.
|
|
2358
|
+
fingerprint: fingerprintError(message2, []),
|
|
2359
|
+
breadcrumbs: this.opts.breadcrumbs.snapshot(),
|
|
2360
|
+
context: this.opts.getContext(),
|
|
2361
|
+
tags: { ...this.opts.getTags(), cross_origin: "true" }
|
|
2362
|
+
};
|
|
2363
|
+
}
|
|
2364
|
+
const payload = coerceErrorPayload(err);
|
|
2365
|
+
const message = (payload.message || event.message || "Unknown error").slice(0, 1024);
|
|
2202
2366
|
const stack = err instanceof Error ? err.stack ?? null : null;
|
|
2203
2367
|
const frames = parseStack(stack);
|
|
2368
|
+
const errorType = payload.errorType ?? null;
|
|
2369
|
+
const context = payload.extras ? { ...this.opts.getContext(), __error_extras: payload.extras } : this.opts.getContext();
|
|
2204
2370
|
return {
|
|
2205
2371
|
timestamp: Date.now(),
|
|
2206
2372
|
kind: "error.unhandled",
|
|
2207
2373
|
level: "error",
|
|
2208
|
-
message
|
|
2209
|
-
errorType
|
|
2374
|
+
message,
|
|
2375
|
+
errorType,
|
|
2210
2376
|
frames,
|
|
2211
2377
|
rawStack: stack,
|
|
2212
|
-
filename
|
|
2213
|
-
lineno
|
|
2214
|
-
colno
|
|
2215
|
-
|
|
2378
|
+
filename,
|
|
2379
|
+
lineno,
|
|
2380
|
+
colno,
|
|
2381
|
+
// Location fallback ensures distinct call sites stay separate
|
|
2382
|
+
// even when the message is generic ("Unknown error",
|
|
2383
|
+
// "[object Object]") and there are no parseable frames.
|
|
2384
|
+
fingerprint: fingerprintError(message, frames, {
|
|
2385
|
+
filename,
|
|
2386
|
+
lineno,
|
|
2387
|
+
colno,
|
|
2388
|
+
errorType
|
|
2389
|
+
}),
|
|
2216
2390
|
breadcrumbs: this.opts.breadcrumbs.snapshot(),
|
|
2217
|
-
context
|
|
2391
|
+
context,
|
|
2218
2392
|
tags: this.opts.getTags()
|
|
2219
2393
|
};
|
|
2220
2394
|
}
|
|
2221
2395
|
buildFromUnknown(err, kind, level) {
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
message: String(err.message).slice(0, 1024),
|
|
2229
|
-
errorType: err.name,
|
|
2230
|
-
frames,
|
|
2231
|
-
rawStack: err.stack ?? null,
|
|
2232
|
-
filename: null,
|
|
2233
|
-
lineno: null,
|
|
2234
|
-
colno: null,
|
|
2235
|
-
fingerprint: fingerprintError(err.message, frames),
|
|
2236
|
-
breadcrumbs: this.opts.breadcrumbs.snapshot(),
|
|
2237
|
-
context: this.opts.getContext(),
|
|
2238
|
-
tags: this.opts.getTags()
|
|
2239
|
-
};
|
|
2240
|
-
}
|
|
2241
|
-
const message = safeStringify2(err).slice(0, 1024);
|
|
2396
|
+
const payload = coerceErrorPayload(err);
|
|
2397
|
+
const message = (payload.message || "Unknown error").slice(0, 1024);
|
|
2398
|
+
const stack = err instanceof Error ? err.stack ?? null : null;
|
|
2399
|
+
const frames = parseStack(stack);
|
|
2400
|
+
const errorType = payload.errorType ?? null;
|
|
2401
|
+
const context = payload.extras ? { ...this.opts.getContext(), __error_extras: payload.extras } : this.opts.getContext();
|
|
2242
2402
|
return {
|
|
2243
2403
|
timestamp: Date.now(),
|
|
2244
2404
|
kind,
|
|
2245
2405
|
level,
|
|
2246
2406
|
message,
|
|
2247
|
-
errorType
|
|
2248
|
-
frames
|
|
2249
|
-
rawStack:
|
|
2407
|
+
errorType,
|
|
2408
|
+
frames,
|
|
2409
|
+
rawStack: stack,
|
|
2250
2410
|
filename: null,
|
|
2251
2411
|
lineno: null,
|
|
2252
2412
|
colno: null,
|
|
2253
|
-
fingerprint: fingerprintError(message,
|
|
2413
|
+
fingerprint: fingerprintError(message, frames, { errorType }),
|
|
2254
2414
|
breadcrumbs: this.opts.breadcrumbs.snapshot(),
|
|
2255
|
-
context
|
|
2415
|
+
context,
|
|
2256
2416
|
tags: this.opts.getTags()
|
|
2257
2417
|
};
|
|
2258
2418
|
}
|
|
@@ -2270,7 +2430,10 @@ var ErrorTracker = class {
|
|
|
2270
2430
|
filename: info.url,
|
|
2271
2431
|
lineno: null,
|
|
2272
2432
|
colno: null,
|
|
2273
|
-
fingerprint: fingerprintError(`HTTP ${info.status} ${info.method}`, []
|
|
2433
|
+
fingerprint: fingerprintError(`HTTP ${info.status} ${info.method}`, [], {
|
|
2434
|
+
filename: info.url,
|
|
2435
|
+
errorType: "HTTPError"
|
|
2436
|
+
}),
|
|
2274
2437
|
breadcrumbs: this.opts.breadcrumbs.snapshot(),
|
|
2275
2438
|
context: this.opts.getContext(),
|
|
2276
2439
|
tags: this.opts.getTags(),
|
|
@@ -2349,16 +2512,131 @@ var ErrorTracker = class {
|
|
|
2349
2512
|
return true;
|
|
2350
2513
|
}
|
|
2351
2514
|
};
|
|
2352
|
-
function
|
|
2353
|
-
if (v
|
|
2354
|
-
if (
|
|
2355
|
-
if (typeof v === "
|
|
2515
|
+
function coerceErrorPayload(v) {
|
|
2516
|
+
if (v === null) return { message: "(thrown: null)", errorType: null, extras: null };
|
|
2517
|
+
if (v === void 0) return { message: "(thrown: undefined)", errorType: null, extras: null };
|
|
2518
|
+
if (typeof v === "string") {
|
|
2519
|
+
return { message: v, errorType: null, extras: null };
|
|
2520
|
+
}
|
|
2521
|
+
if (typeof v === "number" || typeof v === "boolean" || typeof v === "bigint") {
|
|
2522
|
+
return { message: String(v), errorType: typeof v, extras: null };
|
|
2523
|
+
}
|
|
2524
|
+
if (typeof v === "symbol") {
|
|
2525
|
+
return { message: v.toString(), errorType: "symbol", extras: null };
|
|
2526
|
+
}
|
|
2527
|
+
if (typeof v === "function") {
|
|
2528
|
+
return { message: `(thrown function: ${v.name || "anonymous"})`, errorType: "function", extras: null };
|
|
2529
|
+
}
|
|
2530
|
+
if (v instanceof Error) {
|
|
2531
|
+
const errorType = v.name || v.constructor?.name || "Error";
|
|
2532
|
+
const message = typeof v.message === "string" && v.message.length > 0 ? v.message : safeToString(v) || errorType;
|
|
2533
|
+
const extras = {};
|
|
2534
|
+
const causeChain = collectCauseChain(v);
|
|
2535
|
+
if (causeChain.length > 0) extras.cause = causeChain;
|
|
2536
|
+
for (const key of ["code", "status", "statusCode", "errno", "response", "data", "detail", "details"]) {
|
|
2537
|
+
const val = v[key];
|
|
2538
|
+
if (val !== void 0 && typeof val !== "function") {
|
|
2539
|
+
extras[key] = safeClone(val);
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
for (const key of Object.keys(v)) {
|
|
2543
|
+
if (key === "message" || key === "stack" || key === "name" || key === "cause") continue;
|
|
2544
|
+
if (key in extras) continue;
|
|
2545
|
+
const val = v[key];
|
|
2546
|
+
if (typeof val === "function") continue;
|
|
2547
|
+
extras[key] = safeClone(val);
|
|
2548
|
+
}
|
|
2549
|
+
return {
|
|
2550
|
+
message,
|
|
2551
|
+
errorType,
|
|
2552
|
+
extras: Object.keys(extras).length > 0 ? extras : null
|
|
2553
|
+
};
|
|
2554
|
+
}
|
|
2555
|
+
if (typeof Response !== "undefined" && v instanceof Response) {
|
|
2556
|
+
return {
|
|
2557
|
+
message: `HTTP ${v.status} ${v.statusText || ""}${v.url ? ` ${v.url}` : ""}`.trim(),
|
|
2558
|
+
errorType: "Response",
|
|
2559
|
+
extras: { status: v.status, statusText: v.statusText, url: v.url, type: v.type }
|
|
2560
|
+
};
|
|
2561
|
+
}
|
|
2562
|
+
if (typeof v === "object") {
|
|
2563
|
+
const obj = v;
|
|
2564
|
+
const ctorName = obj.constructor && typeof obj.constructor === "function" && obj.constructor.name || null;
|
|
2565
|
+
const ownMessage = typeof obj.message === "string" && obj.message ? obj.message : null;
|
|
2566
|
+
const ownName = typeof obj.name === "string" && obj.name ? obj.name : null;
|
|
2567
|
+
let jsonForm = null;
|
|
2568
|
+
try {
|
|
2569
|
+
const serialised = JSON.stringify(obj);
|
|
2570
|
+
jsonForm = serialised === "{}" ? null : serialised;
|
|
2571
|
+
} catch {
|
|
2572
|
+
jsonForm = null;
|
|
2573
|
+
}
|
|
2574
|
+
const fallbackString = safeToString(obj);
|
|
2575
|
+
const message = ownMessage ?? jsonForm ?? (fallbackString && fallbackString !== "[object Object]" ? fallbackString : null) ?? (ctorName ? `(thrown ${ctorName} with no message)` : "(thrown object with no message)");
|
|
2576
|
+
const errorType = ownName ?? ctorName ?? null;
|
|
2577
|
+
const extras = {};
|
|
2578
|
+
let count = 0;
|
|
2579
|
+
for (const key of Object.keys(obj)) {
|
|
2580
|
+
if (count >= 20) break;
|
|
2581
|
+
if (key === "message" || key === "name") continue;
|
|
2582
|
+
const val = obj[key];
|
|
2583
|
+
if (typeof val === "function") continue;
|
|
2584
|
+
extras[key] = safeClone(val);
|
|
2585
|
+
count++;
|
|
2586
|
+
}
|
|
2587
|
+
return {
|
|
2588
|
+
message,
|
|
2589
|
+
errorType,
|
|
2590
|
+
extras: Object.keys(extras).length > 0 ? extras : null
|
|
2591
|
+
};
|
|
2592
|
+
}
|
|
2593
|
+
return { message: safeToString(v) || "(unstringifiable thrown value)", errorType: null, extras: null };
|
|
2594
|
+
}
|
|
2595
|
+
function collectCauseChain(err) {
|
|
2596
|
+
const out = [];
|
|
2597
|
+
let cur = err.cause;
|
|
2598
|
+
let depth = 0;
|
|
2599
|
+
while (cur != null && depth < 5) {
|
|
2600
|
+
if (cur instanceof Error) {
|
|
2601
|
+
out.push({ name: cur.name || "Error", message: cur.message || "" });
|
|
2602
|
+
cur = cur.cause;
|
|
2603
|
+
} else {
|
|
2604
|
+
out.push({ name: "non-Error", message: safeToString(cur) });
|
|
2605
|
+
cur = null;
|
|
2606
|
+
}
|
|
2607
|
+
depth++;
|
|
2608
|
+
}
|
|
2609
|
+
return out;
|
|
2610
|
+
}
|
|
2611
|
+
function safeToString(v) {
|
|
2356
2612
|
try {
|
|
2357
|
-
|
|
2613
|
+
const s = Object.prototype.toString.call(v);
|
|
2614
|
+
if (s !== "[object Object]") return s;
|
|
2615
|
+
const own = v?.toString;
|
|
2616
|
+
if (typeof own === "function" && own !== Object.prototype.toString) {
|
|
2617
|
+
const r = own.call(v);
|
|
2618
|
+
if (typeof r === "string") return r;
|
|
2619
|
+
}
|
|
2620
|
+
return s;
|
|
2358
2621
|
} catch {
|
|
2359
|
-
return
|
|
2622
|
+
return "(throwing toString)";
|
|
2360
2623
|
}
|
|
2361
2624
|
}
|
|
2625
|
+
function safeClone(v) {
|
|
2626
|
+
if (v == null) return v;
|
|
2627
|
+
const t = typeof v;
|
|
2628
|
+
if (t === "string" || t === "number" || t === "boolean") return v;
|
|
2629
|
+
if (t === "bigint") return String(v);
|
|
2630
|
+
try {
|
|
2631
|
+
const s = JSON.stringify(v);
|
|
2632
|
+
return s === void 0 ? safeToString(v) : JSON.parse(s);
|
|
2633
|
+
} catch {
|
|
2634
|
+
return safeToString(v);
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
function safeStringify2(v) {
|
|
2638
|
+
return coerceErrorPayload(v).message;
|
|
2639
|
+
}
|
|
2362
2640
|
|
|
2363
2641
|
// src/crossdeck.ts
|
|
2364
2642
|
var CrossdeckClient = class {
|
|
@@ -2448,7 +2726,10 @@ var CrossdeckClient = class {
|
|
|
2448
2726
|
typeof globalThis.document !== "undefined";
|
|
2449
2727
|
const cookieStore = useCookieRedundancy ? new CookieStorage() : void 0;
|
|
2450
2728
|
const identity = new IdentityStore(effectiveStorage, opts.storagePrefix, cookieStore);
|
|
2451
|
-
const entitlements = new EntitlementCache(
|
|
2729
|
+
const entitlements = new EntitlementCache(
|
|
2730
|
+
effectiveStorage,
|
|
2731
|
+
opts.storagePrefix + "entitlements"
|
|
2732
|
+
);
|
|
2452
2733
|
const persistentEvents = persistIdentity ? new PersistentEventStore({ storage: effectiveStorage, prefix: opts.storagePrefix }) : null;
|
|
2453
2734
|
if (persistentEvents) {
|
|
2454
2735
|
debug.emit(
|
|
@@ -2628,6 +2909,10 @@ var CrossdeckClient = class {
|
|
|
2628
2909
|
const result = await s.http.request("POST", "/identity/alias", {
|
|
2629
2910
|
body
|
|
2630
2911
|
});
|
|
2912
|
+
const priorCdcust = s.identity.crossdeckCustomerId;
|
|
2913
|
+
if (priorCdcust && result.crossdeckCustomerId && priorCdcust !== result.crossdeckCustomerId) {
|
|
2914
|
+
s.entitlements.clear();
|
|
2915
|
+
}
|
|
2631
2916
|
s.identity.setCrossdeckCustomerId(result.crossdeckCustomerId);
|
|
2632
2917
|
s.developerUserId = userId;
|
|
2633
2918
|
return result;
|
|
@@ -2862,11 +3147,17 @@ var CrossdeckClient = class {
|
|
|
2862
3147
|
async getEntitlements() {
|
|
2863
3148
|
const s = this.requireStarted();
|
|
2864
3149
|
const query = this.identityQueryParams();
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
3150
|
+
let result;
|
|
3151
|
+
try {
|
|
3152
|
+
result = await s.http.request(
|
|
3153
|
+
"GET",
|
|
3154
|
+
"/entitlements",
|
|
3155
|
+
{ query }
|
|
3156
|
+
);
|
|
3157
|
+
} catch (err) {
|
|
3158
|
+
s.entitlements.markRefreshFailed();
|
|
3159
|
+
throw err;
|
|
3160
|
+
}
|
|
2870
3161
|
if (result.crossdeckCustomerId) {
|
|
2871
3162
|
s.identity.setCrossdeckCustomerId(result.crossdeckCustomerId);
|
|
2872
3163
|
}
|
|
@@ -2874,8 +3165,12 @@ var CrossdeckClient = class {
|
|
|
2874
3165
|
return result.data;
|
|
2875
3166
|
}
|
|
2876
3167
|
/**
|
|
2877
|
-
* Synchronous read from the local cache
|
|
2878
|
-
*
|
|
3168
|
+
* Synchronous read from the durable local cache — answers from
|
|
3169
|
+
* last-known-good. The cache hydrates from device storage on boot and
|
|
3170
|
+
* survives a Crossdeck outage, so a returning paying customer reads
|
|
3171
|
+
* true even before the session's first network round-trip. Returns
|
|
3172
|
+
* false only for a genuinely new install that has never completed a
|
|
3173
|
+
* getEntitlements(), or for an entitlement past its own validUntil.
|
|
2879
3174
|
*/
|
|
2880
3175
|
isEntitled(key) {
|
|
2881
3176
|
const s = this.requireStarted();
|
|
@@ -3151,7 +3446,7 @@ var CrossdeckClient = class {
|
|
|
3151
3446
|
sdkVersion: null,
|
|
3152
3447
|
baseUrl: null,
|
|
3153
3448
|
clock: { lastServerTime: null, lastClientTime: null, skewMs: null },
|
|
3154
|
-
entitlements: { count: 0, lastUpdated: 0, listenerErrors: 0 },
|
|
3449
|
+
entitlements: { count: 0, lastUpdated: 0, stale: false, listenerErrors: 0 },
|
|
3155
3450
|
events: {
|
|
3156
3451
|
buffered: 0,
|
|
3157
3452
|
dropped: 0,
|
|
@@ -3180,6 +3475,7 @@ var CrossdeckClient = class {
|
|
|
3180
3475
|
entitlements: {
|
|
3181
3476
|
count: s.entitlements.list().length,
|
|
3182
3477
|
lastUpdated: s.entitlements.freshness,
|
|
3478
|
+
stale: s.entitlements.isStale,
|
|
3183
3479
|
listenerErrors: s.entitlements.listenerErrors
|
|
3184
3480
|
},
|
|
3185
3481
|
events: s.events.getStats()
|