@geolonia/geonicdb-sdk 0.5.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 +35 -3
- package/geonicdb.cjs +95 -12
- package/geonicdb.cjs.map +2 -2
- package/geonicdb.iife.js +95 -12
- package/geonicdb.iife.js.map +2 -2
- package/geonicdb.mjs +95 -12
- package/geonicdb.mjs.map +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -84,6 +84,36 @@ db.setCredentials({
|
|
|
84
84
|
});
|
|
85
85
|
```
|
|
86
86
|
|
|
87
|
+
### Anonymous Mode (public viewers)
|
|
88
|
+
|
|
89
|
+
For public-facing apps such as GeoJSON viewers or BI dashboards that should
|
|
90
|
+
serve **unregistered visitors** without forcing a login, pass `anonymous: true`
|
|
91
|
+
to skip token acquisition entirely:
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
const db = new GeonicDB({
|
|
95
|
+
baseUrl: 'https://your-geonicdb.example.com',
|
|
96
|
+
tenant: 'public',
|
|
97
|
+
anonymous: true, // no Authorization header, no /auth/nonce, no /oauth/token
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// The server treats this as `role: 'anonymous'` and the XACML policy engine
|
|
101
|
+
// decides whether to permit. Tenant admins must register a custom policy that
|
|
102
|
+
// permits the anonymous role for the resources to expose — the default
|
|
103
|
+
// ANONYMOUS_DEFAULT_POLICY denies everything.
|
|
104
|
+
const entities = await db.getEntities({ type: 'GeoJSON' });
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
- `apiKey` and `anonymous: true` cannot be combined (constructor throws).
|
|
108
|
+
- `db.login()` upgrades the SDK out of anonymous mode (Bearer JWT).
|
|
109
|
+
- `db.setCredentials({ token, tokenType: 'Bearer', ... })` upgrades from
|
|
110
|
+
anonymous. **`tokenType: 'DPoP'` is rejected** for anonymous-built instances
|
|
111
|
+
(no DPoP key pair is generated for them) — use a non-anonymous SDK instance
|
|
112
|
+
for DPoP credentials.
|
|
113
|
+
- `db.logout()` reverts back to anonymous (`db.isAnonymous() === true` again).
|
|
114
|
+
- WebSocket `db.connect()` is **not supported** in anonymous mode — call
|
|
115
|
+
`login()` / `setCredentials()` first.
|
|
116
|
+
|
|
87
117
|
## Entity CRUD
|
|
88
118
|
|
|
89
119
|
```typescript
|
|
@@ -261,14 +291,16 @@ handle.stop();
|
|
|
261
291
|
| `debug` | `boolean` | No | Enable debug logging to console (default: `false`) |
|
|
262
292
|
| `cache` | `boolean` | No | Enable in-memory cache with ETag/304 + request dedup (default: `true`) |
|
|
263
293
|
| `cacheMaxEntries` | `number` | No | LRU cache capacity when `cache` is enabled (default: `1000`) |
|
|
294
|
+
| `anonymous` | `boolean` | No | Anonymous mode — skip token acquisition and send no `Authorization` header. Cannot be combined with `apiKey`. See [Anonymous Mode](#anonymous-mode-public-viewers) (default: `false`) |
|
|
264
295
|
|
|
265
296
|
### Authentication
|
|
266
297
|
|
|
267
298
|
| Method | Parameters | Returns | Description |
|
|
268
299
|
|--------|-----------|---------|-------------|
|
|
269
|
-
| `login(email, password)` | `string, string` | `Promise<LoginResponse>` | Login with email and password (Bearer JWT) |
|
|
270
|
-
| `setCredentials(opts)` | `CredentialsOptions` | `this` | Set credentials externally (e.g. from a login API response) |
|
|
271
|
-
| `logout()` | — | `void` | Clear all credentials and disconnect WebSocket |
|
|
300
|
+
| `login(email, password)` | `string, string` | `Promise<LoginResponse>` | Login with email and password (Bearer JWT). Upgrades the SDK out of anonymous mode |
|
|
301
|
+
| `setCredentials(opts)` | `CredentialsOptions` | `this` | Set credentials externally (e.g. from a login API response). Upgrades the SDK out of anonymous mode when `tokenType: 'Bearer'`. **Throws** when `tokenType: 'DPoP'` is passed to an anonymous-built instance — anonymous instances do not generate a DPoP key pair |
|
|
302
|
+
| `logout()` | — | `void` | Clear all credentials and disconnect WebSocket. Reverts to anonymous mode if `anonymous: true` was set at construction |
|
|
303
|
+
| `isAnonymous()` | — | `boolean` | Whether explicit anonymous mode is active. Returns the explicit mode flag — not derived from credential presence — so a refresh failure that clears `_token` does NOT silently flip this back to `true` |
|
|
272
304
|
|
|
273
305
|
### Entity CRUD
|
|
274
306
|
|
package/geonicdb.cjs
CHANGED
|
@@ -347,11 +347,27 @@ function isCacheableMethod(method) {
|
|
|
347
347
|
|
|
348
348
|
// src/sdk/auth.ts
|
|
349
349
|
var _AuthManager = class _AuthManager {
|
|
350
|
-
constructor(baseUrl, apiKey, tenant, debug = false) {
|
|
350
|
+
constructor(baseUrl, apiKey, tenant, debug = false, anonymous = false) {
|
|
351
351
|
__publicField(this, "_baseUrl");
|
|
352
352
|
__publicField(this, "_apiKey");
|
|
353
353
|
__publicField(this, "_tenant");
|
|
354
354
|
__publicField(this, "_debug");
|
|
355
|
+
/**
|
|
356
|
+
* #1105: anonymous モード。true の場合、トークンを取得せず Authorization
|
|
357
|
+
* ヘッダ無しで送信する。サーバー側 (`optionalAuth`) で role='anonymous' として
|
|
358
|
+
* 通り、XACML が認可判定する。
|
|
359
|
+
*
|
|
360
|
+
* `_initialAnonymous` はコンストラクタで指定された希望モード (immutable)。
|
|
361
|
+
* `_anonymous` は現在のモード状態で、`login()` / `setCredentials()` で false
|
|
362
|
+
* に降り、`logout()` で `_initialAnonymous` の値に戻る。
|
|
363
|
+
*
|
|
364
|
+
* トークン有無 (`_token === null`) からは推論しない。`_token` は refresh
|
|
365
|
+
* 失敗時に null へ落ちるが、ユーザが明示的に `logout()` していない以上、
|
|
366
|
+
* 次のリクエストを勝手に Authorization ヘッダ無しで送ってはならない
|
|
367
|
+
* (#1113 review)。
|
|
368
|
+
*/
|
|
369
|
+
__publicField(this, "_initialAnonymous");
|
|
370
|
+
__publicField(this, "_anonymous");
|
|
355
371
|
__publicField(this, "_token", null);
|
|
356
372
|
__publicField(this, "_tokenExpiry", 0);
|
|
357
373
|
__publicField(this, "_tokenType", "Bearer");
|
|
@@ -372,11 +388,18 @@ var _AuthManager = class _AuthManager {
|
|
|
372
388
|
__publicField(this, "_emitCacheEvent", null);
|
|
373
389
|
/** In-flight request map keyed by `${METHOD}:${path}` for request deduplication. */
|
|
374
390
|
__publicField(this, "_inFlight", /* @__PURE__ */ new Map());
|
|
391
|
+
if (anonymous && apiKey) {
|
|
392
|
+
throw new Error(
|
|
393
|
+
"GeonicDB SDK: `anonymous: true` cannot be combined with `apiKey`. Anonymous mode skips token acquisition entirely."
|
|
394
|
+
);
|
|
395
|
+
}
|
|
375
396
|
this._baseUrl = baseUrl;
|
|
376
397
|
this._apiKey = apiKey;
|
|
377
398
|
this._tenant = tenant;
|
|
378
399
|
this._debug = debug;
|
|
379
|
-
|
|
400
|
+
this._initialAnonymous = anonymous;
|
|
401
|
+
this._anonymous = anonymous;
|
|
402
|
+
if (!anonymous && dpopSupported) {
|
|
380
403
|
this._dpopReady = generateDPoPKeyPair().then((kp) => {
|
|
381
404
|
this._dpopKeyPair = kp;
|
|
382
405
|
}).catch(() => {
|
|
@@ -384,6 +407,16 @@ var _AuthManager = class _AuthManager {
|
|
|
384
407
|
});
|
|
385
408
|
}
|
|
386
409
|
}
|
|
410
|
+
/**
|
|
411
|
+
* True when running unauthenticated.
|
|
412
|
+
*
|
|
413
|
+
* Returns the explicit mode flag, NOT derived from `_token === null`. After
|
|
414
|
+
* a refresh failure clears `_token`, the SDK must NOT silently switch to
|
|
415
|
+
* anonymous; the caller must explicitly `logout()` to revert (#1113 review).
|
|
416
|
+
*/
|
|
417
|
+
isAnonymous() {
|
|
418
|
+
return this._anonymous;
|
|
419
|
+
}
|
|
387
420
|
_log(...args) {
|
|
388
421
|
if (this._debug) console.log("[GeonicDB]", ...args);
|
|
389
422
|
}
|
|
@@ -422,6 +455,7 @@ var _AuthManager = class _AuthManager {
|
|
|
422
455
|
);
|
|
423
456
|
}
|
|
424
457
|
const data = await res.json();
|
|
458
|
+
this._anonymous = false;
|
|
425
459
|
this._token = data.accessToken;
|
|
426
460
|
this._tokenExpiry = Date.now() + (data.expiresIn - 60) * 1e3;
|
|
427
461
|
this._tokenType = "Bearer";
|
|
@@ -436,6 +470,12 @@ var _AuthManager = class _AuthManager {
|
|
|
436
470
|
*/
|
|
437
471
|
setCredentials(opts) {
|
|
438
472
|
if (!opts || !opts.token) throw new Error("token is required");
|
|
473
|
+
if (this._initialAnonymous && opts.tokenType === "DPoP") {
|
|
474
|
+
throw new Error(
|
|
475
|
+
"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."
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
this._anonymous = false;
|
|
439
479
|
this._token = opts.token;
|
|
440
480
|
this._tokenType = opts.tokenType || "Bearer";
|
|
441
481
|
this._tokenExpiry = opts.expiresIn != null ? Date.now() + (opts.expiresIn - 60) * 1e3 : Date.now() + DEFAULT_TOKEN_TTL_SEC * 1e3;
|
|
@@ -449,6 +489,7 @@ var _AuthManager = class _AuthManager {
|
|
|
449
489
|
this._tokenExpiry = 0;
|
|
450
490
|
this._refreshToken = null;
|
|
451
491
|
this._tokenPromise = null;
|
|
492
|
+
this._anonymous = this._initialAnonymous;
|
|
452
493
|
this._invalidateAuthScopedState();
|
|
453
494
|
}
|
|
454
495
|
/**
|
|
@@ -461,7 +502,13 @@ var _AuthManager = class _AuthManager {
|
|
|
461
502
|
this._cache?.clear();
|
|
462
503
|
this._inFlight.clear();
|
|
463
504
|
}
|
|
464
|
-
/**
|
|
505
|
+
/**
|
|
506
|
+
* Ensure a valid token is available, refreshing or acquiring as needed.
|
|
507
|
+
*
|
|
508
|
+
* #1105: In anonymous mode (no apiKey, no Bearer credentials) this throws
|
|
509
|
+
* `AuthenticationError`. Callers that may run anonymously should branch on
|
|
510
|
+
* `isAnonymous()` first instead of calling `ensureToken()` blindly.
|
|
511
|
+
*/
|
|
465
512
|
async ensureToken() {
|
|
466
513
|
if (this._token && Date.now() < this._tokenExpiry) {
|
|
467
514
|
return this._token;
|
|
@@ -473,6 +520,16 @@ var _AuthManager = class _AuthManager {
|
|
|
473
520
|
this._tokenPromise = this._refreshBearerToken();
|
|
474
521
|
return this._tokenPromise;
|
|
475
522
|
}
|
|
523
|
+
if (this._anonymous) {
|
|
524
|
+
throw new AuthenticationError(
|
|
525
|
+
"Anonymous mode: no token available. Call login() or setCredentials() to authenticate."
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
if (!this._apiKey) {
|
|
529
|
+
throw new AuthenticationError(
|
|
530
|
+
"No authentication method available. Call login() / setCredentials() to authenticate, or construct the SDK with an apiKey."
|
|
531
|
+
);
|
|
532
|
+
}
|
|
476
533
|
this._tokenPromise = this._acquireTokenViaPow();
|
|
477
534
|
return this._tokenPromise;
|
|
478
535
|
}
|
|
@@ -593,7 +650,7 @@ var _AuthManager = class _AuthManager {
|
|
|
593
650
|
async request(method, path, body) {
|
|
594
651
|
this._log(method, path);
|
|
595
652
|
if (!this._cache || !isCacheableMethod(method) || body !== void 0) {
|
|
596
|
-
const token = await this.ensureToken();
|
|
653
|
+
const token = this.isAnonymous() ? null : await this.ensureToken();
|
|
597
654
|
const res = await this._doAuthenticatedRequest(method, path, body, token);
|
|
598
655
|
this._log(method, path, "\u2192", res.status);
|
|
599
656
|
return res;
|
|
@@ -614,14 +671,14 @@ var _AuthManager = class _AuthManager {
|
|
|
614
671
|
async _cachedRequest(method, path, key) {
|
|
615
672
|
const cache = this._cache;
|
|
616
673
|
if (!cache) {
|
|
617
|
-
const token2 = await this.ensureToken();
|
|
674
|
+
const token2 = this.isAnonymous() ? null : await this.ensureToken();
|
|
618
675
|
return this._doAuthenticatedRequest(method, path, void 0, token2);
|
|
619
676
|
}
|
|
620
677
|
const cached = cache.get(key);
|
|
621
678
|
const conditional = {};
|
|
622
679
|
if (cached?.etag) conditional["If-None-Match"] = cached.etag;
|
|
623
680
|
if (cached?.lastModified) conditional["If-Modified-Since"] = cached.lastModified;
|
|
624
|
-
const token = await this.ensureToken();
|
|
681
|
+
const token = this.isAnonymous() ? null : await this.ensureToken();
|
|
625
682
|
const res = await this._doAuthenticatedRequest(method, path, void 0, token, 0, conditional);
|
|
626
683
|
this._log(method, path, "\u2192", res.status);
|
|
627
684
|
if (res.status === 304 && cached) {
|
|
@@ -662,11 +719,19 @@ var _AuthManager = class _AuthManager {
|
|
|
662
719
|
return res;
|
|
663
720
|
}
|
|
664
721
|
async _doAuthenticatedRequest(method, path, body, token, retryCount = 0, extraHeaders = {}) {
|
|
722
|
+
if (this._tokenType === "DPoP" && token !== null) {
|
|
723
|
+
if (this._dpopReady) await this._dpopReady;
|
|
724
|
+
if (!this._dpopKeyPair) {
|
|
725
|
+
throw new AuthenticationError(
|
|
726
|
+
"DPoP credentials require a generated DPoP key pair, but key generation was unavailable or failed."
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
665
730
|
const url = this._baseUrl + path;
|
|
666
|
-
const isDPoP = this._tokenType === "DPoP" && !!this._dpopKeyPair;
|
|
731
|
+
const isDPoP = this._tokenType === "DPoP" && !!this._dpopKeyPair && token !== null;
|
|
667
732
|
const bodyStr = body !== void 0 ? JSON.stringify(body) : void 0;
|
|
668
733
|
const doRequest = async (reqToken, dpopNonce) => {
|
|
669
|
-
const reqIsDPoP = this._tokenType === "DPoP" && !!this._dpopKeyPair;
|
|
734
|
+
const reqIsDPoP = this._tokenType === "DPoP" && !!this._dpopKeyPair && reqToken !== null;
|
|
670
735
|
const dpopProof = reqIsDPoP ? await createDPoPProof(
|
|
671
736
|
this._dpopKeyPair,
|
|
672
737
|
method,
|
|
@@ -675,11 +740,13 @@ var _AuthManager = class _AuthManager {
|
|
|
675
740
|
dpopNonce
|
|
676
741
|
) : null;
|
|
677
742
|
const headers = {
|
|
678
|
-
Authorization: (reqIsDPoP ? "DPoP " : "Bearer ") + reqToken,
|
|
679
743
|
"Content-Type": "application/ld+json",
|
|
680
744
|
Accept: "application/ld+json",
|
|
681
745
|
...extraHeaders
|
|
682
746
|
};
|
|
747
|
+
if (reqToken !== null) {
|
|
748
|
+
headers["Authorization"] = (reqIsDPoP ? "DPoP " : "Bearer ") + reqToken;
|
|
749
|
+
}
|
|
683
750
|
if (dpopProof) headers["DPoP"] = dpopProof;
|
|
684
751
|
if (this._tenant) headers["Fiware-Service"] = this._tenant;
|
|
685
752
|
return fetch(url, { method, headers, body: bodyStr });
|
|
@@ -689,11 +756,12 @@ var _AuthManager = class _AuthManager {
|
|
|
689
756
|
const previousNonce = this._dpopNonce;
|
|
690
757
|
const newNonce = res.headers.get("DPoP-Nonce");
|
|
691
758
|
if (newNonce) this._dpopNonce = newNonce;
|
|
759
|
+
const canRetry401 = currentToken !== null;
|
|
692
760
|
if (res.status === 401 && isDPoP && newNonce && newNonce !== previousNonce) {
|
|
693
761
|
res = await doRequest(currentToken, newNonce);
|
|
694
762
|
const rn = res.headers.get("DPoP-Nonce");
|
|
695
763
|
if (rn) this._dpopNonce = rn;
|
|
696
|
-
if (res.status === 401) {
|
|
764
|
+
if (res.status === 401 && canRetry401) {
|
|
697
765
|
this._token = null;
|
|
698
766
|
this._tokenPromise = null;
|
|
699
767
|
currentToken = await this.ensureToken();
|
|
@@ -701,7 +769,7 @@ var _AuthManager = class _AuthManager {
|
|
|
701
769
|
const fn = res.headers.get("DPoP-Nonce");
|
|
702
770
|
if (fn) this._dpopNonce = fn;
|
|
703
771
|
}
|
|
704
|
-
} else if (res.status === 401) {
|
|
772
|
+
} else if (res.status === 401 && canRetry401) {
|
|
705
773
|
this._token = null;
|
|
706
774
|
this._tokenPromise = null;
|
|
707
775
|
currentToken = await this.ensureToken();
|
|
@@ -750,6 +818,13 @@ var WebSocketManager = class {
|
|
|
750
818
|
}
|
|
751
819
|
/** Establish WebSocket connection (authentication is automatic). */
|
|
752
820
|
async connect() {
|
|
821
|
+
if (this._auth.isAnonymous()) {
|
|
822
|
+
const err = new Error(
|
|
823
|
+
"WebSocket connect() is not supported in anonymous mode. Authenticate via login() or setCredentials() before connect()."
|
|
824
|
+
);
|
|
825
|
+
this._emit("error", err);
|
|
826
|
+
throw err;
|
|
827
|
+
}
|
|
753
828
|
if (this._reconnectTimer) {
|
|
754
829
|
clearTimeout(this._reconnectTimer);
|
|
755
830
|
this._reconnectTimer = null;
|
|
@@ -985,7 +1060,7 @@ var GeonicDB = class extends EventEmitter {
|
|
|
985
1060
|
if (!tenant) tenant = script?.getAttribute?.("data-tenant") || "";
|
|
986
1061
|
if (!baseUrl) baseUrl = script?.getAttribute?.("data-base-url") || "";
|
|
987
1062
|
}
|
|
988
|
-
this._auth = new AuthManager(baseUrl, apiKey, tenant, opts.debug);
|
|
1063
|
+
this._auth = new AuthManager(baseUrl, apiKey, tenant, opts.debug, opts.anonymous);
|
|
989
1064
|
this._auth.onTokenRefresh = (creds) => {
|
|
990
1065
|
this.onTokenRefresh?.(creds);
|
|
991
1066
|
this.emit("tokenRefresh", creds);
|
|
@@ -1026,6 +1101,14 @@ var GeonicDB = class extends EventEmitter {
|
|
|
1026
1101
|
async login(email, password) {
|
|
1027
1102
|
return this._auth.login(email, password);
|
|
1028
1103
|
}
|
|
1104
|
+
/**
|
|
1105
|
+
* Whether the SDK is currently operating in anonymous mode (no token held).
|
|
1106
|
+
* Returns true only when `anonymous: true` was passed to the constructor
|
|
1107
|
+
* AND no credentials have been set via `login()` / `setCredentials()`.
|
|
1108
|
+
*/
|
|
1109
|
+
isAnonymous() {
|
|
1110
|
+
return this._auth.isAnonymous();
|
|
1111
|
+
}
|
|
1029
1112
|
/**
|
|
1030
1113
|
* Set credentials externally (e.g. from a login API response).
|
|
1031
1114
|
* When tokenType is 'Bearer' with a refreshToken, DPoP/PoW is bypassed entirely.
|