@geolonia/geonicdb-sdk 0.5.0 → 0.7.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
@@ -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
- if (dpopSupported) {
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
- /** Ensure a valid token is available, refreshing or acquiring as needed. */
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);
@@ -993,6 +1068,14 @@ var GeonicDB = class extends EventEmitter {
993
1068
  async login(email, password) {
994
1069
  return this._auth.login(email, password);
995
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
+ }
996
1079
  /**
997
1080
  * Set credentials externally (e.g. from a login API response).
998
1081
  * When tokenType is 'Bearer' with a refreshToken, DPoP/PoW is bypassed entirely.