@auxilium/datalynk-client 1.6.2 → 1.6.4
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/README.md +13 -0
- package/dist/api.d.ts +9 -0
- package/dist/api.d.ts.map +1 -1
- package/dist/auth.d.ts +6 -2
- package/dist/auth.d.ts.map +1 -1
- package/dist/chat-gui.d.ts +11 -0
- package/dist/chat-gui.d.ts.map +1 -1
- package/dist/chat-gui.view.d.ts +3 -1
- package/dist/chat-gui.view.d.ts.map +1 -1
- package/dist/chat.cjs +212 -43
- package/dist/chat.d.ts +13 -0
- package/dist/chat.d.ts.map +1 -1
- package/dist/chat.mjs +212 -43
- package/dist/index.cjs +455 -104
- package/dist/index.mjs +455 -104
- package/dist/offline-auth.d.ts +68 -4
- package/dist/offline-auth.d.ts.map +1 -1
- package/dist/service.worker.mjs +156 -40
- package/dist/slice.d.ts.map +1 -1
- package/dist/utils.d.ts +13 -0
- package/dist/utils.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -189,6 +189,8 @@ class Database {
|
|
|
189
189
|
constructor(database, tables, version2) {
|
|
190
190
|
__publicField(this, "connection");
|
|
191
191
|
__publicField(this, "tables");
|
|
192
|
+
/** Encrypts values at rest when set */
|
|
193
|
+
__publicField(this, "cipher");
|
|
192
194
|
this.database = database;
|
|
193
195
|
this.version = version2;
|
|
194
196
|
this.tables = tables.map((table) => typeof table === "object" ? { ...table, name: table.name.toString() } : { name: table.toString() });
|
|
@@ -237,22 +239,46 @@ class Table {
|
|
|
237
239
|
async tx(operation, readonly = false) {
|
|
238
240
|
const db = await this.database.connection;
|
|
239
241
|
return new Promise((resolve, reject) => {
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
242
|
+
const transaction = db.transaction(this.name, readonly ? "readonly" : "readwrite");
|
|
243
|
+
let result;
|
|
244
|
+
transaction.oncomplete = () => resolve(result);
|
|
245
|
+
transaction.onabort = () => reject(transaction.error || new Error("IndexedDB transaction aborted"));
|
|
246
|
+
transaction.onerror = () => reject(transaction.error || new Error("IndexedDB transaction failed"));
|
|
247
|
+
try {
|
|
248
|
+
const request = operation(transaction.objectStore(this.name));
|
|
249
|
+
request.onsuccess = () => {
|
|
250
|
+
result = request.result;
|
|
251
|
+
};
|
|
252
|
+
request.onerror = () => reject(request.error || new Error("IndexedDB request failed"));
|
|
253
|
+
} catch (error) {
|
|
254
|
+
transaction.abort();
|
|
255
|
+
reject(error);
|
|
256
|
+
}
|
|
243
257
|
});
|
|
244
258
|
}
|
|
245
|
-
|
|
246
|
-
|
|
259
|
+
seal(value) {
|
|
260
|
+
const cipher = this.database.cipher;
|
|
261
|
+
return (cipher == null ? void 0 : cipher.enabled) ? cipher.seal(value) : Promise.resolve(value);
|
|
262
|
+
}
|
|
263
|
+
open(value) {
|
|
264
|
+
const cipher = this.database.cipher;
|
|
265
|
+
return cipher ? cipher.open(value) : Promise.resolve(value);
|
|
247
266
|
}
|
|
248
|
-
|
|
249
|
-
|
|
267
|
+
async add(value, key) {
|
|
268
|
+
const sealed = await this.seal(value);
|
|
269
|
+
return this.tx((store) => store.add(sealed, key));
|
|
250
270
|
}
|
|
251
|
-
|
|
252
|
-
|
|
271
|
+
async put(key, value) {
|
|
272
|
+
const sealed = await this.seal(value);
|
|
273
|
+
return this.tx((store) => store.put(sealed, key));
|
|
253
274
|
}
|
|
254
|
-
|
|
255
|
-
return this.tx((store) => store.
|
|
275
|
+
async get(key) {
|
|
276
|
+
return await this.open(await this.tx((store) => store.get(key), true));
|
|
277
|
+
}
|
|
278
|
+
/** All readable values. Values sealed with another key are skipped. */
|
|
279
|
+
async getAll() {
|
|
280
|
+
const values = await Promise.all((await this.tx((store) => store.getAll(), true)).map((v) => this.open(v)));
|
|
281
|
+
return values.filter((v) => v !== void 0);
|
|
256
282
|
}
|
|
257
283
|
getAllKeys() {
|
|
258
284
|
return this.tx((store) => store.getAllKeys(), true);
|
|
@@ -263,6 +289,20 @@ class Table {
|
|
|
263
289
|
clear() {
|
|
264
290
|
return this.tx((store) => store.clear());
|
|
265
291
|
}
|
|
292
|
+
/** Replace the whole table in one transaction (values are sealed first, so nothing is lost while locked) */
|
|
293
|
+
async replace(entries) {
|
|
294
|
+
const sealed = await Promise.all(entries.map(async ([key, value]) => [key, await this.seal(value)]));
|
|
295
|
+
const db = await this.database.connection;
|
|
296
|
+
await new Promise((resolve, reject) => {
|
|
297
|
+
const tx = db.transaction(this.name, "readwrite");
|
|
298
|
+
const store = tx.objectStore(this.name);
|
|
299
|
+
store.clear();
|
|
300
|
+
sealed.forEach(([key, value]) => store.put(value, key));
|
|
301
|
+
tx.oncomplete = () => resolve();
|
|
302
|
+
tx.onerror = () => reject(tx.error);
|
|
303
|
+
tx.onabort = () => reject(tx.error);
|
|
304
|
+
});
|
|
305
|
+
}
|
|
266
306
|
count() {
|
|
267
307
|
return this.tx((store) => store.count(), true);
|
|
268
308
|
}
|
|
@@ -1016,6 +1056,9 @@ var BehaviorSubject = function(_super) {
|
|
|
1016
1056
|
};
|
|
1017
1057
|
return BehaviorSubject2;
|
|
1018
1058
|
}(Subject);
|
|
1059
|
+
var EMPTY = new Observable(function(subscriber) {
|
|
1060
|
+
return subscriber.complete();
|
|
1061
|
+
});
|
|
1019
1062
|
var EmptyError = createErrorClass(function(_super) {
|
|
1020
1063
|
return function EmptyErrorImpl() {
|
|
1021
1064
|
_super(this);
|
|
@@ -1043,6 +1086,23 @@ function lastValueFrom(source, config2) {
|
|
|
1043
1086
|
});
|
|
1044
1087
|
});
|
|
1045
1088
|
}
|
|
1089
|
+
function firstValueFrom(source, config2) {
|
|
1090
|
+
return new Promise(function(resolve, reject) {
|
|
1091
|
+
var subscriber = new SafeSubscriber({
|
|
1092
|
+
next: function(value) {
|
|
1093
|
+
resolve(value);
|
|
1094
|
+
subscriber.unsubscribe();
|
|
1095
|
+
},
|
|
1096
|
+
error: reject,
|
|
1097
|
+
complete: function() {
|
|
1098
|
+
{
|
|
1099
|
+
reject(new EmptyError());
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
});
|
|
1103
|
+
source.subscribe(subscriber);
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1046
1106
|
function filter(predicate, thisArg) {
|
|
1047
1107
|
return operate(function(source, subscriber) {
|
|
1048
1108
|
var index = 0;
|
|
@@ -1051,6 +1111,21 @@ function filter(predicate, thisArg) {
|
|
|
1051
1111
|
}));
|
|
1052
1112
|
});
|
|
1053
1113
|
}
|
|
1114
|
+
function take(count) {
|
|
1115
|
+
return count <= 0 ? function() {
|
|
1116
|
+
return EMPTY;
|
|
1117
|
+
} : operate(function(source, subscriber) {
|
|
1118
|
+
var seen = 0;
|
|
1119
|
+
source.subscribe(createOperatorSubscriber(subscriber, function(value) {
|
|
1120
|
+
if (++seen <= count) {
|
|
1121
|
+
subscriber.next(value);
|
|
1122
|
+
if (count <= seen) {
|
|
1123
|
+
subscriber.complete();
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
}));
|
|
1127
|
+
});
|
|
1128
|
+
}
|
|
1054
1129
|
function distinctUntilChanged(comparator, keySelector) {
|
|
1055
1130
|
if (keySelector === void 0) {
|
|
1056
1131
|
keySelector = identity;
|
|
@@ -1532,52 +1607,304 @@ __publicField(_LoginPrompt, "template", (options) => {
|
|
|
1532
1607
|
`;
|
|
1533
1608
|
});
|
|
1534
1609
|
let LoginPrompt = _LoginPrompt;
|
|
1535
|
-
const
|
|
1610
|
+
const RECORD_KEY = "datalynk-offline-login";
|
|
1611
|
+
const PROFILE_KEY = "datalynk-user";
|
|
1612
|
+
const VAULT_DB = "datalynk-vault";
|
|
1613
|
+
const VAULT_STORE = "keys";
|
|
1614
|
+
const DEVICE_KEY = "device";
|
|
1536
1615
|
const ITERATIONS = 31e4;
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
}
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
}
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
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;
|
|
1616
|
+
const DAY = 864e5;
|
|
1617
|
+
const offlineLoginMessages = {
|
|
1618
|
+
"none": "Offline login is not available on this device. Connect to the internet to sign in.",
|
|
1619
|
+
"wrong-user": "Offline login is only available for the last user who signed in on this device.",
|
|
1620
|
+
"expired": "Offline login has expired on this device. Connect to the internet to sign in.",
|
|
1621
|
+
"locked": "Offline login is locked after too many attempts. Connect to the internet to sign in.",
|
|
1622
|
+
"bad-password": "Incorrect password."
|
|
1623
|
+
};
|
|
1624
|
+
const toB64 = (bytes) => {
|
|
1625
|
+
let out = "";
|
|
1626
|
+
new Uint8Array(bytes).forEach((b) => out += String.fromCharCode(b));
|
|
1627
|
+
return btoa(out);
|
|
1628
|
+
};
|
|
1629
|
+
const fromB64 = (text) => Uint8Array.from(atob(text), (c) => c.charCodeAt(0));
|
|
1630
|
+
const normalizeLogin = (login) => (login || "").trim().toLowerCase();
|
|
1631
|
+
function isSealed(value) {
|
|
1632
|
+
return !!value && typeof value == "object" && value.__dlv === 1 && "iv" in value && "ct" in value;
|
|
1565
1633
|
}
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1634
|
+
class OfflineVault {
|
|
1635
|
+
constructor(enabled, options = {}, startOffline) {
|
|
1636
|
+
/** False when offline login is disabled or the environment lacks WebCrypto/IndexedDB/localStorage */
|
|
1637
|
+
__publicField(this, "enabled");
|
|
1638
|
+
/** True once the data key is available in memory */
|
|
1639
|
+
__publicField(this, "unlocked$", new BehaviorSubject(false));
|
|
1640
|
+
/** Resolves once startup key loading has finished */
|
|
1641
|
+
__publicField(this, "ready");
|
|
1642
|
+
__publicField(this, "dek", null);
|
|
1643
|
+
__publicField(this, "days");
|
|
1644
|
+
__publicField(this, "attempts");
|
|
1645
|
+
this.enabled = enabled && typeof crypto != "undefined" && !!crypto.subtle && typeof indexedDB != "undefined" && typeof localStorage != "undefined";
|
|
1646
|
+
this.days = options.days ?? 7;
|
|
1647
|
+
this.attempts = options.attempts ?? 5;
|
|
1648
|
+
if (startOffline === void 0) startOffline = typeof navigator != "undefined" && navigator.onLine === false;
|
|
1649
|
+
this.ready = this.enabled ? this.init(startOffline).catch((error) => {
|
|
1650
|
+
console.error("Unable to open the Datalynk offline vault", error);
|
|
1651
|
+
}) : Promise.resolve();
|
|
1652
|
+
}
|
|
1653
|
+
get unlocked() {
|
|
1654
|
+
return this.unlocked$.getValue();
|
|
1655
|
+
}
|
|
1656
|
+
/** A saved offline login exists, but this session has not unlocked it yet */
|
|
1657
|
+
get needsPassword() {
|
|
1658
|
+
return this.enabled && !this.unlocked && !!this.readRecord();
|
|
1659
|
+
}
|
|
1660
|
+
/** Wait until the data key is available (resolves immediately when the vault is disabled) */
|
|
1661
|
+
whenUnlocked() {
|
|
1662
|
+
if (!this.enabled || this.unlocked) return Promise.resolve(true);
|
|
1663
|
+
return firstValueFrom(this.unlocked$.pipe(filter(Boolean)));
|
|
1664
|
+
}
|
|
1665
|
+
/** Saved offline login matches this user */
|
|
1666
|
+
has(realm, uid) {
|
|
1667
|
+
const record = this.readRecord();
|
|
1668
|
+
return !!record && record.realm === realm && record.uid === uid;
|
|
1669
|
+
}
|
|
1670
|
+
/**
|
|
1671
|
+
* Called after a successful online password login. Wraps the data key with the
|
|
1672
|
+
* password so this user can sign in offline later.
|
|
1673
|
+
*/
|
|
1674
|
+
async rememberLogin(realm, login, uid, password) {
|
|
1675
|
+
if (!this.enabled) return;
|
|
1676
|
+
await this.ready;
|
|
1677
|
+
const record = this.readRecord();
|
|
1678
|
+
let dek = null;
|
|
1679
|
+
if (record && record.realm === realm && record.uid === uid) {
|
|
1680
|
+
dek = await this.unwrapWithPassword(record, password).catch(() => null);
|
|
1681
|
+
if (!dek) dek = this.dek;
|
|
1682
|
+
} else if (!record) {
|
|
1683
|
+
dek = this.dek;
|
|
1684
|
+
}
|
|
1685
|
+
if (!dek) dek = await this.newDataKey();
|
|
1686
|
+
const salt = crypto.getRandomValues(new Uint8Array(16));
|
|
1687
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
1688
|
+
const kek = await this.passwordKey(password, salt);
|
|
1689
|
+
const wrapped = await crypto.subtle.wrapKey("raw", dek, kek, { name: "AES-GCM", iv });
|
|
1690
|
+
this.writeRecord({
|
|
1691
|
+
v: 2,
|
|
1692
|
+
realm,
|
|
1693
|
+
uid,
|
|
1694
|
+
login: normalizeLogin(login),
|
|
1695
|
+
salt: toB64(salt),
|
|
1696
|
+
iv: toB64(iv),
|
|
1697
|
+
wrapped: toB64(wrapped),
|
|
1698
|
+
verifiedAt: Date.now(),
|
|
1699
|
+
failures: 0
|
|
1700
|
+
});
|
|
1701
|
+
await this.setKey(dek);
|
|
1702
|
+
}
|
|
1703
|
+
/** Try to unlock offline with a password */
|
|
1704
|
+
async unlockOffline(realm, login, uid, password) {
|
|
1705
|
+
if (!this.enabled) return "none";
|
|
1706
|
+
await this.ready;
|
|
1707
|
+
const record = this.readRecord();
|
|
1708
|
+
if (!record) return "none";
|
|
1709
|
+
if (record.realm !== realm || record.login !== normalizeLogin(login) || uid != null && record.uid !== uid) return "wrong-user";
|
|
1710
|
+
if (record.locked) return "locked";
|
|
1711
|
+
if (Date.now() - record.verifiedAt > this.days * DAY) return "expired";
|
|
1712
|
+
try {
|
|
1713
|
+
const dek = await this.unwrapWithPassword(record, password);
|
|
1714
|
+
this.writeRecord({ ...record, failures: 0 });
|
|
1715
|
+
await this.setKey(dek);
|
|
1716
|
+
return "ok";
|
|
1717
|
+
} catch {
|
|
1718
|
+
const failures = (record.failures || 0) + 1;
|
|
1719
|
+
const locked = failures >= this.attempts;
|
|
1720
|
+
this.writeRecord({ ...record, failures, locked });
|
|
1721
|
+
return locked ? "locked" : "bad-password";
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
/** The server confirmed this user's session: restart the offline window */
|
|
1725
|
+
touch(realm, uid) {
|
|
1726
|
+
if (!this.enabled || realm == null || uid == null) return;
|
|
1727
|
+
const record = this.readRecord();
|
|
1728
|
+
if (record && record.realm === realm && record.uid === uid) this.writeRecord({ ...record, verifiedAt: Date.now() });
|
|
1729
|
+
}
|
|
1730
|
+
/** Drop the in-memory and device keys but keep the password copy (queued work stays recoverable) */
|
|
1731
|
+
async lock() {
|
|
1732
|
+
this.dek = null;
|
|
1733
|
+
if (this.unlocked) this.unlocked$.next(false);
|
|
1734
|
+
if (this.enabled) await this.deleteDevice();
|
|
1735
|
+
}
|
|
1736
|
+
/** Remove offline login completely; anything sealed becomes unreadable */
|
|
1737
|
+
async forget() {
|
|
1738
|
+
if (typeof localStorage != "undefined") {
|
|
1739
|
+
localStorage.removeItem(RECORD_KEY);
|
|
1740
|
+
localStorage.removeItem(PROFILE_KEY);
|
|
1741
|
+
}
|
|
1742
|
+
await this.lock();
|
|
1743
|
+
}
|
|
1744
|
+
/** Encrypt a value for IndexedDB (passes through when disabled) */
|
|
1745
|
+
async seal(value) {
|
|
1746
|
+
if (!this.enabled || value === void 0) return value;
|
|
1747
|
+
await this.whenUnlocked();
|
|
1748
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
1749
|
+
const ct = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, this.dek, new TextEncoder().encode(JSON.stringify(value)));
|
|
1750
|
+
return { __dlv: 1, iv, ct };
|
|
1751
|
+
}
|
|
1752
|
+
/** Decrypt a value from IndexedDB. Unreadable values (old key / other user) return undefined. */
|
|
1753
|
+
async open(value) {
|
|
1754
|
+
if (!isSealed(value)) return value;
|
|
1755
|
+
if (!this.enabled) return void 0;
|
|
1756
|
+
await this.whenUnlocked();
|
|
1757
|
+
try {
|
|
1758
|
+
const iv = typeof value.iv == "string" ? fromB64(value.iv) : value.iv;
|
|
1759
|
+
const ct = typeof value.ct == "string" ? fromB64(value.ct) : value.ct;
|
|
1760
|
+
const plain = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, this.dek, ct);
|
|
1761
|
+
return JSON.parse(new TextDecoder().decode(plain));
|
|
1762
|
+
} catch {
|
|
1763
|
+
return void 0;
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
/** Save the cached user profile (sealed when enabled) */
|
|
1767
|
+
async setProfile(user) {
|
|
1768
|
+
if (typeof localStorage == "undefined") return;
|
|
1769
|
+
if (user == null) return localStorage.removeItem(PROFILE_KEY);
|
|
1770
|
+
if (!this.enabled) return localStorage.setItem(PROFILE_KEY, JSON.stringify(user));
|
|
1771
|
+
const sealed = await this.seal(user);
|
|
1772
|
+
localStorage.setItem(PROFILE_KEY, JSON.stringify({ __dlv: 1, iv: toB64(sealed.iv), ct: toB64(sealed.ct) }));
|
|
1773
|
+
}
|
|
1774
|
+
/** Read the cached user profile; null while locked */
|
|
1775
|
+
async getProfile() {
|
|
1776
|
+
if (typeof localStorage == "undefined") return null;
|
|
1777
|
+
let saved;
|
|
1778
|
+
try {
|
|
1779
|
+
saved = JSON.parse(localStorage.getItem(PROFILE_KEY) || "null");
|
|
1780
|
+
} catch {
|
|
1781
|
+
return null;
|
|
1782
|
+
}
|
|
1783
|
+
if (!isSealed(saved)) return this.enabled ? null : saved;
|
|
1784
|
+
if (!this.unlocked) return null;
|
|
1785
|
+
return await this.open(saved) ?? null;
|
|
1786
|
+
}
|
|
1787
|
+
// ----- internals -----
|
|
1788
|
+
async init(startOffline) {
|
|
1789
|
+
if (startOffline) {
|
|
1790
|
+
await this.deleteDevice();
|
|
1791
|
+
return;
|
|
1792
|
+
}
|
|
1793
|
+
const device = await this.readDevice();
|
|
1794
|
+
if (device) {
|
|
1795
|
+
try {
|
|
1796
|
+
const dek = await crypto.subtle.unwrapKey(
|
|
1797
|
+
"raw",
|
|
1798
|
+
device.wrapped,
|
|
1799
|
+
device.key,
|
|
1800
|
+
{ name: "AES-GCM", iv: device.iv },
|
|
1801
|
+
{ name: "AES-GCM" },
|
|
1802
|
+
true,
|
|
1803
|
+
["encrypt", "decrypt"]
|
|
1804
|
+
);
|
|
1805
|
+
this.dek = dek;
|
|
1806
|
+
this.unlocked$.next(true);
|
|
1807
|
+
return;
|
|
1808
|
+
} catch {
|
|
1809
|
+
await this.deleteDevice();
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
if (!this.readRecord()) await this.setKey(await this.newDataKey());
|
|
1813
|
+
}
|
|
1814
|
+
async setKey(dek) {
|
|
1815
|
+
this.dek = dek;
|
|
1816
|
+
try {
|
|
1817
|
+
await this.writeDevice(dek);
|
|
1818
|
+
} catch (error) {
|
|
1819
|
+
console.warn("Unable to store the Datalynk device key; offline data will need the password", error);
|
|
1820
|
+
}
|
|
1821
|
+
if (!this.unlocked) this.unlocked$.next(true);
|
|
1822
|
+
}
|
|
1823
|
+
newDataKey() {
|
|
1824
|
+
return crypto.subtle.generateKey({ name: "AES-GCM", length: 256 }, true, ["encrypt", "decrypt"]);
|
|
1825
|
+
}
|
|
1826
|
+
async passwordKey(password, salt) {
|
|
1827
|
+
const base = await crypto.subtle.importKey("raw", new TextEncoder().encode(password), "PBKDF2", false, ["deriveKey"]);
|
|
1828
|
+
return crypto.subtle.deriveKey(
|
|
1829
|
+
{ name: "PBKDF2", salt, iterations: ITERATIONS, hash: "SHA-256" },
|
|
1830
|
+
base,
|
|
1831
|
+
{ name: "AES-GCM", length: 256 },
|
|
1832
|
+
false,
|
|
1833
|
+
["wrapKey", "unwrapKey"]
|
|
1834
|
+
);
|
|
1835
|
+
}
|
|
1836
|
+
async unwrapWithPassword(record, password) {
|
|
1837
|
+
const kek = await this.passwordKey(password, fromB64(record.salt));
|
|
1838
|
+
return crypto.subtle.unwrapKey(
|
|
1839
|
+
"raw",
|
|
1840
|
+
fromB64(record.wrapped),
|
|
1841
|
+
kek,
|
|
1842
|
+
{ name: "AES-GCM", iv: fromB64(record.iv) },
|
|
1843
|
+
{ name: "AES-GCM" },
|
|
1844
|
+
true,
|
|
1845
|
+
["encrypt", "decrypt"]
|
|
1846
|
+
);
|
|
1847
|
+
}
|
|
1848
|
+
readRecord() {
|
|
1849
|
+
if (typeof localStorage == "undefined") return null;
|
|
1850
|
+
try {
|
|
1851
|
+
const record = JSON.parse(localStorage.getItem(RECORD_KEY) || "null");
|
|
1852
|
+
if ((record == null ? void 0 : record.v) === 2) return record;
|
|
1853
|
+
if (record) localStorage.removeItem(RECORD_KEY);
|
|
1854
|
+
} catch {
|
|
1855
|
+
localStorage.removeItem(RECORD_KEY);
|
|
1856
|
+
}
|
|
1857
|
+
return null;
|
|
1858
|
+
}
|
|
1859
|
+
writeRecord(record) {
|
|
1860
|
+
localStorage.setItem(RECORD_KEY, JSON.stringify(record));
|
|
1861
|
+
}
|
|
1862
|
+
vaultDb() {
|
|
1863
|
+
return new Promise((resolve, reject) => {
|
|
1864
|
+
const request = indexedDB.open(VAULT_DB, 1);
|
|
1865
|
+
request.onupgradeneeded = () => request.result.createObjectStore(VAULT_STORE);
|
|
1866
|
+
request.onsuccess = () => resolve(request.result);
|
|
1867
|
+
request.onerror = () => reject(request.error);
|
|
1868
|
+
});
|
|
1869
|
+
}
|
|
1870
|
+
async vaultTx(mode, op) {
|
|
1871
|
+
const db = await this.vaultDb();
|
|
1872
|
+
try {
|
|
1873
|
+
return await new Promise((resolve, reject) => {
|
|
1874
|
+
const tx = db.transaction(VAULT_STORE, mode);
|
|
1875
|
+
const request = op(tx.objectStore(VAULT_STORE));
|
|
1876
|
+
tx.oncomplete = () => resolve(request.result);
|
|
1877
|
+
tx.onerror = () => reject(tx.error);
|
|
1878
|
+
tx.onabort = () => reject(tx.error);
|
|
1879
|
+
});
|
|
1880
|
+
} finally {
|
|
1881
|
+
db.close();
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
async readDevice() {
|
|
1885
|
+
try {
|
|
1886
|
+
return await this.vaultTx("readonly", (store) => store.get(DEVICE_KEY)) || null;
|
|
1887
|
+
} catch {
|
|
1888
|
+
return null;
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
async writeDevice(dek) {
|
|
1892
|
+
const key = await crypto.subtle.generateKey({ name: "AES-GCM", length: 256 }, false, ["wrapKey", "unwrapKey"]);
|
|
1893
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
1894
|
+
const wrapped = await crypto.subtle.wrapKey("raw", dek, key, { name: "AES-GCM", iv });
|
|
1895
|
+
await this.vaultTx("readwrite", (store) => store.put({ key, iv, wrapped }, DEVICE_KEY));
|
|
1896
|
+
}
|
|
1897
|
+
async deleteDevice() {
|
|
1898
|
+
try {
|
|
1899
|
+
await this.vaultTx("readwrite", (store) => store.delete(DEVICE_KEY));
|
|
1900
|
+
} catch (error) {
|
|
1901
|
+
console.warn("Unable to remove the Datalynk device key", error);
|
|
1902
|
+
}
|
|
1572
1903
|
}
|
|
1573
|
-
}
|
|
1574
|
-
function forgetOfflineLogin() {
|
|
1575
|
-
localStorage.removeItem(KEY);
|
|
1576
1904
|
}
|
|
1577
1905
|
class Auth {
|
|
1578
1906
|
constructor(api) {
|
|
1579
1907
|
__publicField(this, "onlinePrompt");
|
|
1580
|
-
__publicField(this, "offlineUnlocked", false);
|
|
1581
1908
|
__publicField(this, "wasOffline", false);
|
|
1582
1909
|
/** Current user as an observable */
|
|
1583
1910
|
__publicField(this, "user$", new BehaviorSubject(void 0));
|
|
@@ -1587,21 +1914,12 @@ class Auth {
|
|
|
1587
1914
|
if (status === "offline" || status === "unavailable") this.wasOffline = true;
|
|
1588
1915
|
if (status !== "online" || !this.wasOffline) return;
|
|
1589
1916
|
this.wasOffline = false;
|
|
1590
|
-
const wasUnlocked = this.offlineUnlocked;
|
|
1591
1917
|
const previousUser = this.user;
|
|
1592
|
-
this.offlineUnlocked = false;
|
|
1593
|
-
this.user = void 0;
|
|
1594
1918
|
try {
|
|
1595
|
-
|
|
1596
|
-
const identity2 = this.api.jwtPayload;
|
|
1597
|
-
if (!user || user.guest || !identity2 || !hasOfflineLogin(identity2.realm, identity2.uid))
|
|
1598
|
-
forgetOfflineLogin();
|
|
1599
|
-
this.user = user;
|
|
1919
|
+
this.user = await this.current(this.api.token, true);
|
|
1600
1920
|
} catch (error) {
|
|
1601
|
-
if (this.api.status
|
|
1602
|
-
else {
|
|
1921
|
+
if (this.api.status !== "unauthorized") {
|
|
1603
1922
|
this.wasOffline = true;
|
|
1604
|
-
this.offlineUnlocked = wasUnlocked;
|
|
1605
1923
|
this.user = previousUser;
|
|
1606
1924
|
}
|
|
1607
1925
|
console.error("Unable to recheck the Datalynk user", error);
|
|
@@ -1618,11 +1936,22 @@ class Auth {
|
|
|
1618
1936
|
});
|
|
1619
1937
|
if ((_a = this.api.options.offline) == null ? void 0 : _a.length)
|
|
1620
1938
|
this.user$.pipe(filter((u) => u !== void 0)).subscribe((u) => {
|
|
1621
|
-
if (u == null)
|
|
1622
|
-
|
|
1623
|
-
|
|
1939
|
+
if (u == null && !this.api.online) return;
|
|
1940
|
+
const vault = this.api.vault;
|
|
1941
|
+
if (vault) vault.setProfile(u).catch((error) => console.warn("Unable to cache the Datalynk user", error));
|
|
1942
|
+
else if (u == null) localStorage.removeItem("datalynk-user");
|
|
1943
|
+
else localStorage.setItem("datalynk-user", JSON.stringify(u));
|
|
1624
1944
|
});
|
|
1625
1945
|
}
|
|
1946
|
+
/** Offline login/encryption is active for this app */
|
|
1947
|
+
get vault() {
|
|
1948
|
+
var _a;
|
|
1949
|
+
return ((_a = this.api.vault) == null ? void 0 : _a.enabled) ? this.api.vault : void 0;
|
|
1950
|
+
}
|
|
1951
|
+
/** This session may use offline data (always true when offline login is opted out) */
|
|
1952
|
+
get offlineUnlocked() {
|
|
1953
|
+
return !this.vault || this.vault.unlocked;
|
|
1954
|
+
}
|
|
1626
1955
|
/** Current user */
|
|
1627
1956
|
get user() {
|
|
1628
1957
|
return this.user$.getValue();
|
|
@@ -1640,14 +1969,18 @@ class Auth {
|
|
|
1640
1969
|
*
|
|
1641
1970
|
* @return {Promise<User | null>}
|
|
1642
1971
|
*/
|
|
1643
|
-
async current(token = this.api.token) {
|
|
1644
|
-
var _a;
|
|
1972
|
+
async current(token = this.api.token, refresh = false) {
|
|
1973
|
+
var _a, _b, _c;
|
|
1645
1974
|
if (!token) return null;
|
|
1646
|
-
else if (token == ((_a = this.user) == null ? void 0 : _a.token)) return this.user;
|
|
1647
|
-
else if (!this.api.online
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1975
|
+
else if (!refresh && token == ((_a = this.user) == null ? void 0 : _a.token)) return this.user;
|
|
1976
|
+
else if (!this.api.online) {
|
|
1977
|
+
if (!this.offlineUnlocked || !((_b = this.api.options.offline) == null ? void 0 : _b.length)) return null;
|
|
1978
|
+
if (this.api.vault) return this.api.vault.getProfile();
|
|
1979
|
+
return null;
|
|
1980
|
+
}
|
|
1981
|
+
const user = await this.api.request([{ "$/auth/current": {} }, { "$/env/me": {} }], { token }).then((resp) => resp[0] || resp[1] ? { ...resp[0], ...resp[1] } : null);
|
|
1982
|
+
if (user && !user.guest && this.api.jwtPayload) (_c = this.vault) == null ? void 0 : _c.touch(this.api.jwtPayload.realm, this.api.jwtPayload.uid);
|
|
1983
|
+
return user;
|
|
1651
1984
|
}
|
|
1652
1985
|
/**
|
|
1653
1986
|
* Automatically handle sessions by checking localStorage & URL parameters for a token & prompting
|
|
@@ -1658,16 +1991,17 @@ class Auth {
|
|
|
1658
1991
|
* @return {Promise<void>} Login complete
|
|
1659
1992
|
*/
|
|
1660
1993
|
async handleLogin(spoke, options) {
|
|
1661
|
-
var _a, _b, _c, _d;
|
|
1994
|
+
var _a, _b, _c, _d, _e;
|
|
1662
1995
|
if (this.onlinePrompt) window.removeEventListener("online", this.onlinePrompt);
|
|
1663
1996
|
if ((_a = this.api.options.offline) == null ? void 0 : _a.length) window.removeEventListener("online", this.onlinePrompt = () => this.handleLogin(spoke, options));
|
|
1997
|
+
await ((_b = this.api.vault) == null ? void 0 : _b.ready);
|
|
1664
1998
|
const urlParams = new URLSearchParams(location.search);
|
|
1665
1999
|
const urlToken = urlParams.getAll("datalynkToken").filter(Boolean).pop();
|
|
1666
2000
|
if (urlToken) {
|
|
1667
2001
|
this.api.token = urlToken;
|
|
1668
2002
|
urlParams.delete("datalynkToken");
|
|
1669
2003
|
location.href = location.pathname + (urlParams.toString() ? "?" + urlParams.toString() : "") + location.hash;
|
|
1670
|
-
} else if (
|
|
2004
|
+
} else if (this.isOffline() && this.vault && !this.vault.unlocked) {
|
|
1671
2005
|
await this.loginPrompt(spoke, options).done;
|
|
1672
2006
|
return;
|
|
1673
2007
|
} else if (this.api.reauthenticationPending && !this.api.online && ((_c = this.api.jwtPayload) == null ? void 0 : _c.realm) == spoke) {
|
|
@@ -1676,6 +2010,8 @@ class Auth {
|
|
|
1676
2010
|
if (((_d = this.api.jwtPayload) == null ? void 0 : _d.realm) != spoke) {
|
|
1677
2011
|
this.api.token = null;
|
|
1678
2012
|
location.reload();
|
|
2013
|
+
} else if ((_e = this.vault) == null ? void 0 : _e.needsPassword) {
|
|
2014
|
+
await this.loginPrompt(spoke, options).done;
|
|
1679
2015
|
}
|
|
1680
2016
|
} else {
|
|
1681
2017
|
this.api.token = null;
|
|
@@ -1683,6 +2019,9 @@ class Auth {
|
|
|
1683
2019
|
location.reload();
|
|
1684
2020
|
}
|
|
1685
2021
|
}
|
|
2022
|
+
isOffline() {
|
|
2023
|
+
return !this.api.online || typeof navigator !== "undefined" && navigator.onLine === false;
|
|
2024
|
+
}
|
|
1686
2025
|
/**
|
|
1687
2026
|
* Check whether user has a token
|
|
1688
2027
|
*
|
|
@@ -1734,16 +2073,14 @@ class Auth {
|
|
|
1734
2073
|
* @returns {Promise<User>} User account
|
|
1735
2074
|
*/
|
|
1736
2075
|
async login(spoke, login, password, opts) {
|
|
1737
|
-
var _a
|
|
1738
|
-
if (
|
|
2076
|
+
var _a;
|
|
2077
|
+
if (this.vault && this.isOffline()) {
|
|
1739
2078
|
const identity2 = this.api.jwtPayload;
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
const user =
|
|
1744
|
-
if (!
|
|
1745
|
-
throw new Error("Offline login is not available for this account on this device.");
|
|
1746
|
-
this.offlineUnlocked = true;
|
|
2079
|
+
if (!identity2 || identity2.realm !== spoke) throw new Error(offlineLoginMessages.none);
|
|
2080
|
+
const result = await this.vault.unlockOffline(spoke, login, identity2.uid, password.trim());
|
|
2081
|
+
if (result !== "ok") throw new Error(offlineLoginMessages[result]);
|
|
2082
|
+
const user = await this.vault.getProfile();
|
|
2083
|
+
if (!user) throw new Error(offlineLoginMessages.none);
|
|
1747
2084
|
this.user = user;
|
|
1748
2085
|
return user;
|
|
1749
2086
|
}
|
|
@@ -1764,8 +2101,8 @@ class Auth {
|
|
|
1764
2101
|
const data = await resp.json().catch(() => ({}));
|
|
1765
2102
|
if (!resp.ok || data["error"]) throw Object.assign(errorFromCode(resp.status, data.error) || {}, data);
|
|
1766
2103
|
this.api.token = data["token"];
|
|
1767
|
-
if (
|
|
1768
|
-
await
|
|
2104
|
+
if (this.vault && ((_a = this.api.jwtPayload) == null ? void 0 : _a.uid) != null)
|
|
2105
|
+
await this.vault.rememberLogin(spoke, login, this.api.jwtPayload.uid, password.trim()).catch((error) => console.error("Unable to enable offline login", error));
|
|
1769
2106
|
return data;
|
|
1770
2107
|
}
|
|
1771
2108
|
/**
|
|
@@ -1797,8 +2134,9 @@ class Auth {
|
|
|
1797
2134
|
* @return {Promise<{closed: string, new: string}>}
|
|
1798
2135
|
*/
|
|
1799
2136
|
logout(reload = true) {
|
|
2137
|
+
var _a;
|
|
1800
2138
|
localStorage.removeItem("datalynk-user");
|
|
1801
|
-
|
|
2139
|
+
void ((_a = this.api.vault) == null ? void 0 : _a.forget());
|
|
1802
2140
|
return this.api.request({ "$/auth/logout": {} }).then((resp) => {
|
|
1803
2141
|
if (reload) location.reload();
|
|
1804
2142
|
this.api.token = null;
|
|
@@ -2746,15 +3084,17 @@ const _Slice = class _Slice {
|
|
|
2746
3084
|
const lowest = resp.reduce((acc, r) => Math.min(acc, r.id), 0);
|
|
2747
3085
|
if (lowest < _Slice.nextId) _Slice.nextId = lowest;
|
|
2748
3086
|
}).finally(() => this.loaded = true);
|
|
2749
|
-
this.cache$.pipe(skip(1)).subscribe(
|
|
3087
|
+
this.cache$.pipe(skip(1)).subscribe((cache) => {
|
|
3088
|
+
const table = this.table;
|
|
3089
|
+
if (!table) return;
|
|
3090
|
+
const save = typeof table.replace == "function" ? table.replace(cache.map((c) => [c.id, c])) : table.clear().then(() => Promise.all(cache.map((c) => table.put(c.id, c))));
|
|
3091
|
+
save.catch((error) => console.warn("Unable to save offline slice cache", error));
|
|
3092
|
+
});
|
|
3093
|
+
this.api.token$.pipe(filter((token) => !!token), take(1)).subscribe(async () => {
|
|
2750
3094
|
var _a2;
|
|
2751
|
-
await ((_a2 = this.
|
|
2752
|
-
|
|
2753
|
-
var _a3;
|
|
2754
|
-
return (_a3 = this.table) == null ? void 0 : _a3.put(c.id, c);
|
|
2755
|
-
}));
|
|
3095
|
+
await ((_a2 = this.api.vault) == null ? void 0 : _a2.whenUnlocked());
|
|
3096
|
+
this.sync();
|
|
2756
3097
|
});
|
|
2757
|
-
this.sync();
|
|
2758
3098
|
this.api.online$.pipe(distinctUntilChanged()).subscribe(async (online) => {
|
|
2759
3099
|
if (!online) return;
|
|
2760
3100
|
if (this.api.expired) await lastValueFrom(this.api.auth.user$.pipe(skip(1), takeWhile((u) => !u || this.api.expired, true)));
|
|
@@ -3406,7 +3746,7 @@ class Superuser {
|
|
|
3406
3746
|
} });
|
|
3407
3747
|
}
|
|
3408
3748
|
}
|
|
3409
|
-
const version = "1.6.
|
|
3749
|
+
const version = "1.6.4";
|
|
3410
3750
|
class WebRtc {
|
|
3411
3751
|
constructor(api) {
|
|
3412
3752
|
__publicField(this, "ice");
|
|
@@ -3996,6 +4336,8 @@ const _Api = class _Api {
|
|
|
3996
4336
|
__publicField(this, "webrtc");
|
|
3997
4337
|
/** Offline database */
|
|
3998
4338
|
__publicField(this, "database");
|
|
4339
|
+
/** Offline login + offline cache encryption */
|
|
4340
|
+
__publicField(this, "vault");
|
|
3999
4341
|
/** Options */
|
|
4000
4342
|
__publicField(this, "options");
|
|
4001
4343
|
/** Created slices */
|
|
@@ -4021,7 +4363,7 @@ const _Api = class _Api {
|
|
|
4021
4363
|
__publicField(this, "online$", new BehaviorSubject(this.initialOnline));
|
|
4022
4364
|
/** API Session token */
|
|
4023
4365
|
__publicField(this, "token$", new BehaviorSubject(void 0));
|
|
4024
|
-
var _a, _b;
|
|
4366
|
+
var _a, _b, _c;
|
|
4025
4367
|
this.origin = origin;
|
|
4026
4368
|
this.url = `${new URL(origin).origin}/api/`;
|
|
4027
4369
|
const development = this.isDevelopmentEnvironment(origin);
|
|
@@ -4045,6 +4387,11 @@ const _Api = class _Api {
|
|
|
4045
4387
|
}
|
|
4046
4388
|
};
|
|
4047
4389
|
if (development) this.options.watchTokenExpiry = false;
|
|
4390
|
+
const offlineLogin = this.options.offlineLogin ?? true;
|
|
4391
|
+
this.vault = new OfflineVault(
|
|
4392
|
+
!!((_a = this.options.offline) == null ? void 0 : _a.length) && !!this.options.saveSession && offlineLogin !== false,
|
|
4393
|
+
typeof offlineLogin == "object" ? offlineLogin : {}
|
|
4394
|
+
);
|
|
4048
4395
|
if (this.options.saveSession && typeof localStorage != "undefined") {
|
|
4049
4396
|
this.token = localStorage.getItem(this.localStorageKey) || null;
|
|
4050
4397
|
this.token$.pipe(distinctUntilChanged()).subscribe((token) => {
|
|
@@ -4076,6 +4423,7 @@ const _Api = class _Api {
|
|
|
4076
4423
|
this.webrtc = new WebRtc(this);
|
|
4077
4424
|
if (typeof indexedDB != "undefined") {
|
|
4078
4425
|
this.database = new Database("datalynk", ["pending", ...this.options.offline || []]);
|
|
4426
|
+
this.database.cipher = this.vault;
|
|
4079
4427
|
if (this.options.replayOfflineQueue !== false) this.online$.subscribe((online) => {
|
|
4080
4428
|
if (online) void this.replayPendingQueue();
|
|
4081
4429
|
});
|
|
@@ -4085,10 +4433,10 @@ const _Api = class _Api {
|
|
|
4085
4433
|
window.addEventListener("offline", () => this.setConnectionStatus("offline"));
|
|
4086
4434
|
this.startHeartbeat();
|
|
4087
4435
|
}
|
|
4088
|
-
if ((
|
|
4436
|
+
if ((_b = this.options.offline) == null ? void 0 : _b.length) {
|
|
4089
4437
|
this.pwa.setup();
|
|
4090
4438
|
if (typeof indexedDB == "undefined") throw new Error("Cannot enable offline support, indexedDB is not available in this environment");
|
|
4091
|
-
(
|
|
4439
|
+
(_c = this.options.offline) == null ? void 0 : _c.forEach((id) => this.slice(id));
|
|
4092
4440
|
if (this.options.serviceWorker && typeof navigator["serviceWorker"] != "undefined") {
|
|
4093
4441
|
(async () => {
|
|
4094
4442
|
let initialized = false;
|
|
@@ -4400,7 +4748,7 @@ const _Api = class _Api {
|
|
|
4400
4748
|
} catch {
|
|
4401
4749
|
throw new UnexpectedApiResponseError(resp, body);
|
|
4402
4750
|
}
|
|
4403
|
-
if (resp.status === 401) this.markUnauthorized(token);
|
|
4751
|
+
if (resp.status === 401 && token) this.markUnauthorized(token);
|
|
4404
4752
|
if (!resp.ok || (data == null ? void 0 : data.error)) {
|
|
4405
4753
|
const error = Object.assign(errorFromCode(resp.status, data == null ? void 0 : data.error), data);
|
|
4406
4754
|
Object.defineProperty(error, "response", { value: resp, configurable: true });
|
|
@@ -4652,9 +5000,10 @@ const _Api = class _Api {
|
|
|
4652
5000
|
}));
|
|
4653
5001
|
}
|
|
4654
5002
|
async finishUnauthorizedLogout(preservePending = false) {
|
|
4655
|
-
var _a;
|
|
5003
|
+
var _a, _b, _c;
|
|
4656
5004
|
if (this.options.manageSession === false) return;
|
|
4657
|
-
|
|
5005
|
+
if (preservePending) await ((_a = this.vault) == null ? void 0 : _a.lock());
|
|
5006
|
+
else await ((_b = this.vault) == null ? void 0 : _b.forget());
|
|
4658
5007
|
if (typeof localStorage != "undefined") {
|
|
4659
5008
|
localStorage.removeItem(this.localStorageKey);
|
|
4660
5009
|
localStorage.removeItem("datalynk-user");
|
|
@@ -4675,7 +5024,7 @@ const _Api = class _Api {
|
|
|
4675
5024
|
console.error("Unable to clear Datalynk service worker cache", error);
|
|
4676
5025
|
}
|
|
4677
5026
|
}
|
|
4678
|
-
if (typeof navigator != "undefined" && ((
|
|
5027
|
+
if (typeof navigator != "undefined" && ((_c = navigator.serviceWorker) == null ? void 0 : _c.controller)) {
|
|
4679
5028
|
navigator.serviceWorker.controller.postMessage({ token: null });
|
|
4680
5029
|
}
|
|
4681
5030
|
if (typeof location != "undefined" && typeof location.reload == "function")
|
|
@@ -4816,7 +5165,10 @@ const _Api = class _Api {
|
|
|
4816
5165
|
if (!this.online || this.authenticationInvalid || this.offlineAuthenticationExpired) break;
|
|
4817
5166
|
if (owner && !this.ownersMatch(owner, this.tokenIdentity(this.token))) break;
|
|
4818
5167
|
const request = await table.get(key);
|
|
4819
|
-
if (request == null)
|
|
5168
|
+
if (request == null) {
|
|
5169
|
+
await table.delete(key);
|
|
5170
|
+
continue;
|
|
5171
|
+
}
|
|
4820
5172
|
try {
|
|
4821
5173
|
await this.request(request);
|
|
4822
5174
|
await table.delete(key);
|
|
@@ -4942,7 +5294,6 @@ const _Api = class _Api {
|
|
|
4942
5294
|
});
|
|
4943
5295
|
}
|
|
4944
5296
|
request(data, options = {}) {
|
|
4945
|
-
var _a, _b;
|
|
4946
5297
|
data = typeof data == "string" ? { [data]: {} } : data;
|
|
4947
5298
|
let key = JSON.stringify(data);
|
|
4948
5299
|
if (this.offline && typeof navigator != "undefined") {
|
|
@@ -4950,8 +5301,8 @@ const _Api = class _Api {
|
|
|
4950
5301
|
if (options.offline) {
|
|
4951
5302
|
if (!this.ensureOfflineSessionOwner(this.token))
|
|
4952
5303
|
return Promise.reject(errorFromCode(409, "Offline queue belongs to another authenticated user"));
|
|
4953
|
-
|
|
4954
|
-
return
|
|
5304
|
+
if (!this.database) return Promise.reject(new Error("Offline queue storage is unavailable"));
|
|
5305
|
+
return this.database.table("pending").add(data, key).then(() => void 0);
|
|
4955
5306
|
}
|
|
4956
5307
|
return Promise.reject(errorFromCode(503, this.status === "unavailable" ? "Datalynk is unavailable" : "You are offline"));
|
|
4957
5308
|
}
|