@geolonia/geonicdb-sdk 0.4.0 → 0.6.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 +56 -10
- package/geonicdb.cjs +102 -32
- package/geonicdb.cjs.map +2 -2
- package/geonicdb.iife.js +102 -32
- package/geonicdb.iife.js.map +2 -2
- package/geonicdb.mjs +102 -32
- package/geonicdb.mjs.map +2 -2
- package/package.json +1 -1
package/geonicdb.mjs
CHANGED
|
@@ -314,11 +314,27 @@ function isCacheableMethod(method) {
|
|
|
314
314
|
|
|
315
315
|
// src/sdk/auth.ts
|
|
316
316
|
var _AuthManager = class _AuthManager {
|
|
317
|
-
constructor(baseUrl, apiKey, tenant, debug = false) {
|
|
317
|
+
constructor(baseUrl, apiKey, tenant, debug = false, anonymous = false) {
|
|
318
318
|
__publicField(this, "_baseUrl");
|
|
319
319
|
__publicField(this, "_apiKey");
|
|
320
320
|
__publicField(this, "_tenant");
|
|
321
321
|
__publicField(this, "_debug");
|
|
322
|
+
/**
|
|
323
|
+
* #1105: anonymous モード。true の場合、トークンを取得せず Authorization
|
|
324
|
+
* ヘッダ無しで送信する。サーバー側 (`optionalAuth`) で role='anonymous' として
|
|
325
|
+
* 通り、XACML が認可判定する。
|
|
326
|
+
*
|
|
327
|
+
* `_initialAnonymous` はコンストラクタで指定された希望モード (immutable)。
|
|
328
|
+
* `_anonymous` は現在のモード状態で、`login()` / `setCredentials()` で false
|
|
329
|
+
* に降り、`logout()` で `_initialAnonymous` の値に戻る。
|
|
330
|
+
*
|
|
331
|
+
* トークン有無 (`_token === null`) からは推論しない。`_token` は refresh
|
|
332
|
+
* 失敗時に null へ落ちるが、ユーザが明示的に `logout()` していない以上、
|
|
333
|
+
* 次のリクエストを勝手に Authorization ヘッダ無しで送ってはならない
|
|
334
|
+
* (#1113 review)。
|
|
335
|
+
*/
|
|
336
|
+
__publicField(this, "_initialAnonymous");
|
|
337
|
+
__publicField(this, "_anonymous");
|
|
322
338
|
__publicField(this, "_token", null);
|
|
323
339
|
__publicField(this, "_tokenExpiry", 0);
|
|
324
340
|
__publicField(this, "_tokenType", "Bearer");
|
|
@@ -339,11 +355,18 @@ var _AuthManager = class _AuthManager {
|
|
|
339
355
|
__publicField(this, "_emitCacheEvent", null);
|
|
340
356
|
/** In-flight request map keyed by `${METHOD}:${path}` for request deduplication. */
|
|
341
357
|
__publicField(this, "_inFlight", /* @__PURE__ */ new Map());
|
|
358
|
+
if (anonymous && apiKey) {
|
|
359
|
+
throw new Error(
|
|
360
|
+
"GeonicDB SDK: `anonymous: true` cannot be combined with `apiKey`. Anonymous mode skips token acquisition entirely."
|
|
361
|
+
);
|
|
362
|
+
}
|
|
342
363
|
this._baseUrl = baseUrl;
|
|
343
364
|
this._apiKey = apiKey;
|
|
344
365
|
this._tenant = tenant;
|
|
345
366
|
this._debug = debug;
|
|
346
|
-
|
|
367
|
+
this._initialAnonymous = anonymous;
|
|
368
|
+
this._anonymous = anonymous;
|
|
369
|
+
if (!anonymous && dpopSupported) {
|
|
347
370
|
this._dpopReady = generateDPoPKeyPair().then((kp) => {
|
|
348
371
|
this._dpopKeyPair = kp;
|
|
349
372
|
}).catch(() => {
|
|
@@ -351,6 +374,16 @@ var _AuthManager = class _AuthManager {
|
|
|
351
374
|
});
|
|
352
375
|
}
|
|
353
376
|
}
|
|
377
|
+
/**
|
|
378
|
+
* True when running unauthenticated.
|
|
379
|
+
*
|
|
380
|
+
* Returns the explicit mode flag, NOT derived from `_token === null`. After
|
|
381
|
+
* a refresh failure clears `_token`, the SDK must NOT silently switch to
|
|
382
|
+
* anonymous; the caller must explicitly `logout()` to revert (#1113 review).
|
|
383
|
+
*/
|
|
384
|
+
isAnonymous() {
|
|
385
|
+
return this._anonymous;
|
|
386
|
+
}
|
|
354
387
|
_log(...args) {
|
|
355
388
|
if (this._debug) console.log("[GeonicDB]", ...args);
|
|
356
389
|
}
|
|
@@ -389,6 +422,7 @@ var _AuthManager = class _AuthManager {
|
|
|
389
422
|
);
|
|
390
423
|
}
|
|
391
424
|
const data = await res.json();
|
|
425
|
+
this._anonymous = false;
|
|
392
426
|
this._token = data.accessToken;
|
|
393
427
|
this._tokenExpiry = Date.now() + (data.expiresIn - 60) * 1e3;
|
|
394
428
|
this._tokenType = "Bearer";
|
|
@@ -403,6 +437,12 @@ var _AuthManager = class _AuthManager {
|
|
|
403
437
|
*/
|
|
404
438
|
setCredentials(opts) {
|
|
405
439
|
if (!opts || !opts.token) throw new Error("token is required");
|
|
440
|
+
if (this._initialAnonymous && opts.tokenType === "DPoP") {
|
|
441
|
+
throw new Error(
|
|
442
|
+
"DPoP credentials cannot be set on an SDK instance constructed with `anonymous: true` (no DPoP key pair is generated for anonymous instances). Use Bearer credentials, or construct the SDK in non-anonymous mode."
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
this._anonymous = false;
|
|
406
446
|
this._token = opts.token;
|
|
407
447
|
this._tokenType = opts.tokenType || "Bearer";
|
|
408
448
|
this._tokenExpiry = opts.expiresIn != null ? Date.now() + (opts.expiresIn - 60) * 1e3 : Date.now() + DEFAULT_TOKEN_TTL_SEC * 1e3;
|
|
@@ -416,6 +456,7 @@ var _AuthManager = class _AuthManager {
|
|
|
416
456
|
this._tokenExpiry = 0;
|
|
417
457
|
this._refreshToken = null;
|
|
418
458
|
this._tokenPromise = null;
|
|
459
|
+
this._anonymous = this._initialAnonymous;
|
|
419
460
|
this._invalidateAuthScopedState();
|
|
420
461
|
}
|
|
421
462
|
/**
|
|
@@ -428,7 +469,13 @@ var _AuthManager = class _AuthManager {
|
|
|
428
469
|
this._cache?.clear();
|
|
429
470
|
this._inFlight.clear();
|
|
430
471
|
}
|
|
431
|
-
/**
|
|
472
|
+
/**
|
|
473
|
+
* Ensure a valid token is available, refreshing or acquiring as needed.
|
|
474
|
+
*
|
|
475
|
+
* #1105: In anonymous mode (no apiKey, no Bearer credentials) this throws
|
|
476
|
+
* `AuthenticationError`. Callers that may run anonymously should branch on
|
|
477
|
+
* `isAnonymous()` first instead of calling `ensureToken()` blindly.
|
|
478
|
+
*/
|
|
432
479
|
async ensureToken() {
|
|
433
480
|
if (this._token && Date.now() < this._tokenExpiry) {
|
|
434
481
|
return this._token;
|
|
@@ -440,6 +487,16 @@ var _AuthManager = class _AuthManager {
|
|
|
440
487
|
this._tokenPromise = this._refreshBearerToken();
|
|
441
488
|
return this._tokenPromise;
|
|
442
489
|
}
|
|
490
|
+
if (this._anonymous) {
|
|
491
|
+
throw new AuthenticationError(
|
|
492
|
+
"Anonymous mode: no token available. Call login() or setCredentials() to authenticate."
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
if (!this._apiKey) {
|
|
496
|
+
throw new AuthenticationError(
|
|
497
|
+
"No authentication method available. Call login() / setCredentials() to authenticate, or construct the SDK with an apiKey."
|
|
498
|
+
);
|
|
499
|
+
}
|
|
443
500
|
this._tokenPromise = this._acquireTokenViaPow();
|
|
444
501
|
return this._tokenPromise;
|
|
445
502
|
}
|
|
@@ -560,7 +617,7 @@ var _AuthManager = class _AuthManager {
|
|
|
560
617
|
async request(method, path, body) {
|
|
561
618
|
this._log(method, path);
|
|
562
619
|
if (!this._cache || !isCacheableMethod(method) || body !== void 0) {
|
|
563
|
-
const token = await this.ensureToken();
|
|
620
|
+
const token = this.isAnonymous() ? null : await this.ensureToken();
|
|
564
621
|
const res = await this._doAuthenticatedRequest(method, path, body, token);
|
|
565
622
|
this._log(method, path, "\u2192", res.status);
|
|
566
623
|
return res;
|
|
@@ -581,14 +638,14 @@ var _AuthManager = class _AuthManager {
|
|
|
581
638
|
async _cachedRequest(method, path, key) {
|
|
582
639
|
const cache = this._cache;
|
|
583
640
|
if (!cache) {
|
|
584
|
-
const token2 = await this.ensureToken();
|
|
641
|
+
const token2 = this.isAnonymous() ? null : await this.ensureToken();
|
|
585
642
|
return this._doAuthenticatedRequest(method, path, void 0, token2);
|
|
586
643
|
}
|
|
587
644
|
const cached = cache.get(key);
|
|
588
645
|
const conditional = {};
|
|
589
646
|
if (cached?.etag) conditional["If-None-Match"] = cached.etag;
|
|
590
647
|
if (cached?.lastModified) conditional["If-Modified-Since"] = cached.lastModified;
|
|
591
|
-
const token = await this.ensureToken();
|
|
648
|
+
const token = this.isAnonymous() ? null : await this.ensureToken();
|
|
592
649
|
const res = await this._doAuthenticatedRequest(method, path, void 0, token, 0, conditional);
|
|
593
650
|
this._log(method, path, "\u2192", res.status);
|
|
594
651
|
if (res.status === 304 && cached) {
|
|
@@ -629,11 +686,19 @@ var _AuthManager = class _AuthManager {
|
|
|
629
686
|
return res;
|
|
630
687
|
}
|
|
631
688
|
async _doAuthenticatedRequest(method, path, body, token, retryCount = 0, extraHeaders = {}) {
|
|
689
|
+
if (this._tokenType === "DPoP" && token !== null) {
|
|
690
|
+
if (this._dpopReady) await this._dpopReady;
|
|
691
|
+
if (!this._dpopKeyPair) {
|
|
692
|
+
throw new AuthenticationError(
|
|
693
|
+
"DPoP credentials require a generated DPoP key pair, but key generation was unavailable or failed."
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
632
697
|
const url = this._baseUrl + path;
|
|
633
|
-
const isDPoP = this._tokenType === "DPoP" && !!this._dpopKeyPair;
|
|
698
|
+
const isDPoP = this._tokenType === "DPoP" && !!this._dpopKeyPair && token !== null;
|
|
634
699
|
const bodyStr = body !== void 0 ? JSON.stringify(body) : void 0;
|
|
635
700
|
const doRequest = async (reqToken, dpopNonce) => {
|
|
636
|
-
const reqIsDPoP = this._tokenType === "DPoP" && !!this._dpopKeyPair;
|
|
701
|
+
const reqIsDPoP = this._tokenType === "DPoP" && !!this._dpopKeyPair && reqToken !== null;
|
|
637
702
|
const dpopProof = reqIsDPoP ? await createDPoPProof(
|
|
638
703
|
this._dpopKeyPair,
|
|
639
704
|
method,
|
|
@@ -642,11 +707,13 @@ var _AuthManager = class _AuthManager {
|
|
|
642
707
|
dpopNonce
|
|
643
708
|
) : null;
|
|
644
709
|
const headers = {
|
|
645
|
-
Authorization: (reqIsDPoP ? "DPoP " : "Bearer ") + reqToken,
|
|
646
710
|
"Content-Type": "application/ld+json",
|
|
647
711
|
Accept: "application/ld+json",
|
|
648
712
|
...extraHeaders
|
|
649
713
|
};
|
|
714
|
+
if (reqToken !== null) {
|
|
715
|
+
headers["Authorization"] = (reqIsDPoP ? "DPoP " : "Bearer ") + reqToken;
|
|
716
|
+
}
|
|
650
717
|
if (dpopProof) headers["DPoP"] = dpopProof;
|
|
651
718
|
if (this._tenant) headers["Fiware-Service"] = this._tenant;
|
|
652
719
|
return fetch(url, { method, headers, body: bodyStr });
|
|
@@ -656,11 +723,12 @@ var _AuthManager = class _AuthManager {
|
|
|
656
723
|
const previousNonce = this._dpopNonce;
|
|
657
724
|
const newNonce = res.headers.get("DPoP-Nonce");
|
|
658
725
|
if (newNonce) this._dpopNonce = newNonce;
|
|
726
|
+
const canRetry401 = currentToken !== null;
|
|
659
727
|
if (res.status === 401 && isDPoP && newNonce && newNonce !== previousNonce) {
|
|
660
728
|
res = await doRequest(currentToken, newNonce);
|
|
661
729
|
const rn = res.headers.get("DPoP-Nonce");
|
|
662
730
|
if (rn) this._dpopNonce = rn;
|
|
663
|
-
if (res.status === 401) {
|
|
731
|
+
if (res.status === 401 && canRetry401) {
|
|
664
732
|
this._token = null;
|
|
665
733
|
this._tokenPromise = null;
|
|
666
734
|
currentToken = await this.ensureToken();
|
|
@@ -668,7 +736,7 @@ var _AuthManager = class _AuthManager {
|
|
|
668
736
|
const fn = res.headers.get("DPoP-Nonce");
|
|
669
737
|
if (fn) this._dpopNonce = fn;
|
|
670
738
|
}
|
|
671
|
-
} else if (res.status === 401) {
|
|
739
|
+
} else if (res.status === 401 && canRetry401) {
|
|
672
740
|
this._token = null;
|
|
673
741
|
this._tokenPromise = null;
|
|
674
742
|
currentToken = await this.ensureToken();
|
|
@@ -717,6 +785,13 @@ var WebSocketManager = class {
|
|
|
717
785
|
}
|
|
718
786
|
/** Establish WebSocket connection (authentication is automatic). */
|
|
719
787
|
async connect() {
|
|
788
|
+
if (this._auth.isAnonymous()) {
|
|
789
|
+
const err = new Error(
|
|
790
|
+
"WebSocket connect() is not supported in anonymous mode. Authenticate via login() or setCredentials() before connect()."
|
|
791
|
+
);
|
|
792
|
+
this._emit("error", err);
|
|
793
|
+
throw err;
|
|
794
|
+
}
|
|
720
795
|
if (this._reconnectTimer) {
|
|
721
796
|
clearTimeout(this._reconnectTimer);
|
|
722
797
|
this._reconnectTimer = null;
|
|
@@ -952,7 +1027,7 @@ var GeonicDB = class extends EventEmitter {
|
|
|
952
1027
|
if (!tenant) tenant = script?.getAttribute?.("data-tenant") || "";
|
|
953
1028
|
if (!baseUrl) baseUrl = script?.getAttribute?.("data-base-url") || "";
|
|
954
1029
|
}
|
|
955
|
-
this._auth = new AuthManager(baseUrl, apiKey, tenant, opts.debug);
|
|
1030
|
+
this._auth = new AuthManager(baseUrl, apiKey, tenant, opts.debug, opts.anonymous);
|
|
956
1031
|
this._auth.onTokenRefresh = (creds) => {
|
|
957
1032
|
this.onTokenRefresh?.(creds);
|
|
958
1033
|
this.emit("tokenRefresh", creds);
|
|
@@ -968,44 +1043,39 @@ var GeonicDB = class extends EventEmitter {
|
|
|
968
1043
|
tenant,
|
|
969
1044
|
(event, data) => {
|
|
970
1045
|
this.emit(event, data);
|
|
971
|
-
if (event === "entityCreated" || event === "entityUpdated" || event === "entityDeleted") {
|
|
972
|
-
this._invalidateCacheForEntityEvent(data);
|
|
973
|
-
}
|
|
974
1046
|
},
|
|
975
1047
|
opts.wsEndpoint
|
|
976
1048
|
);
|
|
977
1049
|
}
|
|
978
1050
|
/**
|
|
979
|
-
* Drop
|
|
1051
|
+
* Drop every cached response. Useful in tests and on manual auth changes.
|
|
980
1052
|
*
|
|
981
|
-
*
|
|
982
|
-
*
|
|
983
|
-
*
|
|
984
|
-
*
|
|
985
|
-
* over-invalidating.
|
|
1053
|
+
* Emits a `cacheInvalidated` event for each removed entry so listeners can
|
|
1054
|
+
* track explicit flushes (the WebSocket-driven auto-invalidation was removed
|
|
1055
|
+
* in #1060 to preserve the ETag/304 revalidation path; `clearCache()` is now
|
|
1056
|
+
* the only path that emits this event).
|
|
986
1057
|
*/
|
|
987
|
-
|
|
1058
|
+
clearCache() {
|
|
988
1059
|
const cache = this._auth.getCache();
|
|
989
1060
|
if (!cache) return;
|
|
990
|
-
const
|
|
991
|
-
const removed = cache.deleteWhere((key) => {
|
|
992
|
-
if (key.includes("/ngsi-ld/v1/entities") || key.includes("/v2/entities")) return true;
|
|
993
|
-
if (entityId && key.includes(entityId)) return true;
|
|
994
|
-
return false;
|
|
995
|
-
});
|
|
1061
|
+
const removed = cache.deleteWhere(() => true);
|
|
996
1062
|
for (const key of removed) {
|
|
997
1063
|
this._auth.emitCacheEvent("cacheInvalidated", { key, path: key.split(":").slice(1).join(":") });
|
|
998
1064
|
}
|
|
999
1065
|
}
|
|
1000
|
-
/** Drop every cached response. Useful in tests and on manual auth changes. */
|
|
1001
|
-
clearCache() {
|
|
1002
|
-
this._auth.getCache()?.clear();
|
|
1003
|
-
}
|
|
1004
1066
|
// --- Authentication ---
|
|
1005
1067
|
/** Login with email and password (Bearer JWT). */
|
|
1006
1068
|
async login(email, password) {
|
|
1007
1069
|
return this._auth.login(email, password);
|
|
1008
1070
|
}
|
|
1071
|
+
/**
|
|
1072
|
+
* Whether the SDK is currently operating in anonymous mode (no token held).
|
|
1073
|
+
* Returns true only when `anonymous: true` was passed to the constructor
|
|
1074
|
+
* AND no credentials have been set via `login()` / `setCredentials()`.
|
|
1075
|
+
*/
|
|
1076
|
+
isAnonymous() {
|
|
1077
|
+
return this._auth.isAnonymous();
|
|
1078
|
+
}
|
|
1009
1079
|
/**
|
|
1010
1080
|
* Set credentials externally (e.g. from a login API response).
|
|
1011
1081
|
* When tokenType is 'Bearer' with a refreshToken, DPoP/PoW is bypassed entirely.
|