@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/react.cjs
CHANGED
|
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/react.ts
|
|
21
21
|
var react_exports = {};
|
|
22
22
|
__export(react_exports, {
|
|
23
|
+
CrossdeckProvider: () => CrossdeckProvider,
|
|
23
24
|
useEntitlement: () => useEntitlement,
|
|
24
25
|
useEntitlements: () => useEntitlements
|
|
25
26
|
});
|
|
@@ -93,7 +94,7 @@ function typeMapForStatus(status) {
|
|
|
93
94
|
|
|
94
95
|
// src/http.ts
|
|
95
96
|
var SDK_NAME = "@cross-deck/web";
|
|
96
|
-
var SDK_VERSION = "1.
|
|
97
|
+
var SDK_VERSION = "1.1.0";
|
|
97
98
|
var DEFAULT_BASE_URL = "https://api.cross-deck.com/v1";
|
|
98
99
|
var DEFAULT_TIMEOUT_MS = 15e3;
|
|
99
100
|
var HttpClient = class {
|
|
@@ -341,70 +342,120 @@ function randomChars(count) {
|
|
|
341
342
|
}
|
|
342
343
|
|
|
343
344
|
// src/entitlement-cache.ts
|
|
345
|
+
var DEFAULT_STALE_AFTER_MS = 24 * 60 * 60 * 1e3;
|
|
344
346
|
var EntitlementCache = class {
|
|
345
|
-
|
|
346
|
-
|
|
347
|
+
/**
|
|
348
|
+
* @param storage Device storage adapter. When omitted (tests) or
|
|
349
|
+
* a MemoryStorage (strict-consent / no-persistence
|
|
350
|
+
* mode) the cache is session-only — durability is
|
|
351
|
+
* simply absent, never wrong.
|
|
352
|
+
* @param storageKey Full key the persisted blob lives under.
|
|
353
|
+
* @param staleAfterMs Age past which last-known-good is flagged stale
|
|
354
|
+
* even without a failed refresh. Default 24h.
|
|
355
|
+
*/
|
|
356
|
+
constructor(storage, storageKey = "crossdeck:entitlements", staleAfterMs = DEFAULT_STALE_AFTER_MS) {
|
|
347
357
|
this.all = [];
|
|
348
358
|
this.lastUpdated = 0;
|
|
359
|
+
this.lastRefreshFailedAt = 0;
|
|
349
360
|
this.listeners = /* @__PURE__ */ new Set();
|
|
350
361
|
this.listenerErrorCount = 0;
|
|
362
|
+
this.storage = storage;
|
|
363
|
+
this.storageKey = storageKey;
|
|
364
|
+
this.staleAfterMs = staleAfterMs;
|
|
365
|
+
this.hydrate();
|
|
351
366
|
}
|
|
352
|
-
/**
|
|
367
|
+
/**
|
|
368
|
+
* Sync read — true iff the entitlement is currently granting access.
|
|
369
|
+
*
|
|
370
|
+
* Served from last-known-good: a stale cache (server unreachable since
|
|
371
|
+
* the last successful fetch) still answers true for a still-valid
|
|
372
|
+
* entitlement. The ONLY thing that turns it false is the entitlement's
|
|
373
|
+
* own expiry (validUntil) — never overall cache staleness.
|
|
374
|
+
*/
|
|
353
375
|
isEntitled(key) {
|
|
354
|
-
|
|
376
|
+
const nowSec = Date.now() / 1e3;
|
|
377
|
+
return this.all.some(
|
|
378
|
+
(e) => e.key === key && e.isActive && (e.validUntil == null || e.validUntil > nowSec)
|
|
379
|
+
);
|
|
355
380
|
}
|
|
356
381
|
/** Full snapshot for callers that need source / validUntil details. */
|
|
357
382
|
list() {
|
|
358
383
|
return this.all.slice();
|
|
359
384
|
}
|
|
360
|
-
/** When the cache was last refreshed. 0 means "never". */
|
|
385
|
+
/** When the cache was last refreshed from the server. 0 means "never". */
|
|
361
386
|
get freshness() {
|
|
362
387
|
return this.lastUpdated;
|
|
363
388
|
}
|
|
364
389
|
/**
|
|
365
|
-
*
|
|
366
|
-
*
|
|
367
|
-
*
|
|
368
|
-
*
|
|
390
|
+
* Whether the cache is knowingly serving older-than-trustworthy data.
|
|
391
|
+
*
|
|
392
|
+
* True when the most recent refresh ATTEMPT failed (Crossdeck
|
|
393
|
+
* unreachable since the last success — the outage case, distinct from
|
|
394
|
+
* a benign idle tab that simply hasn't re-fetched), OR when
|
|
395
|
+
* last-known-good has aged past staleAfterMs.
|
|
396
|
+
*
|
|
397
|
+
* isStale never changes what isEntitled() returns — the cache still
|
|
398
|
+
* serves last-known-good. It exists so the staleness is observable
|
|
399
|
+
* (diagnostics()) instead of an unbounded silent window where a
|
|
400
|
+
* revoked customer holds access with nobody able to see it.
|
|
369
401
|
*/
|
|
402
|
+
get isStale() {
|
|
403
|
+
if (this.lastRefreshFailedAt > this.lastUpdated) return true;
|
|
404
|
+
return this.lastUpdated > 0 && Date.now() - this.lastUpdated > this.staleAfterMs;
|
|
405
|
+
}
|
|
406
|
+
/** Epoch ms of the last failed refresh attempt. 0 if none since the last success. */
|
|
407
|
+
get refreshFailedAt() {
|
|
408
|
+
return this.lastRefreshFailedAt;
|
|
409
|
+
}
|
|
370
410
|
get listenerErrors() {
|
|
371
411
|
return this.listenerErrorCount;
|
|
372
412
|
}
|
|
373
413
|
/**
|
|
374
|
-
*
|
|
375
|
-
*
|
|
376
|
-
*
|
|
414
|
+
* Record that a refresh attempt failed (Crossdeck unreachable / a
|
|
415
|
+
* transient error). The SDK's getEntitlements() calls this in its
|
|
416
|
+
* catch path. It does NOT touch the cached entitlements — last-known-
|
|
417
|
+
* good keeps serving — it only flips isStale so the staleness shows
|
|
418
|
+
* up in diagnostics() rather than being silent.
|
|
419
|
+
*/
|
|
420
|
+
markRefreshFailed() {
|
|
421
|
+
this.lastRefreshFailedAt = Date.now();
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Replace the cache with a fresh server response and persist it to
|
|
425
|
+
* device storage so it survives a reload / app restart.
|
|
377
426
|
*
|
|
378
|
-
*
|
|
427
|
+
* Called ONLY after a successful server read — a failed fetch throws
|
|
428
|
+
* before it reaches here, so last-known-good is preserved through an
|
|
429
|
+
* outage. A success also clears the stale flag.
|
|
379
430
|
*/
|
|
380
431
|
setFromList(entitlements) {
|
|
381
432
|
this.all = entitlements.slice();
|
|
382
|
-
this.active = new Set(entitlements.filter((e) => e.isActive).map((e) => e.key));
|
|
383
433
|
this.lastUpdated = Date.now();
|
|
434
|
+
this.lastRefreshFailedAt = 0;
|
|
435
|
+
this.persist();
|
|
384
436
|
this.notify();
|
|
385
437
|
}
|
|
386
438
|
/**
|
|
387
|
-
* Wipe — used on reset() (logout)
|
|
388
|
-
*
|
|
389
|
-
*
|
|
390
|
-
* Fires listeners so React/SwiftUI/etc bindings re-render to the
|
|
391
|
-
* logged-out state immediately.
|
|
439
|
+
* Wipe — used on reset() (logout) and on an identity switch. Clears
|
|
440
|
+
* BOTH memory and durable storage so a prior user's entitlements can
|
|
441
|
+
* never leak to the next person on this device.
|
|
392
442
|
*/
|
|
393
443
|
clear() {
|
|
394
|
-
this.active.clear();
|
|
395
444
|
this.all = [];
|
|
396
445
|
this.lastUpdated = 0;
|
|
446
|
+
this.lastRefreshFailedAt = 0;
|
|
447
|
+
if (this.storage) {
|
|
448
|
+
try {
|
|
449
|
+
this.storage.removeItem(this.storageKey);
|
|
450
|
+
} catch {
|
|
451
|
+
}
|
|
452
|
+
}
|
|
397
453
|
this.notify();
|
|
398
454
|
}
|
|
399
455
|
/**
|
|
400
|
-
* Subscribe to cache mutations. Returns an unsubscribe
|
|
401
|
-
*
|
|
402
|
-
*
|
|
403
|
-
* current snapshot. Throwing inside a listener is non-fatal — the
|
|
404
|
-
* error is swallowed and subsequent listeners still run.
|
|
405
|
-
*
|
|
406
|
-
* Used by `@cross-deck/web/react`'s `useEntitlement` hook to
|
|
407
|
-
* trigger re-renders when entitlements change.
|
|
456
|
+
* Subscribe to cache mutations. Returns an idempotent unsubscribe fn.
|
|
457
|
+
* The listener fires AFTER setFromList() or clear() with the current
|
|
458
|
+
* snapshot. Used by `@cross-deck/web/react`'s `useEntitlement` hook.
|
|
408
459
|
*/
|
|
409
460
|
subscribe(listener) {
|
|
410
461
|
this.listeners.add(listener);
|
|
@@ -415,6 +466,39 @@ var EntitlementCache = class {
|
|
|
415
466
|
this.listeners.delete(listener);
|
|
416
467
|
};
|
|
417
468
|
}
|
|
469
|
+
// ----- Durable persistence -----
|
|
470
|
+
/**
|
|
471
|
+
* Load last-known-good from device storage. Runs once in the
|
|
472
|
+
* constructor, synchronously, so isEntitled() is correct from boot.
|
|
473
|
+
* Any corrupt / unparseable blob degrades silently to an empty cache —
|
|
474
|
+
* boot must never throw.
|
|
475
|
+
*/
|
|
476
|
+
hydrate() {
|
|
477
|
+
if (!this.storage) return;
|
|
478
|
+
try {
|
|
479
|
+
const raw = this.storage.getItem(this.storageKey);
|
|
480
|
+
if (!raw) return;
|
|
481
|
+
const parsed = JSON.parse(raw);
|
|
482
|
+
if (parsed && parsed.v === 1 && Array.isArray(parsed.entitlements)) {
|
|
483
|
+
this.all = parsed.entitlements;
|
|
484
|
+
this.lastUpdated = typeof parsed.lastUpdated === "number" ? parsed.lastUpdated : 0;
|
|
485
|
+
}
|
|
486
|
+
} catch {
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
/** Write last-known-good to device storage. Best-effort. */
|
|
490
|
+
persist() {
|
|
491
|
+
if (!this.storage) return;
|
|
492
|
+
try {
|
|
493
|
+
const blob = {
|
|
494
|
+
v: 1,
|
|
495
|
+
entitlements: this.all,
|
|
496
|
+
lastUpdated: this.lastUpdated
|
|
497
|
+
};
|
|
498
|
+
this.storage.setItem(this.storageKey, JSON.stringify(blob));
|
|
499
|
+
} catch {
|
|
500
|
+
}
|
|
501
|
+
}
|
|
418
502
|
notify() {
|
|
419
503
|
if (this.listeners.size === 0) return;
|
|
420
504
|
const snapshot = this.all.slice();
|
|
@@ -1211,11 +1295,50 @@ function isInsidePasswordField(el) {
|
|
|
1211
1295
|
return false;
|
|
1212
1296
|
}
|
|
1213
1297
|
function extractText(el) {
|
|
1298
|
+
const clean = (s) => s.replace(/\s+/g, " ").trim();
|
|
1299
|
+
const explicit = el.getAttribute("data-cd-track") || el.getAttribute("data-track") || el.getAttribute("data-testid");
|
|
1300
|
+
if (explicit) {
|
|
1301
|
+
const t = clean(explicit);
|
|
1302
|
+
if (t) return t;
|
|
1303
|
+
}
|
|
1214
1304
|
const aria = el.getAttribute("aria-label");
|
|
1215
|
-
if (aria)
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1305
|
+
if (aria) {
|
|
1306
|
+
const t = clean(aria);
|
|
1307
|
+
if (t) return t;
|
|
1308
|
+
}
|
|
1309
|
+
const labelledBy = el.getAttribute("aria-labelledby");
|
|
1310
|
+
if (labelledBy && typeof el.ownerDocument?.getElementById === "function") {
|
|
1311
|
+
const parts = [];
|
|
1312
|
+
for (const id of labelledBy.split(/\s+/)) {
|
|
1313
|
+
const ref = el.ownerDocument.getElementById(id);
|
|
1314
|
+
const t = ref?.textContent ? clean(ref.textContent) : "";
|
|
1315
|
+
if (t) parts.push(t);
|
|
1316
|
+
}
|
|
1317
|
+
if (parts.length > 0) return parts.join(" ");
|
|
1318
|
+
}
|
|
1319
|
+
if (el instanceof HTMLInputElement && el.value) {
|
|
1320
|
+
const t = clean(el.value);
|
|
1321
|
+
if (t) return t;
|
|
1322
|
+
}
|
|
1323
|
+
const text = clean(el.textContent || "");
|
|
1324
|
+
if (text) return text;
|
|
1325
|
+
const title = el.getAttribute("title");
|
|
1326
|
+
if (title) {
|
|
1327
|
+
const t = clean(title);
|
|
1328
|
+
if (t) return t;
|
|
1329
|
+
}
|
|
1330
|
+
const img = el.querySelector("img[alt]");
|
|
1331
|
+
if (img) {
|
|
1332
|
+
const alt = img.getAttribute("alt") ?? "";
|
|
1333
|
+
const t = clean(alt);
|
|
1334
|
+
if (t) return t;
|
|
1335
|
+
}
|
|
1336
|
+
const svgTitle = el.querySelector("svg title");
|
|
1337
|
+
if (svgTitle?.textContent) {
|
|
1338
|
+
const t = clean(svgTitle.textContent);
|
|
1339
|
+
if (t) return t;
|
|
1340
|
+
}
|
|
1341
|
+
return "";
|
|
1219
1342
|
}
|
|
1220
1343
|
function trimText(s, cap) {
|
|
1221
1344
|
if (s.length <= cap) return s;
|
|
@@ -1920,13 +2043,22 @@ function isInAppFrame(filename) {
|
|
|
1920
2043
|
if (/\/crossdeck\.umd\.min\.js$/.test(filename)) return false;
|
|
1921
2044
|
return true;
|
|
1922
2045
|
}
|
|
1923
|
-
function fingerprintError(message, frames) {
|
|
2046
|
+
function fingerprintError(message, frames, location) {
|
|
1924
2047
|
const inAppFrames = frames.filter((f) => f.in_app).slice(0, 3);
|
|
1925
|
-
const
|
|
2048
|
+
const parts = [
|
|
1926
2049
|
(message || "").slice(0, 200),
|
|
1927
2050
|
...inAppFrames.map((f) => `${f.function}@${f.filename}:${f.lineno}`)
|
|
1928
|
-
]
|
|
1929
|
-
|
|
2051
|
+
];
|
|
2052
|
+
if (inAppFrames.length === 0 && location) {
|
|
2053
|
+
const loc = [
|
|
2054
|
+
location.errorType ?? "",
|
|
2055
|
+
location.filename ?? "",
|
|
2056
|
+
location.lineno ?? "",
|
|
2057
|
+
location.colno ?? ""
|
|
2058
|
+
].join(":");
|
|
2059
|
+
if (loc !== ":::") parts.push(loc);
|
|
2060
|
+
}
|
|
2061
|
+
return djb2Hex(parts.join("|"));
|
|
1930
2062
|
}
|
|
1931
2063
|
function djb2Hex(input) {
|
|
1932
2064
|
let h = 5381;
|
|
@@ -1948,11 +2080,17 @@ var DEFAULT_ERROR_CAPTURE = {
|
|
|
1948
2080
|
// Classic browser noise. These aren't application bugs.
|
|
1949
2081
|
"ResizeObserver loop limit exceeded",
|
|
1950
2082
|
"ResizeObserver loop completed with undelivered notifications",
|
|
1951
|
-
"Non-Error promise rejection captured"
|
|
1952
|
-
//
|
|
1953
|
-
//
|
|
1954
|
-
"
|
|
1955
|
-
"
|
|
2083
|
+
"Non-Error promise rejection captured"
|
|
2084
|
+
// NOTE: We deliberately do NOT drop cross-origin "Script error."
|
|
2085
|
+
// events here. They used to be silently filtered as noise, but
|
|
2086
|
+
// silent drops are the opposite of "developer sleeps well at
|
|
2087
|
+
// night". We now capture them with a clear label and the
|
|
2088
|
+
// `cross_origin` tag so the dashboard surfaces them as a
|
|
2089
|
+
// distinct, actionable category (the fix is always the same —
|
|
2090
|
+
// add `crossorigin="anonymous"` to the script tag + CORS
|
|
2091
|
+
// headers on the script's origin). Apps that genuinely want
|
|
2092
|
+
// them muted can re-add "Script error" to ignoreErrors via
|
|
2093
|
+
// init config.
|
|
1956
2094
|
],
|
|
1957
2095
|
allowUrls: [],
|
|
1958
2096
|
denyUrls: [
|
|
@@ -2192,61 +2330,84 @@ var ErrorTracker = class {
|
|
|
2192
2330
|
// ============================================================
|
|
2193
2331
|
buildFromErrorEvent(event) {
|
|
2194
2332
|
const err = event.error;
|
|
2195
|
-
const
|
|
2333
|
+
const filename = event.filename || null;
|
|
2334
|
+
const lineno = typeof event.lineno === "number" && event.lineno > 0 ? event.lineno : null;
|
|
2335
|
+
const colno = typeof event.colno === "number" && event.colno > 0 ? event.colno : null;
|
|
2336
|
+
const isCrossOriginStripped = err == null && !filename && lineno == null && (event.message === "Script error." || event.message === "Script error" || !event.message);
|
|
2337
|
+
if (isCrossOriginStripped) {
|
|
2338
|
+
const message2 = "Cross-origin script error (browser hid details \u2014 script needs crossorigin attribute + CORS headers)";
|
|
2339
|
+
return {
|
|
2340
|
+
timestamp: Date.now(),
|
|
2341
|
+
kind: "error.unhandled",
|
|
2342
|
+
level: "error",
|
|
2343
|
+
message: message2,
|
|
2344
|
+
errorType: "ScriptError",
|
|
2345
|
+
frames: [],
|
|
2346
|
+
rawStack: null,
|
|
2347
|
+
filename: null,
|
|
2348
|
+
lineno: null,
|
|
2349
|
+
colno: null,
|
|
2350
|
+
// No location to fingerprint by — all of these will share one
|
|
2351
|
+
// group, which is correct: developer fixes them all with the
|
|
2352
|
+
// same CORS change.
|
|
2353
|
+
fingerprint: fingerprintError(message2, []),
|
|
2354
|
+
breadcrumbs: this.opts.breadcrumbs.snapshot(),
|
|
2355
|
+
context: this.opts.getContext(),
|
|
2356
|
+
tags: { ...this.opts.getTags(), cross_origin: "true" }
|
|
2357
|
+
};
|
|
2358
|
+
}
|
|
2359
|
+
const payload = coerceErrorPayload(err);
|
|
2360
|
+
const message = (payload.message || event.message || "Unknown error").slice(0, 1024);
|
|
2196
2361
|
const stack = err instanceof Error ? err.stack ?? null : null;
|
|
2197
2362
|
const frames = parseStack(stack);
|
|
2363
|
+
const errorType = payload.errorType ?? null;
|
|
2364
|
+
const context = payload.extras ? { ...this.opts.getContext(), __error_extras: payload.extras } : this.opts.getContext();
|
|
2198
2365
|
return {
|
|
2199
2366
|
timestamp: Date.now(),
|
|
2200
2367
|
kind: "error.unhandled",
|
|
2201
2368
|
level: "error",
|
|
2202
|
-
message
|
|
2203
|
-
errorType
|
|
2369
|
+
message,
|
|
2370
|
+
errorType,
|
|
2204
2371
|
frames,
|
|
2205
2372
|
rawStack: stack,
|
|
2206
|
-
filename
|
|
2207
|
-
lineno
|
|
2208
|
-
colno
|
|
2209
|
-
|
|
2373
|
+
filename,
|
|
2374
|
+
lineno,
|
|
2375
|
+
colno,
|
|
2376
|
+
// Location fallback ensures distinct call sites stay separate
|
|
2377
|
+
// even when the message is generic ("Unknown error",
|
|
2378
|
+
// "[object Object]") and there are no parseable frames.
|
|
2379
|
+
fingerprint: fingerprintError(message, frames, {
|
|
2380
|
+
filename,
|
|
2381
|
+
lineno,
|
|
2382
|
+
colno,
|
|
2383
|
+
errorType
|
|
2384
|
+
}),
|
|
2210
2385
|
breadcrumbs: this.opts.breadcrumbs.snapshot(),
|
|
2211
|
-
context
|
|
2386
|
+
context,
|
|
2212
2387
|
tags: this.opts.getTags()
|
|
2213
2388
|
};
|
|
2214
2389
|
}
|
|
2215
2390
|
buildFromUnknown(err, kind, level) {
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
message: String(err.message).slice(0, 1024),
|
|
2223
|
-
errorType: err.name,
|
|
2224
|
-
frames,
|
|
2225
|
-
rawStack: err.stack ?? null,
|
|
2226
|
-
filename: null,
|
|
2227
|
-
lineno: null,
|
|
2228
|
-
colno: null,
|
|
2229
|
-
fingerprint: fingerprintError(err.message, frames),
|
|
2230
|
-
breadcrumbs: this.opts.breadcrumbs.snapshot(),
|
|
2231
|
-
context: this.opts.getContext(),
|
|
2232
|
-
tags: this.opts.getTags()
|
|
2233
|
-
};
|
|
2234
|
-
}
|
|
2235
|
-
const message = safeStringify2(err).slice(0, 1024);
|
|
2391
|
+
const payload = coerceErrorPayload(err);
|
|
2392
|
+
const message = (payload.message || "Unknown error").slice(0, 1024);
|
|
2393
|
+
const stack = err instanceof Error ? err.stack ?? null : null;
|
|
2394
|
+
const frames = parseStack(stack);
|
|
2395
|
+
const errorType = payload.errorType ?? null;
|
|
2396
|
+
const context = payload.extras ? { ...this.opts.getContext(), __error_extras: payload.extras } : this.opts.getContext();
|
|
2236
2397
|
return {
|
|
2237
2398
|
timestamp: Date.now(),
|
|
2238
2399
|
kind,
|
|
2239
2400
|
level,
|
|
2240
2401
|
message,
|
|
2241
|
-
errorType
|
|
2242
|
-
frames
|
|
2243
|
-
rawStack:
|
|
2402
|
+
errorType,
|
|
2403
|
+
frames,
|
|
2404
|
+
rawStack: stack,
|
|
2244
2405
|
filename: null,
|
|
2245
2406
|
lineno: null,
|
|
2246
2407
|
colno: null,
|
|
2247
|
-
fingerprint: fingerprintError(message,
|
|
2408
|
+
fingerprint: fingerprintError(message, frames, { errorType }),
|
|
2248
2409
|
breadcrumbs: this.opts.breadcrumbs.snapshot(),
|
|
2249
|
-
context
|
|
2410
|
+
context,
|
|
2250
2411
|
tags: this.opts.getTags()
|
|
2251
2412
|
};
|
|
2252
2413
|
}
|
|
@@ -2264,7 +2425,10 @@ var ErrorTracker = class {
|
|
|
2264
2425
|
filename: info.url,
|
|
2265
2426
|
lineno: null,
|
|
2266
2427
|
colno: null,
|
|
2267
|
-
fingerprint: fingerprintError(`HTTP ${info.status} ${info.method}`, []
|
|
2428
|
+
fingerprint: fingerprintError(`HTTP ${info.status} ${info.method}`, [], {
|
|
2429
|
+
filename: info.url,
|
|
2430
|
+
errorType: "HTTPError"
|
|
2431
|
+
}),
|
|
2268
2432
|
breadcrumbs: this.opts.breadcrumbs.snapshot(),
|
|
2269
2433
|
context: this.opts.getContext(),
|
|
2270
2434
|
tags: this.opts.getTags(),
|
|
@@ -2343,16 +2507,131 @@ var ErrorTracker = class {
|
|
|
2343
2507
|
return true;
|
|
2344
2508
|
}
|
|
2345
2509
|
};
|
|
2346
|
-
function
|
|
2347
|
-
if (v
|
|
2348
|
-
if (
|
|
2349
|
-
if (typeof v === "
|
|
2510
|
+
function coerceErrorPayload(v) {
|
|
2511
|
+
if (v === null) return { message: "(thrown: null)", errorType: null, extras: null };
|
|
2512
|
+
if (v === void 0) return { message: "(thrown: undefined)", errorType: null, extras: null };
|
|
2513
|
+
if (typeof v === "string") {
|
|
2514
|
+
return { message: v, errorType: null, extras: null };
|
|
2515
|
+
}
|
|
2516
|
+
if (typeof v === "number" || typeof v === "boolean" || typeof v === "bigint") {
|
|
2517
|
+
return { message: String(v), errorType: typeof v, extras: null };
|
|
2518
|
+
}
|
|
2519
|
+
if (typeof v === "symbol") {
|
|
2520
|
+
return { message: v.toString(), errorType: "symbol", extras: null };
|
|
2521
|
+
}
|
|
2522
|
+
if (typeof v === "function") {
|
|
2523
|
+
return { message: `(thrown function: ${v.name || "anonymous"})`, errorType: "function", extras: null };
|
|
2524
|
+
}
|
|
2525
|
+
if (v instanceof Error) {
|
|
2526
|
+
const errorType = v.name || v.constructor?.name || "Error";
|
|
2527
|
+
const message = typeof v.message === "string" && v.message.length > 0 ? v.message : safeToString(v) || errorType;
|
|
2528
|
+
const extras = {};
|
|
2529
|
+
const causeChain = collectCauseChain(v);
|
|
2530
|
+
if (causeChain.length > 0) extras.cause = causeChain;
|
|
2531
|
+
for (const key of ["code", "status", "statusCode", "errno", "response", "data", "detail", "details"]) {
|
|
2532
|
+
const val = v[key];
|
|
2533
|
+
if (val !== void 0 && typeof val !== "function") {
|
|
2534
|
+
extras[key] = safeClone(val);
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
for (const key of Object.keys(v)) {
|
|
2538
|
+
if (key === "message" || key === "stack" || key === "name" || key === "cause") continue;
|
|
2539
|
+
if (key in extras) continue;
|
|
2540
|
+
const val = v[key];
|
|
2541
|
+
if (typeof val === "function") continue;
|
|
2542
|
+
extras[key] = safeClone(val);
|
|
2543
|
+
}
|
|
2544
|
+
return {
|
|
2545
|
+
message,
|
|
2546
|
+
errorType,
|
|
2547
|
+
extras: Object.keys(extras).length > 0 ? extras : null
|
|
2548
|
+
};
|
|
2549
|
+
}
|
|
2550
|
+
if (typeof Response !== "undefined" && v instanceof Response) {
|
|
2551
|
+
return {
|
|
2552
|
+
message: `HTTP ${v.status} ${v.statusText || ""}${v.url ? ` ${v.url}` : ""}`.trim(),
|
|
2553
|
+
errorType: "Response",
|
|
2554
|
+
extras: { status: v.status, statusText: v.statusText, url: v.url, type: v.type }
|
|
2555
|
+
};
|
|
2556
|
+
}
|
|
2557
|
+
if (typeof v === "object") {
|
|
2558
|
+
const obj = v;
|
|
2559
|
+
const ctorName = obj.constructor && typeof obj.constructor === "function" && obj.constructor.name || null;
|
|
2560
|
+
const ownMessage = typeof obj.message === "string" && obj.message ? obj.message : null;
|
|
2561
|
+
const ownName = typeof obj.name === "string" && obj.name ? obj.name : null;
|
|
2562
|
+
let jsonForm = null;
|
|
2563
|
+
try {
|
|
2564
|
+
const serialised = JSON.stringify(obj);
|
|
2565
|
+
jsonForm = serialised === "{}" ? null : serialised;
|
|
2566
|
+
} catch {
|
|
2567
|
+
jsonForm = null;
|
|
2568
|
+
}
|
|
2569
|
+
const fallbackString = safeToString(obj);
|
|
2570
|
+
const message = ownMessage ?? jsonForm ?? (fallbackString && fallbackString !== "[object Object]" ? fallbackString : null) ?? (ctorName ? `(thrown ${ctorName} with no message)` : "(thrown object with no message)");
|
|
2571
|
+
const errorType = ownName ?? ctorName ?? null;
|
|
2572
|
+
const extras = {};
|
|
2573
|
+
let count = 0;
|
|
2574
|
+
for (const key of Object.keys(obj)) {
|
|
2575
|
+
if (count >= 20) break;
|
|
2576
|
+
if (key === "message" || key === "name") continue;
|
|
2577
|
+
const val = obj[key];
|
|
2578
|
+
if (typeof val === "function") continue;
|
|
2579
|
+
extras[key] = safeClone(val);
|
|
2580
|
+
count++;
|
|
2581
|
+
}
|
|
2582
|
+
return {
|
|
2583
|
+
message,
|
|
2584
|
+
errorType,
|
|
2585
|
+
extras: Object.keys(extras).length > 0 ? extras : null
|
|
2586
|
+
};
|
|
2587
|
+
}
|
|
2588
|
+
return { message: safeToString(v) || "(unstringifiable thrown value)", errorType: null, extras: null };
|
|
2589
|
+
}
|
|
2590
|
+
function collectCauseChain(err) {
|
|
2591
|
+
const out = [];
|
|
2592
|
+
let cur = err.cause;
|
|
2593
|
+
let depth = 0;
|
|
2594
|
+
while (cur != null && depth < 5) {
|
|
2595
|
+
if (cur instanceof Error) {
|
|
2596
|
+
out.push({ name: cur.name || "Error", message: cur.message || "" });
|
|
2597
|
+
cur = cur.cause;
|
|
2598
|
+
} else {
|
|
2599
|
+
out.push({ name: "non-Error", message: safeToString(cur) });
|
|
2600
|
+
cur = null;
|
|
2601
|
+
}
|
|
2602
|
+
depth++;
|
|
2603
|
+
}
|
|
2604
|
+
return out;
|
|
2605
|
+
}
|
|
2606
|
+
function safeToString(v) {
|
|
2607
|
+
try {
|
|
2608
|
+
const s = Object.prototype.toString.call(v);
|
|
2609
|
+
if (s !== "[object Object]") return s;
|
|
2610
|
+
const own = v?.toString;
|
|
2611
|
+
if (typeof own === "function" && own !== Object.prototype.toString) {
|
|
2612
|
+
const r = own.call(v);
|
|
2613
|
+
if (typeof r === "string") return r;
|
|
2614
|
+
}
|
|
2615
|
+
return s;
|
|
2616
|
+
} catch {
|
|
2617
|
+
return "(throwing toString)";
|
|
2618
|
+
}
|
|
2619
|
+
}
|
|
2620
|
+
function safeClone(v) {
|
|
2621
|
+
if (v == null) return v;
|
|
2622
|
+
const t = typeof v;
|
|
2623
|
+
if (t === "string" || t === "number" || t === "boolean") return v;
|
|
2624
|
+
if (t === "bigint") return String(v);
|
|
2350
2625
|
try {
|
|
2351
|
-
|
|
2626
|
+
const s = JSON.stringify(v);
|
|
2627
|
+
return s === void 0 ? safeToString(v) : JSON.parse(s);
|
|
2352
2628
|
} catch {
|
|
2353
|
-
return
|
|
2629
|
+
return safeToString(v);
|
|
2354
2630
|
}
|
|
2355
2631
|
}
|
|
2632
|
+
function safeStringify2(v) {
|
|
2633
|
+
return coerceErrorPayload(v).message;
|
|
2634
|
+
}
|
|
2356
2635
|
|
|
2357
2636
|
// src/crossdeck.ts
|
|
2358
2637
|
var CrossdeckClient = class {
|
|
@@ -2442,7 +2721,10 @@ var CrossdeckClient = class {
|
|
|
2442
2721
|
typeof globalThis.document !== "undefined";
|
|
2443
2722
|
const cookieStore = useCookieRedundancy ? new CookieStorage() : void 0;
|
|
2444
2723
|
const identity = new IdentityStore(effectiveStorage, opts.storagePrefix, cookieStore);
|
|
2445
|
-
const entitlements = new EntitlementCache(
|
|
2724
|
+
const entitlements = new EntitlementCache(
|
|
2725
|
+
effectiveStorage,
|
|
2726
|
+
opts.storagePrefix + "entitlements"
|
|
2727
|
+
);
|
|
2446
2728
|
const persistentEvents = persistIdentity ? new PersistentEventStore({ storage: effectiveStorage, prefix: opts.storagePrefix }) : null;
|
|
2447
2729
|
if (persistentEvents) {
|
|
2448
2730
|
debug.emit(
|
|
@@ -2622,6 +2904,10 @@ var CrossdeckClient = class {
|
|
|
2622
2904
|
const result = await s.http.request("POST", "/identity/alias", {
|
|
2623
2905
|
body
|
|
2624
2906
|
});
|
|
2907
|
+
const priorCdcust = s.identity.crossdeckCustomerId;
|
|
2908
|
+
if (priorCdcust && result.crossdeckCustomerId && priorCdcust !== result.crossdeckCustomerId) {
|
|
2909
|
+
s.entitlements.clear();
|
|
2910
|
+
}
|
|
2625
2911
|
s.identity.setCrossdeckCustomerId(result.crossdeckCustomerId);
|
|
2626
2912
|
s.developerUserId = userId;
|
|
2627
2913
|
return result;
|
|
@@ -2856,11 +3142,17 @@ var CrossdeckClient = class {
|
|
|
2856
3142
|
async getEntitlements() {
|
|
2857
3143
|
const s = this.requireStarted();
|
|
2858
3144
|
const query = this.identityQueryParams();
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
3145
|
+
let result;
|
|
3146
|
+
try {
|
|
3147
|
+
result = await s.http.request(
|
|
3148
|
+
"GET",
|
|
3149
|
+
"/entitlements",
|
|
3150
|
+
{ query }
|
|
3151
|
+
);
|
|
3152
|
+
} catch (err) {
|
|
3153
|
+
s.entitlements.markRefreshFailed();
|
|
3154
|
+
throw err;
|
|
3155
|
+
}
|
|
2864
3156
|
if (result.crossdeckCustomerId) {
|
|
2865
3157
|
s.identity.setCrossdeckCustomerId(result.crossdeckCustomerId);
|
|
2866
3158
|
}
|
|
@@ -2868,8 +3160,12 @@ var CrossdeckClient = class {
|
|
|
2868
3160
|
return result.data;
|
|
2869
3161
|
}
|
|
2870
3162
|
/**
|
|
2871
|
-
* Synchronous read from the local cache
|
|
2872
|
-
*
|
|
3163
|
+
* Synchronous read from the durable local cache — answers from
|
|
3164
|
+
* last-known-good. The cache hydrates from device storage on boot and
|
|
3165
|
+
* survives a Crossdeck outage, so a returning paying customer reads
|
|
3166
|
+
* true even before the session's first network round-trip. Returns
|
|
3167
|
+
* false only for a genuinely new install that has never completed a
|
|
3168
|
+
* getEntitlements(), or for an entitlement past its own validUntil.
|
|
2873
3169
|
*/
|
|
2874
3170
|
isEntitled(key) {
|
|
2875
3171
|
const s = this.requireStarted();
|
|
@@ -3145,7 +3441,7 @@ var CrossdeckClient = class {
|
|
|
3145
3441
|
sdkVersion: null,
|
|
3146
3442
|
baseUrl: null,
|
|
3147
3443
|
clock: { lastServerTime: null, lastClientTime: null, skewMs: null },
|
|
3148
|
-
entitlements: { count: 0, lastUpdated: 0, listenerErrors: 0 },
|
|
3444
|
+
entitlements: { count: 0, lastUpdated: 0, stale: false, listenerErrors: 0 },
|
|
3149
3445
|
events: {
|
|
3150
3446
|
buffered: 0,
|
|
3151
3447
|
dropped: 0,
|
|
@@ -3174,6 +3470,7 @@ var CrossdeckClient = class {
|
|
|
3174
3470
|
entitlements: {
|
|
3175
3471
|
count: s.entitlements.list().length,
|
|
3176
3472
|
lastUpdated: s.entitlements.freshness,
|
|
3473
|
+
stale: s.entitlements.isStale,
|
|
3177
3474
|
listenerErrors: s.entitlements.listenerErrors
|
|
3178
3475
|
},
|
|
3179
3476
|
events: s.events.getStats()
|
|
@@ -3294,6 +3591,39 @@ function installUnloadFlush(onUnload) {
|
|
|
3294
3591
|
}
|
|
3295
3592
|
|
|
3296
3593
|
// src/react.ts
|
|
3594
|
+
var _moduleInitDone = false;
|
|
3595
|
+
function CrossdeckProvider(props) {
|
|
3596
|
+
const { userId, children, ...initOptions } = props;
|
|
3597
|
+
const lastUserIdRef = (0, import_react.useRef)(void 0);
|
|
3598
|
+
(0, import_react.useEffect)(() => {
|
|
3599
|
+
if (_moduleInitDone) return;
|
|
3600
|
+
try {
|
|
3601
|
+
Crossdeck.init(initOptions);
|
|
3602
|
+
_moduleInitDone = true;
|
|
3603
|
+
} catch (err) {
|
|
3604
|
+
if (typeof console !== "undefined") {
|
|
3605
|
+
console.error("[CrossdeckProvider] init failed:", err);
|
|
3606
|
+
}
|
|
3607
|
+
}
|
|
3608
|
+
}, []);
|
|
3609
|
+
(0, import_react.useEffect)(() => {
|
|
3610
|
+
if (!_moduleInitDone) return;
|
|
3611
|
+
if (lastUserIdRef.current === userId) return;
|
|
3612
|
+
lastUserIdRef.current = userId;
|
|
3613
|
+
try {
|
|
3614
|
+
if (userId) {
|
|
3615
|
+
void Crossdeck.identify(userId);
|
|
3616
|
+
} else {
|
|
3617
|
+
Crossdeck.reset();
|
|
3618
|
+
}
|
|
3619
|
+
} catch (err) {
|
|
3620
|
+
if (typeof console !== "undefined") {
|
|
3621
|
+
console.error("[CrossdeckProvider] identity sync failed:", err);
|
|
3622
|
+
}
|
|
3623
|
+
}
|
|
3624
|
+
}, [userId]);
|
|
3625
|
+
return children;
|
|
3626
|
+
}
|
|
3297
3627
|
function useEntitlement(key) {
|
|
3298
3628
|
const [isEntitled, setIsEntitled] = (0, import_react.useState)(() => safeIsEntitled(key));
|
|
3299
3629
|
(0, import_react.useEffect)(() => {
|
|
@@ -3344,6 +3674,7 @@ function safeListKeys() {
|
|
|
3344
3674
|
}
|
|
3345
3675
|
// Annotate the CommonJS export names for ESM import in node:
|
|
3346
3676
|
0 && (module.exports = {
|
|
3677
|
+
CrossdeckProvider,
|
|
3347
3678
|
useEntitlement,
|
|
3348
3679
|
useEntitlements
|
|
3349
3680
|
});
|