@auxilium/datalynk-client 1.5.2 → 1.6.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/dist/index.cjs CHANGED
@@ -3106,6 +3106,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
3106
3106
  // [ Callback, Re-subscribe ]
3107
3107
  __publicField(this, "retry");
3108
3108
  __publicField(this, "socket");
3109
+ __publicField(this, "connectGeneration", 0);
3109
3110
  __publicField(this, "open", false);
3110
3111
  this.api = api;
3111
3112
  this.options = options;
@@ -3114,7 +3115,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
3114
3115
  this.options.url = origin.replace("http", "ws").replace(/:\d+/g, "") + `/s/`;
3115
3116
  }
3116
3117
  if (this.options.url !== false)
3117
- api.token$.pipe(filter((u) => u !== void 0), distinctUntilChanged()).subscribe(() => this.connect());
3118
+ api.token$.pipe(filter((u) => u !== void 0), distinctUntilChanged()).subscribe(() => void this.connect());
3118
3119
  }
3119
3120
  /**
3120
3121
  * Add listener for all socket events
@@ -3136,6 +3137,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
3136
3137
  */
3137
3138
  close() {
3138
3139
  var _a;
3140
+ if (this.options.authentication === "ticket") this.connectGeneration++;
3139
3141
  if (this.open) console.debug("Datalynk socket: disconnected");
3140
3142
  this.open = false;
3141
3143
  (_a = this.socket) == null ? void 0 : _a.close();
@@ -3148,6 +3150,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
3148
3150
  * @param {number} timeout Retry to connect every x seconds
3149
3151
  */
3150
3152
  connect(timeout = 30) {
3153
+ if (this.options.authentication === "ticket") return this.connectWithTicket(timeout);
3151
3154
  if (this.options.url === false) return console.warn("Datalynk socket disabled");
3152
3155
  if (this.open) this.close();
3153
3156
  this.retry = setTimeout(() => {
@@ -3177,6 +3180,80 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
3177
3180
  };
3178
3181
  }
3179
3182
  }
3183
+ /**
3184
+ * Ticket-authenticated connection path, used only once a chat-capable host switches
3185
+ * this instance to `authentication: 'ticket'` (see Api.openChat). Entirely separate
3186
+ * from the token path above so existing non-chat consumers are never affected.
3187
+ */
3188
+ async connectWithTicket(timeout = 30) {
3189
+ var _a;
3190
+ if (this.options.url === false) return console.warn("Datalynk socket disabled");
3191
+ const generation = ++this.connectGeneration;
3192
+ if (this.open || this.socket) {
3193
+ this.open = false;
3194
+ (_a = this.socket) == null ? void 0 : _a.close();
3195
+ this.socket = void 0;
3196
+ }
3197
+ if (this.retry) clearTimeout(this.retry);
3198
+ this.retry = setTimeout(() => {
3199
+ if (this.open || generation !== this.connectGeneration) return;
3200
+ void this.connect(timeout);
3201
+ }, timeout * 1e3);
3202
+ if (!this.api.online || !this.api.token) return;
3203
+ let socketUrl = this.normalizeUrl(String(this.options.url || ""));
3204
+ try {
3205
+ const response = await this.api.request({ "$/chat/socketTicket": {} }, { noOptimize: true });
3206
+ if (generation !== this.connectGeneration) return;
3207
+ if (response == null ? void 0 : response.ticket) socketUrl += `${socketUrl.includes("?") ? "&" : "?"}ticket=${encodeURIComponent(response.ticket)}`;
3208
+ else throw new Error("Missing socket ticket");
3209
+ } catch (_) {
3210
+ if (generation !== this.connectGeneration) return;
3211
+ this.scheduleReconnect(generation, timeout);
3212
+ return;
3213
+ }
3214
+ if (generation !== this.connectGeneration) return;
3215
+ this.socket = new WebSocket(socketUrl);
3216
+ this.socket.onopen = () => void 0;
3217
+ this.socket.onclose = () => {
3218
+ this.open = false;
3219
+ this.socket = void 0;
3220
+ this.scheduleReconnect(generation, timeout);
3221
+ };
3222
+ this.socket.onerror = () => {
3223
+ };
3224
+ this.socket.onmessage = (message) => {
3225
+ var _a2;
3226
+ let payload;
3227
+ try {
3228
+ payload = JSON.parse(message.data);
3229
+ } catch {
3230
+ return;
3231
+ }
3232
+ if (payload.connected != void 0) {
3233
+ if (payload.connected) {
3234
+ this.open = true;
3235
+ if (this.retry) clearTimeout(this.retry);
3236
+ this.retry = null;
3237
+ console.debug("Datalynk socket: connected");
3238
+ this.listeners.forEach((l) => l[1]());
3239
+ } else {
3240
+ this.open = false;
3241
+ console.warn(`Datalynk socket failed: ${payload.error || "authentication failed"}`);
3242
+ (_a2 = this.socket) == null ? void 0 : _a2.close();
3243
+ }
3244
+ } else {
3245
+ this.listeners.forEach((l) => l[0](payload));
3246
+ }
3247
+ };
3248
+ }
3249
+ scheduleReconnect(generation, timeout) {
3250
+ if (generation !== this.connectGeneration || this.options.url === false) return;
3251
+ if (this.retry) clearTimeout(this.retry);
3252
+ this.retry = setTimeout(() => {
3253
+ if (generation !== this.connectGeneration) return;
3254
+ void this.connect(timeout);
3255
+ }, timeout * 1e3);
3256
+ }
3180
3257
  /**
3181
3258
  * Send data to socket server
3182
3259
  *
@@ -3203,6 +3280,17 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
3203
3280
  unsubscribe();
3204
3281
  };
3205
3282
  }
3283
+ normalizeUrl(value) {
3284
+ let url = value.replace(/^http:\/\//i, "ws://").replace(/^https:\/\//i, "wss://");
3285
+ try {
3286
+ const parsed = new URL(url);
3287
+ if (parsed.pathname === "/" || parsed.pathname === "") parsed.pathname = "/s/";
3288
+ else if (parsed.pathname === "/s") parsed.pathname = "/s/";
3289
+ url = parsed.toString();
3290
+ } catch {
3291
+ }
3292
+ return url;
3293
+ }
3206
3294
  }
3207
3295
  class Superuser {
3208
3296
  constructor(api) {
@@ -3234,7 +3322,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
3234
3322
  } });
3235
3323
  }
3236
3324
  }
3237
- const version = "1.5.2";
3325
+ const version = "1.6.0";
3238
3326
  class WebRtc {
3239
3327
  constructor(api) {
3240
3328
  __publicField(this, "ice");
@@ -3801,6 +3889,10 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
3801
3889
  __publicField(this, "tokenStorageListener", null);
3802
3890
  /** Pending requests cache */
3803
3891
  __publicField(this, "pending", {});
3892
+ __publicField(this, "chatModulePromise", null);
3893
+ __publicField(this, "chatInstance", null);
3894
+ __publicField(this, "chatLauncherHost", null);
3895
+ __publicField(this, "chatLauncherButton", null);
3804
3896
  /** Helpers */
3805
3897
  /** Authentication */
3806
3898
  __publicField(this, "auth");
@@ -3855,6 +3947,8 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
3855
3947
  offline: [],
3856
3948
  origin: typeof location !== "undefined" ? location.host : "Unknown",
3857
3949
  saveSession: true,
3950
+ manageSession: true,
3951
+ chat: false,
3858
3952
  replayOfflineQueue: true,
3859
3953
  watchTokenExpiry: true,
3860
3954
  serviceWorker: "/service.worker.mjs",
@@ -3886,9 +3980,9 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
3886
3980
  window.addEventListener("storage", this.tokenStorageListener);
3887
3981
  }
3888
3982
  }
3889
- if (this.options.watchTokenExpiry)
3983
+ if (this.options.manageSession !== false && this.options.watchTokenExpiry)
3890
3984
  this.token$.pipe(distinctUntilChanged()).subscribe((token) => this.scheduleTokenExpiry(token));
3891
- this.socket = new Socket(this, { url: options.socket });
3985
+ this.socket = new Socket(this, { url: options.socket, authentication: options.socketAuthentication, realm: options.socketRealm });
3892
3986
  this.gps = new Gps(this, options.gps);
3893
3987
  this.auth = new Auth(this);
3894
3988
  this.files = new Files(this);
@@ -3929,6 +4023,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
3929
4023
  })();
3930
4024
  }
3931
4025
  }
4026
+ this.setupChatLauncher();
3932
4027
  }
3933
4028
  /** Is token expired */
3934
4029
  get expired() {
@@ -4000,7 +4095,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
4000
4095
  var _a;
4001
4096
  if (this.recovery && token !== this.recovery.token)
4002
4097
  this.cancelRecovery(errorFromCode(401, "Session changed during API recovery"));
4003
- if (token && this.isTokenExpired(token)) {
4098
+ if (this.options.manageSession !== false && token && this.isTokenExpired(token)) {
4004
4099
  if (this.adoptStoredToken(token)) return;
4005
4100
  if (this.shouldDeferLocalExpiry()) {
4006
4101
  this.deferOfflineExpiry(token);
@@ -4018,6 +4113,138 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
4018
4113
  }
4019
4114
  if (token && ((_a = this.options) == null ? void 0 : _a.replayOfflineQueue) !== false) void this.replayPendingQueue();
4020
4115
  }
4116
+ /** Mount the optional launcher without loading or initializing the chat bundle. */
4117
+ setupChatLauncher() {
4118
+ if (!this.options.chat || typeof document === "undefined") return;
4119
+ const mount = () => {
4120
+ if (this.chatLauncherHost || !document.body || document.querySelector("[data-datalynk-chat-launcher]")) return;
4121
+ const host = document.createElement("div");
4122
+ host.setAttribute("data-datalynk-chat-launcher", "");
4123
+ const root = host.attachShadow({ mode: "open" });
4124
+ root.innerHTML = `
4125
+ <style>
4126
+ :host {
4127
+ all: initial;
4128
+ position: fixed;
4129
+ right: 20px;
4130
+ bottom: 20px;
4131
+ z-index: 2147482999;
4132
+ pointer-events: none;
4133
+ }
4134
+ button {
4135
+ display: grid;
4136
+ place-items: center;
4137
+ width: 52px;
4138
+ height: 52px;
4139
+ padding: 0;
4140
+ border: 1px solid #2f596d;
4141
+ border-radius: 50%;
4142
+ background: #3f6f85;
4143
+ color: #fff;
4144
+ box-shadow: 0 4px 14px rgb(15 42 54 / 24%);
4145
+ cursor: pointer;
4146
+ pointer-events: auto;
4147
+ transition: background-color 120ms ease-out, box-shadow 120ms ease-out, transform 120ms ease-out;
4148
+ }
4149
+ button:hover { background: #315d71; box-shadow: 0 6px 18px rgb(15 42 54 / 30%); }
4150
+ button:active { transform: translateY(1px); }
4151
+ button:focus-visible { outline: 3px solid #f2c94c; outline-offset: 3px; }
4152
+ button:disabled { cursor: wait; opacity: .72; }
4153
+ .status {
4154
+ position: absolute;
4155
+ right: 62px;
4156
+ bottom: 7px;
4157
+ width: max-content;
4158
+ max-width: min(260px, calc(100vw - 94px));
4159
+ padding: 9px 11px;
4160
+ border: 1px solid #c6d3d9;
4161
+ border-radius: 6px;
4162
+ background: #fff;
4163
+ box-shadow: 0 3px 12px rgb(15 42 54 / 16%);
4164
+ color: #263942;
4165
+ font: 600 13px/1.35 "Segoe UI", Arial, sans-serif;
4166
+ pointer-events: none;
4167
+ }
4168
+ .status:empty { display: none; }
4169
+ svg { width: 25px; height: 25px; }
4170
+ @media (max-width: 600px) { :host { right: 12px; bottom: 12px; } }
4171
+ @media (prefers-reduced-motion: reduce) { button { transition: none; } }
4172
+ </style>
4173
+ <button type="button" aria-label="Open Datalynk Chat" title="Chat">
4174
+ <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
4175
+ <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"/>
4176
+ </svg>
4177
+ </button>
4178
+ <span class="status" role="status" aria-live="polite"></span>`;
4179
+ const button = root.querySelector("button");
4180
+ button.addEventListener("click", () => void this.openChatFromLauncher());
4181
+ document.body.appendChild(host);
4182
+ this.chatLauncherHost = host;
4183
+ this.chatLauncherButton = button;
4184
+ };
4185
+ if (document.body) mount();
4186
+ else document.addEventListener("DOMContentLoaded", mount, { once: true });
4187
+ }
4188
+ async openChatFromLauncher() {
4189
+ const button = this.chatLauncherButton;
4190
+ if (!button) return;
4191
+ const status = button.getRootNode().querySelector(".status");
4192
+ button.disabled = true;
4193
+ button.setAttribute("aria-busy", "true");
4194
+ status.textContent = "Opening chat…";
4195
+ try {
4196
+ await this.openChat();
4197
+ status.textContent = "";
4198
+ } catch (error) {
4199
+ button.disabled = false;
4200
+ button.removeAttribute("aria-busy");
4201
+ status.textContent = "Chat is unavailable for this session.";
4202
+ console.error("Unable to open Datalynk Chat.", error);
4203
+ }
4204
+ }
4205
+ restoreChatLauncher() {
4206
+ if (!this.chatLauncherHost || !this.chatLauncherButton) return;
4207
+ this.chatLauncherHost.hidden = false;
4208
+ this.chatLauncherButton.disabled = false;
4209
+ this.chatLauncherButton.removeAttribute("aria-busy");
4210
+ this.chatLauncherButton.focus();
4211
+ }
4212
+ /** Lazily load and construct chat. No chat code or requests run before this call. */
4213
+ async chat() {
4214
+ if (this.chatInstance) return this.chatInstance;
4215
+ const client = await this.loadChatModule();
4216
+ return this.chatInstance || (this.chatInstance = new client.Chat(this));
4217
+ }
4218
+ /** Lazily load, authorize, and open the reusable chat window. */
4219
+ async openChat(options = {}) {
4220
+ const client = await this.loadChatModule();
4221
+ const chat = await this.chat();
4222
+ if (!await chat.available()) throw new Error("Chat is disabled or this session is not authorized.");
4223
+ if (this.socket.options.authentication !== "ticket") {
4224
+ this.socket.options.authentication = "ticket";
4225
+ void this.socket.connect();
4226
+ }
4227
+ const onClose = options.onClose;
4228
+ const chatWindow = client.openChat(chat, {
4229
+ ...options,
4230
+ onClose: () => {
4231
+ this.restoreChatLauncher();
4232
+ onClose == null ? void 0 : onClose();
4233
+ }
4234
+ });
4235
+ if (this.chatLauncherHost) this.chatLauncherHost.hidden = true;
4236
+ return chatWindow;
4237
+ }
4238
+ loadChatModule() {
4239
+ if (!this.chatModulePromise) {
4240
+ const moduleUrl = this.options.chatModule || "./chat.mjs";
4241
+ this.chatModulePromise = import(
4242
+ /* @vite-ignore */
4243
+ moduleUrl
4244
+ );
4245
+ }
4246
+ return this.chatModulePromise;
4247
+ }
4021
4248
  async _request(req, options = {}) {
4022
4249
  if (this.recovery) throw errorFromCode(503, "Datalynk is unavailable");
4023
4250
  const retryRequest = deepCopy(req);
@@ -4034,7 +4261,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
4034
4261
  /** Execute exactly one HTTP API attempt. Recovery uses this directly to avoid recursive retry loops. */
4035
4262
  async _requestOnce(req, options = {}, recoveryAttempt = false) {
4036
4263
  const token = options.token || this.token;
4037
- if (token && this.isTokenExpired(token)) {
4264
+ if (this.options.manageSession !== false && token && this.isTokenExpired(token)) {
4038
4265
  if (token === this.token) this.expireToken(token);
4039
4266
  throw errorFromCode(401, "Session token expired");
4040
4267
  }
@@ -4201,7 +4428,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
4201
4428
  }
4202
4429
  return;
4203
4430
  }
4204
- if (this.token && this.isTokenExpired(this.token)) {
4431
+ if (this.options.manageSession !== false && this.token && this.isTokenExpired(this.token)) {
4205
4432
  this.expireToken(this.token);
4206
4433
  return;
4207
4434
  }
@@ -4320,6 +4547,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
4320
4547
  }
4321
4548
  async finishUnauthorizedLogout(preservePending = false) {
4322
4549
  var _a;
4550
+ if (this.options.manageSession === false) return;
4323
4551
  if (typeof localStorage != "undefined") {
4324
4552
  localStorage.removeItem(this.localStorageKey);
4325
4553
  localStorage.removeItem("datalynk-user");