@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/api.d.ts +27 -0
- package/dist/api.d.ts.map +1 -1
- package/dist/auth.d.ts +2 -0
- package/dist/auth.d.ts.map +1 -1
- package/dist/chat-entry.d.ts +3 -0
- package/dist/chat-entry.d.ts.map +1 -0
- package/dist/chat-gui.d.ts +64 -0
- package/dist/chat-gui.d.ts.map +1 -0
- package/dist/chat-gui.view.d.ts +3 -0
- package/dist/chat-gui.view.d.ts.map +1 -0
- package/dist/chat.cjs +2202 -0
- package/dist/chat.d.ts +75 -0
- package/dist/chat.d.ts.map +1 -0
- package/dist/chat.mjs +2198 -0
- package/dist/index.cjs +347 -23
- package/dist/index.mjs +347 -23
- package/dist/offline-auth.d.ts +5 -0
- package/dist/offline-auth.d.ts.map +1 -0
- package/dist/service.worker.mjs +113 -14
- package/dist/socket.d.ts +14 -1
- package/dist/socket.d.ts.map +1 -1
- package/package.json +7 -2
package/dist/index.mjs
CHANGED
|
@@ -1532,13 +1532,81 @@ __publicField(_LoginPrompt, "template", (options) => {
|
|
|
1532
1532
|
`;
|
|
1533
1533
|
});
|
|
1534
1534
|
let LoginPrompt = _LoginPrompt;
|
|
1535
|
+
const KEY = "datalynk-offline-login";
|
|
1536
|
+
const ITERATIONS = 31e4;
|
|
1537
|
+
async function hash(password, salt) {
|
|
1538
|
+
const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(password), "PBKDF2", false, ["deriveBits"]);
|
|
1539
|
+
return Array.from(new Uint8Array(await crypto.subtle.deriveBits(
|
|
1540
|
+
{ name: "PBKDF2", salt: new Uint8Array(salt), iterations: ITERATIONS, hash: "SHA-256" },
|
|
1541
|
+
key,
|
|
1542
|
+
256
|
|
1543
|
+
)));
|
|
1544
|
+
}
|
|
1545
|
+
async function rememberOfflineLogin(realm, login, uid, password) {
|
|
1546
|
+
const salt = crypto.getRandomValues(new Uint8Array(16));
|
|
1547
|
+
const record = { realm, login: login.trim().toLowerCase(), uid, salt: Array.from(salt), hash: await hash(password, salt) };
|
|
1548
|
+
localStorage.setItem(KEY, JSON.stringify(record));
|
|
1549
|
+
}
|
|
1550
|
+
async function verifyOfflineLogin(realm, login, uid, password) {
|
|
1551
|
+
const saved = localStorage.getItem(KEY);
|
|
1552
|
+
if (!saved) return false;
|
|
1553
|
+
let record;
|
|
1554
|
+
try {
|
|
1555
|
+
record = JSON.parse(saved);
|
|
1556
|
+
} catch {
|
|
1557
|
+
return false;
|
|
1558
|
+
}
|
|
1559
|
+
if (record.realm !== realm || record.login !== login.trim().toLowerCase() || record.uid !== uid || !Array.isArray(record.salt) || !Array.isArray(record.hash)) return false;
|
|
1560
|
+
const candidate = await hash(password, new Uint8Array(record.salt));
|
|
1561
|
+
if (candidate.length !== record.hash.length) return false;
|
|
1562
|
+
let difference = 0;
|
|
1563
|
+
for (let i = 0; i < candidate.length; i++) difference |= candidate[i] ^ record.hash[i];
|
|
1564
|
+
return difference === 0;
|
|
1565
|
+
}
|
|
1566
|
+
function hasOfflineLogin(realm, uid) {
|
|
1567
|
+
try {
|
|
1568
|
+
const record = JSON.parse(localStorage.getItem(KEY) || "null");
|
|
1569
|
+
return (record == null ? void 0 : record.realm) === realm && (record == null ? void 0 : record.uid) === uid;
|
|
1570
|
+
} catch {
|
|
1571
|
+
return false;
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
function forgetOfflineLogin() {
|
|
1575
|
+
localStorage.removeItem(KEY);
|
|
1576
|
+
}
|
|
1535
1577
|
class Auth {
|
|
1536
1578
|
constructor(api) {
|
|
1537
1579
|
__publicField(this, "onlinePrompt");
|
|
1580
|
+
__publicField(this, "offlineUnlocked", false);
|
|
1581
|
+
__publicField(this, "wasOffline", false);
|
|
1538
1582
|
/** Current user as an observable */
|
|
1539
1583
|
__publicField(this, "user$", new BehaviorSubject(void 0));
|
|
1540
1584
|
var _a;
|
|
1541
1585
|
this.api = api;
|
|
1586
|
+
this.api.status$.subscribe(async (status) => {
|
|
1587
|
+
if (status === "offline" || status === "unavailable") this.wasOffline = true;
|
|
1588
|
+
if (status !== "online" || !this.wasOffline) return;
|
|
1589
|
+
this.wasOffline = false;
|
|
1590
|
+
const wasUnlocked = this.offlineUnlocked;
|
|
1591
|
+
const previousUser = this.user;
|
|
1592
|
+
this.offlineUnlocked = false;
|
|
1593
|
+
this.user = void 0;
|
|
1594
|
+
try {
|
|
1595
|
+
const user = await this.current();
|
|
1596
|
+
const identity2 = this.api.jwtPayload;
|
|
1597
|
+
if (!user || user.guest || !identity2 || !hasOfflineLogin(identity2.realm, identity2.uid))
|
|
1598
|
+
forgetOfflineLogin();
|
|
1599
|
+
this.user = user;
|
|
1600
|
+
} catch (error) {
|
|
1601
|
+
if (this.api.status === "unauthorized") forgetOfflineLogin();
|
|
1602
|
+
else {
|
|
1603
|
+
this.wasOffline = true;
|
|
1604
|
+
this.offlineUnlocked = wasUnlocked;
|
|
1605
|
+
this.user = previousUser;
|
|
1606
|
+
}
|
|
1607
|
+
console.error("Unable to recheck the Datalynk user", error);
|
|
1608
|
+
}
|
|
1609
|
+
});
|
|
1542
1610
|
this.api.token$.subscribe(async (token) => {
|
|
1543
1611
|
if (token === void 0) return;
|
|
1544
1612
|
try {
|
|
@@ -1550,8 +1618,9 @@ class Auth {
|
|
|
1550
1618
|
});
|
|
1551
1619
|
if ((_a = this.api.options.offline) == null ? void 0 : _a.length)
|
|
1552
1620
|
this.user$.pipe(filter((u) => u !== void 0)).subscribe((u) => {
|
|
1553
|
-
if (u == null)
|
|
1554
|
-
|
|
1621
|
+
if (u == null) {
|
|
1622
|
+
if (this.api.online) localStorage.removeItem("datalynk-user");
|
|
1623
|
+
} else localStorage.setItem("datalynk-user", JSON.stringify(u));
|
|
1555
1624
|
});
|
|
1556
1625
|
}
|
|
1557
1626
|
/** Current user */
|
|
@@ -1575,8 +1644,9 @@ class Auth {
|
|
|
1575
1644
|
var _a;
|
|
1576
1645
|
if (!token) return null;
|
|
1577
1646
|
else if (token == ((_a = this.user) == null ? void 0 : _a.token)) return this.user;
|
|
1578
|
-
else if (!this.api.online && typeof localStorage != "undefined" && localStorage.getItem("datalynk-user"))
|
|
1647
|
+
else if (!this.api.online && this.offlineUnlocked && typeof localStorage != "undefined" && localStorage.getItem("datalynk-user"))
|
|
1579
1648
|
return JSON.parse(localStorage.getItem("datalynk-user"));
|
|
1649
|
+
else if (!this.api.online) return null;
|
|
1580
1650
|
return this.api.request([{ "$/auth/current": {} }, { "$/env/me": {} }], { token }).then((resp) => resp[0] || resp[1] ? { ...resp[0], ...resp[1] } : null);
|
|
1581
1651
|
}
|
|
1582
1652
|
/**
|
|
@@ -1588,7 +1658,7 @@ class Auth {
|
|
|
1588
1658
|
* @return {Promise<void>} Login complete
|
|
1589
1659
|
*/
|
|
1590
1660
|
async handleLogin(spoke, options) {
|
|
1591
|
-
var _a, _b, _c;
|
|
1661
|
+
var _a, _b, _c, _d;
|
|
1592
1662
|
if (this.onlinePrompt) window.removeEventListener("online", this.onlinePrompt);
|
|
1593
1663
|
if ((_a = this.api.options.offline) == null ? void 0 : _a.length) window.removeEventListener("online", this.onlinePrompt = () => this.handleLogin(spoke, options));
|
|
1594
1664
|
const urlParams = new URLSearchParams(location.search);
|
|
@@ -1597,10 +1667,13 @@ class Auth {
|
|
|
1597
1667
|
this.api.token = urlToken;
|
|
1598
1668
|
urlParams.delete("datalynkToken");
|
|
1599
1669
|
location.href = location.pathname + (urlParams.toString() ? "?" + urlParams.toString() : "") + location.hash;
|
|
1600
|
-
} else if (
|
|
1670
|
+
} else if (!this.api.online && ((_b = this.api.options.offline) == null ? void 0 : _b.length) && !this.offlineUnlocked) {
|
|
1671
|
+
await this.loginPrompt(spoke, options).done;
|
|
1672
|
+
return;
|
|
1673
|
+
} else if (this.api.reauthenticationPending && !this.api.online && ((_c = this.api.jwtPayload) == null ? void 0 : _c.realm) == spoke) {
|
|
1601
1674
|
return;
|
|
1602
1675
|
} else if (this.api.token && !this.api.expired) {
|
|
1603
|
-
if (((
|
|
1676
|
+
if (((_d = this.api.jwtPayload) == null ? void 0 : _d.realm) != spoke) {
|
|
1604
1677
|
this.api.token = null;
|
|
1605
1678
|
location.reload();
|
|
1606
1679
|
}
|
|
@@ -1660,10 +1733,23 @@ class Auth {
|
|
|
1660
1733
|
* @param {twoFactor?: string, expire?: string} opts 2FA code & expire date (YYYY-MM-DD)
|
|
1661
1734
|
* @returns {Promise<User>} User account
|
|
1662
1735
|
*/
|
|
1663
|
-
login(spoke, login, password, opts) {
|
|
1736
|
+
async login(spoke, login, password, opts) {
|
|
1737
|
+
var _a, _b, _c;
|
|
1738
|
+
if (((_a = this.api.options.offline) == null ? void 0 : _a.length) && (!this.api.online || typeof navigator !== "undefined" && !navigator.onLine)) {
|
|
1739
|
+
const identity2 = this.api.jwtPayload;
|
|
1740
|
+
const saved = localStorage.getItem("datalynk-user");
|
|
1741
|
+
if (!identity2 || identity2.realm !== spoke || !saved || !await verifyOfflineLogin(spoke, login, identity2.uid, password.trim()))
|
|
1742
|
+
throw new Error("Offline login is not available for this account on this device.");
|
|
1743
|
+
const user = JSON.parse(saved);
|
|
1744
|
+
if (![user.login, user.email].some((value) => (value == null ? void 0 : value.toLowerCase()) === login.trim().toLowerCase()))
|
|
1745
|
+
throw new Error("Offline login is not available for this account on this device.");
|
|
1746
|
+
this.offlineUnlocked = true;
|
|
1747
|
+
this.user = user;
|
|
1748
|
+
return user;
|
|
1749
|
+
}
|
|
1664
1750
|
const date = /* @__PURE__ */ new Date();
|
|
1665
1751
|
date.setFullYear(date.getFullYear() + 1);
|
|
1666
|
-
|
|
1752
|
+
const resp = await fetch(`${this.api.url}login`, {
|
|
1667
1753
|
method: "POST",
|
|
1668
1754
|
headers: { "Content-Type": "application/json" },
|
|
1669
1755
|
body: JSON.stringify(clean({
|
|
@@ -1674,12 +1760,13 @@ class Auth {
|
|
|
1674
1760
|
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,
|
|
1675
1761
|
dateFormat: "ISO8601"
|
|
1676
1762
|
}))
|
|
1677
|
-
}).then(async (resp) => {
|
|
1678
|
-
const data = await resp.json().catch(() => ({}));
|
|
1679
|
-
if (!resp.ok || data["error"]) throw Object.assign(errorFromCode(resp.status, data.error) || {}, data);
|
|
1680
|
-
this.api.token = data["token"];
|
|
1681
|
-
return data;
|
|
1682
1763
|
});
|
|
1764
|
+
const data = await resp.json().catch(() => ({}));
|
|
1765
|
+
if (!resp.ok || data["error"]) throw Object.assign(errorFromCode(resp.status, data.error) || {}, data);
|
|
1766
|
+
this.api.token = data["token"];
|
|
1767
|
+
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)
|
|
1768
|
+
await rememberOfflineLogin(spoke, login, this.api.jwtPayload.uid, password.trim());
|
|
1769
|
+
return data;
|
|
1683
1770
|
}
|
|
1684
1771
|
/**
|
|
1685
1772
|
* Login as guest user
|
|
@@ -1711,6 +1798,7 @@ class Auth {
|
|
|
1711
1798
|
*/
|
|
1712
1799
|
logout(reload = true) {
|
|
1713
1800
|
localStorage.removeItem("datalynk-user");
|
|
1801
|
+
forgetOfflineLogin();
|
|
1714
1802
|
return this.api.request({ "$/auth/logout": {} }).then((resp) => {
|
|
1715
1803
|
if (reload) location.reload();
|
|
1716
1804
|
this.api.token = null;
|
|
@@ -3102,6 +3190,7 @@ class Socket {
|
|
|
3102
3190
|
// [ Callback, Re-subscribe ]
|
|
3103
3191
|
__publicField(this, "retry");
|
|
3104
3192
|
__publicField(this, "socket");
|
|
3193
|
+
__publicField(this, "connectGeneration", 0);
|
|
3105
3194
|
__publicField(this, "open", false);
|
|
3106
3195
|
this.api = api;
|
|
3107
3196
|
this.options = options;
|
|
@@ -3110,7 +3199,7 @@ class Socket {
|
|
|
3110
3199
|
this.options.url = origin.replace("http", "ws").replace(/:\d+/g, "") + `/s/`;
|
|
3111
3200
|
}
|
|
3112
3201
|
if (this.options.url !== false)
|
|
3113
|
-
api.token$.pipe(filter((u) => u !== void 0), distinctUntilChanged()).subscribe(() => this.connect());
|
|
3202
|
+
api.token$.pipe(filter((u) => u !== void 0), distinctUntilChanged()).subscribe(() => void this.connect());
|
|
3114
3203
|
}
|
|
3115
3204
|
/**
|
|
3116
3205
|
* Add listener for all socket events
|
|
@@ -3132,6 +3221,7 @@ class Socket {
|
|
|
3132
3221
|
*/
|
|
3133
3222
|
close() {
|
|
3134
3223
|
var _a;
|
|
3224
|
+
if (this.options.authentication === "ticket") this.connectGeneration++;
|
|
3135
3225
|
if (this.open) console.debug("Datalynk socket: disconnected");
|
|
3136
3226
|
this.open = false;
|
|
3137
3227
|
(_a = this.socket) == null ? void 0 : _a.close();
|
|
@@ -3144,6 +3234,7 @@ class Socket {
|
|
|
3144
3234
|
* @param {number} timeout Retry to connect every x seconds
|
|
3145
3235
|
*/
|
|
3146
3236
|
connect(timeout = 30) {
|
|
3237
|
+
if (this.options.authentication === "ticket") return this.connectWithTicket(timeout);
|
|
3147
3238
|
if (this.options.url === false) return console.warn("Datalynk socket disabled");
|
|
3148
3239
|
if (this.open) this.close();
|
|
3149
3240
|
this.retry = setTimeout(() => {
|
|
@@ -3173,6 +3264,80 @@ class Socket {
|
|
|
3173
3264
|
};
|
|
3174
3265
|
}
|
|
3175
3266
|
}
|
|
3267
|
+
/**
|
|
3268
|
+
* Ticket-authenticated connection path, used only once a chat-capable host switches
|
|
3269
|
+
* this instance to `authentication: 'ticket'` (see Api.openChat). Entirely separate
|
|
3270
|
+
* from the token path above so existing non-chat consumers are never affected.
|
|
3271
|
+
*/
|
|
3272
|
+
async connectWithTicket(timeout = 30) {
|
|
3273
|
+
var _a;
|
|
3274
|
+
if (this.options.url === false) return console.warn("Datalynk socket disabled");
|
|
3275
|
+
const generation = ++this.connectGeneration;
|
|
3276
|
+
if (this.open || this.socket) {
|
|
3277
|
+
this.open = false;
|
|
3278
|
+
(_a = this.socket) == null ? void 0 : _a.close();
|
|
3279
|
+
this.socket = void 0;
|
|
3280
|
+
}
|
|
3281
|
+
if (this.retry) clearTimeout(this.retry);
|
|
3282
|
+
this.retry = setTimeout(() => {
|
|
3283
|
+
if (this.open || generation !== this.connectGeneration) return;
|
|
3284
|
+
void this.connect(timeout);
|
|
3285
|
+
}, timeout * 1e3);
|
|
3286
|
+
if (!this.api.online || !this.api.token) return;
|
|
3287
|
+
let socketUrl = this.normalizeUrl(String(this.options.url || ""));
|
|
3288
|
+
try {
|
|
3289
|
+
const response = await this.api.request({ "$/chat/socketTicket": {} }, { noOptimize: true });
|
|
3290
|
+
if (generation !== this.connectGeneration) return;
|
|
3291
|
+
if (response == null ? void 0 : response.ticket) socketUrl += `${socketUrl.includes("?") ? "&" : "?"}ticket=${encodeURIComponent(response.ticket)}`;
|
|
3292
|
+
else throw new Error("Missing socket ticket");
|
|
3293
|
+
} catch (_) {
|
|
3294
|
+
if (generation !== this.connectGeneration) return;
|
|
3295
|
+
this.scheduleReconnect(generation, timeout);
|
|
3296
|
+
return;
|
|
3297
|
+
}
|
|
3298
|
+
if (generation !== this.connectGeneration) return;
|
|
3299
|
+
this.socket = new WebSocket(socketUrl);
|
|
3300
|
+
this.socket.onopen = () => void 0;
|
|
3301
|
+
this.socket.onclose = () => {
|
|
3302
|
+
this.open = false;
|
|
3303
|
+
this.socket = void 0;
|
|
3304
|
+
this.scheduleReconnect(generation, timeout);
|
|
3305
|
+
};
|
|
3306
|
+
this.socket.onerror = () => {
|
|
3307
|
+
};
|
|
3308
|
+
this.socket.onmessage = (message) => {
|
|
3309
|
+
var _a2;
|
|
3310
|
+
let payload;
|
|
3311
|
+
try {
|
|
3312
|
+
payload = JSON.parse(message.data);
|
|
3313
|
+
} catch {
|
|
3314
|
+
return;
|
|
3315
|
+
}
|
|
3316
|
+
if (payload.connected != void 0) {
|
|
3317
|
+
if (payload.connected) {
|
|
3318
|
+
this.open = true;
|
|
3319
|
+
if (this.retry) clearTimeout(this.retry);
|
|
3320
|
+
this.retry = null;
|
|
3321
|
+
console.debug("Datalynk socket: connected");
|
|
3322
|
+
this.listeners.forEach((l) => l[1]());
|
|
3323
|
+
} else {
|
|
3324
|
+
this.open = false;
|
|
3325
|
+
console.warn(`Datalynk socket failed: ${payload.error || "authentication failed"}`);
|
|
3326
|
+
(_a2 = this.socket) == null ? void 0 : _a2.close();
|
|
3327
|
+
}
|
|
3328
|
+
} else {
|
|
3329
|
+
this.listeners.forEach((l) => l[0](payload));
|
|
3330
|
+
}
|
|
3331
|
+
};
|
|
3332
|
+
}
|
|
3333
|
+
scheduleReconnect(generation, timeout) {
|
|
3334
|
+
if (generation !== this.connectGeneration || this.options.url === false) return;
|
|
3335
|
+
if (this.retry) clearTimeout(this.retry);
|
|
3336
|
+
this.retry = setTimeout(() => {
|
|
3337
|
+
if (generation !== this.connectGeneration) return;
|
|
3338
|
+
void this.connect(timeout);
|
|
3339
|
+
}, timeout * 1e3);
|
|
3340
|
+
}
|
|
3176
3341
|
/**
|
|
3177
3342
|
* Send data to socket server
|
|
3178
3343
|
*
|
|
@@ -3199,6 +3364,17 @@ class Socket {
|
|
|
3199
3364
|
unsubscribe();
|
|
3200
3365
|
};
|
|
3201
3366
|
}
|
|
3367
|
+
normalizeUrl(value) {
|
|
3368
|
+
let url = value.replace(/^http:\/\//i, "ws://").replace(/^https:\/\//i, "wss://");
|
|
3369
|
+
try {
|
|
3370
|
+
const parsed = new URL(url);
|
|
3371
|
+
if (parsed.pathname === "/" || parsed.pathname === "") parsed.pathname = "/s/";
|
|
3372
|
+
else if (parsed.pathname === "/s") parsed.pathname = "/s/";
|
|
3373
|
+
url = parsed.toString();
|
|
3374
|
+
} catch {
|
|
3375
|
+
}
|
|
3376
|
+
return url;
|
|
3377
|
+
}
|
|
3202
3378
|
}
|
|
3203
3379
|
class Superuser {
|
|
3204
3380
|
constructor(api) {
|
|
@@ -3230,7 +3406,7 @@ class Superuser {
|
|
|
3230
3406
|
} });
|
|
3231
3407
|
}
|
|
3232
3408
|
}
|
|
3233
|
-
const version = "1.
|
|
3409
|
+
const version = "1.6.1";
|
|
3234
3410
|
class WebRtc {
|
|
3235
3411
|
constructor(api) {
|
|
3236
3412
|
__publicField(this, "ice");
|
|
@@ -3797,6 +3973,10 @@ const _Api = class _Api {
|
|
|
3797
3973
|
__publicField(this, "tokenStorageListener", null);
|
|
3798
3974
|
/** Pending requests cache */
|
|
3799
3975
|
__publicField(this, "pending", {});
|
|
3976
|
+
__publicField(this, "chatModulePromise", null);
|
|
3977
|
+
__publicField(this, "chatInstance", null);
|
|
3978
|
+
__publicField(this, "chatLauncherHost", null);
|
|
3979
|
+
__publicField(this, "chatLauncherButton", null);
|
|
3800
3980
|
/** Helpers */
|
|
3801
3981
|
/** Authentication */
|
|
3802
3982
|
__publicField(this, "auth");
|
|
@@ -3851,6 +4031,8 @@ const _Api = class _Api {
|
|
|
3851
4031
|
offline: [],
|
|
3852
4032
|
origin: typeof location !== "undefined" ? location.host : "Unknown",
|
|
3853
4033
|
saveSession: true,
|
|
4034
|
+
manageSession: true,
|
|
4035
|
+
chat: false,
|
|
3854
4036
|
replayOfflineQueue: true,
|
|
3855
4037
|
watchTokenExpiry: true,
|
|
3856
4038
|
serviceWorker: "/service.worker.mjs",
|
|
@@ -3882,9 +4064,9 @@ const _Api = class _Api {
|
|
|
3882
4064
|
window.addEventListener("storage", this.tokenStorageListener);
|
|
3883
4065
|
}
|
|
3884
4066
|
}
|
|
3885
|
-
if (this.options.watchTokenExpiry)
|
|
4067
|
+
if (this.options.manageSession !== false && this.options.watchTokenExpiry)
|
|
3886
4068
|
this.token$.pipe(distinctUntilChanged()).subscribe((token) => this.scheduleTokenExpiry(token));
|
|
3887
|
-
this.socket = new Socket(this, { url: options.socket });
|
|
4069
|
+
this.socket = new Socket(this, { url: options.socket, authentication: options.socketAuthentication, realm: options.socketRealm });
|
|
3888
4070
|
this.gps = new Gps(this, options.gps);
|
|
3889
4071
|
this.auth = new Auth(this);
|
|
3890
4072
|
this.files = new Files(this);
|
|
@@ -3910,12 +4092,19 @@ const _Api = class _Api {
|
|
|
3910
4092
|
if (this.options.serviceWorker && typeof navigator["serviceWorker"] != "undefined") {
|
|
3911
4093
|
(async () => {
|
|
3912
4094
|
const init = () => {
|
|
3913
|
-
var _a2;
|
|
4095
|
+
var _a2, _b2;
|
|
3914
4096
|
(_a2 = navigator.serviceWorker.controller) == null ? void 0 : _a2.postMessage({ options: { ...this.options, url: this.url } });
|
|
3915
4097
|
this.token$.subscribe((token) => {
|
|
3916
4098
|
var _a3;
|
|
3917
4099
|
return (_a3 = navigator.serviceWorker.controller) == null ? void 0 : _a3.postMessage({ token });
|
|
3918
4100
|
});
|
|
4101
|
+
const assets = [
|
|
4102
|
+
location.pathname,
|
|
4103
|
+
...Array.from(document.querySelectorAll('script[src], link[rel="stylesheet"][href], link[rel="icon"][href], img[src]')).map((element) => element.getAttribute("src") || element.getAttribute("href") || ""),
|
|
4104
|
+
...performance.getEntriesByType("resource").filter((entry) => ["script", "link", "css", "img", "font"].includes(entry.initiatorType)).map((entry) => entry.name),
|
|
4105
|
+
...this.options.offlineAssets || []
|
|
4106
|
+
].filter(Boolean).map((value) => new URL(value, location.href).href);
|
|
4107
|
+
(_b2 = navigator.serviceWorker.controller) == null ? void 0 : _b2.postMessage({ offlineAssets: assets });
|
|
3919
4108
|
};
|
|
3920
4109
|
await navigator.serviceWorker.getRegistration(this.options.serviceWorker).then((reg) => reg ?? navigator.serviceWorker.register(this.options.serviceWorker, { scope: "/", type: "module" }));
|
|
3921
4110
|
navigator.serviceWorker.ready.then(() => {
|
|
@@ -3925,6 +4114,7 @@ const _Api = class _Api {
|
|
|
3925
4114
|
})();
|
|
3926
4115
|
}
|
|
3927
4116
|
}
|
|
4117
|
+
this.setupChatLauncher();
|
|
3928
4118
|
}
|
|
3929
4119
|
/** Is token expired */
|
|
3930
4120
|
get expired() {
|
|
@@ -3996,7 +4186,7 @@ const _Api = class _Api {
|
|
|
3996
4186
|
var _a;
|
|
3997
4187
|
if (this.recovery && token !== this.recovery.token)
|
|
3998
4188
|
this.cancelRecovery(errorFromCode(401, "Session changed during API recovery"));
|
|
3999
|
-
if (token && this.isTokenExpired(token)) {
|
|
4189
|
+
if (this.options.manageSession !== false && token && this.isTokenExpired(token)) {
|
|
4000
4190
|
if (this.adoptStoredToken(token)) return;
|
|
4001
4191
|
if (this.shouldDeferLocalExpiry()) {
|
|
4002
4192
|
this.deferOfflineExpiry(token);
|
|
@@ -4014,6 +4204,138 @@ const _Api = class _Api {
|
|
|
4014
4204
|
}
|
|
4015
4205
|
if (token && ((_a = this.options) == null ? void 0 : _a.replayOfflineQueue) !== false) void this.replayPendingQueue();
|
|
4016
4206
|
}
|
|
4207
|
+
/** Mount the optional launcher without loading or initializing the chat bundle. */
|
|
4208
|
+
setupChatLauncher() {
|
|
4209
|
+
if (!this.options.chat || typeof document === "undefined") return;
|
|
4210
|
+
const mount = () => {
|
|
4211
|
+
if (this.chatLauncherHost || !document.body || document.querySelector("[data-datalynk-chat-launcher]")) return;
|
|
4212
|
+
const host = document.createElement("div");
|
|
4213
|
+
host.setAttribute("data-datalynk-chat-launcher", "");
|
|
4214
|
+
const root = host.attachShadow({ mode: "open" });
|
|
4215
|
+
root.innerHTML = `
|
|
4216
|
+
<style>
|
|
4217
|
+
:host {
|
|
4218
|
+
all: initial;
|
|
4219
|
+
position: fixed;
|
|
4220
|
+
right: 20px;
|
|
4221
|
+
bottom: 20px;
|
|
4222
|
+
z-index: 2147482999;
|
|
4223
|
+
pointer-events: none;
|
|
4224
|
+
}
|
|
4225
|
+
button {
|
|
4226
|
+
display: grid;
|
|
4227
|
+
place-items: center;
|
|
4228
|
+
width: 52px;
|
|
4229
|
+
height: 52px;
|
|
4230
|
+
padding: 0;
|
|
4231
|
+
border: 1px solid #2f596d;
|
|
4232
|
+
border-radius: 50%;
|
|
4233
|
+
background: #3f6f85;
|
|
4234
|
+
color: #fff;
|
|
4235
|
+
box-shadow: 0 4px 14px rgb(15 42 54 / 24%);
|
|
4236
|
+
cursor: pointer;
|
|
4237
|
+
pointer-events: auto;
|
|
4238
|
+
transition: background-color 120ms ease-out, box-shadow 120ms ease-out, transform 120ms ease-out;
|
|
4239
|
+
}
|
|
4240
|
+
button:hover { background: #315d71; box-shadow: 0 6px 18px rgb(15 42 54 / 30%); }
|
|
4241
|
+
button:active { transform: translateY(1px); }
|
|
4242
|
+
button:focus-visible { outline: 3px solid #f2c94c; outline-offset: 3px; }
|
|
4243
|
+
button:disabled { cursor: wait; opacity: .72; }
|
|
4244
|
+
.status {
|
|
4245
|
+
position: absolute;
|
|
4246
|
+
right: 62px;
|
|
4247
|
+
bottom: 7px;
|
|
4248
|
+
width: max-content;
|
|
4249
|
+
max-width: min(260px, calc(100vw - 94px));
|
|
4250
|
+
padding: 9px 11px;
|
|
4251
|
+
border: 1px solid #c6d3d9;
|
|
4252
|
+
border-radius: 6px;
|
|
4253
|
+
background: #fff;
|
|
4254
|
+
box-shadow: 0 3px 12px rgb(15 42 54 / 16%);
|
|
4255
|
+
color: #263942;
|
|
4256
|
+
font: 600 13px/1.35 "Segoe UI", Arial, sans-serif;
|
|
4257
|
+
pointer-events: none;
|
|
4258
|
+
}
|
|
4259
|
+
.status:empty { display: none; }
|
|
4260
|
+
svg { width: 25px; height: 25px; }
|
|
4261
|
+
@media (max-width: 600px) { :host { right: 12px; bottom: 12px; } }
|
|
4262
|
+
@media (prefers-reduced-motion: reduce) { button { transition: none; } }
|
|
4263
|
+
</style>
|
|
4264
|
+
<button type="button" aria-label="Open Datalynk Chat" title="Chat">
|
|
4265
|
+
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
|
4266
|
+
<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"/>
|
|
4267
|
+
</svg>
|
|
4268
|
+
</button>
|
|
4269
|
+
<span class="status" role="status" aria-live="polite"></span>`;
|
|
4270
|
+
const button = root.querySelector("button");
|
|
4271
|
+
button.addEventListener("click", () => void this.openChatFromLauncher());
|
|
4272
|
+
document.body.appendChild(host);
|
|
4273
|
+
this.chatLauncherHost = host;
|
|
4274
|
+
this.chatLauncherButton = button;
|
|
4275
|
+
};
|
|
4276
|
+
if (document.body) mount();
|
|
4277
|
+
else document.addEventListener("DOMContentLoaded", mount, { once: true });
|
|
4278
|
+
}
|
|
4279
|
+
async openChatFromLauncher() {
|
|
4280
|
+
const button = this.chatLauncherButton;
|
|
4281
|
+
if (!button) return;
|
|
4282
|
+
const status = button.getRootNode().querySelector(".status");
|
|
4283
|
+
button.disabled = true;
|
|
4284
|
+
button.setAttribute("aria-busy", "true");
|
|
4285
|
+
status.textContent = "Opening chat…";
|
|
4286
|
+
try {
|
|
4287
|
+
await this.openChat();
|
|
4288
|
+
status.textContent = "";
|
|
4289
|
+
} catch (error) {
|
|
4290
|
+
button.disabled = false;
|
|
4291
|
+
button.removeAttribute("aria-busy");
|
|
4292
|
+
status.textContent = "Chat is unavailable for this session.";
|
|
4293
|
+
console.error("Unable to open Datalynk Chat.", error);
|
|
4294
|
+
}
|
|
4295
|
+
}
|
|
4296
|
+
restoreChatLauncher() {
|
|
4297
|
+
if (!this.chatLauncherHost || !this.chatLauncherButton) return;
|
|
4298
|
+
this.chatLauncherHost.hidden = false;
|
|
4299
|
+
this.chatLauncherButton.disabled = false;
|
|
4300
|
+
this.chatLauncherButton.removeAttribute("aria-busy");
|
|
4301
|
+
this.chatLauncherButton.focus();
|
|
4302
|
+
}
|
|
4303
|
+
/** Lazily load and construct chat. No chat code or requests run before this call. */
|
|
4304
|
+
async chat() {
|
|
4305
|
+
if (this.chatInstance) return this.chatInstance;
|
|
4306
|
+
const client = await this.loadChatModule();
|
|
4307
|
+
return this.chatInstance || (this.chatInstance = new client.Chat(this));
|
|
4308
|
+
}
|
|
4309
|
+
/** Lazily load, authorize, and open the reusable chat window. */
|
|
4310
|
+
async openChat(options = {}) {
|
|
4311
|
+
const client = await this.loadChatModule();
|
|
4312
|
+
const chat = await this.chat();
|
|
4313
|
+
if (!await chat.available()) throw new Error("Chat is disabled or this session is not authorized.");
|
|
4314
|
+
if (this.socket.options.authentication !== "ticket") {
|
|
4315
|
+
this.socket.options.authentication = "ticket";
|
|
4316
|
+
void this.socket.connect();
|
|
4317
|
+
}
|
|
4318
|
+
const onClose = options.onClose;
|
|
4319
|
+
const chatWindow = client.openChat(chat, {
|
|
4320
|
+
...options,
|
|
4321
|
+
onClose: () => {
|
|
4322
|
+
this.restoreChatLauncher();
|
|
4323
|
+
onClose == null ? void 0 : onClose();
|
|
4324
|
+
}
|
|
4325
|
+
});
|
|
4326
|
+
if (this.chatLauncherHost) this.chatLauncherHost.hidden = true;
|
|
4327
|
+
return chatWindow;
|
|
4328
|
+
}
|
|
4329
|
+
loadChatModule() {
|
|
4330
|
+
if (!this.chatModulePromise) {
|
|
4331
|
+
const moduleUrl = this.options.chatModule || "./chat.mjs";
|
|
4332
|
+
this.chatModulePromise = import(
|
|
4333
|
+
/* @vite-ignore */
|
|
4334
|
+
moduleUrl
|
|
4335
|
+
);
|
|
4336
|
+
}
|
|
4337
|
+
return this.chatModulePromise;
|
|
4338
|
+
}
|
|
4017
4339
|
async _request(req, options = {}) {
|
|
4018
4340
|
if (this.recovery) throw errorFromCode(503, "Datalynk is unavailable");
|
|
4019
4341
|
const retryRequest = deepCopy(req);
|
|
@@ -4030,7 +4352,7 @@ const _Api = class _Api {
|
|
|
4030
4352
|
/** Execute exactly one HTTP API attempt. Recovery uses this directly to avoid recursive retry loops. */
|
|
4031
4353
|
async _requestOnce(req, options = {}, recoveryAttempt = false) {
|
|
4032
4354
|
const token = options.token || this.token;
|
|
4033
|
-
if (token && this.isTokenExpired(token)) {
|
|
4355
|
+
if (this.options.manageSession !== false && token && this.isTokenExpired(token)) {
|
|
4034
4356
|
if (token === this.token) this.expireToken(token);
|
|
4035
4357
|
throw errorFromCode(401, "Session token expired");
|
|
4036
4358
|
}
|
|
@@ -4197,7 +4519,7 @@ const _Api = class _Api {
|
|
|
4197
4519
|
}
|
|
4198
4520
|
return;
|
|
4199
4521
|
}
|
|
4200
|
-
if (this.token && this.isTokenExpired(this.token)) {
|
|
4522
|
+
if (this.options.manageSession !== false && this.token && this.isTokenExpired(this.token)) {
|
|
4201
4523
|
this.expireToken(this.token);
|
|
4202
4524
|
return;
|
|
4203
4525
|
}
|
|
@@ -4316,6 +4638,8 @@ const _Api = class _Api {
|
|
|
4316
4638
|
}
|
|
4317
4639
|
async finishUnauthorizedLogout(preservePending = false) {
|
|
4318
4640
|
var _a;
|
|
4641
|
+
if (this.options.manageSession === false) return;
|
|
4642
|
+
forgetOfflineLogin();
|
|
4319
4643
|
if (typeof localStorage != "undefined") {
|
|
4320
4644
|
localStorage.removeItem(this.localStorageKey);
|
|
4321
4645
|
localStorage.removeItem("datalynk-user");
|
|
@@ -4331,13 +4655,13 @@ const _Api = class _Api {
|
|
|
4331
4655
|
await this.clearOfflineSessionCache(preservePending);
|
|
4332
4656
|
if (typeof caches != "undefined") {
|
|
4333
4657
|
try {
|
|
4334
|
-
await
|
|
4658
|
+
await caches.delete("api-settings");
|
|
4335
4659
|
} catch (error) {
|
|
4336
4660
|
console.error("Unable to clear Datalynk service worker cache", error);
|
|
4337
4661
|
}
|
|
4338
4662
|
}
|
|
4339
4663
|
if (typeof navigator != "undefined" && ((_a = navigator.serviceWorker) == null ? void 0 : _a.controller)) {
|
|
4340
|
-
navigator.serviceWorker.controller.postMessage({ token: null
|
|
4664
|
+
navigator.serviceWorker.controller.postMessage({ token: null });
|
|
4341
4665
|
}
|
|
4342
4666
|
if (typeof location != "undefined" && typeof location.reload == "function")
|
|
4343
4667
|
location.reload();
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare function rememberOfflineLogin(realm: string, login: string, uid: number, password: string): Promise<void>;
|
|
2
|
+
export declare function verifyOfflineLogin(realm: string, login: string, uid: number, password: string): Promise<boolean>;
|
|
3
|
+
export declare function hasOfflineLogin(realm: string, uid: number): boolean;
|
|
4
|
+
export declare function forgetOfflineLogin(): void;
|
|
5
|
+
//# sourceMappingURL=offline-auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"offline-auth.d.ts","sourceRoot":"","sources":["../src/offline-auth.ts"],"names":[],"mappings":"AAYA,wBAAsB,oBAAoB,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAIrH;AAED,wBAAsB,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAYtH;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAKnE;AAED,wBAAgB,kBAAkB,IAAI,IAAI,CAEzC"}
|