@auxilium/datalynk-client 1.6.3 → 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 +438 -98
- package/dist/index.mjs +438 -98
- package/dist/offline-auth.d.ts +68 -4
- package/dist/offline-auth.d.ts.map +1 -1
- package/dist/service.worker.mjs +2 -1
- 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.cjs
CHANGED
|
@@ -193,6 +193,8 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
193
193
|
constructor(database, tables, version2) {
|
|
194
194
|
__publicField(this, "connection");
|
|
195
195
|
__publicField(this, "tables");
|
|
196
|
+
/** Encrypts values at rest when set */
|
|
197
|
+
__publicField(this, "cipher");
|
|
196
198
|
this.database = database;
|
|
197
199
|
this.version = version2;
|
|
198
200
|
this.tables = tables.map((table) => typeof table === "object" ? { ...table, name: table.name.toString() } : { name: table.toString() });
|
|
@@ -258,17 +260,29 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
258
260
|
}
|
|
259
261
|
});
|
|
260
262
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
+
seal(value) {
|
|
264
|
+
const cipher = this.database.cipher;
|
|
265
|
+
return (cipher == null ? void 0 : cipher.enabled) ? cipher.seal(value) : Promise.resolve(value);
|
|
263
266
|
}
|
|
264
|
-
|
|
265
|
-
|
|
267
|
+
open(value) {
|
|
268
|
+
const cipher = this.database.cipher;
|
|
269
|
+
return cipher ? cipher.open(value) : Promise.resolve(value);
|
|
266
270
|
}
|
|
267
|
-
|
|
268
|
-
|
|
271
|
+
async add(value, key) {
|
|
272
|
+
const sealed = await this.seal(value);
|
|
273
|
+
return this.tx((store) => store.add(sealed, key));
|
|
269
274
|
}
|
|
270
|
-
|
|
271
|
-
|
|
275
|
+
async put(key, value) {
|
|
276
|
+
const sealed = await this.seal(value);
|
|
277
|
+
return this.tx((store) => store.put(sealed, key));
|
|
278
|
+
}
|
|
279
|
+
async get(key) {
|
|
280
|
+
return await this.open(await this.tx((store) => store.get(key), true));
|
|
281
|
+
}
|
|
282
|
+
/** All readable values. Values sealed with another key are skipped. */
|
|
283
|
+
async getAll() {
|
|
284
|
+
const values = await Promise.all((await this.tx((store) => store.getAll(), true)).map((v) => this.open(v)));
|
|
285
|
+
return values.filter((v) => v !== void 0);
|
|
272
286
|
}
|
|
273
287
|
getAllKeys() {
|
|
274
288
|
return this.tx((store) => store.getAllKeys(), true);
|
|
@@ -279,6 +293,20 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
279
293
|
clear() {
|
|
280
294
|
return this.tx((store) => store.clear());
|
|
281
295
|
}
|
|
296
|
+
/** Replace the whole table in one transaction (values are sealed first, so nothing is lost while locked) */
|
|
297
|
+
async replace(entries) {
|
|
298
|
+
const sealed = await Promise.all(entries.map(async ([key, value]) => [key, await this.seal(value)]));
|
|
299
|
+
const db = await this.database.connection;
|
|
300
|
+
await new Promise((resolve, reject) => {
|
|
301
|
+
const tx = db.transaction(this.name, "readwrite");
|
|
302
|
+
const store = tx.objectStore(this.name);
|
|
303
|
+
store.clear();
|
|
304
|
+
sealed.forEach(([key, value]) => store.put(value, key));
|
|
305
|
+
tx.oncomplete = () => resolve();
|
|
306
|
+
tx.onerror = () => reject(tx.error);
|
|
307
|
+
tx.onabort = () => reject(tx.error);
|
|
308
|
+
});
|
|
309
|
+
}
|
|
282
310
|
count() {
|
|
283
311
|
return this.tx((store) => store.count(), true);
|
|
284
312
|
}
|
|
@@ -1032,6 +1060,9 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
1032
1060
|
};
|
|
1033
1061
|
return BehaviorSubject2;
|
|
1034
1062
|
}(Subject);
|
|
1063
|
+
var EMPTY = new Observable(function(subscriber) {
|
|
1064
|
+
return subscriber.complete();
|
|
1065
|
+
});
|
|
1035
1066
|
var EmptyError = createErrorClass(function(_super) {
|
|
1036
1067
|
return function EmptyErrorImpl() {
|
|
1037
1068
|
_super(this);
|
|
@@ -1059,6 +1090,23 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
1059
1090
|
});
|
|
1060
1091
|
});
|
|
1061
1092
|
}
|
|
1093
|
+
function firstValueFrom(source, config2) {
|
|
1094
|
+
return new Promise(function(resolve, reject) {
|
|
1095
|
+
var subscriber = new SafeSubscriber({
|
|
1096
|
+
next: function(value) {
|
|
1097
|
+
resolve(value);
|
|
1098
|
+
subscriber.unsubscribe();
|
|
1099
|
+
},
|
|
1100
|
+
error: reject,
|
|
1101
|
+
complete: function() {
|
|
1102
|
+
{
|
|
1103
|
+
reject(new EmptyError());
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
});
|
|
1107
|
+
source.subscribe(subscriber);
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
1062
1110
|
function filter(predicate, thisArg) {
|
|
1063
1111
|
return operate(function(source, subscriber) {
|
|
1064
1112
|
var index = 0;
|
|
@@ -1067,6 +1115,21 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
1067
1115
|
}));
|
|
1068
1116
|
});
|
|
1069
1117
|
}
|
|
1118
|
+
function take(count) {
|
|
1119
|
+
return count <= 0 ? function() {
|
|
1120
|
+
return EMPTY;
|
|
1121
|
+
} : operate(function(source, subscriber) {
|
|
1122
|
+
var seen = 0;
|
|
1123
|
+
source.subscribe(createOperatorSubscriber(subscriber, function(value) {
|
|
1124
|
+
if (++seen <= count) {
|
|
1125
|
+
subscriber.next(value);
|
|
1126
|
+
if (count <= seen) {
|
|
1127
|
+
subscriber.complete();
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
}));
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1070
1133
|
function distinctUntilChanged(comparator, keySelector) {
|
|
1071
1134
|
if (keySelector === void 0) {
|
|
1072
1135
|
keySelector = identity;
|
|
@@ -1548,52 +1611,304 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
1548
1611
|
`;
|
|
1549
1612
|
});
|
|
1550
1613
|
let LoginPrompt = _LoginPrompt;
|
|
1551
|
-
const
|
|
1614
|
+
const RECORD_KEY = "datalynk-offline-login";
|
|
1615
|
+
const PROFILE_KEY = "datalynk-user";
|
|
1616
|
+
const VAULT_DB = "datalynk-vault";
|
|
1617
|
+
const VAULT_STORE = "keys";
|
|
1618
|
+
const DEVICE_KEY = "device";
|
|
1552
1619
|
const ITERATIONS = 31e4;
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
}
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
}
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
try {
|
|
1571
|
-
record = JSON.parse(saved);
|
|
1572
|
-
} catch {
|
|
1573
|
-
return false;
|
|
1574
|
-
}
|
|
1575
|
-
if (record.realm !== realm || record.login !== login.trim().toLowerCase() || record.uid !== uid || !Array.isArray(record.salt) || !Array.isArray(record.hash)) return false;
|
|
1576
|
-
const candidate = await hash(password, new Uint8Array(record.salt));
|
|
1577
|
-
if (candidate.length !== record.hash.length) return false;
|
|
1578
|
-
let difference = 0;
|
|
1579
|
-
for (let i = 0; i < candidate.length; i++) difference |= candidate[i] ^ record.hash[i];
|
|
1580
|
-
return difference === 0;
|
|
1620
|
+
const DAY = 864e5;
|
|
1621
|
+
const offlineLoginMessages = {
|
|
1622
|
+
"none": "Offline login is not available on this device. Connect to the internet to sign in.",
|
|
1623
|
+
"wrong-user": "Offline login is only available for the last user who signed in on this device.",
|
|
1624
|
+
"expired": "Offline login has expired on this device. Connect to the internet to sign in.",
|
|
1625
|
+
"locked": "Offline login is locked after too many attempts. Connect to the internet to sign in.",
|
|
1626
|
+
"bad-password": "Incorrect password."
|
|
1627
|
+
};
|
|
1628
|
+
const toB64 = (bytes) => {
|
|
1629
|
+
let out = "";
|
|
1630
|
+
new Uint8Array(bytes).forEach((b) => out += String.fromCharCode(b));
|
|
1631
|
+
return btoa(out);
|
|
1632
|
+
};
|
|
1633
|
+
const fromB64 = (text) => Uint8Array.from(atob(text), (c) => c.charCodeAt(0));
|
|
1634
|
+
const normalizeLogin = (login) => (login || "").trim().toLowerCase();
|
|
1635
|
+
function isSealed(value) {
|
|
1636
|
+
return !!value && typeof value == "object" && value.__dlv === 1 && "iv" in value && "ct" in value;
|
|
1581
1637
|
}
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1638
|
+
class OfflineVault {
|
|
1639
|
+
constructor(enabled, options = {}, startOffline) {
|
|
1640
|
+
/** False when offline login is disabled or the environment lacks WebCrypto/IndexedDB/localStorage */
|
|
1641
|
+
__publicField(this, "enabled");
|
|
1642
|
+
/** True once the data key is available in memory */
|
|
1643
|
+
__publicField(this, "unlocked$", new BehaviorSubject(false));
|
|
1644
|
+
/** Resolves once startup key loading has finished */
|
|
1645
|
+
__publicField(this, "ready");
|
|
1646
|
+
__publicField(this, "dek", null);
|
|
1647
|
+
__publicField(this, "days");
|
|
1648
|
+
__publicField(this, "attempts");
|
|
1649
|
+
this.enabled = enabled && typeof crypto != "undefined" && !!crypto.subtle && typeof indexedDB != "undefined" && typeof localStorage != "undefined";
|
|
1650
|
+
this.days = options.days ?? 7;
|
|
1651
|
+
this.attempts = options.attempts ?? 5;
|
|
1652
|
+
if (startOffline === void 0) startOffline = typeof navigator != "undefined" && navigator.onLine === false;
|
|
1653
|
+
this.ready = this.enabled ? this.init(startOffline).catch((error) => {
|
|
1654
|
+
console.error("Unable to open the Datalynk offline vault", error);
|
|
1655
|
+
}) : Promise.resolve();
|
|
1656
|
+
}
|
|
1657
|
+
get unlocked() {
|
|
1658
|
+
return this.unlocked$.getValue();
|
|
1659
|
+
}
|
|
1660
|
+
/** A saved offline login exists, but this session has not unlocked it yet */
|
|
1661
|
+
get needsPassword() {
|
|
1662
|
+
return this.enabled && !this.unlocked && !!this.readRecord();
|
|
1663
|
+
}
|
|
1664
|
+
/** Wait until the data key is available (resolves immediately when the vault is disabled) */
|
|
1665
|
+
whenUnlocked() {
|
|
1666
|
+
if (!this.enabled || this.unlocked) return Promise.resolve(true);
|
|
1667
|
+
return firstValueFrom(this.unlocked$.pipe(filter(Boolean)));
|
|
1668
|
+
}
|
|
1669
|
+
/** Saved offline login matches this user */
|
|
1670
|
+
has(realm, uid) {
|
|
1671
|
+
const record = this.readRecord();
|
|
1672
|
+
return !!record && record.realm === realm && record.uid === uid;
|
|
1673
|
+
}
|
|
1674
|
+
/**
|
|
1675
|
+
* Called after a successful online password login. Wraps the data key with the
|
|
1676
|
+
* password so this user can sign in offline later.
|
|
1677
|
+
*/
|
|
1678
|
+
async rememberLogin(realm, login, uid, password) {
|
|
1679
|
+
if (!this.enabled) return;
|
|
1680
|
+
await this.ready;
|
|
1681
|
+
const record = this.readRecord();
|
|
1682
|
+
let dek = null;
|
|
1683
|
+
if (record && record.realm === realm && record.uid === uid) {
|
|
1684
|
+
dek = await this.unwrapWithPassword(record, password).catch(() => null);
|
|
1685
|
+
if (!dek) dek = this.dek;
|
|
1686
|
+
} else if (!record) {
|
|
1687
|
+
dek = this.dek;
|
|
1688
|
+
}
|
|
1689
|
+
if (!dek) dek = await this.newDataKey();
|
|
1690
|
+
const salt = crypto.getRandomValues(new Uint8Array(16));
|
|
1691
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
1692
|
+
const kek = await this.passwordKey(password, salt);
|
|
1693
|
+
const wrapped = await crypto.subtle.wrapKey("raw", dek, kek, { name: "AES-GCM", iv });
|
|
1694
|
+
this.writeRecord({
|
|
1695
|
+
v: 2,
|
|
1696
|
+
realm,
|
|
1697
|
+
uid,
|
|
1698
|
+
login: normalizeLogin(login),
|
|
1699
|
+
salt: toB64(salt),
|
|
1700
|
+
iv: toB64(iv),
|
|
1701
|
+
wrapped: toB64(wrapped),
|
|
1702
|
+
verifiedAt: Date.now(),
|
|
1703
|
+
failures: 0
|
|
1704
|
+
});
|
|
1705
|
+
await this.setKey(dek);
|
|
1706
|
+
}
|
|
1707
|
+
/** Try to unlock offline with a password */
|
|
1708
|
+
async unlockOffline(realm, login, uid, password) {
|
|
1709
|
+
if (!this.enabled) return "none";
|
|
1710
|
+
await this.ready;
|
|
1711
|
+
const record = this.readRecord();
|
|
1712
|
+
if (!record) return "none";
|
|
1713
|
+
if (record.realm !== realm || record.login !== normalizeLogin(login) || uid != null && record.uid !== uid) return "wrong-user";
|
|
1714
|
+
if (record.locked) return "locked";
|
|
1715
|
+
if (Date.now() - record.verifiedAt > this.days * DAY) return "expired";
|
|
1716
|
+
try {
|
|
1717
|
+
const dek = await this.unwrapWithPassword(record, password);
|
|
1718
|
+
this.writeRecord({ ...record, failures: 0 });
|
|
1719
|
+
await this.setKey(dek);
|
|
1720
|
+
return "ok";
|
|
1721
|
+
} catch {
|
|
1722
|
+
const failures = (record.failures || 0) + 1;
|
|
1723
|
+
const locked = failures >= this.attempts;
|
|
1724
|
+
this.writeRecord({ ...record, failures, locked });
|
|
1725
|
+
return locked ? "locked" : "bad-password";
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
/** The server confirmed this user's session: restart the offline window */
|
|
1729
|
+
touch(realm, uid) {
|
|
1730
|
+
if (!this.enabled || realm == null || uid == null) return;
|
|
1731
|
+
const record = this.readRecord();
|
|
1732
|
+
if (record && record.realm === realm && record.uid === uid) this.writeRecord({ ...record, verifiedAt: Date.now() });
|
|
1733
|
+
}
|
|
1734
|
+
/** Drop the in-memory and device keys but keep the password copy (queued work stays recoverable) */
|
|
1735
|
+
async lock() {
|
|
1736
|
+
this.dek = null;
|
|
1737
|
+
if (this.unlocked) this.unlocked$.next(false);
|
|
1738
|
+
if (this.enabled) await this.deleteDevice();
|
|
1739
|
+
}
|
|
1740
|
+
/** Remove offline login completely; anything sealed becomes unreadable */
|
|
1741
|
+
async forget() {
|
|
1742
|
+
if (typeof localStorage != "undefined") {
|
|
1743
|
+
localStorage.removeItem(RECORD_KEY);
|
|
1744
|
+
localStorage.removeItem(PROFILE_KEY);
|
|
1745
|
+
}
|
|
1746
|
+
await this.lock();
|
|
1747
|
+
}
|
|
1748
|
+
/** Encrypt a value for IndexedDB (passes through when disabled) */
|
|
1749
|
+
async seal(value) {
|
|
1750
|
+
if (!this.enabled || value === void 0) return value;
|
|
1751
|
+
await this.whenUnlocked();
|
|
1752
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
1753
|
+
const ct = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, this.dek, new TextEncoder().encode(JSON.stringify(value)));
|
|
1754
|
+
return { __dlv: 1, iv, ct };
|
|
1755
|
+
}
|
|
1756
|
+
/** Decrypt a value from IndexedDB. Unreadable values (old key / other user) return undefined. */
|
|
1757
|
+
async open(value) {
|
|
1758
|
+
if (!isSealed(value)) return value;
|
|
1759
|
+
if (!this.enabled) return void 0;
|
|
1760
|
+
await this.whenUnlocked();
|
|
1761
|
+
try {
|
|
1762
|
+
const iv = typeof value.iv == "string" ? fromB64(value.iv) : value.iv;
|
|
1763
|
+
const ct = typeof value.ct == "string" ? fromB64(value.ct) : value.ct;
|
|
1764
|
+
const plain = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, this.dek, ct);
|
|
1765
|
+
return JSON.parse(new TextDecoder().decode(plain));
|
|
1766
|
+
} catch {
|
|
1767
|
+
return void 0;
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
/** Save the cached user profile (sealed when enabled) */
|
|
1771
|
+
async setProfile(user) {
|
|
1772
|
+
if (typeof localStorage == "undefined") return;
|
|
1773
|
+
if (user == null) return localStorage.removeItem(PROFILE_KEY);
|
|
1774
|
+
if (!this.enabled) return localStorage.setItem(PROFILE_KEY, JSON.stringify(user));
|
|
1775
|
+
const sealed = await this.seal(user);
|
|
1776
|
+
localStorage.setItem(PROFILE_KEY, JSON.stringify({ __dlv: 1, iv: toB64(sealed.iv), ct: toB64(sealed.ct) }));
|
|
1777
|
+
}
|
|
1778
|
+
/** Read the cached user profile; null while locked */
|
|
1779
|
+
async getProfile() {
|
|
1780
|
+
if (typeof localStorage == "undefined") return null;
|
|
1781
|
+
let saved;
|
|
1782
|
+
try {
|
|
1783
|
+
saved = JSON.parse(localStorage.getItem(PROFILE_KEY) || "null");
|
|
1784
|
+
} catch {
|
|
1785
|
+
return null;
|
|
1786
|
+
}
|
|
1787
|
+
if (!isSealed(saved)) return this.enabled ? null : saved;
|
|
1788
|
+
if (!this.unlocked) return null;
|
|
1789
|
+
return await this.open(saved) ?? null;
|
|
1790
|
+
}
|
|
1791
|
+
// ----- internals -----
|
|
1792
|
+
async init(startOffline) {
|
|
1793
|
+
if (startOffline) {
|
|
1794
|
+
await this.deleteDevice();
|
|
1795
|
+
return;
|
|
1796
|
+
}
|
|
1797
|
+
const device = await this.readDevice();
|
|
1798
|
+
if (device) {
|
|
1799
|
+
try {
|
|
1800
|
+
const dek = await crypto.subtle.unwrapKey(
|
|
1801
|
+
"raw",
|
|
1802
|
+
device.wrapped,
|
|
1803
|
+
device.key,
|
|
1804
|
+
{ name: "AES-GCM", iv: device.iv },
|
|
1805
|
+
{ name: "AES-GCM" },
|
|
1806
|
+
true,
|
|
1807
|
+
["encrypt", "decrypt"]
|
|
1808
|
+
);
|
|
1809
|
+
this.dek = dek;
|
|
1810
|
+
this.unlocked$.next(true);
|
|
1811
|
+
return;
|
|
1812
|
+
} catch {
|
|
1813
|
+
await this.deleteDevice();
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
if (!this.readRecord()) await this.setKey(await this.newDataKey());
|
|
1817
|
+
}
|
|
1818
|
+
async setKey(dek) {
|
|
1819
|
+
this.dek = dek;
|
|
1820
|
+
try {
|
|
1821
|
+
await this.writeDevice(dek);
|
|
1822
|
+
} catch (error) {
|
|
1823
|
+
console.warn("Unable to store the Datalynk device key; offline data will need the password", error);
|
|
1824
|
+
}
|
|
1825
|
+
if (!this.unlocked) this.unlocked$.next(true);
|
|
1826
|
+
}
|
|
1827
|
+
newDataKey() {
|
|
1828
|
+
return crypto.subtle.generateKey({ name: "AES-GCM", length: 256 }, true, ["encrypt", "decrypt"]);
|
|
1829
|
+
}
|
|
1830
|
+
async passwordKey(password, salt) {
|
|
1831
|
+
const base = await crypto.subtle.importKey("raw", new TextEncoder().encode(password), "PBKDF2", false, ["deriveKey"]);
|
|
1832
|
+
return crypto.subtle.deriveKey(
|
|
1833
|
+
{ name: "PBKDF2", salt, iterations: ITERATIONS, hash: "SHA-256" },
|
|
1834
|
+
base,
|
|
1835
|
+
{ name: "AES-GCM", length: 256 },
|
|
1836
|
+
false,
|
|
1837
|
+
["wrapKey", "unwrapKey"]
|
|
1838
|
+
);
|
|
1839
|
+
}
|
|
1840
|
+
async unwrapWithPassword(record, password) {
|
|
1841
|
+
const kek = await this.passwordKey(password, fromB64(record.salt));
|
|
1842
|
+
return crypto.subtle.unwrapKey(
|
|
1843
|
+
"raw",
|
|
1844
|
+
fromB64(record.wrapped),
|
|
1845
|
+
kek,
|
|
1846
|
+
{ name: "AES-GCM", iv: fromB64(record.iv) },
|
|
1847
|
+
{ name: "AES-GCM" },
|
|
1848
|
+
true,
|
|
1849
|
+
["encrypt", "decrypt"]
|
|
1850
|
+
);
|
|
1851
|
+
}
|
|
1852
|
+
readRecord() {
|
|
1853
|
+
if (typeof localStorage == "undefined") return null;
|
|
1854
|
+
try {
|
|
1855
|
+
const record = JSON.parse(localStorage.getItem(RECORD_KEY) || "null");
|
|
1856
|
+
if ((record == null ? void 0 : record.v) === 2) return record;
|
|
1857
|
+
if (record) localStorage.removeItem(RECORD_KEY);
|
|
1858
|
+
} catch {
|
|
1859
|
+
localStorage.removeItem(RECORD_KEY);
|
|
1860
|
+
}
|
|
1861
|
+
return null;
|
|
1862
|
+
}
|
|
1863
|
+
writeRecord(record) {
|
|
1864
|
+
localStorage.setItem(RECORD_KEY, JSON.stringify(record));
|
|
1865
|
+
}
|
|
1866
|
+
vaultDb() {
|
|
1867
|
+
return new Promise((resolve, reject) => {
|
|
1868
|
+
const request = indexedDB.open(VAULT_DB, 1);
|
|
1869
|
+
request.onupgradeneeded = () => request.result.createObjectStore(VAULT_STORE);
|
|
1870
|
+
request.onsuccess = () => resolve(request.result);
|
|
1871
|
+
request.onerror = () => reject(request.error);
|
|
1872
|
+
});
|
|
1873
|
+
}
|
|
1874
|
+
async vaultTx(mode, op) {
|
|
1875
|
+
const db = await this.vaultDb();
|
|
1876
|
+
try {
|
|
1877
|
+
return await new Promise((resolve, reject) => {
|
|
1878
|
+
const tx = db.transaction(VAULT_STORE, mode);
|
|
1879
|
+
const request = op(tx.objectStore(VAULT_STORE));
|
|
1880
|
+
tx.oncomplete = () => resolve(request.result);
|
|
1881
|
+
tx.onerror = () => reject(tx.error);
|
|
1882
|
+
tx.onabort = () => reject(tx.error);
|
|
1883
|
+
});
|
|
1884
|
+
} finally {
|
|
1885
|
+
db.close();
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
async readDevice() {
|
|
1889
|
+
try {
|
|
1890
|
+
return await this.vaultTx("readonly", (store) => store.get(DEVICE_KEY)) || null;
|
|
1891
|
+
} catch {
|
|
1892
|
+
return null;
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
async writeDevice(dek) {
|
|
1896
|
+
const key = await crypto.subtle.generateKey({ name: "AES-GCM", length: 256 }, false, ["wrapKey", "unwrapKey"]);
|
|
1897
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
1898
|
+
const wrapped = await crypto.subtle.wrapKey("raw", dek, key, { name: "AES-GCM", iv });
|
|
1899
|
+
await this.vaultTx("readwrite", (store) => store.put({ key, iv, wrapped }, DEVICE_KEY));
|
|
1900
|
+
}
|
|
1901
|
+
async deleteDevice() {
|
|
1902
|
+
try {
|
|
1903
|
+
await this.vaultTx("readwrite", (store) => store.delete(DEVICE_KEY));
|
|
1904
|
+
} catch (error) {
|
|
1905
|
+
console.warn("Unable to remove the Datalynk device key", error);
|
|
1906
|
+
}
|
|
1588
1907
|
}
|
|
1589
|
-
}
|
|
1590
|
-
function forgetOfflineLogin() {
|
|
1591
|
-
localStorage.removeItem(KEY);
|
|
1592
1908
|
}
|
|
1593
1909
|
class Auth {
|
|
1594
1910
|
constructor(api) {
|
|
1595
1911
|
__publicField(this, "onlinePrompt");
|
|
1596
|
-
__publicField(this, "offlineUnlocked", false);
|
|
1597
1912
|
__publicField(this, "wasOffline", false);
|
|
1598
1913
|
/** Current user as an observable */
|
|
1599
1914
|
__publicField(this, "user$", new BehaviorSubject(void 0));
|
|
@@ -1603,21 +1918,12 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
1603
1918
|
if (status === "offline" || status === "unavailable") this.wasOffline = true;
|
|
1604
1919
|
if (status !== "online" || !this.wasOffline) return;
|
|
1605
1920
|
this.wasOffline = false;
|
|
1606
|
-
const wasUnlocked = this.offlineUnlocked;
|
|
1607
1921
|
const previousUser = this.user;
|
|
1608
|
-
this.offlineUnlocked = false;
|
|
1609
|
-
this.user = void 0;
|
|
1610
1922
|
try {
|
|
1611
|
-
|
|
1612
|
-
const identity2 = this.api.jwtPayload;
|
|
1613
|
-
if (!user || user.guest || !identity2 || !hasOfflineLogin(identity2.realm, identity2.uid))
|
|
1614
|
-
forgetOfflineLogin();
|
|
1615
|
-
this.user = user;
|
|
1923
|
+
this.user = await this.current(this.api.token, true);
|
|
1616
1924
|
} catch (error) {
|
|
1617
|
-
if (this.api.status
|
|
1618
|
-
else {
|
|
1925
|
+
if (this.api.status !== "unauthorized") {
|
|
1619
1926
|
this.wasOffline = true;
|
|
1620
|
-
this.offlineUnlocked = wasUnlocked;
|
|
1621
1927
|
this.user = previousUser;
|
|
1622
1928
|
}
|
|
1623
1929
|
console.error("Unable to recheck the Datalynk user", error);
|
|
@@ -1634,11 +1940,22 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
1634
1940
|
});
|
|
1635
1941
|
if ((_a = this.api.options.offline) == null ? void 0 : _a.length)
|
|
1636
1942
|
this.user$.pipe(filter((u) => u !== void 0)).subscribe((u) => {
|
|
1637
|
-
if (u == null)
|
|
1638
|
-
|
|
1639
|
-
|
|
1943
|
+
if (u == null && !this.api.online) return;
|
|
1944
|
+
const vault = this.api.vault;
|
|
1945
|
+
if (vault) vault.setProfile(u).catch((error) => console.warn("Unable to cache the Datalynk user", error));
|
|
1946
|
+
else if (u == null) localStorage.removeItem("datalynk-user");
|
|
1947
|
+
else localStorage.setItem("datalynk-user", JSON.stringify(u));
|
|
1640
1948
|
});
|
|
1641
1949
|
}
|
|
1950
|
+
/** Offline login/encryption is active for this app */
|
|
1951
|
+
get vault() {
|
|
1952
|
+
var _a;
|
|
1953
|
+
return ((_a = this.api.vault) == null ? void 0 : _a.enabled) ? this.api.vault : void 0;
|
|
1954
|
+
}
|
|
1955
|
+
/** This session may use offline data (always true when offline login is opted out) */
|
|
1956
|
+
get offlineUnlocked() {
|
|
1957
|
+
return !this.vault || this.vault.unlocked;
|
|
1958
|
+
}
|
|
1642
1959
|
/** Current user */
|
|
1643
1960
|
get user() {
|
|
1644
1961
|
return this.user$.getValue();
|
|
@@ -1656,14 +1973,18 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
1656
1973
|
*
|
|
1657
1974
|
* @return {Promise<User | null>}
|
|
1658
1975
|
*/
|
|
1659
|
-
async current(token = this.api.token) {
|
|
1660
|
-
var _a;
|
|
1976
|
+
async current(token = this.api.token, refresh = false) {
|
|
1977
|
+
var _a, _b, _c;
|
|
1661
1978
|
if (!token) return null;
|
|
1662
|
-
else if (token == ((_a = this.user) == null ? void 0 : _a.token)) return this.user;
|
|
1663
|
-
else if (!this.api.online
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1979
|
+
else if (!refresh && token == ((_a = this.user) == null ? void 0 : _a.token)) return this.user;
|
|
1980
|
+
else if (!this.api.online) {
|
|
1981
|
+
if (!this.offlineUnlocked || !((_b = this.api.options.offline) == null ? void 0 : _b.length)) return null;
|
|
1982
|
+
if (this.api.vault) return this.api.vault.getProfile();
|
|
1983
|
+
return null;
|
|
1984
|
+
}
|
|
1985
|
+
const user = await this.api.request([{ "$/auth/current": {} }, { "$/env/me": {} }], { token }).then((resp) => resp[0] || resp[1] ? { ...resp[0], ...resp[1] } : null);
|
|
1986
|
+
if (user && !user.guest && this.api.jwtPayload) (_c = this.vault) == null ? void 0 : _c.touch(this.api.jwtPayload.realm, this.api.jwtPayload.uid);
|
|
1987
|
+
return user;
|
|
1667
1988
|
}
|
|
1668
1989
|
/**
|
|
1669
1990
|
* Automatically handle sessions by checking localStorage & URL parameters for a token & prompting
|
|
@@ -1674,16 +1995,17 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
1674
1995
|
* @return {Promise<void>} Login complete
|
|
1675
1996
|
*/
|
|
1676
1997
|
async handleLogin(spoke, options) {
|
|
1677
|
-
var _a, _b, _c, _d;
|
|
1998
|
+
var _a, _b, _c, _d, _e;
|
|
1678
1999
|
if (this.onlinePrompt) window.removeEventListener("online", this.onlinePrompt);
|
|
1679
2000
|
if ((_a = this.api.options.offline) == null ? void 0 : _a.length) window.removeEventListener("online", this.onlinePrompt = () => this.handleLogin(spoke, options));
|
|
2001
|
+
await ((_b = this.api.vault) == null ? void 0 : _b.ready);
|
|
1680
2002
|
const urlParams = new URLSearchParams(location.search);
|
|
1681
2003
|
const urlToken = urlParams.getAll("datalynkToken").filter(Boolean).pop();
|
|
1682
2004
|
if (urlToken) {
|
|
1683
2005
|
this.api.token = urlToken;
|
|
1684
2006
|
urlParams.delete("datalynkToken");
|
|
1685
2007
|
location.href = location.pathname + (urlParams.toString() ? "?" + urlParams.toString() : "") + location.hash;
|
|
1686
|
-
} else if (
|
|
2008
|
+
} else if (this.isOffline() && this.vault && !this.vault.unlocked) {
|
|
1687
2009
|
await this.loginPrompt(spoke, options).done;
|
|
1688
2010
|
return;
|
|
1689
2011
|
} else if (this.api.reauthenticationPending && !this.api.online && ((_c = this.api.jwtPayload) == null ? void 0 : _c.realm) == spoke) {
|
|
@@ -1692,6 +2014,8 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
1692
2014
|
if (((_d = this.api.jwtPayload) == null ? void 0 : _d.realm) != spoke) {
|
|
1693
2015
|
this.api.token = null;
|
|
1694
2016
|
location.reload();
|
|
2017
|
+
} else if ((_e = this.vault) == null ? void 0 : _e.needsPassword) {
|
|
2018
|
+
await this.loginPrompt(spoke, options).done;
|
|
1695
2019
|
}
|
|
1696
2020
|
} else {
|
|
1697
2021
|
this.api.token = null;
|
|
@@ -1699,6 +2023,9 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
1699
2023
|
location.reload();
|
|
1700
2024
|
}
|
|
1701
2025
|
}
|
|
2026
|
+
isOffline() {
|
|
2027
|
+
return !this.api.online || typeof navigator !== "undefined" && navigator.onLine === false;
|
|
2028
|
+
}
|
|
1702
2029
|
/**
|
|
1703
2030
|
* Check whether user has a token
|
|
1704
2031
|
*
|
|
@@ -1750,16 +2077,14 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
1750
2077
|
* @returns {Promise<User>} User account
|
|
1751
2078
|
*/
|
|
1752
2079
|
async login(spoke, login, password, opts) {
|
|
1753
|
-
var _a
|
|
1754
|
-
if (
|
|
2080
|
+
var _a;
|
|
2081
|
+
if (this.vault && this.isOffline()) {
|
|
1755
2082
|
const identity2 = this.api.jwtPayload;
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
const user =
|
|
1760
|
-
if (!
|
|
1761
|
-
throw new Error("Offline login is not available for this account on this device.");
|
|
1762
|
-
this.offlineUnlocked = true;
|
|
2083
|
+
if (!identity2 || identity2.realm !== spoke) throw new Error(offlineLoginMessages.none);
|
|
2084
|
+
const result = await this.vault.unlockOffline(spoke, login, identity2.uid, password.trim());
|
|
2085
|
+
if (result !== "ok") throw new Error(offlineLoginMessages[result]);
|
|
2086
|
+
const user = await this.vault.getProfile();
|
|
2087
|
+
if (!user) throw new Error(offlineLoginMessages.none);
|
|
1763
2088
|
this.user = user;
|
|
1764
2089
|
return user;
|
|
1765
2090
|
}
|
|
@@ -1780,8 +2105,8 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
1780
2105
|
const data = await resp.json().catch(() => ({}));
|
|
1781
2106
|
if (!resp.ok || data["error"]) throw Object.assign(errorFromCode(resp.status, data.error) || {}, data);
|
|
1782
2107
|
this.api.token = data["token"];
|
|
1783
|
-
if (
|
|
1784
|
-
await
|
|
2108
|
+
if (this.vault && ((_a = this.api.jwtPayload) == null ? void 0 : _a.uid) != null)
|
|
2109
|
+
await this.vault.rememberLogin(spoke, login, this.api.jwtPayload.uid, password.trim()).catch((error) => console.error("Unable to enable offline login", error));
|
|
1785
2110
|
return data;
|
|
1786
2111
|
}
|
|
1787
2112
|
/**
|
|
@@ -1813,8 +2138,9 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
1813
2138
|
* @return {Promise<{closed: string, new: string}>}
|
|
1814
2139
|
*/
|
|
1815
2140
|
logout(reload = true) {
|
|
2141
|
+
var _a;
|
|
1816
2142
|
localStorage.removeItem("datalynk-user");
|
|
1817
|
-
|
|
2143
|
+
void ((_a = this.api.vault) == null ? void 0 : _a.forget());
|
|
1818
2144
|
return this.api.request({ "$/auth/logout": {} }).then((resp) => {
|
|
1819
2145
|
if (reload) location.reload();
|
|
1820
2146
|
this.api.token = null;
|
|
@@ -2762,15 +3088,17 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
2762
3088
|
const lowest = resp.reduce((acc, r) => Math.min(acc, r.id), 0);
|
|
2763
3089
|
if (lowest < _Slice.nextId) _Slice.nextId = lowest;
|
|
2764
3090
|
}).finally(() => this.loaded = true);
|
|
2765
|
-
this.cache$.pipe(skip(1)).subscribe(
|
|
3091
|
+
this.cache$.pipe(skip(1)).subscribe((cache) => {
|
|
3092
|
+
const table = this.table;
|
|
3093
|
+
if (!table) return;
|
|
3094
|
+
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))));
|
|
3095
|
+
save.catch((error) => console.warn("Unable to save offline slice cache", error));
|
|
3096
|
+
});
|
|
3097
|
+
this.api.token$.pipe(filter((token) => !!token), take(1)).subscribe(async () => {
|
|
2766
3098
|
var _a2;
|
|
2767
|
-
await ((_a2 = this.
|
|
2768
|
-
|
|
2769
|
-
var _a3;
|
|
2770
|
-
return (_a3 = this.table) == null ? void 0 : _a3.put(c.id, c);
|
|
2771
|
-
}));
|
|
3099
|
+
await ((_a2 = this.api.vault) == null ? void 0 : _a2.whenUnlocked());
|
|
3100
|
+
this.sync();
|
|
2772
3101
|
});
|
|
2773
|
-
this.sync();
|
|
2774
3102
|
this.api.online$.pipe(distinctUntilChanged()).subscribe(async (online) => {
|
|
2775
3103
|
if (!online) return;
|
|
2776
3104
|
if (this.api.expired) await lastValueFrom(this.api.auth.user$.pipe(skip(1), takeWhile((u) => !u || this.api.expired, true)));
|
|
@@ -3422,7 +3750,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
3422
3750
|
} });
|
|
3423
3751
|
}
|
|
3424
3752
|
}
|
|
3425
|
-
const version = "1.6.
|
|
3753
|
+
const version = "1.6.4";
|
|
3426
3754
|
class WebRtc {
|
|
3427
3755
|
constructor(api) {
|
|
3428
3756
|
__publicField(this, "ice");
|
|
@@ -4012,6 +4340,8 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
4012
4340
|
__publicField(this, "webrtc");
|
|
4013
4341
|
/** Offline database */
|
|
4014
4342
|
__publicField(this, "database");
|
|
4343
|
+
/** Offline login + offline cache encryption */
|
|
4344
|
+
__publicField(this, "vault");
|
|
4015
4345
|
/** Options */
|
|
4016
4346
|
__publicField(this, "options");
|
|
4017
4347
|
/** Created slices */
|
|
@@ -4037,7 +4367,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
4037
4367
|
__publicField(this, "online$", new BehaviorSubject(this.initialOnline));
|
|
4038
4368
|
/** API Session token */
|
|
4039
4369
|
__publicField(this, "token$", new BehaviorSubject(void 0));
|
|
4040
|
-
var _a, _b;
|
|
4370
|
+
var _a, _b, _c;
|
|
4041
4371
|
this.origin = origin;
|
|
4042
4372
|
this.url = `${new URL(origin).origin}/api/`;
|
|
4043
4373
|
const development = this.isDevelopmentEnvironment(origin);
|
|
@@ -4061,6 +4391,11 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
4061
4391
|
}
|
|
4062
4392
|
};
|
|
4063
4393
|
if (development) this.options.watchTokenExpiry = false;
|
|
4394
|
+
const offlineLogin = this.options.offlineLogin ?? true;
|
|
4395
|
+
this.vault = new OfflineVault(
|
|
4396
|
+
!!((_a = this.options.offline) == null ? void 0 : _a.length) && !!this.options.saveSession && offlineLogin !== false,
|
|
4397
|
+
typeof offlineLogin == "object" ? offlineLogin : {}
|
|
4398
|
+
);
|
|
4064
4399
|
if (this.options.saveSession && typeof localStorage != "undefined") {
|
|
4065
4400
|
this.token = localStorage.getItem(this.localStorageKey) || null;
|
|
4066
4401
|
this.token$.pipe(distinctUntilChanged()).subscribe((token) => {
|
|
@@ -4092,6 +4427,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
4092
4427
|
this.webrtc = new WebRtc(this);
|
|
4093
4428
|
if (typeof indexedDB != "undefined") {
|
|
4094
4429
|
this.database = new Database("datalynk", ["pending", ...this.options.offline || []]);
|
|
4430
|
+
this.database.cipher = this.vault;
|
|
4095
4431
|
if (this.options.replayOfflineQueue !== false) this.online$.subscribe((online) => {
|
|
4096
4432
|
if (online) void this.replayPendingQueue();
|
|
4097
4433
|
});
|
|
@@ -4101,10 +4437,10 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
4101
4437
|
window.addEventListener("offline", () => this.setConnectionStatus("offline"));
|
|
4102
4438
|
this.startHeartbeat();
|
|
4103
4439
|
}
|
|
4104
|
-
if ((
|
|
4440
|
+
if ((_b = this.options.offline) == null ? void 0 : _b.length) {
|
|
4105
4441
|
this.pwa.setup();
|
|
4106
4442
|
if (typeof indexedDB == "undefined") throw new Error("Cannot enable offline support, indexedDB is not available in this environment");
|
|
4107
|
-
(
|
|
4443
|
+
(_c = this.options.offline) == null ? void 0 : _c.forEach((id) => this.slice(id));
|
|
4108
4444
|
if (this.options.serviceWorker && typeof navigator["serviceWorker"] != "undefined") {
|
|
4109
4445
|
(async () => {
|
|
4110
4446
|
let initialized = false;
|
|
@@ -4416,7 +4752,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
4416
4752
|
} catch {
|
|
4417
4753
|
throw new UnexpectedApiResponseError(resp, body);
|
|
4418
4754
|
}
|
|
4419
|
-
if (resp.status === 401) this.markUnauthorized(token);
|
|
4755
|
+
if (resp.status === 401 && token) this.markUnauthorized(token);
|
|
4420
4756
|
if (!resp.ok || (data == null ? void 0 : data.error)) {
|
|
4421
4757
|
const error = Object.assign(errorFromCode(resp.status, data == null ? void 0 : data.error), data);
|
|
4422
4758
|
Object.defineProperty(error, "response", { value: resp, configurable: true });
|
|
@@ -4668,9 +5004,10 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
4668
5004
|
}));
|
|
4669
5005
|
}
|
|
4670
5006
|
async finishUnauthorizedLogout(preservePending = false) {
|
|
4671
|
-
var _a;
|
|
5007
|
+
var _a, _b, _c;
|
|
4672
5008
|
if (this.options.manageSession === false) return;
|
|
4673
|
-
|
|
5009
|
+
if (preservePending) await ((_a = this.vault) == null ? void 0 : _a.lock());
|
|
5010
|
+
else await ((_b = this.vault) == null ? void 0 : _b.forget());
|
|
4674
5011
|
if (typeof localStorage != "undefined") {
|
|
4675
5012
|
localStorage.removeItem(this.localStorageKey);
|
|
4676
5013
|
localStorage.removeItem("datalynk-user");
|
|
@@ -4691,7 +5028,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
4691
5028
|
console.error("Unable to clear Datalynk service worker cache", error);
|
|
4692
5029
|
}
|
|
4693
5030
|
}
|
|
4694
|
-
if (typeof navigator != "undefined" && ((
|
|
5031
|
+
if (typeof navigator != "undefined" && ((_c = navigator.serviceWorker) == null ? void 0 : _c.controller)) {
|
|
4695
5032
|
navigator.serviceWorker.controller.postMessage({ token: null });
|
|
4696
5033
|
}
|
|
4697
5034
|
if (typeof location != "undefined" && typeof location.reload == "function")
|
|
@@ -4832,7 +5169,10 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
4832
5169
|
if (!this.online || this.authenticationInvalid || this.offlineAuthenticationExpired) break;
|
|
4833
5170
|
if (owner && !this.ownersMatch(owner, this.tokenIdentity(this.token))) break;
|
|
4834
5171
|
const request = await table.get(key);
|
|
4835
|
-
if (request == null)
|
|
5172
|
+
if (request == null) {
|
|
5173
|
+
await table.delete(key);
|
|
5174
|
+
continue;
|
|
5175
|
+
}
|
|
4836
5176
|
try {
|
|
4837
5177
|
await this.request(request);
|
|
4838
5178
|
await table.delete(key);
|