@geolonia/geonicdb-sdk 0.10.0 → 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/geonicdb.mjs CHANGED
@@ -443,11 +443,12 @@ var _AuthManager = class _AuthManager {
443
443
  __publicField(this, "_dpopReady", null);
444
444
  __publicField(this, "_dpopNonce", null);
445
445
  /**
446
- * #1230: when true, every DPoP token mutation (login bind, refresh) is
447
- * mirrored to IndexedDB so the session survives a page reload. Toggled
448
- * by `login({ dpopPersist: true })` and `restoreSession()`.
446
+ * When true, every DPoP token mutation (login bind, refresh) is mirrored to
447
+ * IndexedDB so the session survives a page reload (#1230). Toggled by
448
+ * `login(email, password, { rememberSession: true })` and
449
+ * `restoreSession()` (#1244).
449
450
  */
450
- __publicField(this, "_dpopPersist", false);
451
+ __publicField(this, "_rememberSession", false);
451
452
  /** Email captured at login, persisted for UI display. */
452
453
  __publicField(this, "_email", null);
453
454
  __publicField(this, "_tokenPromise", null);
@@ -512,22 +513,22 @@ var _AuthManager = class _AuthManager {
512
513
  this._emitCacheEvent?.(name, payload);
513
514
  }
514
515
  /**
515
- * Login with email and password (Bearer JWT).
516
+ * Login with email and password.
516
517
  *
517
- * Pass `{ dpop: true }` to immediately exchange the Bearer session for a
518
- * DPoP sender-constrained session via `/auth/dpop-bind` (RFC 9449). The
519
- * returned tokens become unusable without this SDK instance's
520
- * non-extractable private key, so a token leaked from storage (e.g.
521
- * localStorage XSS) cannot be replayed from another origin.
518
+ * #1244: DPoP (RFC 9449) is transparent and always-on — there is no `dpop`
519
+ * option to set. After the Bearer login succeeds, the SDK automatically
520
+ * exchanges it for a DPoP sender-constrained session so a token leaked from
521
+ * storage (e.g. localStorage XSS) cannot be replayed from another origin.
522
+ *
523
+ * When the runtime lacks Web Crypto (`crypto.subtle.generateKey`, e.g. a
524
+ * non-HTTPS context), DPoP cannot be used and the SDK silently keeps the
525
+ * Bearer session — login still succeeds. Callers never deal with DPoP
526
+ * directly; pass `{ rememberSession: true }` only to keep the user signed in
527
+ * across reloads.
522
528
  */
523
529
  async login(email, password, options) {
524
530
  this._log("login", email);
525
- const wantsDpopPersist = options?.dpopPersist === true;
526
- if (wantsDpopPersist && !options?.dpop) {
527
- throw new AuthenticationError(
528
- "dpopPersist: true requires dpop: true. Persisting Bearer tokens is the caller's responsibility (localStorage / sessionStorage)."
529
- );
530
- }
531
+ const wantsRemember = options?.rememberSession === true;
531
532
  const headers = {
532
533
  "Content-Type": "application/json"
533
534
  };
@@ -550,14 +551,30 @@ var _AuthManager = class _AuthManager {
550
551
  this._tokenType = "Bearer";
551
552
  this._refreshToken = data.refreshToken;
552
553
  this._email = email;
553
- this._dpopPersist = wantsDpopPersist;
554
+ this._rememberSession = wantsRemember;
554
555
  this._invalidateAuthScopedState();
555
- if (options?.dpop) {
556
- const bound = await this._bindDpopSession();
557
- if (this._dpopPersist) {
558
- await this._persistDpopSession();
556
+ if (!wantsRemember && this._tenant) {
557
+ void clearDPoPSession(this._tenant);
558
+ }
559
+ if (this._dpopReady) await this._dpopReady;
560
+ if (this._dpopKeyPair) {
561
+ try {
562
+ const bound = await this._bindDpopSession();
563
+ if (this._rememberSession) {
564
+ await this._persistDpopSession();
565
+ }
566
+ return bound;
567
+ } catch (err) {
568
+ this._token = null;
569
+ this._tokenExpiry = 0;
570
+ this._tokenType = "Bearer";
571
+ this._refreshToken = null;
572
+ this._tokenPromise = null;
573
+ this._rememberSession = false;
574
+ this._email = null;
575
+ this._invalidateAuthScopedState();
576
+ throw err;
559
577
  }
560
- return bound;
561
578
  }
562
579
  return data;
563
580
  }
@@ -651,18 +668,19 @@ var _AuthManager = class _AuthManager {
651
668
  this._refreshToken = null;
652
669
  this._tokenPromise = null;
653
670
  this._email = null;
654
- if (this._dpopPersist && this._tenant) {
671
+ if (this._tenant) {
655
672
  void clearDPoPSession(this._tenant);
656
673
  }
657
- this._dpopPersist = false;
674
+ this._rememberSession = false;
658
675
  this._anonymous = this._initialAnonymous;
659
676
  this._invalidateAuthScopedState();
660
677
  }
661
678
  /**
662
- * Restore a DPoP session previously persisted via `login({ dpop: true,
663
- * dpopPersist: true })`. Returns true if a usable session was found and
664
- * loaded into the SDK, false otherwise (no entry, IndexedDB unavailable,
665
- * or the entry was for a different tenant).
679
+ * Restore a session previously persisted via
680
+ * `login(email, password, { rememberSession: true })`. Returns true if a
681
+ * usable session was found and loaded into the
682
+ * SDK, false otherwise (no entry, IndexedDB unavailable, or the entry was
683
+ * for a different tenant).
666
684
  *
667
685
  * After a successful restore the SDK behaves as if `login()` had just
668
686
  * succeeded: tokens are set, the DPoP key pair is rehydrated as a
@@ -690,19 +708,19 @@ var _AuthManager = class _AuthManager {
690
708
  this._dpopKeyPair = persisted.keyPair;
691
709
  this._dpopReady = Promise.resolve();
692
710
  this._dpopNonce = null;
693
- this._dpopPersist = true;
711
+ this._rememberSession = true;
694
712
  this._email = persisted.email ?? null;
695
713
  this._tokenPromise = null;
696
714
  this._invalidateAuthScopedState();
697
715
  return true;
698
716
  }
699
717
  /**
700
- * Whether this AuthManager currently has DPoP persistence enabled.
701
- * Exposed for the `GeonicDB` wrapper to advertise behavior; internal
702
- * consumers should read `_dpopPersist` directly.
718
+ * Whether this AuthManager currently keeps the session persisted across
719
+ * reloads (`rememberSession`). Exposed for the `GeonicDB` wrapper; internal
720
+ * consumers should read `_rememberSession` directly.
703
721
  */
704
- isDpopPersisted() {
705
- return this._dpopPersist;
722
+ isSessionRemembered() {
723
+ return this._rememberSession;
706
724
  }
707
725
  /**
708
726
  * Persist the current DPoP session to IndexedDB. Best-effort: a failed
@@ -712,7 +730,7 @@ var _AuthManager = class _AuthManager {
712
730
  * @internal
713
731
  */
714
732
  async _persistDpopSession() {
715
- if (!this._dpopPersist || !this._tenant || !this._dpopKeyPair || !this._token || !this._refreshToken) {
733
+ if (!this._rememberSession || !this._tenant || !this._dpopKeyPair || !this._token || !this._refreshToken) {
716
734
  return;
717
735
  }
718
736
  const ok = await saveDPoPSession({
@@ -795,7 +813,7 @@ var _AuthManager = class _AuthManager {
795
813
  expiresIn: data.expiresIn,
796
814
  refreshToken: this._refreshToken
797
815
  });
798
- if (this._dpopPersist) void this._persistDpopSession();
816
+ if (this._rememberSession) void this._persistDpopSession();
799
817
  return this._token;
800
818
  } catch (err) {
801
819
  this._tokenPromise = null;
@@ -1376,35 +1394,29 @@ var GeonicDB = class extends EventEmitter {
1376
1394
  }
1377
1395
  // --- Authentication ---
1378
1396
  /**
1379
- * Login with email and password (Bearer JWT).
1397
+ * Login with email and password.
1380
1398
  *
1381
- * Pass `{ dpop: true }` to immediately upgrade the session to a DPoP
1382
- * sender-constrained token (RFC 9449). Recommended for browser apps that
1383
- * persist tokens in storage exposed to XSS (localStorage / sessionStorage),
1384
- * since DPoP-bound tokens are unusable without this SDK instance's
1385
- * non-extractable private key.
1399
+ * The session is sender-constrained with DPoP (RFC 9449) automatically and
1400
+ * transparently — there is no DPoP option to set. A token leaked from
1401
+ * storage (e.g. localStorage XSS) is therefore unusable from another origin.
1402
+ * On runtimes without Web Crypto, the SDK silently keeps a plain Bearer
1403
+ * session and login still succeeds.
1386
1404
  *
1387
1405
  * @example
1388
1406
  * ```ts
1389
- * // Bearer (default, backward compatible)
1407
+ * // Standard login — DPoP applied transparently when supported.
1390
1408
  * await db.login('user@example.com', 'password');
1391
1409
  *
1392
- * // DPoP-bound (recommended for SPAs)
1393
- * await db.login('user@example.com', 'password', { dpop: true });
1394
- *
1395
- * // DPoP-bound + persisted across reloads (browser SPA UX)
1396
- * await db.login('user@example.com', 'password', {
1397
- * dpop: true,
1398
- * dpopPersist: true,
1399
- * });
1410
+ * // Keep the user signed in across page reloads (browser SPA UX).
1411
+ * await db.login('user@example.com', 'password', { rememberSession: true });
1400
1412
  * ```
1401
1413
  */
1402
1414
  async login(email, password, options) {
1403
1415
  return this._auth.login(email, password, options);
1404
1416
  }
1405
1417
  /**
1406
- * Rehydrate a DPoP session previously persisted via
1407
- * `login({ dpop: true, dpopPersist: true })`.
1418
+ * Rehydrate a session previously persisted via
1419
+ * `login(email, password, { rememberSession: true })`.
1408
1420
  *
1409
1421
  * Returns true if a session for this tenant was found and loaded; false
1410
1422
  * if there was no entry, IndexedDB is unavailable, or the persisted
@@ -1415,7 +1427,7 @@ var GeonicDB = class extends EventEmitter {
1415
1427
  * ```ts
1416
1428
  * const db = new GeonicDB({ baseUrl, tenant });
1417
1429
  * if (await db.restoreSession()) {
1418
- * // Tokens + DPoP private key recovered from IndexedDB — boot the app.
1430
+ * // Session (tokens + non-extractable key) recovered — boot the app.
1419
1431
  * } else {
1420
1432
  * // No session; show the login form.
1421
1433
  * }