@geolonia/geonicdb-sdk 0.8.0 → 0.9.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 +29 -1
- package/geonicdb.cjs +144 -9
- package/geonicdb.cjs.map +2 -2
- package/geonicdb.d.ts +1 -0
- package/geonicdb.iife.js +144 -9
- package/geonicdb.iife.js.map +2 -2
- package/geonicdb.mjs +144 -9
- package/geonicdb.mjs.map +2 -2
- package/package.json +1 -1
package/geonicdb.iife.js
CHANGED
|
@@ -437,8 +437,16 @@ var GeonicDBModule = (() => {
|
|
|
437
437
|
emitCacheEvent(name, payload) {
|
|
438
438
|
this._emitCacheEvent?.(name, payload);
|
|
439
439
|
}
|
|
440
|
-
/**
|
|
441
|
-
|
|
440
|
+
/**
|
|
441
|
+
* Login with email and password (Bearer JWT).
|
|
442
|
+
*
|
|
443
|
+
* Pass `{ dpop: true }` to immediately exchange the Bearer session for a
|
|
444
|
+
* DPoP sender-constrained session via `/auth/dpop-bind` (RFC 9449). The
|
|
445
|
+
* returned tokens become unusable without this SDK instance's
|
|
446
|
+
* non-extractable private key, so a token leaked from storage (e.g.
|
|
447
|
+
* localStorage XSS) cannot be replayed from another origin.
|
|
448
|
+
*/
|
|
449
|
+
async login(email, password, options) {
|
|
442
450
|
this._log("login", email);
|
|
443
451
|
const headers = {
|
|
444
452
|
"Content-Type": "application/json"
|
|
@@ -462,8 +470,75 @@ var GeonicDBModule = (() => {
|
|
|
462
470
|
this._tokenType = "Bearer";
|
|
463
471
|
this._refreshToken = data.refreshToken;
|
|
464
472
|
this._invalidateAuthScopedState();
|
|
473
|
+
if (options?.dpop) {
|
|
474
|
+
const bound = await this._bindDpopSession();
|
|
475
|
+
return bound;
|
|
476
|
+
}
|
|
465
477
|
return data;
|
|
466
478
|
}
|
|
479
|
+
/**
|
|
480
|
+
* Exchange the current Bearer session for a DPoP-bound session via
|
|
481
|
+
* `POST /auth/dpop-bind` (RFC 9449). The server re-issues access and
|
|
482
|
+
* refresh tokens with `cnf.jkt` matching this SDK's DPoP key pair.
|
|
483
|
+
*
|
|
484
|
+
* Assumes `_token` / `_refreshToken` are already populated by a prior
|
|
485
|
+
* `login()` call (callers should not invoke this directly).
|
|
486
|
+
*
|
|
487
|
+
* @internal
|
|
488
|
+
*/
|
|
489
|
+
async _bindDpopSession() {
|
|
490
|
+
if (!this._token) {
|
|
491
|
+
throw new AuthenticationError(
|
|
492
|
+
"Cannot bind DPoP session: no Bearer token. login() must succeed first."
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
if (this._dpopReady) await this._dpopReady;
|
|
496
|
+
if (!this._dpopKeyPair) {
|
|
497
|
+
throw new AuthenticationError(
|
|
498
|
+
"DPoP binding requires a generated DPoP key pair, but key generation was unavailable or failed. DPoP requires Web Crypto subtle.generateKey."
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
const bindUrl = this._baseUrl + "/auth/dpop-bind";
|
|
502
|
+
const accessToken = this._token;
|
|
503
|
+
const data = await this._doDpopBind(bindUrl, accessToken, this._dpopNonce, 0);
|
|
504
|
+
this._token = data.accessToken;
|
|
505
|
+
this._tokenExpiry = Date.now() + (data.expiresIn - 60) * 1e3;
|
|
506
|
+
this._tokenType = "DPoP";
|
|
507
|
+
this._refreshToken = data.refreshToken;
|
|
508
|
+
this._invalidateAuthScopedState();
|
|
509
|
+
return { ...data, tokenType: "DPoP" };
|
|
510
|
+
}
|
|
511
|
+
async _doDpopBind(bindUrl, accessToken, dpopNonce, retryCount) {
|
|
512
|
+
const dpopProof = await createDPoPProof(
|
|
513
|
+
this._dpopKeyPair,
|
|
514
|
+
"POST",
|
|
515
|
+
bindUrl,
|
|
516
|
+
accessToken,
|
|
517
|
+
dpopNonce
|
|
518
|
+
);
|
|
519
|
+
if (!dpopProof) {
|
|
520
|
+
throw new AuthenticationError("DPoP proof generation failed");
|
|
521
|
+
}
|
|
522
|
+
const headers = {
|
|
523
|
+
"Content-Type": "application/json",
|
|
524
|
+
Authorization: "Bearer " + accessToken,
|
|
525
|
+
DPoP: dpopProof
|
|
526
|
+
};
|
|
527
|
+
if (this._tenant) headers["NGSILD-Tenant"] = this._tenant;
|
|
528
|
+
const res = await fetch(bindUrl, { method: "POST", headers });
|
|
529
|
+
const serverNonce = res.headers.get("DPoP-Nonce");
|
|
530
|
+
if (serverNonce) this._dpopNonce = serverNonce;
|
|
531
|
+
if (res.status === 401 && serverNonce && serverNonce !== dpopNonce && retryCount < _AuthManager.MAX_DPOP_RETRIES) {
|
|
532
|
+
return this._doDpopBind(bindUrl, accessToken, serverNonce, retryCount + 1);
|
|
533
|
+
}
|
|
534
|
+
if (!res.ok) {
|
|
535
|
+
const e = await res.json().catch(() => ({}));
|
|
536
|
+
throw new AuthenticationError(
|
|
537
|
+
e.detail || e.description || "DPoP bind failed: " + res.status
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
return await res.json();
|
|
541
|
+
}
|
|
467
542
|
/**
|
|
468
543
|
* Set credentials externally (e.g. from a login API response).
|
|
469
544
|
* When tokenType is 'Bearer' and refreshToken is provided, all subsequent
|
|
@@ -535,12 +610,15 @@ var GeonicDBModule = (() => {
|
|
|
535
610
|
return this._tokenPromise;
|
|
536
611
|
}
|
|
537
612
|
async _refreshBearerToken() {
|
|
538
|
-
this._log("refreshing
|
|
613
|
+
this._log("refreshing token (type=%s)", this._tokenType);
|
|
539
614
|
try {
|
|
540
|
-
const
|
|
615
|
+
const isDPoP = this._tokenType === "DPoP";
|
|
616
|
+
const refreshUrl = this._baseUrl + "/auth/refresh";
|
|
617
|
+
const body = JSON.stringify({ refreshToken: this._refreshToken });
|
|
618
|
+
const res = isDPoP ? await this._doDpopRefresh(refreshUrl, body, this._dpopNonce, 0) : await fetch(refreshUrl, {
|
|
541
619
|
method: "POST",
|
|
542
620
|
headers: { "Content-Type": "application/json" },
|
|
543
|
-
body
|
|
621
|
+
body
|
|
544
622
|
});
|
|
545
623
|
if (!res.ok) {
|
|
546
624
|
this._refreshToken = null;
|
|
@@ -551,7 +629,7 @@ var GeonicDBModule = (() => {
|
|
|
551
629
|
const data = await res.json();
|
|
552
630
|
this._token = data.accessToken;
|
|
553
631
|
this._tokenExpiry = Date.now() + (data.expiresIn - 60) * 1e3;
|
|
554
|
-
this._tokenType = "Bearer";
|
|
632
|
+
this._tokenType = data.tokenType ?? (isDPoP ? "DPoP" : "Bearer");
|
|
555
633
|
this._refreshToken = data.refreshToken;
|
|
556
634
|
this._tokenPromise = null;
|
|
557
635
|
this.onTokenRefresh?.({
|
|
@@ -566,6 +644,44 @@ var GeonicDBModule = (() => {
|
|
|
566
644
|
throw err;
|
|
567
645
|
}
|
|
568
646
|
}
|
|
647
|
+
/**
|
|
648
|
+
* Perform a DPoP-bound `/auth/refresh` call with the standard RFC 9449 §8
|
|
649
|
+
* `use_dpop_nonce` retry handshake.
|
|
650
|
+
*
|
|
651
|
+
* @internal
|
|
652
|
+
*/
|
|
653
|
+
async _doDpopRefresh(refreshUrl, body, dpopNonce, retryCount) {
|
|
654
|
+
if (this._dpopReady) await this._dpopReady;
|
|
655
|
+
if (!this._dpopKeyPair) {
|
|
656
|
+
throw new AuthenticationError(
|
|
657
|
+
"DPoP-bound refresh requires a DPoP key pair, but key generation was unavailable."
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
const proof = await createDPoPProof(
|
|
661
|
+
this._dpopKeyPair,
|
|
662
|
+
"POST",
|
|
663
|
+
refreshUrl,
|
|
664
|
+
null,
|
|
665
|
+
dpopNonce
|
|
666
|
+
);
|
|
667
|
+
if (!proof) {
|
|
668
|
+
throw new AuthenticationError("DPoP proof generation failed");
|
|
669
|
+
}
|
|
670
|
+
const res = await fetch(refreshUrl, {
|
|
671
|
+
method: "POST",
|
|
672
|
+
headers: { "Content-Type": "application/json", DPoP: proof },
|
|
673
|
+
body
|
|
674
|
+
});
|
|
675
|
+
const serverNonce = res.headers.get("DPoP-Nonce");
|
|
676
|
+
if (serverNonce) this._dpopNonce = serverNonce;
|
|
677
|
+
if (res.status === 401 && serverNonce && serverNonce !== dpopNonce && retryCount < _AuthManager.MAX_DPOP_RETRIES) {
|
|
678
|
+
return this._doDpopRefresh(refreshUrl, body, serverNonce, retryCount + 1);
|
|
679
|
+
}
|
|
680
|
+
return res;
|
|
681
|
+
}
|
|
682
|
+
/* istanbul ignore next: PoW + DPoP token-exchange flow is exercised by E2E
|
|
683
|
+
against a live server. Unit-mocking each fetch + crypto step would not
|
|
684
|
+
verify the actual nonce/proof exchange. */
|
|
569
685
|
async _acquireTokenViaPow() {
|
|
570
686
|
try {
|
|
571
687
|
if (this._dpopReady) await this._dpopReady;
|
|
@@ -602,6 +718,8 @@ var GeonicDBModule = (() => {
|
|
|
602
718
|
throw err;
|
|
603
719
|
}
|
|
604
720
|
}
|
|
721
|
+
/* istanbul ignore next: DPoP nonce-retry handshake (RFC 9449 §8) — requires
|
|
722
|
+
a real server emitting `use_dpop_nonce` 400s. Exercised by E2E. */
|
|
605
723
|
async _doTokenExchange(tokenUrl, tokenBody, dpopNonce, retryCount = 0) {
|
|
606
724
|
const reqHeaders = {
|
|
607
725
|
"Content-Type": "application/json"
|
|
@@ -1099,9 +1217,26 @@ var GeonicDBModule = (() => {
|
|
|
1099
1217
|
}
|
|
1100
1218
|
}
|
|
1101
1219
|
// --- Authentication ---
|
|
1102
|
-
/**
|
|
1103
|
-
|
|
1104
|
-
|
|
1220
|
+
/**
|
|
1221
|
+
* Login with email and password (Bearer JWT).
|
|
1222
|
+
*
|
|
1223
|
+
* Pass `{ dpop: true }` to immediately upgrade the session to a DPoP
|
|
1224
|
+
* sender-constrained token (RFC 9449). Recommended for browser apps that
|
|
1225
|
+
* persist tokens in storage exposed to XSS (localStorage / sessionStorage),
|
|
1226
|
+
* since DPoP-bound tokens are unusable without this SDK instance's
|
|
1227
|
+
* non-extractable private key.
|
|
1228
|
+
*
|
|
1229
|
+
* @example
|
|
1230
|
+
* ```ts
|
|
1231
|
+
* // Bearer (default, backward compatible)
|
|
1232
|
+
* await db.login('user@example.com', 'password');
|
|
1233
|
+
*
|
|
1234
|
+
* // DPoP-bound (recommended for SPAs)
|
|
1235
|
+
* await db.login('user@example.com', 'password', { dpop: true });
|
|
1236
|
+
* ```
|
|
1237
|
+
*/
|
|
1238
|
+
async login(email, password, options) {
|
|
1239
|
+
return this._auth.login(email, password, options);
|
|
1105
1240
|
}
|
|
1106
1241
|
/**
|
|
1107
1242
|
* Whether the SDK is currently operating in anonymous mode (no token held).
|