@geolonia/geonicdb-sdk 0.10.1 → 0.11.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 +30 -44
- package/geonicdb.cjs +66 -54
- package/geonicdb.cjs.map +2 -2
- package/geonicdb.d.ts +22 -45
- package/geonicdb.iife.js +66 -54
- package/geonicdb.iife.js.map +2 -2
- package/geonicdb.mjs +66 -54
- package/geonicdb.mjs.map +2 -2
- package/package.json +1 -1
package/geonicdb.d.ts
CHANGED
|
@@ -171,37 +171,20 @@ export interface ReconnectingEvent {
|
|
|
171
171
|
/** Options for `db.login(...)`. */
|
|
172
172
|
export interface LoginOptions {
|
|
173
173
|
/**
|
|
174
|
-
* If true,
|
|
175
|
-
*
|
|
174
|
+
* If true, keep the user signed in across page reloads by persisting the
|
|
175
|
+
* session to IndexedDB. After a reload, call `db.restoreSession()` to
|
|
176
|
+
* rehydrate the session; `db.logout()` clears the persisted entry.
|
|
176
177
|
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
* Throws `AuthenticationError` if DPoP is not supported in the runtime
|
|
183
|
-
* (no Web Crypto subtle.generateKey).
|
|
184
|
-
*
|
|
185
|
-
* Default: false (Bearer remains).
|
|
186
|
-
*/
|
|
187
|
-
dpop?: boolean;
|
|
188
|
-
/**
|
|
189
|
-
* If true, persist the DPoP session (non-extractable private key + tokens)
|
|
190
|
-
* to IndexedDB so the session survives a page reload. Implies `dpop: true`
|
|
191
|
-
* — passing `dpopPersist: true` without `dpop: true` throws.
|
|
192
|
-
*
|
|
193
|
-
* The private key is stored as a `CryptoKey` via the structured clone
|
|
194
|
-
* algorithm; `extractable: false` is preserved, so the key cannot be read
|
|
195
|
-
* back as JWK / PEM even with full DOM access. A token leaked from
|
|
196
|
-
* localStorage remains unusable without this SDK origin's IndexedDB.
|
|
197
|
-
*
|
|
198
|
-
* After a reload, call `db.restoreSession()` to rehydrate. Use `db.logout()`
|
|
199
|
-
* to clear the persisted entry.
|
|
178
|
+
* The session is bound to a non-extractable key (DPoP, RFC 9449) stored via
|
|
179
|
+
* the structured clone algorithm — `extractable: false` is preserved, so the
|
|
180
|
+
* key cannot be read back as JWK / PEM even with full DOM access. A token
|
|
181
|
+
* leaked from localStorage stays unusable without this origin's IndexedDB.
|
|
200
182
|
*
|
|
201
183
|
* Default: false. Opt-out by design: shared-device flows should not enable
|
|
202
|
-
* this.
|
|
184
|
+
* this. Has no effect when the runtime lacks Web Crypto — the session cannot
|
|
185
|
+
* be sender-constrained, so it is not persisted.
|
|
203
186
|
*/
|
|
204
|
-
|
|
187
|
+
rememberSession?: boolean;
|
|
205
188
|
}
|
|
206
189
|
/** Response from the login endpoint. */
|
|
207
190
|
export interface LoginResponse {
|
|
@@ -364,33 +347,27 @@ export declare class GeonicDB extends EventEmitter {
|
|
|
364
347
|
*/
|
|
365
348
|
clearCache(): void;
|
|
366
349
|
/**
|
|
367
|
-
* Login with email and password
|
|
350
|
+
* Login with email and password.
|
|
368
351
|
*
|
|
369
|
-
*
|
|
370
|
-
*
|
|
371
|
-
*
|
|
372
|
-
*
|
|
373
|
-
*
|
|
352
|
+
* The session is sender-constrained with DPoP (RFC 9449) automatically and
|
|
353
|
+
* transparently — there is no DPoP option to set. A token leaked from
|
|
354
|
+
* storage (e.g. localStorage XSS) is therefore unusable from another origin.
|
|
355
|
+
* On runtimes without Web Crypto, the SDK silently keeps a plain Bearer
|
|
356
|
+
* session and login still succeeds.
|
|
374
357
|
*
|
|
375
358
|
* @example
|
|
376
359
|
* ```ts
|
|
377
|
-
* //
|
|
360
|
+
* // Standard login — DPoP applied transparently when supported.
|
|
378
361
|
* await db.login('user@example.com', 'password');
|
|
379
362
|
*
|
|
380
|
-
* //
|
|
381
|
-
* await db.login('user@example.com', 'password', {
|
|
382
|
-
*
|
|
383
|
-
* // DPoP-bound + persisted across reloads (browser SPA UX)
|
|
384
|
-
* await db.login('user@example.com', 'password', {
|
|
385
|
-
* dpop: true,
|
|
386
|
-
* dpopPersist: true,
|
|
387
|
-
* });
|
|
363
|
+
* // Keep the user signed in across page reloads (browser SPA UX).
|
|
364
|
+
* await db.login('user@example.com', 'password', { rememberSession: true });
|
|
388
365
|
* ```
|
|
389
366
|
*/
|
|
390
367
|
login(email: string, password: string, options?: LoginOptions): Promise<LoginResponse>;
|
|
391
368
|
/**
|
|
392
|
-
* Rehydrate a
|
|
393
|
-
* `login(
|
|
369
|
+
* Rehydrate a session previously persisted via
|
|
370
|
+
* `login(email, password, { rememberSession: true })`.
|
|
394
371
|
*
|
|
395
372
|
* Returns true if a session for this tenant was found and loaded; false
|
|
396
373
|
* if there was no entry, IndexedDB is unavailable, or the persisted
|
|
@@ -401,7 +378,7 @@ export declare class GeonicDB extends EventEmitter {
|
|
|
401
378
|
* ```ts
|
|
402
379
|
* const db = new GeonicDB({ baseUrl, tenant });
|
|
403
380
|
* if (await db.restoreSession()) {
|
|
404
|
-
* //
|
|
381
|
+
* // Session (tokens + non-extractable key) recovered — boot the app.
|
|
405
382
|
* } else {
|
|
406
383
|
* // No session; show the login form.
|
|
407
384
|
* }
|
package/geonicdb.iife.js
CHANGED
|
@@ -477,11 +477,12 @@ var GeonicDBModule = (() => {
|
|
|
477
477
|
__publicField(this, "_dpopReady", null);
|
|
478
478
|
__publicField(this, "_dpopNonce", null);
|
|
479
479
|
/**
|
|
480
|
-
*
|
|
481
|
-
*
|
|
482
|
-
*
|
|
480
|
+
* When true, every DPoP token mutation (login bind, refresh) is mirrored to
|
|
481
|
+
* IndexedDB so the session survives a page reload (#1230). Toggled by
|
|
482
|
+
* `login(email, password, { rememberSession: true })` and
|
|
483
|
+
* `restoreSession()` (#1244).
|
|
483
484
|
*/
|
|
484
|
-
__publicField(this, "
|
|
485
|
+
__publicField(this, "_rememberSession", false);
|
|
485
486
|
/** Email captured at login, persisted for UI display. */
|
|
486
487
|
__publicField(this, "_email", null);
|
|
487
488
|
__publicField(this, "_tokenPromise", null);
|
|
@@ -546,22 +547,22 @@ var GeonicDBModule = (() => {
|
|
|
546
547
|
this._emitCacheEvent?.(name, payload);
|
|
547
548
|
}
|
|
548
549
|
/**
|
|
549
|
-
* Login with email and password
|
|
550
|
+
* Login with email and password.
|
|
550
551
|
*
|
|
551
|
-
*
|
|
552
|
-
*
|
|
553
|
-
*
|
|
554
|
-
*
|
|
555
|
-
*
|
|
552
|
+
* #1244: DPoP (RFC 9449) is transparent and always-on — there is no `dpop`
|
|
553
|
+
* option to set. After the Bearer login succeeds, the SDK automatically
|
|
554
|
+
* exchanges it for a DPoP sender-constrained session so a token leaked from
|
|
555
|
+
* storage (e.g. localStorage XSS) cannot be replayed from another origin.
|
|
556
|
+
*
|
|
557
|
+
* When the runtime lacks Web Crypto (`crypto.subtle.generateKey`, e.g. a
|
|
558
|
+
* non-HTTPS context), DPoP cannot be used and the SDK silently keeps the
|
|
559
|
+
* Bearer session — login still succeeds. Callers never deal with DPoP
|
|
560
|
+
* directly; pass `{ rememberSession: true }` only to keep the user signed in
|
|
561
|
+
* across reloads.
|
|
556
562
|
*/
|
|
557
563
|
async login(email, password, options) {
|
|
558
564
|
this._log("login", email);
|
|
559
|
-
const
|
|
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
|
-
}
|
|
565
|
+
const wantsRemember = options?.rememberSession === true;
|
|
565
566
|
const headers = {
|
|
566
567
|
"Content-Type": "application/json"
|
|
567
568
|
};
|
|
@@ -584,14 +585,30 @@ var GeonicDBModule = (() => {
|
|
|
584
585
|
this._tokenType = "Bearer";
|
|
585
586
|
this._refreshToken = data.refreshToken;
|
|
586
587
|
this._email = email;
|
|
587
|
-
this.
|
|
588
|
+
this._rememberSession = wantsRemember;
|
|
588
589
|
this._invalidateAuthScopedState();
|
|
589
|
-
if (
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
590
|
+
if (!wantsRemember && this._tenant) {
|
|
591
|
+
void clearDPoPSession(this._tenant);
|
|
592
|
+
}
|
|
593
|
+
if (this._dpopReady) await this._dpopReady;
|
|
594
|
+
if (this._dpopKeyPair) {
|
|
595
|
+
try {
|
|
596
|
+
const bound = await this._bindDpopSession();
|
|
597
|
+
if (this._rememberSession) {
|
|
598
|
+
await this._persistDpopSession();
|
|
599
|
+
}
|
|
600
|
+
return bound;
|
|
601
|
+
} catch (err) {
|
|
602
|
+
this._token = null;
|
|
603
|
+
this._tokenExpiry = 0;
|
|
604
|
+
this._tokenType = "Bearer";
|
|
605
|
+
this._refreshToken = null;
|
|
606
|
+
this._tokenPromise = null;
|
|
607
|
+
this._rememberSession = false;
|
|
608
|
+
this._email = null;
|
|
609
|
+
this._invalidateAuthScopedState();
|
|
610
|
+
throw err;
|
|
593
611
|
}
|
|
594
|
-
return bound;
|
|
595
612
|
}
|
|
596
613
|
return data;
|
|
597
614
|
}
|
|
@@ -685,18 +702,19 @@ var GeonicDBModule = (() => {
|
|
|
685
702
|
this._refreshToken = null;
|
|
686
703
|
this._tokenPromise = null;
|
|
687
704
|
this._email = null;
|
|
688
|
-
if (this.
|
|
705
|
+
if (this._tenant) {
|
|
689
706
|
void clearDPoPSession(this._tenant);
|
|
690
707
|
}
|
|
691
|
-
this.
|
|
708
|
+
this._rememberSession = false;
|
|
692
709
|
this._anonymous = this._initialAnonymous;
|
|
693
710
|
this._invalidateAuthScopedState();
|
|
694
711
|
}
|
|
695
712
|
/**
|
|
696
|
-
* Restore a
|
|
697
|
-
*
|
|
698
|
-
*
|
|
699
|
-
*
|
|
713
|
+
* Restore a session previously persisted via
|
|
714
|
+
* `login(email, password, { rememberSession: true })`. Returns true if a
|
|
715
|
+
* usable session was found and loaded into the
|
|
716
|
+
* SDK, false otherwise (no entry, IndexedDB unavailable, or the entry was
|
|
717
|
+
* for a different tenant).
|
|
700
718
|
*
|
|
701
719
|
* After a successful restore the SDK behaves as if `login()` had just
|
|
702
720
|
* succeeded: tokens are set, the DPoP key pair is rehydrated as a
|
|
@@ -724,19 +742,19 @@ var GeonicDBModule = (() => {
|
|
|
724
742
|
this._dpopKeyPair = persisted.keyPair;
|
|
725
743
|
this._dpopReady = Promise.resolve();
|
|
726
744
|
this._dpopNonce = null;
|
|
727
|
-
this.
|
|
745
|
+
this._rememberSession = true;
|
|
728
746
|
this._email = persisted.email ?? null;
|
|
729
747
|
this._tokenPromise = null;
|
|
730
748
|
this._invalidateAuthScopedState();
|
|
731
749
|
return true;
|
|
732
750
|
}
|
|
733
751
|
/**
|
|
734
|
-
* Whether this AuthManager currently
|
|
735
|
-
* Exposed for the `GeonicDB` wrapper
|
|
736
|
-
* consumers should read `
|
|
752
|
+
* Whether this AuthManager currently keeps the session persisted across
|
|
753
|
+
* reloads (`rememberSession`). Exposed for the `GeonicDB` wrapper; internal
|
|
754
|
+
* consumers should read `_rememberSession` directly.
|
|
737
755
|
*/
|
|
738
|
-
|
|
739
|
-
return this.
|
|
756
|
+
isSessionRemembered() {
|
|
757
|
+
return this._rememberSession;
|
|
740
758
|
}
|
|
741
759
|
/**
|
|
742
760
|
* Persist the current DPoP session to IndexedDB. Best-effort: a failed
|
|
@@ -746,7 +764,7 @@ var GeonicDBModule = (() => {
|
|
|
746
764
|
* @internal
|
|
747
765
|
*/
|
|
748
766
|
async _persistDpopSession() {
|
|
749
|
-
if (!this.
|
|
767
|
+
if (!this._rememberSession || !this._tenant || !this._dpopKeyPair || !this._token || !this._refreshToken) {
|
|
750
768
|
return;
|
|
751
769
|
}
|
|
752
770
|
const ok = await saveDPoPSession({
|
|
@@ -829,7 +847,7 @@ var GeonicDBModule = (() => {
|
|
|
829
847
|
expiresIn: data.expiresIn,
|
|
830
848
|
refreshToken: this._refreshToken
|
|
831
849
|
});
|
|
832
|
-
if (this.
|
|
850
|
+
if (this._rememberSession) void this._persistDpopSession();
|
|
833
851
|
return this._token;
|
|
834
852
|
} catch (err) {
|
|
835
853
|
this._tokenPromise = null;
|
|
@@ -1410,35 +1428,29 @@ var GeonicDBModule = (() => {
|
|
|
1410
1428
|
}
|
|
1411
1429
|
// --- Authentication ---
|
|
1412
1430
|
/**
|
|
1413
|
-
* Login with email and password
|
|
1431
|
+
* Login with email and password.
|
|
1414
1432
|
*
|
|
1415
|
-
*
|
|
1416
|
-
*
|
|
1417
|
-
*
|
|
1418
|
-
*
|
|
1419
|
-
*
|
|
1433
|
+
* The session is sender-constrained with DPoP (RFC 9449) automatically and
|
|
1434
|
+
* transparently — there is no DPoP option to set. A token leaked from
|
|
1435
|
+
* storage (e.g. localStorage XSS) is therefore unusable from another origin.
|
|
1436
|
+
* On runtimes without Web Crypto, the SDK silently keeps a plain Bearer
|
|
1437
|
+
* session and login still succeeds.
|
|
1420
1438
|
*
|
|
1421
1439
|
* @example
|
|
1422
1440
|
* ```ts
|
|
1423
|
-
* //
|
|
1441
|
+
* // Standard login — DPoP applied transparently when supported.
|
|
1424
1442
|
* await db.login('user@example.com', 'password');
|
|
1425
1443
|
*
|
|
1426
|
-
* //
|
|
1427
|
-
* await db.login('user@example.com', 'password', {
|
|
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
|
-
* });
|
|
1444
|
+
* // Keep the user signed in across page reloads (browser SPA UX).
|
|
1445
|
+
* await db.login('user@example.com', 'password', { rememberSession: true });
|
|
1434
1446
|
* ```
|
|
1435
1447
|
*/
|
|
1436
1448
|
async login(email, password, options) {
|
|
1437
1449
|
return this._auth.login(email, password, options);
|
|
1438
1450
|
}
|
|
1439
1451
|
/**
|
|
1440
|
-
* Rehydrate a
|
|
1441
|
-
* `login(
|
|
1452
|
+
* Rehydrate a session previously persisted via
|
|
1453
|
+
* `login(email, password, { rememberSession: true })`.
|
|
1442
1454
|
*
|
|
1443
1455
|
* Returns true if a session for this tenant was found and loaded; false
|
|
1444
1456
|
* if there was no entry, IndexedDB is unavailable, or the persisted
|
|
@@ -1449,7 +1461,7 @@ var GeonicDBModule = (() => {
|
|
|
1449
1461
|
* ```ts
|
|
1450
1462
|
* const db = new GeonicDB({ baseUrl, tenant });
|
|
1451
1463
|
* if (await db.restoreSession()) {
|
|
1452
|
-
* //
|
|
1464
|
+
* // Session (tokens + non-extractable key) recovered — boot the app.
|
|
1453
1465
|
* } else {
|
|
1454
1466
|
* // No session; show the login form.
|
|
1455
1467
|
* }
|