@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/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
- add(value, key) {
262
- return this.tx((store) => store.add(value, key));
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
- put(key, value) {
265
- return this.tx((store) => store.put(value, key));
267
+ open(value) {
268
+ const cipher = this.database.cipher;
269
+ return cipher ? cipher.open(value) : Promise.resolve(value);
266
270
  }
267
- get(key) {
268
- return this.tx((store) => store.get(key), true);
271
+ async add(value, key) {
272
+ const sealed = await this.seal(value);
273
+ return this.tx((store) => store.add(sealed, key));
269
274
  }
270
- getAll() {
271
- return this.tx((store) => store.getAll(), true);
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 KEY = "datalynk-offline-login";
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
- async function hash(password, salt) {
1554
- const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(password), "PBKDF2", false, ["deriveBits"]);
1555
- return Array.from(new Uint8Array(await crypto.subtle.deriveBits(
1556
- { name: "PBKDF2", salt: new Uint8Array(salt), iterations: ITERATIONS, hash: "SHA-256" },
1557
- key,
1558
- 256
1559
- )));
1560
- }
1561
- async function rememberOfflineLogin(realm, login, uid, password) {
1562
- const salt = crypto.getRandomValues(new Uint8Array(16));
1563
- const record = { realm, login: login.trim().toLowerCase(), uid, salt: Array.from(salt), hash: await hash(password, salt) };
1564
- localStorage.setItem(KEY, JSON.stringify(record));
1565
- }
1566
- async function verifyOfflineLogin(realm, login, uid, password) {
1567
- const saved = localStorage.getItem(KEY);
1568
- if (!saved) return false;
1569
- let record;
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
- function hasOfflineLogin(realm, uid) {
1583
- try {
1584
- const record = JSON.parse(localStorage.getItem(KEY) || "null");
1585
- return (record == null ? void 0 : record.realm) === realm && (record == null ? void 0 : record.uid) === uid;
1586
- } catch {
1587
- return false;
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
- const user = await this.current();
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 === "unauthorized") forgetOfflineLogin();
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
- if (this.api.online) localStorage.removeItem("datalynk-user");
1639
- } else localStorage.setItem("datalynk-user", JSON.stringify(u));
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 && this.offlineUnlocked && typeof localStorage != "undefined" && localStorage.getItem("datalynk-user"))
1664
- return JSON.parse(localStorage.getItem("datalynk-user"));
1665
- else if (!this.api.online) return null;
1666
- return this.api.request([{ "$/auth/current": {} }, { "$/env/me": {} }], { token }).then((resp) => resp[0] || resp[1] ? { ...resp[0], ...resp[1] } : null);
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 (!this.api.online && ((_b = this.api.options.offline) == null ? void 0 : _b.length) && !this.offlineUnlocked) {
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, _b, _c;
1754
- if (((_a = this.api.options.offline) == null ? void 0 : _a.length) && (!this.api.online || typeof navigator !== "undefined" && !navigator.onLine)) {
2080
+ var _a;
2081
+ if (this.vault && this.isOffline()) {
1755
2082
  const identity2 = this.api.jwtPayload;
1756
- const saved = localStorage.getItem("datalynk-user");
1757
- if (!identity2 || identity2.realm !== spoke || !saved || !await verifyOfflineLogin(spoke, login, identity2.uid, password.trim()))
1758
- throw new Error("Offline login is not available for this account on this device.");
1759
- const user = JSON.parse(saved);
1760
- if (![user.login, user.email].some((value) => (value == null ? void 0 : value.toLowerCase()) === login.trim().toLowerCase()))
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 (((_b = this.api.options.offline) == null ? void 0 : _b.length) && this.api.options.saveSession && ((_c = this.api.jwtPayload) == null ? void 0 : _c.uid) != null)
1784
- await rememberOfflineLogin(spoke, login, this.api.jwtPayload.uid, password.trim());
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
- forgetOfflineLogin();
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(async (cache) => {
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.table) == null ? void 0 : _a2.clear());
2768
- await Promise.all(cache.map((c) => {
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.3";
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 ((_a = this.options.offline) == null ? void 0 : _a.length) {
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
- (_b = this.options.offline) == null ? void 0 : _b.forEach((id) => this.slice(id));
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
- forgetOfflineLogin();
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" && ((_a = navigator.serviceWorker) == null ? void 0 : _a.controller)) {
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) continue;
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);