@geolonia/geonicdb-sdk 0.7.1 → 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 +43 -3
- package/geonicdb.cjs +194 -43
- package/geonicdb.cjs.map +2 -2
- package/geonicdb.d.ts +2 -0
- package/geonicdb.iife.js +194 -43
- package/geonicdb.iife.js.map +2 -2
- package/geonicdb.mjs +194 -43
- package/geonicdb.mjs.map +2 -2
- package/package.json +1 -1
package/geonicdb.iife.js
CHANGED
|
@@ -32,6 +32,7 @@ var GeonicDBModule = (() => {
|
|
|
32
32
|
NotFoundError: () => NotFoundError,
|
|
33
33
|
RateLimitError: () => RateLimitError,
|
|
34
34
|
ValidationError: () => ValidationError,
|
|
35
|
+
buildPathWithParams: () => buildPathWithParams,
|
|
35
36
|
default: () => index_default
|
|
36
37
|
});
|
|
37
38
|
|
|
@@ -436,8 +437,16 @@ var GeonicDBModule = (() => {
|
|
|
436
437
|
emitCacheEvent(name, payload) {
|
|
437
438
|
this._emitCacheEvent?.(name, payload);
|
|
438
439
|
}
|
|
439
|
-
/**
|
|
440
|
-
|
|
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) {
|
|
441
450
|
this._log("login", email);
|
|
442
451
|
const headers = {
|
|
443
452
|
"Content-Type": "application/json"
|
|
@@ -461,8 +470,75 @@ var GeonicDBModule = (() => {
|
|
|
461
470
|
this._tokenType = "Bearer";
|
|
462
471
|
this._refreshToken = data.refreshToken;
|
|
463
472
|
this._invalidateAuthScopedState();
|
|
473
|
+
if (options?.dpop) {
|
|
474
|
+
const bound = await this._bindDpopSession();
|
|
475
|
+
return bound;
|
|
476
|
+
}
|
|
464
477
|
return data;
|
|
465
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
|
+
}
|
|
466
542
|
/**
|
|
467
543
|
* Set credentials externally (e.g. from a login API response).
|
|
468
544
|
* When tokenType is 'Bearer' and refreshToken is provided, all subsequent
|
|
@@ -534,12 +610,15 @@ var GeonicDBModule = (() => {
|
|
|
534
610
|
return this._tokenPromise;
|
|
535
611
|
}
|
|
536
612
|
async _refreshBearerToken() {
|
|
537
|
-
this._log("refreshing
|
|
613
|
+
this._log("refreshing token (type=%s)", this._tokenType);
|
|
538
614
|
try {
|
|
539
|
-
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, {
|
|
540
619
|
method: "POST",
|
|
541
620
|
headers: { "Content-Type": "application/json" },
|
|
542
|
-
body
|
|
621
|
+
body
|
|
543
622
|
});
|
|
544
623
|
if (!res.ok) {
|
|
545
624
|
this._refreshToken = null;
|
|
@@ -550,7 +629,7 @@ var GeonicDBModule = (() => {
|
|
|
550
629
|
const data = await res.json();
|
|
551
630
|
this._token = data.accessToken;
|
|
552
631
|
this._tokenExpiry = Date.now() + (data.expiresIn - 60) * 1e3;
|
|
553
|
-
this._tokenType = "Bearer";
|
|
632
|
+
this._tokenType = data.tokenType ?? (isDPoP ? "DPoP" : "Bearer");
|
|
554
633
|
this._refreshToken = data.refreshToken;
|
|
555
634
|
this._tokenPromise = null;
|
|
556
635
|
this.onTokenRefresh?.({
|
|
@@ -565,6 +644,44 @@ var GeonicDBModule = (() => {
|
|
|
565
644
|
throw err;
|
|
566
645
|
}
|
|
567
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. */
|
|
568
685
|
async _acquireTokenViaPow() {
|
|
569
686
|
try {
|
|
570
687
|
if (this._dpopReady) await this._dpopReady;
|
|
@@ -601,6 +718,8 @@ var GeonicDBModule = (() => {
|
|
|
601
718
|
throw err;
|
|
602
719
|
}
|
|
603
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. */
|
|
604
723
|
async _doTokenExchange(tokenUrl, tokenBody, dpopNonce, retryCount = 0) {
|
|
605
724
|
const reqHeaders = {
|
|
606
725
|
"Content-Type": "application/json"
|
|
@@ -872,6 +991,7 @@ var GeonicDBModule = (() => {
|
|
|
872
991
|
if (options) {
|
|
873
992
|
if (options.entityTypes) msg.entityTypes = options.entityTypes;
|
|
874
993
|
if (options.idPattern) msg.idPattern = options.idPattern;
|
|
994
|
+
if (options.scopeQ) msg.scopeQ = options.scopeQ;
|
|
875
995
|
}
|
|
876
996
|
this._subscription = msg;
|
|
877
997
|
if (this._pendingSubscription) return;
|
|
@@ -1097,9 +1217,26 @@ var GeonicDBModule = (() => {
|
|
|
1097
1217
|
}
|
|
1098
1218
|
}
|
|
1099
1219
|
// --- Authentication ---
|
|
1100
|
-
/**
|
|
1101
|
-
|
|
1102
|
-
|
|
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);
|
|
1103
1240
|
}
|
|
1104
1241
|
/**
|
|
1105
1242
|
* Whether the SDK is currently operating in anonymous mode (no token held).
|
|
@@ -1224,14 +1361,14 @@ var GeonicDBModule = (() => {
|
|
|
1224
1361
|
}
|
|
1225
1362
|
/** Count entities matching the given filters. */
|
|
1226
1363
|
async count(params) {
|
|
1227
|
-
const
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
);
|
|
1364
|
+
const path = buildPathWithParams("/ngsi-ld/v1/entities", {
|
|
1365
|
+
count: true,
|
|
1366
|
+
limit: 0,
|
|
1367
|
+
type: params?.type,
|
|
1368
|
+
q: params?.q,
|
|
1369
|
+
scopeQ: params?.scopeQ
|
|
1370
|
+
});
|
|
1371
|
+
const res = await this._auth.request("GET", path);
|
|
1235
1372
|
if (!res.ok) {
|
|
1236
1373
|
const e = await res.json().catch(() => ({}));
|
|
1237
1374
|
throw createErrorFromResponse(res.status, e, "Count failed");
|
|
@@ -1290,24 +1427,11 @@ var GeonicDBModule = (() => {
|
|
|
1290
1427
|
// --- Temporal API ---
|
|
1291
1428
|
/** Query temporal entities. */
|
|
1292
1429
|
async getTemporalEntities(params) {
|
|
1293
|
-
let qs = "";
|
|
1294
|
-
if (params) {
|
|
1295
|
-
const parts = [];
|
|
1296
|
-
if (params.type) parts.push("type=" + encodeURIComponent(params.type));
|
|
1297
|
-
if (params.id) parts.push("id=" + encodeURIComponent(params.id));
|
|
1298
|
-
if (params.idPattern) parts.push("idPattern=" + encodeURIComponent(params.idPattern));
|
|
1299
|
-
if (params.attrs) parts.push("attrs=" + encodeURIComponent(params.attrs));
|
|
1300
|
-
if (params.q) parts.push("q=" + encodeURIComponent(params.q));
|
|
1301
|
-
if (params.timeproperty) parts.push("timeproperty=" + encodeURIComponent(params.timeproperty));
|
|
1302
|
-
if (params.timeAt) parts.push("timeAt=" + encodeURIComponent(params.timeAt));
|
|
1303
|
-
if (params.endTimeAt) parts.push("endTimeAt=" + encodeURIComponent(params.endTimeAt));
|
|
1304
|
-
if (params.timerel) parts.push("timerel=" + encodeURIComponent(params.timerel));
|
|
1305
|
-
if (params.limit != null) parts.push("limit=" + params.limit);
|
|
1306
|
-
if (params.offset != null) parts.push("offset=" + params.offset);
|
|
1307
|
-
if (parts.length) qs = "?" + parts.join("&");
|
|
1308
|
-
}
|
|
1309
1430
|
return this._jsonGet(
|
|
1310
|
-
|
|
1431
|
+
buildPathWithParams(
|
|
1432
|
+
"/ngsi-ld/v1/temporal/entities",
|
|
1433
|
+
params
|
|
1434
|
+
),
|
|
1311
1435
|
"Temporal query failed"
|
|
1312
1436
|
);
|
|
1313
1437
|
}
|
|
@@ -1375,6 +1499,11 @@ var GeonicDBModule = (() => {
|
|
|
1375
1499
|
/**
|
|
1376
1500
|
* Make an authenticated API request.
|
|
1377
1501
|
* Automatically checks response status, parses JSON, and throws on error.
|
|
1502
|
+
*
|
|
1503
|
+
* Empty bodies (`Content-Length: 0` 含む) は Content-Type に関係なく `null`
|
|
1504
|
+
* を返す (#1145)。NGSI-LD の POST / DELETE / PATCH 系は仕様上ボディが
|
|
1505
|
+
* 空だが、サーバ実装が `Content-Type: application/ld+json` を付けて返す
|
|
1506
|
+
* ケースがあり、そこで `res.json()` が SyntaxError を投げて止まっていた。
|
|
1378
1507
|
*/
|
|
1379
1508
|
async request(method, path, body) {
|
|
1380
1509
|
const res = await this._auth.request(method, path, body);
|
|
@@ -1382,9 +1511,15 @@ var GeonicDBModule = (() => {
|
|
|
1382
1511
|
const e = await res.json().catch(() => ({}));
|
|
1383
1512
|
throw createErrorFromResponse(res.status, e, "Request failed: " + res.status);
|
|
1384
1513
|
}
|
|
1514
|
+
if (res.status === 204) return null;
|
|
1515
|
+
if (res.headers.get("Content-Length") === "0") return null;
|
|
1385
1516
|
const ct = res.headers.get("Content-Type") || "";
|
|
1386
|
-
if (
|
|
1387
|
-
if (ct.indexOf("json") !== -1)
|
|
1517
|
+
if (!ct) return null;
|
|
1518
|
+
if (ct.indexOf("json") !== -1) {
|
|
1519
|
+
const text = await res.text();
|
|
1520
|
+
if (!text) return null;
|
|
1521
|
+
return JSON.parse(text);
|
|
1522
|
+
}
|
|
1388
1523
|
return res.text();
|
|
1389
1524
|
}
|
|
1390
1525
|
/**
|
|
@@ -1421,13 +1556,29 @@ var GeonicDBModule = (() => {
|
|
|
1421
1556
|
window.GeonicDB = GeonicDB;
|
|
1422
1557
|
}
|
|
1423
1558
|
function buildEntitiesPath(params) {
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1559
|
+
return buildPathWithParams(
|
|
1560
|
+
"/ngsi-ld/v1/entities",
|
|
1561
|
+
params
|
|
1562
|
+
);
|
|
1563
|
+
}
|
|
1564
|
+
function buildPathWithParams(path, params) {
|
|
1565
|
+
if (!params) return path;
|
|
1566
|
+
const sp = new URLSearchParams();
|
|
1567
|
+
for (const [key, value] of Object.entries(params)) {
|
|
1568
|
+
if (value === void 0 || value === null) continue;
|
|
1569
|
+
if (typeof value === "boolean") {
|
|
1570
|
+
sp.append(key, value ? "true" : "false");
|
|
1571
|
+
} else if (typeof value === "number") {
|
|
1572
|
+
sp.append(key, String(value));
|
|
1573
|
+
} else if (typeof value === "string") {
|
|
1574
|
+
if (value === "") continue;
|
|
1575
|
+
sp.append(key, value);
|
|
1576
|
+
} else {
|
|
1577
|
+
continue;
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
const qs = sp.toString();
|
|
1581
|
+
return qs ? `${path}?${qs}` : path;
|
|
1431
1582
|
}
|
|
1432
1583
|
return __toCommonJS(index_exports);
|
|
1433
1584
|
})();
|