@geolonia/geonicdb-sdk 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -1
- package/geonicdb.cjs +233 -13
- package/geonicdb.cjs.map +3 -3
- package/geonicdb.iife.js +233 -13
- package/geonicdb.iife.js.map +3 -3
- package/geonicdb.mjs +233 -13
- package/geonicdb.mjs.map +3 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -124,6 +124,49 @@ Notes:
|
|
|
124
124
|
- Omitting the option preserves the previous behavior (Bearer only) for
|
|
125
125
|
backward compatibility.
|
|
126
126
|
|
|
127
|
+
#### Persisting DPoP across page reloads
|
|
128
|
+
|
|
129
|
+
By default, a DPoP session is held in memory only. Because the private key
|
|
130
|
+
is generated as a **non-extractable** `CryptoKey`, it cannot be serialized
|
|
131
|
+
to `localStorage` — and reloading the page would lose it, forcing the user
|
|
132
|
+
to log in again.
|
|
133
|
+
|
|
134
|
+
Pass `{ dpop: true, dpopPersist: true }` to persist the session to
|
|
135
|
+
**IndexedDB**. The structured clone algorithm stores the `CryptoKey` as-is,
|
|
136
|
+
preserving `extractable: false`: the key survives reloads, but JavaScript
|
|
137
|
+
on the page still cannot read it back as JWK / PEM. Tokens leaked from any
|
|
138
|
+
other storage remain unusable without this origin's IndexedDB.
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
const db = new GeonicDB({ baseUrl, tenant });
|
|
142
|
+
await db.login('user@example.com', 'password', {
|
|
143
|
+
dpop: true,
|
|
144
|
+
dpopPersist: true,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// On the next page load:
|
|
148
|
+
const db2 = new GeonicDB({ baseUrl, tenant });
|
|
149
|
+
if (await db2.restoreSession()) {
|
|
150
|
+
// Session was rehydrated from IndexedDB — boot the app.
|
|
151
|
+
} else {
|
|
152
|
+
// No persisted session; show the login form.
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// On logout, the persisted entry is cleared automatically.
|
|
156
|
+
await db2.logout();
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Notes:
|
|
160
|
+
|
|
161
|
+
- `dpopPersist: true` requires `dpop: true` (throws otherwise). The
|
|
162
|
+
persistence mechanism is for DPoP key pairs specifically; persisting
|
|
163
|
+
Bearer tokens is the caller's responsibility.
|
|
164
|
+
- The IndexedDB store is keyed by `tenant`, so multi-tenant apps in the
|
|
165
|
+
same origin keep separate sessions.
|
|
166
|
+
- Persistence is best-effort: a failed IDB write does not break login.
|
|
167
|
+
- iOS Safari may evict IndexedDB under ITP after 7 days of no user
|
|
168
|
+
interaction with the origin.
|
|
169
|
+
|
|
127
170
|
### External Token
|
|
128
171
|
|
|
129
172
|
```typescript
|
|
@@ -360,7 +403,8 @@ handle.stop();
|
|
|
360
403
|
|
|
361
404
|
| Method | Parameters | Returns | Description |
|
|
362
405
|
|--------|-----------|---------|-------------|
|
|
363
|
-
| `login(email, password, options?)` | `string, string, LoginOptions?` | `Promise<LoginResponse>` | Login with email and password (Bearer JWT). Upgrades the SDK out of anonymous mode. Pass `{ dpop: true }` to immediately exchange the Bearer session for a DPoP sender-constrained session (RFC 9449) — recommended when persisting tokens in storage exposed to XSS |
|
|
406
|
+
| `login(email, password, options?)` | `string, string, LoginOptions?` | `Promise<LoginResponse>` | Login with email and password (Bearer JWT). Upgrades the SDK out of anonymous mode. Pass `{ dpop: true }` to immediately exchange the Bearer session for a DPoP sender-constrained session (RFC 9449) — recommended when persisting tokens in storage exposed to XSS. Pass `{ dpop: true, dpopPersist: true }` to also persist the session to IndexedDB so it survives a page reload (#1230) |
|
|
407
|
+
| `restoreSession()` | — | `Promise<boolean>` | Rehydrate a DPoP session previously persisted via `login({ dpop: true, dpopPersist: true })`. Returns `true` if a session was found and loaded, `false` otherwise. Call at app startup before showing the login form |
|
|
364
408
|
| `setCredentials(opts)` | `CredentialsOptions` | `this` | Set credentials externally (e.g. from a login API response). Upgrades the SDK out of anonymous mode when `tokenType: 'Bearer'`. **Throws** when `tokenType: 'DPoP'` is passed to an anonymous-built instance — anonymous instances do not generate a DPoP key pair |
|
|
365
409
|
| `logout()` | — | `void` | Clear all credentials and disconnect WebSocket. Reverts to anonymous mode if `anonymous: true` was set at construction |
|
|
366
410
|
| `isAnonymous()` | — | `boolean` | Whether explicit anonymous mode is active. Returns the explicit mode flag — not derived from credential presence — so a refresh failure that clears `_token` does NOT silently flip this back to `true` |
|
package/geonicdb.cjs
CHANGED
|
@@ -72,8 +72,23 @@ var EventEmitter = class {
|
|
|
72
72
|
}
|
|
73
73
|
};
|
|
74
74
|
|
|
75
|
+
// src/sdk/constants.ts
|
|
76
|
+
var DEFAULT_TOKEN_TTL_SEC = 3600;
|
|
77
|
+
var TOKEN_REFRESH_LEEWAY_MS = 12e4;
|
|
78
|
+
var TOKEN_REFRESH_MIN_MS = 1e4;
|
|
79
|
+
var RECONNECT_MAX_ATTEMPTS = 10;
|
|
80
|
+
var RECONNECT_BASE_MS = 1e3;
|
|
81
|
+
var RECONNECT_MAX_DELAY_MS = 3e4;
|
|
82
|
+
var SUB_PROTOCOL = "access_token";
|
|
83
|
+
var SDK_CACHE_MAX_ENTRIES_DEFAULT = 1e3;
|
|
84
|
+
var SDK_POLL_INTERVAL_MS_DEFAULT = 5e3;
|
|
85
|
+
var DPOP_IDB_NAME = "geonicdb-sdk";
|
|
86
|
+
var DPOP_IDB_STORE = "dpop-sessions";
|
|
87
|
+
var DPOP_IDB_VERSION = 1;
|
|
88
|
+
|
|
75
89
|
// src/sdk/dpop.ts
|
|
76
90
|
var dpopSupported = typeof crypto !== "undefined" && !!crypto.subtle && !!crypto.subtle.generateKey;
|
|
91
|
+
var dpopPersistenceSupported = typeof globalThis.indexedDB !== "undefined" && typeof structuredClone !== "undefined";
|
|
77
92
|
function b64url(buf) {
|
|
78
93
|
const arr = buf instanceof ArrayBuffer ? new Uint8Array(buf) : buf;
|
|
79
94
|
let str = "";
|
|
@@ -130,6 +145,102 @@ async function createDPoPProof(keyPair, htm, htu, accessToken, nonce) {
|
|
|
130
145
|
);
|
|
131
146
|
return signingInput + "." + b64url(new Uint8Array(sigBuf));
|
|
132
147
|
}
|
|
148
|
+
function openSessionDB() {
|
|
149
|
+
return new Promise((resolve, reject) => {
|
|
150
|
+
const req = globalThis.indexedDB.open(DPOP_IDB_NAME, DPOP_IDB_VERSION);
|
|
151
|
+
req.onupgradeneeded = () => {
|
|
152
|
+
const db = req.result;
|
|
153
|
+
if (!db.objectStoreNames.contains(DPOP_IDB_STORE)) {
|
|
154
|
+
db.createObjectStore(DPOP_IDB_STORE);
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
req.onsuccess = () => {
|
|
158
|
+
resolve(req.result);
|
|
159
|
+
};
|
|
160
|
+
req.onerror = () => {
|
|
161
|
+
reject(req.error ?? new Error("IndexedDB open failed"));
|
|
162
|
+
};
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
async function saveDPoPSession(session) {
|
|
166
|
+
if (!dpopPersistenceSupported) return false;
|
|
167
|
+
let db = null;
|
|
168
|
+
try {
|
|
169
|
+
db = await openSessionDB();
|
|
170
|
+
await new Promise((resolve, reject) => {
|
|
171
|
+
const tx = db.transaction(DPOP_IDB_STORE, "readwrite");
|
|
172
|
+
tx.objectStore(DPOP_IDB_STORE).put(session, session.tenant);
|
|
173
|
+
tx.oncomplete = () => {
|
|
174
|
+
resolve();
|
|
175
|
+
};
|
|
176
|
+
tx.onerror = () => {
|
|
177
|
+
reject(tx.error ?? new Error("IndexedDB put failed"));
|
|
178
|
+
};
|
|
179
|
+
tx.onabort = () => {
|
|
180
|
+
reject(tx.error ?? new Error("IndexedDB transaction aborted"));
|
|
181
|
+
};
|
|
182
|
+
});
|
|
183
|
+
return true;
|
|
184
|
+
} catch {
|
|
185
|
+
return false;
|
|
186
|
+
} finally {
|
|
187
|
+
db?.close();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
async function loadDPoPSession(tenant) {
|
|
191
|
+
if (!dpopPersistenceSupported) return null;
|
|
192
|
+
let db = null;
|
|
193
|
+
try {
|
|
194
|
+
db = await openSessionDB();
|
|
195
|
+
const data = await new Promise((resolve, reject) => {
|
|
196
|
+
const tx = db.transaction(DPOP_IDB_STORE, "readonly");
|
|
197
|
+
const req = tx.objectStore(DPOP_IDB_STORE).get(tenant);
|
|
198
|
+
req.onsuccess = () => {
|
|
199
|
+
resolve(req.result);
|
|
200
|
+
};
|
|
201
|
+
req.onerror = () => {
|
|
202
|
+
reject(req.error ?? new Error("IndexedDB get failed"));
|
|
203
|
+
};
|
|
204
|
+
});
|
|
205
|
+
return validatePersistedSession(data);
|
|
206
|
+
} catch {
|
|
207
|
+
return null;
|
|
208
|
+
} finally {
|
|
209
|
+
db?.close();
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
async function clearDPoPSession(tenant) {
|
|
213
|
+
if (!dpopPersistenceSupported) return;
|
|
214
|
+
let db = null;
|
|
215
|
+
try {
|
|
216
|
+
db = await openSessionDB();
|
|
217
|
+
await new Promise((resolve, reject) => {
|
|
218
|
+
const tx = db.transaction(DPOP_IDB_STORE, "readwrite");
|
|
219
|
+
tx.objectStore(DPOP_IDB_STORE).delete(tenant);
|
|
220
|
+
tx.oncomplete = () => {
|
|
221
|
+
resolve();
|
|
222
|
+
};
|
|
223
|
+
tx.onerror = () => {
|
|
224
|
+
reject(tx.error ?? new Error("IndexedDB delete failed"));
|
|
225
|
+
};
|
|
226
|
+
tx.onabort = () => {
|
|
227
|
+
reject(tx.error ?? new Error("IndexedDB transaction aborted"));
|
|
228
|
+
};
|
|
229
|
+
});
|
|
230
|
+
} catch {
|
|
231
|
+
} finally {
|
|
232
|
+
db?.close();
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
function validatePersistedSession(data) {
|
|
236
|
+
if (!data || typeof data !== "object") return null;
|
|
237
|
+
const s = data;
|
|
238
|
+
const kp = s.keyPair;
|
|
239
|
+
if (typeof s.tenant !== "string" || typeof s.accessToken !== "string" || typeof s.refreshToken !== "string" || typeof s.expiresAt !== "number" || !kp || !kp.privateKey || !kp.publicJwk) {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
return data;
|
|
243
|
+
}
|
|
133
244
|
|
|
134
245
|
// src/sdk/pow.ts
|
|
135
246
|
var BATCH_SIZE = 1e3;
|
|
@@ -173,17 +284,6 @@ async function solvePoW(challenge, difficulty) {
|
|
|
173
284
|
return tryBatch();
|
|
174
285
|
}
|
|
175
286
|
|
|
176
|
-
// src/sdk/constants.ts
|
|
177
|
-
var DEFAULT_TOKEN_TTL_SEC = 3600;
|
|
178
|
-
var TOKEN_REFRESH_LEEWAY_MS = 12e4;
|
|
179
|
-
var TOKEN_REFRESH_MIN_MS = 1e4;
|
|
180
|
-
var RECONNECT_MAX_ATTEMPTS = 10;
|
|
181
|
-
var RECONNECT_BASE_MS = 1e3;
|
|
182
|
-
var RECONNECT_MAX_DELAY_MS = 3e4;
|
|
183
|
-
var SUB_PROTOCOL = "access_token";
|
|
184
|
-
var SDK_CACHE_MAX_ENTRIES_DEFAULT = 1e3;
|
|
185
|
-
var SDK_POLL_INTERVAL_MS_DEFAULT = 5e3;
|
|
186
|
-
|
|
187
287
|
// src/sdk/errors.ts
|
|
188
288
|
var GeonicDBError = class extends Error {
|
|
189
289
|
constructor(message, statusCode = 0) {
|
|
@@ -376,6 +476,14 @@ var _AuthManager = class _AuthManager {
|
|
|
376
476
|
__publicField(this, "_dpopKeyPair", null);
|
|
377
477
|
__publicField(this, "_dpopReady", null);
|
|
378
478
|
__publicField(this, "_dpopNonce", null);
|
|
479
|
+
/**
|
|
480
|
+
* #1230: when true, every DPoP token mutation (login bind, refresh) is
|
|
481
|
+
* mirrored to IndexedDB so the session survives a page reload. Toggled
|
|
482
|
+
* by `login({ dpopPersist: true })` and `restoreSession()`.
|
|
483
|
+
*/
|
|
484
|
+
__publicField(this, "_dpopPersist", false);
|
|
485
|
+
/** Email captured at login, persisted for UI display. */
|
|
486
|
+
__publicField(this, "_email", null);
|
|
379
487
|
__publicField(this, "_tokenPromise", null);
|
|
380
488
|
/** Callback to emit tokenRefresh events. Set by GeonicDB class. */
|
|
381
489
|
__publicField(this, "onTokenRefresh", null);
|
|
@@ -402,9 +510,9 @@ var _AuthManager = class _AuthManager {
|
|
|
402
510
|
this._anonymous = anonymous;
|
|
403
511
|
if (!anonymous && dpopSupported) {
|
|
404
512
|
this._dpopReady = generateDPoPKeyPair().then((kp) => {
|
|
405
|
-
this._dpopKeyPair = kp;
|
|
513
|
+
if (!this._dpopKeyPair) this._dpopKeyPair = kp;
|
|
406
514
|
}).catch(() => {
|
|
407
|
-
this._dpopKeyPair = null;
|
|
515
|
+
if (!this._dpopKeyPair) this._dpopKeyPair = null;
|
|
408
516
|
});
|
|
409
517
|
}
|
|
410
518
|
}
|
|
@@ -448,6 +556,12 @@ var _AuthManager = class _AuthManager {
|
|
|
448
556
|
*/
|
|
449
557
|
async login(email, password, options) {
|
|
450
558
|
this._log("login", email);
|
|
559
|
+
const wantsDpopPersist = options?.dpopPersist === true;
|
|
560
|
+
if (wantsDpopPersist && !options?.dpop) {
|
|
561
|
+
throw new AuthenticationError(
|
|
562
|
+
"dpopPersist: true requires dpop: true. Persisting Bearer tokens is the caller's responsibility (localStorage / sessionStorage)."
|
|
563
|
+
);
|
|
564
|
+
}
|
|
451
565
|
const headers = {
|
|
452
566
|
"Content-Type": "application/json"
|
|
453
567
|
};
|
|
@@ -469,9 +583,14 @@ var _AuthManager = class _AuthManager {
|
|
|
469
583
|
this._tokenExpiry = Date.now() + (data.expiresIn - 60) * 1e3;
|
|
470
584
|
this._tokenType = "Bearer";
|
|
471
585
|
this._refreshToken = data.refreshToken;
|
|
586
|
+
this._email = email;
|
|
587
|
+
this._dpopPersist = wantsDpopPersist;
|
|
472
588
|
this._invalidateAuthScopedState();
|
|
473
589
|
if (options?.dpop) {
|
|
474
590
|
const bound = await this._bindDpopSession();
|
|
591
|
+
if (this._dpopPersist) {
|
|
592
|
+
await this._persistDpopSession();
|
|
593
|
+
}
|
|
475
594
|
return bound;
|
|
476
595
|
}
|
|
477
596
|
return data;
|
|
@@ -565,9 +684,81 @@ var _AuthManager = class _AuthManager {
|
|
|
565
684
|
this._tokenExpiry = 0;
|
|
566
685
|
this._refreshToken = null;
|
|
567
686
|
this._tokenPromise = null;
|
|
687
|
+
this._email = null;
|
|
688
|
+
if (this._dpopPersist && this._tenant) {
|
|
689
|
+
void clearDPoPSession(this._tenant);
|
|
690
|
+
}
|
|
691
|
+
this._dpopPersist = false;
|
|
568
692
|
this._anonymous = this._initialAnonymous;
|
|
569
693
|
this._invalidateAuthScopedState();
|
|
570
694
|
}
|
|
695
|
+
/**
|
|
696
|
+
* Restore a DPoP session previously persisted via `login({ dpop: true,
|
|
697
|
+
* dpopPersist: true })`. Returns true if a usable session was found and
|
|
698
|
+
* loaded into the SDK, false otherwise (no entry, IndexedDB unavailable,
|
|
699
|
+
* or the entry was for a different tenant).
|
|
700
|
+
*
|
|
701
|
+
* After a successful restore the SDK behaves as if `login()` had just
|
|
702
|
+
* succeeded: tokens are set, the DPoP key pair is rehydrated as a
|
|
703
|
+
* non-extractable `CryptoKey`, and subsequent requests automatically
|
|
704
|
+
* carry a fresh DPoP proof. Token refresh continues to flow through
|
|
705
|
+
* `/auth/refresh` and emits `tokenRefresh` events as usual.
|
|
706
|
+
*
|
|
707
|
+
* Token expiry is NOT validated here — the next `ensureToken()` call will
|
|
708
|
+
* trigger a refresh if the access token has expired (#1230). An expired
|
|
709
|
+
* refresh token will manifest as an `AuthenticationError` from that
|
|
710
|
+
* refresh attempt; consumers should treat such errors as "session ended,
|
|
711
|
+
* fall back to login()".
|
|
712
|
+
*/
|
|
713
|
+
async restoreSession() {
|
|
714
|
+
if (this._initialAnonymous) return false;
|
|
715
|
+
if (!this._tenant) return false;
|
|
716
|
+
const persisted = await loadDPoPSession(this._tenant);
|
|
717
|
+
if (!persisted) return false;
|
|
718
|
+
if (persisted.tenant !== this._tenant) return false;
|
|
719
|
+
this._anonymous = false;
|
|
720
|
+
this._token = persisted.accessToken;
|
|
721
|
+
this._refreshToken = persisted.refreshToken;
|
|
722
|
+
this._tokenExpiry = persisted.expiresAt;
|
|
723
|
+
this._tokenType = "DPoP";
|
|
724
|
+
this._dpopKeyPair = persisted.keyPair;
|
|
725
|
+
this._dpopReady = Promise.resolve();
|
|
726
|
+
this._dpopNonce = null;
|
|
727
|
+
this._dpopPersist = true;
|
|
728
|
+
this._email = persisted.email ?? null;
|
|
729
|
+
this._tokenPromise = null;
|
|
730
|
+
this._invalidateAuthScopedState();
|
|
731
|
+
return true;
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* Whether this AuthManager currently has DPoP persistence enabled.
|
|
735
|
+
* Exposed for the `GeonicDB` wrapper to advertise behavior; internal
|
|
736
|
+
* consumers should read `_dpopPersist` directly.
|
|
737
|
+
*/
|
|
738
|
+
isDpopPersisted() {
|
|
739
|
+
return this._dpopPersist;
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* Persist the current DPoP session to IndexedDB. Best-effort: a failed
|
|
743
|
+
* write is logged in debug mode but never propagated, since persistence
|
|
744
|
+
* is purely a UX optimization.
|
|
745
|
+
*
|
|
746
|
+
* @internal
|
|
747
|
+
*/
|
|
748
|
+
async _persistDpopSession() {
|
|
749
|
+
if (!this._dpopPersist || !this._tenant || !this._dpopKeyPair || !this._token || !this._refreshToken) {
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
const ok = await saveDPoPSession({
|
|
753
|
+
tenant: this._tenant,
|
|
754
|
+
accessToken: this._token,
|
|
755
|
+
refreshToken: this._refreshToken,
|
|
756
|
+
expiresAt: this._tokenExpiry,
|
|
757
|
+
email: this._email ?? void 0,
|
|
758
|
+
keyPair: this._dpopKeyPair
|
|
759
|
+
});
|
|
760
|
+
if (!ok) this._log("DPoP session persistence failed (IndexedDB unavailable?)");
|
|
761
|
+
}
|
|
571
762
|
/**
|
|
572
763
|
* Drop everything that may have been associated with the previous auth
|
|
573
764
|
* context. Called on `login()` (after token assignment), `setCredentials()`,
|
|
@@ -638,6 +829,7 @@ var _AuthManager = class _AuthManager {
|
|
|
638
829
|
expiresIn: data.expiresIn,
|
|
639
830
|
refreshToken: this._refreshToken
|
|
640
831
|
});
|
|
832
|
+
if (this._dpopPersist) void this._persistDpopSession();
|
|
641
833
|
return this._token;
|
|
642
834
|
} catch (err) {
|
|
643
835
|
this._tokenPromise = null;
|
|
@@ -1233,11 +1425,39 @@ var GeonicDB = class extends EventEmitter {
|
|
|
1233
1425
|
*
|
|
1234
1426
|
* // DPoP-bound (recommended for SPAs)
|
|
1235
1427
|
* await db.login('user@example.com', 'password', { dpop: true });
|
|
1428
|
+
*
|
|
1429
|
+
* // DPoP-bound + persisted across reloads (browser SPA UX)
|
|
1430
|
+
* await db.login('user@example.com', 'password', {
|
|
1431
|
+
* dpop: true,
|
|
1432
|
+
* dpopPersist: true,
|
|
1433
|
+
* });
|
|
1236
1434
|
* ```
|
|
1237
1435
|
*/
|
|
1238
1436
|
async login(email, password, options) {
|
|
1239
1437
|
return this._auth.login(email, password, options);
|
|
1240
1438
|
}
|
|
1439
|
+
/**
|
|
1440
|
+
* Rehydrate a DPoP session previously persisted via
|
|
1441
|
+
* `login({ dpop: true, dpopPersist: true })`.
|
|
1442
|
+
*
|
|
1443
|
+
* Returns true if a session for this tenant was found and loaded; false
|
|
1444
|
+
* if there was no entry, IndexedDB is unavailable, or the persisted
|
|
1445
|
+
* session was malformed. Call this at app startup before deciding
|
|
1446
|
+
* whether to render the login form.
|
|
1447
|
+
*
|
|
1448
|
+
* @example
|
|
1449
|
+
* ```ts
|
|
1450
|
+
* const db = new GeonicDB({ baseUrl, tenant });
|
|
1451
|
+
* if (await db.restoreSession()) {
|
|
1452
|
+
* // Tokens + DPoP private key recovered from IndexedDB — boot the app.
|
|
1453
|
+
* } else {
|
|
1454
|
+
* // No session; show the login form.
|
|
1455
|
+
* }
|
|
1456
|
+
* ```
|
|
1457
|
+
*/
|
|
1458
|
+
async restoreSession() {
|
|
1459
|
+
return this._auth.restoreSession();
|
|
1460
|
+
}
|
|
1241
1461
|
/**
|
|
1242
1462
|
* Whether the SDK is currently operating in anonymous mode (no token held).
|
|
1243
1463
|
* Returns true only when `anonymous: true` was passed to the constructor
|