@auxilium/datalynk-client 1.5.2 → 1.6.1

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/dist/index.cjs CHANGED
@@ -1536,13 +1536,81 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
1536
1536
  `;
1537
1537
  });
1538
1538
  let LoginPrompt = _LoginPrompt;
1539
+ const KEY = "datalynk-offline-login";
1540
+ const ITERATIONS = 31e4;
1541
+ async function hash(password, salt) {
1542
+ const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(password), "PBKDF2", false, ["deriveBits"]);
1543
+ return Array.from(new Uint8Array(await crypto.subtle.deriveBits(
1544
+ { name: "PBKDF2", salt: new Uint8Array(salt), iterations: ITERATIONS, hash: "SHA-256" },
1545
+ key,
1546
+ 256
1547
+ )));
1548
+ }
1549
+ async function rememberOfflineLogin(realm, login, uid, password) {
1550
+ const salt = crypto.getRandomValues(new Uint8Array(16));
1551
+ const record = { realm, login: login.trim().toLowerCase(), uid, salt: Array.from(salt), hash: await hash(password, salt) };
1552
+ localStorage.setItem(KEY, JSON.stringify(record));
1553
+ }
1554
+ async function verifyOfflineLogin(realm, login, uid, password) {
1555
+ const saved = localStorage.getItem(KEY);
1556
+ if (!saved) return false;
1557
+ let record;
1558
+ try {
1559
+ record = JSON.parse(saved);
1560
+ } catch {
1561
+ return false;
1562
+ }
1563
+ if (record.realm !== realm || record.login !== login.trim().toLowerCase() || record.uid !== uid || !Array.isArray(record.salt) || !Array.isArray(record.hash)) return false;
1564
+ const candidate = await hash(password, new Uint8Array(record.salt));
1565
+ if (candidate.length !== record.hash.length) return false;
1566
+ let difference = 0;
1567
+ for (let i = 0; i < candidate.length; i++) difference |= candidate[i] ^ record.hash[i];
1568
+ return difference === 0;
1569
+ }
1570
+ function hasOfflineLogin(realm, uid) {
1571
+ try {
1572
+ const record = JSON.parse(localStorage.getItem(KEY) || "null");
1573
+ return (record == null ? void 0 : record.realm) === realm && (record == null ? void 0 : record.uid) === uid;
1574
+ } catch {
1575
+ return false;
1576
+ }
1577
+ }
1578
+ function forgetOfflineLogin() {
1579
+ localStorage.removeItem(KEY);
1580
+ }
1539
1581
  class Auth {
1540
1582
  constructor(api) {
1541
1583
  __publicField(this, "onlinePrompt");
1584
+ __publicField(this, "offlineUnlocked", false);
1585
+ __publicField(this, "wasOffline", false);
1542
1586
  /** Current user as an observable */
1543
1587
  __publicField(this, "user$", new BehaviorSubject(void 0));
1544
1588
  var _a;
1545
1589
  this.api = api;
1590
+ this.api.status$.subscribe(async (status) => {
1591
+ if (status === "offline" || status === "unavailable") this.wasOffline = true;
1592
+ if (status !== "online" || !this.wasOffline) return;
1593
+ this.wasOffline = false;
1594
+ const wasUnlocked = this.offlineUnlocked;
1595
+ const previousUser = this.user;
1596
+ this.offlineUnlocked = false;
1597
+ this.user = void 0;
1598
+ try {
1599
+ const user = await this.current();
1600
+ const identity2 = this.api.jwtPayload;
1601
+ if (!user || user.guest || !identity2 || !hasOfflineLogin(identity2.realm, identity2.uid))
1602
+ forgetOfflineLogin();
1603
+ this.user = user;
1604
+ } catch (error) {
1605
+ if (this.api.status === "unauthorized") forgetOfflineLogin();
1606
+ else {
1607
+ this.wasOffline = true;
1608
+ this.offlineUnlocked = wasUnlocked;
1609
+ this.user = previousUser;
1610
+ }
1611
+ console.error("Unable to recheck the Datalynk user", error);
1612
+ }
1613
+ });
1546
1614
  this.api.token$.subscribe(async (token) => {
1547
1615
  if (token === void 0) return;
1548
1616
  try {
@@ -1554,8 +1622,9 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
1554
1622
  });
1555
1623
  if ((_a = this.api.options.offline) == null ? void 0 : _a.length)
1556
1624
  this.user$.pipe(filter((u) => u !== void 0)).subscribe((u) => {
1557
- if (u == null) localStorage.removeItem("datalynk-user");
1558
- else localStorage.setItem("datalynk-user", JSON.stringify(u));
1625
+ if (u == null) {
1626
+ if (this.api.online) localStorage.removeItem("datalynk-user");
1627
+ } else localStorage.setItem("datalynk-user", JSON.stringify(u));
1559
1628
  });
1560
1629
  }
1561
1630
  /** Current user */
@@ -1579,8 +1648,9 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
1579
1648
  var _a;
1580
1649
  if (!token) return null;
1581
1650
  else if (token == ((_a = this.user) == null ? void 0 : _a.token)) return this.user;
1582
- else if (!this.api.online && typeof localStorage != "undefined" && localStorage.getItem("datalynk-user"))
1651
+ else if (!this.api.online && this.offlineUnlocked && typeof localStorage != "undefined" && localStorage.getItem("datalynk-user"))
1583
1652
  return JSON.parse(localStorage.getItem("datalynk-user"));
1653
+ else if (!this.api.online) return null;
1584
1654
  return this.api.request([{ "$/auth/current": {} }, { "$/env/me": {} }], { token }).then((resp) => resp[0] || resp[1] ? { ...resp[0], ...resp[1] } : null);
1585
1655
  }
1586
1656
  /**
@@ -1592,7 +1662,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
1592
1662
  * @return {Promise<void>} Login complete
1593
1663
  */
1594
1664
  async handleLogin(spoke, options) {
1595
- var _a, _b, _c;
1665
+ var _a, _b, _c, _d;
1596
1666
  if (this.onlinePrompt) window.removeEventListener("online", this.onlinePrompt);
1597
1667
  if ((_a = this.api.options.offline) == null ? void 0 : _a.length) window.removeEventListener("online", this.onlinePrompt = () => this.handleLogin(spoke, options));
1598
1668
  const urlParams = new URLSearchParams(location.search);
@@ -1601,10 +1671,13 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
1601
1671
  this.api.token = urlToken;
1602
1672
  urlParams.delete("datalynkToken");
1603
1673
  location.href = location.pathname + (urlParams.toString() ? "?" + urlParams.toString() : "") + location.hash;
1604
- } else if (this.api.reauthenticationPending && !this.api.online && ((_b = this.api.jwtPayload) == null ? void 0 : _b.realm) == spoke) {
1674
+ } else if (!this.api.online && ((_b = this.api.options.offline) == null ? void 0 : _b.length) && !this.offlineUnlocked) {
1675
+ await this.loginPrompt(spoke, options).done;
1676
+ return;
1677
+ } else if (this.api.reauthenticationPending && !this.api.online && ((_c = this.api.jwtPayload) == null ? void 0 : _c.realm) == spoke) {
1605
1678
  return;
1606
1679
  } else if (this.api.token && !this.api.expired) {
1607
- if (((_c = this.api.jwtPayload) == null ? void 0 : _c.realm) != spoke) {
1680
+ if (((_d = this.api.jwtPayload) == null ? void 0 : _d.realm) != spoke) {
1608
1681
  this.api.token = null;
1609
1682
  location.reload();
1610
1683
  }
@@ -1664,10 +1737,23 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
1664
1737
  * @param {twoFactor?: string, expire?: string} opts 2FA code & expire date (YYYY-MM-DD)
1665
1738
  * @returns {Promise<User>} User account
1666
1739
  */
1667
- login(spoke, login, password, opts) {
1740
+ async login(spoke, login, password, opts) {
1741
+ var _a, _b, _c;
1742
+ if (((_a = this.api.options.offline) == null ? void 0 : _a.length) && (!this.api.online || typeof navigator !== "undefined" && !navigator.onLine)) {
1743
+ const identity2 = this.api.jwtPayload;
1744
+ const saved = localStorage.getItem("datalynk-user");
1745
+ if (!identity2 || identity2.realm !== spoke || !saved || !await verifyOfflineLogin(spoke, login, identity2.uid, password.trim()))
1746
+ throw new Error("Offline login is not available for this account on this device.");
1747
+ const user = JSON.parse(saved);
1748
+ if (![user.login, user.email].some((value) => (value == null ? void 0 : value.toLowerCase()) === login.trim().toLowerCase()))
1749
+ throw new Error("Offline login is not available for this account on this device.");
1750
+ this.offlineUnlocked = true;
1751
+ this.user = user;
1752
+ return user;
1753
+ }
1668
1754
  const date = /* @__PURE__ */ new Date();
1669
1755
  date.setFullYear(date.getFullYear() + 1);
1670
- return fetch(`${this.api.url}login`, {
1756
+ const resp = await fetch(`${this.api.url}login`, {
1671
1757
  method: "POST",
1672
1758
  headers: { "Content-Type": "application/json" },
1673
1759
  body: JSON.stringify(clean({
@@ -1678,12 +1764,13 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
1678
1764
  expireAt: (opts == null ? void 0 : opts.expire) == null ? `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, "0")}-${date.getDate().toString().padStart(2, "0")}` : opts == null ? void 0 : opts.expire,
1679
1765
  dateFormat: "ISO8601"
1680
1766
  }))
1681
- }).then(async (resp) => {
1682
- const data = await resp.json().catch(() => ({}));
1683
- if (!resp.ok || data["error"]) throw Object.assign(errorFromCode(resp.status, data.error) || {}, data);
1684
- this.api.token = data["token"];
1685
- return data;
1686
1767
  });
1768
+ const data = await resp.json().catch(() => ({}));
1769
+ if (!resp.ok || data["error"]) throw Object.assign(errorFromCode(resp.status, data.error) || {}, data);
1770
+ this.api.token = data["token"];
1771
+ if (((_b = this.api.options.offline) == null ? void 0 : _b.length) && this.api.options.saveSession && ((_c = this.api.jwtPayload) == null ? void 0 : _c.uid) != null)
1772
+ await rememberOfflineLogin(spoke, login, this.api.jwtPayload.uid, password.trim());
1773
+ return data;
1687
1774
  }
1688
1775
  /**
1689
1776
  * Login as guest user
@@ -1715,6 +1802,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
1715
1802
  */
1716
1803
  logout(reload = true) {
1717
1804
  localStorage.removeItem("datalynk-user");
1805
+ forgetOfflineLogin();
1718
1806
  return this.api.request({ "$/auth/logout": {} }).then((resp) => {
1719
1807
  if (reload) location.reload();
1720
1808
  this.api.token = null;
@@ -3106,6 +3194,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
3106
3194
  // [ Callback, Re-subscribe ]
3107
3195
  __publicField(this, "retry");
3108
3196
  __publicField(this, "socket");
3197
+ __publicField(this, "connectGeneration", 0);
3109
3198
  __publicField(this, "open", false);
3110
3199
  this.api = api;
3111
3200
  this.options = options;
@@ -3114,7 +3203,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
3114
3203
  this.options.url = origin.replace("http", "ws").replace(/:\d+/g, "") + `/s/`;
3115
3204
  }
3116
3205
  if (this.options.url !== false)
3117
- api.token$.pipe(filter((u) => u !== void 0), distinctUntilChanged()).subscribe(() => this.connect());
3206
+ api.token$.pipe(filter((u) => u !== void 0), distinctUntilChanged()).subscribe(() => void this.connect());
3118
3207
  }
3119
3208
  /**
3120
3209
  * Add listener for all socket events
@@ -3136,6 +3225,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
3136
3225
  */
3137
3226
  close() {
3138
3227
  var _a;
3228
+ if (this.options.authentication === "ticket") this.connectGeneration++;
3139
3229
  if (this.open) console.debug("Datalynk socket: disconnected");
3140
3230
  this.open = false;
3141
3231
  (_a = this.socket) == null ? void 0 : _a.close();
@@ -3148,6 +3238,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
3148
3238
  * @param {number} timeout Retry to connect every x seconds
3149
3239
  */
3150
3240
  connect(timeout = 30) {
3241
+ if (this.options.authentication === "ticket") return this.connectWithTicket(timeout);
3151
3242
  if (this.options.url === false) return console.warn("Datalynk socket disabled");
3152
3243
  if (this.open) this.close();
3153
3244
  this.retry = setTimeout(() => {
@@ -3177,6 +3268,80 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
3177
3268
  };
3178
3269
  }
3179
3270
  }
3271
+ /**
3272
+ * Ticket-authenticated connection path, used only once a chat-capable host switches
3273
+ * this instance to `authentication: 'ticket'` (see Api.openChat). Entirely separate
3274
+ * from the token path above so existing non-chat consumers are never affected.
3275
+ */
3276
+ async connectWithTicket(timeout = 30) {
3277
+ var _a;
3278
+ if (this.options.url === false) return console.warn("Datalynk socket disabled");
3279
+ const generation = ++this.connectGeneration;
3280
+ if (this.open || this.socket) {
3281
+ this.open = false;
3282
+ (_a = this.socket) == null ? void 0 : _a.close();
3283
+ this.socket = void 0;
3284
+ }
3285
+ if (this.retry) clearTimeout(this.retry);
3286
+ this.retry = setTimeout(() => {
3287
+ if (this.open || generation !== this.connectGeneration) return;
3288
+ void this.connect(timeout);
3289
+ }, timeout * 1e3);
3290
+ if (!this.api.online || !this.api.token) return;
3291
+ let socketUrl = this.normalizeUrl(String(this.options.url || ""));
3292
+ try {
3293
+ const response = await this.api.request({ "$/chat/socketTicket": {} }, { noOptimize: true });
3294
+ if (generation !== this.connectGeneration) return;
3295
+ if (response == null ? void 0 : response.ticket) socketUrl += `${socketUrl.includes("?") ? "&" : "?"}ticket=${encodeURIComponent(response.ticket)}`;
3296
+ else throw new Error("Missing socket ticket");
3297
+ } catch (_) {
3298
+ if (generation !== this.connectGeneration) return;
3299
+ this.scheduleReconnect(generation, timeout);
3300
+ return;
3301
+ }
3302
+ if (generation !== this.connectGeneration) return;
3303
+ this.socket = new WebSocket(socketUrl);
3304
+ this.socket.onopen = () => void 0;
3305
+ this.socket.onclose = () => {
3306
+ this.open = false;
3307
+ this.socket = void 0;
3308
+ this.scheduleReconnect(generation, timeout);
3309
+ };
3310
+ this.socket.onerror = () => {
3311
+ };
3312
+ this.socket.onmessage = (message) => {
3313
+ var _a2;
3314
+ let payload;
3315
+ try {
3316
+ payload = JSON.parse(message.data);
3317
+ } catch {
3318
+ return;
3319
+ }
3320
+ if (payload.connected != void 0) {
3321
+ if (payload.connected) {
3322
+ this.open = true;
3323
+ if (this.retry) clearTimeout(this.retry);
3324
+ this.retry = null;
3325
+ console.debug("Datalynk socket: connected");
3326
+ this.listeners.forEach((l) => l[1]());
3327
+ } else {
3328
+ this.open = false;
3329
+ console.warn(`Datalynk socket failed: ${payload.error || "authentication failed"}`);
3330
+ (_a2 = this.socket) == null ? void 0 : _a2.close();
3331
+ }
3332
+ } else {
3333
+ this.listeners.forEach((l) => l[0](payload));
3334
+ }
3335
+ };
3336
+ }
3337
+ scheduleReconnect(generation, timeout) {
3338
+ if (generation !== this.connectGeneration || this.options.url === false) return;
3339
+ if (this.retry) clearTimeout(this.retry);
3340
+ this.retry = setTimeout(() => {
3341
+ if (generation !== this.connectGeneration) return;
3342
+ void this.connect(timeout);
3343
+ }, timeout * 1e3);
3344
+ }
3180
3345
  /**
3181
3346
  * Send data to socket server
3182
3347
  *
@@ -3203,6 +3368,17 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
3203
3368
  unsubscribe();
3204
3369
  };
3205
3370
  }
3371
+ normalizeUrl(value) {
3372
+ let url = value.replace(/^http:\/\//i, "ws://").replace(/^https:\/\//i, "wss://");
3373
+ try {
3374
+ const parsed = new URL(url);
3375
+ if (parsed.pathname === "/" || parsed.pathname === "") parsed.pathname = "/s/";
3376
+ else if (parsed.pathname === "/s") parsed.pathname = "/s/";
3377
+ url = parsed.toString();
3378
+ } catch {
3379
+ }
3380
+ return url;
3381
+ }
3206
3382
  }
3207
3383
  class Superuser {
3208
3384
  constructor(api) {
@@ -3234,7 +3410,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
3234
3410
  } });
3235
3411
  }
3236
3412
  }
3237
- const version = "1.5.2";
3413
+ const version = "1.6.1";
3238
3414
  class WebRtc {
3239
3415
  constructor(api) {
3240
3416
  __publicField(this, "ice");
@@ -3801,6 +3977,10 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
3801
3977
  __publicField(this, "tokenStorageListener", null);
3802
3978
  /** Pending requests cache */
3803
3979
  __publicField(this, "pending", {});
3980
+ __publicField(this, "chatModulePromise", null);
3981
+ __publicField(this, "chatInstance", null);
3982
+ __publicField(this, "chatLauncherHost", null);
3983
+ __publicField(this, "chatLauncherButton", null);
3804
3984
  /** Helpers */
3805
3985
  /** Authentication */
3806
3986
  __publicField(this, "auth");
@@ -3855,6 +4035,8 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
3855
4035
  offline: [],
3856
4036
  origin: typeof location !== "undefined" ? location.host : "Unknown",
3857
4037
  saveSession: true,
4038
+ manageSession: true,
4039
+ chat: false,
3858
4040
  replayOfflineQueue: true,
3859
4041
  watchTokenExpiry: true,
3860
4042
  serviceWorker: "/service.worker.mjs",
@@ -3886,9 +4068,9 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
3886
4068
  window.addEventListener("storage", this.tokenStorageListener);
3887
4069
  }
3888
4070
  }
3889
- if (this.options.watchTokenExpiry)
4071
+ if (this.options.manageSession !== false && this.options.watchTokenExpiry)
3890
4072
  this.token$.pipe(distinctUntilChanged()).subscribe((token) => this.scheduleTokenExpiry(token));
3891
- this.socket = new Socket(this, { url: options.socket });
4073
+ this.socket = new Socket(this, { url: options.socket, authentication: options.socketAuthentication, realm: options.socketRealm });
3892
4074
  this.gps = new Gps(this, options.gps);
3893
4075
  this.auth = new Auth(this);
3894
4076
  this.files = new Files(this);
@@ -3914,12 +4096,19 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
3914
4096
  if (this.options.serviceWorker && typeof navigator["serviceWorker"] != "undefined") {
3915
4097
  (async () => {
3916
4098
  const init = () => {
3917
- var _a2;
4099
+ var _a2, _b2;
3918
4100
  (_a2 = navigator.serviceWorker.controller) == null ? void 0 : _a2.postMessage({ options: { ...this.options, url: this.url } });
3919
4101
  this.token$.subscribe((token) => {
3920
4102
  var _a3;
3921
4103
  return (_a3 = navigator.serviceWorker.controller) == null ? void 0 : _a3.postMessage({ token });
3922
4104
  });
4105
+ const assets = [
4106
+ location.pathname,
4107
+ ...Array.from(document.querySelectorAll('script[src], link[rel="stylesheet"][href], link[rel="icon"][href], img[src]')).map((element) => element.getAttribute("src") || element.getAttribute("href") || ""),
4108
+ ...performance.getEntriesByType("resource").filter((entry) => ["script", "link", "css", "img", "font"].includes(entry.initiatorType)).map((entry) => entry.name),
4109
+ ...this.options.offlineAssets || []
4110
+ ].filter(Boolean).map((value) => new URL(value, location.href).href);
4111
+ (_b2 = navigator.serviceWorker.controller) == null ? void 0 : _b2.postMessage({ offlineAssets: assets });
3923
4112
  };
3924
4113
  await navigator.serviceWorker.getRegistration(this.options.serviceWorker).then((reg) => reg ?? navigator.serviceWorker.register(this.options.serviceWorker, { scope: "/", type: "module" }));
3925
4114
  navigator.serviceWorker.ready.then(() => {
@@ -3929,6 +4118,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
3929
4118
  })();
3930
4119
  }
3931
4120
  }
4121
+ this.setupChatLauncher();
3932
4122
  }
3933
4123
  /** Is token expired */
3934
4124
  get expired() {
@@ -4000,7 +4190,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
4000
4190
  var _a;
4001
4191
  if (this.recovery && token !== this.recovery.token)
4002
4192
  this.cancelRecovery(errorFromCode(401, "Session changed during API recovery"));
4003
- if (token && this.isTokenExpired(token)) {
4193
+ if (this.options.manageSession !== false && token && this.isTokenExpired(token)) {
4004
4194
  if (this.adoptStoredToken(token)) return;
4005
4195
  if (this.shouldDeferLocalExpiry()) {
4006
4196
  this.deferOfflineExpiry(token);
@@ -4018,6 +4208,138 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
4018
4208
  }
4019
4209
  if (token && ((_a = this.options) == null ? void 0 : _a.replayOfflineQueue) !== false) void this.replayPendingQueue();
4020
4210
  }
4211
+ /** Mount the optional launcher without loading or initializing the chat bundle. */
4212
+ setupChatLauncher() {
4213
+ if (!this.options.chat || typeof document === "undefined") return;
4214
+ const mount = () => {
4215
+ if (this.chatLauncherHost || !document.body || document.querySelector("[data-datalynk-chat-launcher]")) return;
4216
+ const host = document.createElement("div");
4217
+ host.setAttribute("data-datalynk-chat-launcher", "");
4218
+ const root = host.attachShadow({ mode: "open" });
4219
+ root.innerHTML = `
4220
+ <style>
4221
+ :host {
4222
+ all: initial;
4223
+ position: fixed;
4224
+ right: 20px;
4225
+ bottom: 20px;
4226
+ z-index: 2147482999;
4227
+ pointer-events: none;
4228
+ }
4229
+ button {
4230
+ display: grid;
4231
+ place-items: center;
4232
+ width: 52px;
4233
+ height: 52px;
4234
+ padding: 0;
4235
+ border: 1px solid #2f596d;
4236
+ border-radius: 50%;
4237
+ background: #3f6f85;
4238
+ color: #fff;
4239
+ box-shadow: 0 4px 14px rgb(15 42 54 / 24%);
4240
+ cursor: pointer;
4241
+ pointer-events: auto;
4242
+ transition: background-color 120ms ease-out, box-shadow 120ms ease-out, transform 120ms ease-out;
4243
+ }
4244
+ button:hover { background: #315d71; box-shadow: 0 6px 18px rgb(15 42 54 / 30%); }
4245
+ button:active { transform: translateY(1px); }
4246
+ button:focus-visible { outline: 3px solid #f2c94c; outline-offset: 3px; }
4247
+ button:disabled { cursor: wait; opacity: .72; }
4248
+ .status {
4249
+ position: absolute;
4250
+ right: 62px;
4251
+ bottom: 7px;
4252
+ width: max-content;
4253
+ max-width: min(260px, calc(100vw - 94px));
4254
+ padding: 9px 11px;
4255
+ border: 1px solid #c6d3d9;
4256
+ border-radius: 6px;
4257
+ background: #fff;
4258
+ box-shadow: 0 3px 12px rgb(15 42 54 / 16%);
4259
+ color: #263942;
4260
+ font: 600 13px/1.35 "Segoe UI", Arial, sans-serif;
4261
+ pointer-events: none;
4262
+ }
4263
+ .status:empty { display: none; }
4264
+ svg { width: 25px; height: 25px; }
4265
+ @media (max-width: 600px) { :host { right: 12px; bottom: 12px; } }
4266
+ @media (prefers-reduced-motion: reduce) { button { transition: none; } }
4267
+ </style>
4268
+ <button type="button" aria-label="Open Datalynk Chat" title="Chat">
4269
+ <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
4270
+ <path fill="currentColor" d="M4 4h16a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H9l-5 3v-3a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2Zm2.5 8.25a1.25 1.25 0 1 0 0-2.5 1.25 1.25 0 0 0 0 2.5Zm5.5 0a1.25 1.25 0 1 0 0-2.5 1.25 1.25 0 0 0 0 2.5Zm5.5 0a1.25 1.25 0 1 0 0-2.5 1.25 1.25 0 0 0 0 2.5Z"/>
4271
+ </svg>
4272
+ </button>
4273
+ <span class="status" role="status" aria-live="polite"></span>`;
4274
+ const button = root.querySelector("button");
4275
+ button.addEventListener("click", () => void this.openChatFromLauncher());
4276
+ document.body.appendChild(host);
4277
+ this.chatLauncherHost = host;
4278
+ this.chatLauncherButton = button;
4279
+ };
4280
+ if (document.body) mount();
4281
+ else document.addEventListener("DOMContentLoaded", mount, { once: true });
4282
+ }
4283
+ async openChatFromLauncher() {
4284
+ const button = this.chatLauncherButton;
4285
+ if (!button) return;
4286
+ const status = button.getRootNode().querySelector(".status");
4287
+ button.disabled = true;
4288
+ button.setAttribute("aria-busy", "true");
4289
+ status.textContent = "Opening chat…";
4290
+ try {
4291
+ await this.openChat();
4292
+ status.textContent = "";
4293
+ } catch (error) {
4294
+ button.disabled = false;
4295
+ button.removeAttribute("aria-busy");
4296
+ status.textContent = "Chat is unavailable for this session.";
4297
+ console.error("Unable to open Datalynk Chat.", error);
4298
+ }
4299
+ }
4300
+ restoreChatLauncher() {
4301
+ if (!this.chatLauncherHost || !this.chatLauncherButton) return;
4302
+ this.chatLauncherHost.hidden = false;
4303
+ this.chatLauncherButton.disabled = false;
4304
+ this.chatLauncherButton.removeAttribute("aria-busy");
4305
+ this.chatLauncherButton.focus();
4306
+ }
4307
+ /** Lazily load and construct chat. No chat code or requests run before this call. */
4308
+ async chat() {
4309
+ if (this.chatInstance) return this.chatInstance;
4310
+ const client = await this.loadChatModule();
4311
+ return this.chatInstance || (this.chatInstance = new client.Chat(this));
4312
+ }
4313
+ /** Lazily load, authorize, and open the reusable chat window. */
4314
+ async openChat(options = {}) {
4315
+ const client = await this.loadChatModule();
4316
+ const chat = await this.chat();
4317
+ if (!await chat.available()) throw new Error("Chat is disabled or this session is not authorized.");
4318
+ if (this.socket.options.authentication !== "ticket") {
4319
+ this.socket.options.authentication = "ticket";
4320
+ void this.socket.connect();
4321
+ }
4322
+ const onClose = options.onClose;
4323
+ const chatWindow = client.openChat(chat, {
4324
+ ...options,
4325
+ onClose: () => {
4326
+ this.restoreChatLauncher();
4327
+ onClose == null ? void 0 : onClose();
4328
+ }
4329
+ });
4330
+ if (this.chatLauncherHost) this.chatLauncherHost.hidden = true;
4331
+ return chatWindow;
4332
+ }
4333
+ loadChatModule() {
4334
+ if (!this.chatModulePromise) {
4335
+ const moduleUrl = this.options.chatModule || "./chat.mjs";
4336
+ this.chatModulePromise = import(
4337
+ /* @vite-ignore */
4338
+ moduleUrl
4339
+ );
4340
+ }
4341
+ return this.chatModulePromise;
4342
+ }
4021
4343
  async _request(req, options = {}) {
4022
4344
  if (this.recovery) throw errorFromCode(503, "Datalynk is unavailable");
4023
4345
  const retryRequest = deepCopy(req);
@@ -4034,7 +4356,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
4034
4356
  /** Execute exactly one HTTP API attempt. Recovery uses this directly to avoid recursive retry loops. */
4035
4357
  async _requestOnce(req, options = {}, recoveryAttempt = false) {
4036
4358
  const token = options.token || this.token;
4037
- if (token && this.isTokenExpired(token)) {
4359
+ if (this.options.manageSession !== false && token && this.isTokenExpired(token)) {
4038
4360
  if (token === this.token) this.expireToken(token);
4039
4361
  throw errorFromCode(401, "Session token expired");
4040
4362
  }
@@ -4201,7 +4523,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
4201
4523
  }
4202
4524
  return;
4203
4525
  }
4204
- if (this.token && this.isTokenExpired(this.token)) {
4526
+ if (this.options.manageSession !== false && this.token && this.isTokenExpired(this.token)) {
4205
4527
  this.expireToken(this.token);
4206
4528
  return;
4207
4529
  }
@@ -4320,6 +4642,8 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
4320
4642
  }
4321
4643
  async finishUnauthorizedLogout(preservePending = false) {
4322
4644
  var _a;
4645
+ if (this.options.manageSession === false) return;
4646
+ forgetOfflineLogin();
4323
4647
  if (typeof localStorage != "undefined") {
4324
4648
  localStorage.removeItem(this.localStorageKey);
4325
4649
  localStorage.removeItem("datalynk-user");
@@ -4335,13 +4659,13 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
4335
4659
  await this.clearOfflineSessionCache(preservePending);
4336
4660
  if (typeof caches != "undefined") {
4337
4661
  try {
4338
- await Promise.all([caches.delete("datalynk"), caches.delete("api-settings")]);
4662
+ await caches.delete("api-settings");
4339
4663
  } catch (error) {
4340
4664
  console.error("Unable to clear Datalynk service worker cache", error);
4341
4665
  }
4342
4666
  }
4343
4667
  if (typeof navigator != "undefined" && ((_a = navigator.serviceWorker) == null ? void 0 : _a.controller)) {
4344
- navigator.serviceWorker.controller.postMessage({ token: null, clearCache: true });
4668
+ navigator.serviceWorker.controller.postMessage({ token: null });
4345
4669
  }
4346
4670
  if (typeof location != "undefined" && typeof location.reload == "function")
4347
4671
  location.reload();