@witnium-tech/witniumchain 0.12.0 → 0.12.2

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/dist/index.mjs CHANGED
@@ -92,11 +92,6 @@ var WitniumchainClient = class {
92
92
  // refresh might work even when the in-memory refresh token is undefined.
93
93
  hasRefreshCookie = false;
94
94
  oauthClientId;
95
- // The redirect URI the most recent `beginOAuthLogin` issued, alongside the
96
- // PKCE verifier. `completeOAuthLogin` reads it back so the token-exchange
97
- // request matches the original /auth request (RFC 6749 §4.1.3 requires
98
- // redirect_uri at /token to equal the one at /auth). Keyed by state.
99
- pendingLogins = /* @__PURE__ */ new Map();
100
95
  verifierStorage;
101
96
  // Cache of the parsed OIDC discovery document keyed by issuer URL. Saves a
102
97
  // round trip on every OAuth call after the first. oidc-provider's discovery
@@ -119,6 +114,11 @@ var WitniumchainClient = class {
119
114
  // first BearerJWT call (and any concurrent fan-out) awaits one in-flight
120
115
  // mint rather than minting N tokens.
121
116
  sessionExchangeInFlight;
117
+ // Bumped whenever the cached credentials stop being valid (sign-out, the
118
+ // TOTP-confirm epoch bump). A mint or refresh that started under an older
119
+ // generation must not store its result: it could be a previous user's
120
+ // token, or one the server has already ended.
121
+ credentialGeneration = 0;
122
122
  /** Subscriptions / billing helpers. See {@link Subscriptions}. */
123
123
  subscriptions;
124
124
  /** Delegated-key namespace including the one-call {@link DelegatedKeys.provision} flow. */
@@ -500,18 +500,16 @@ var WitniumchainClient = class {
500
500
  }
501
501
  /**
502
502
  * Confirming bumps the user's security epoch server-side, which ends every
503
- * session-minted access token. A cached one is dropped so the next bearer
504
- * call mints a fresh token.
503
+ * session-minted access token and OAuth artifact of the user. Cached and
504
+ * in-flight tokens are dropped so the next bearer call mints a fresh one
505
+ * from the (surviving) session.
505
506
  */
506
507
  async confirmTotp(body) {
507
508
  const res = await this.req("POST", "/v1/account/mfa/totp/confirm", {
508
509
  auth: "SessionCookie",
509
510
  body
510
511
  });
511
- if (this.accessTokenFromSession) {
512
- this.accessToken = void 0;
513
- this.accessTokenFromSession = false;
514
- }
512
+ this.invalidateCredentials();
515
513
  return res;
516
514
  }
517
515
  /** Step-up: 403 `mfa_step_up_failed` on a wrong proof, 429 `mfa_rate_limited`. */
@@ -561,7 +559,9 @@ var WitniumchainClient = class {
561
559
  // Routing: every method here passes `service: 'chain'` to `req()`. Calls
562
560
  // throw at call time if `chainBaseUrl` wasn't configured.
563
561
  //
564
- // Auth: BearerJWT. The accounts-issued OAuth access token already carries
562
+ // Auth: the contract, witness, transaction and wallet reads are public on
563
+ // chain-api (anyone can check a witness), so they send no credential. The
564
+ // dashboard reads are BearerJWT: the accounts-issued token already carries
565
565
  // `aud=https://api.witniumchain.com`, so the same `accessToken` works
566
566
  // unchanged against both services.
567
567
  //
@@ -575,49 +575,49 @@ var WitniumchainClient = class {
575
575
  return this.req(
576
576
  "GET",
577
577
  `/v5/contracts/${encodeURIComponent(contractAddress)}/info`,
578
- { auth: "BearerJWT", service: "chain" }
578
+ { auth: "Public", service: "chain" }
579
579
  );
580
580
  }
581
581
  getContractVerification(contractAddress) {
582
582
  return this.req(
583
583
  "GET",
584
584
  `/v5/contracts/${encodeURIComponent(contractAddress)}/verify`,
585
- { auth: "BearerJWT", service: "chain" }
585
+ { auth: "Public", service: "chain" }
586
586
  );
587
587
  }
588
588
  listWitnessesV5(contractAddress, params) {
589
589
  return this.req(
590
590
  "GET",
591
591
  `/v5/contracts/${encodeURIComponent(contractAddress)}/witnesses`,
592
- { auth: "BearerJWT", service: "chain", query: params }
592
+ { auth: "Public", service: "chain", query: params }
593
593
  );
594
594
  }
595
595
  getWitnessV5(contractAddress, witnessId) {
596
596
  return this.req(
597
597
  "GET",
598
598
  `/v5/contracts/${encodeURIComponent(contractAddress)}/witnesses/${encodeURIComponent(witnessId)}`,
599
- { auth: "BearerJWT", service: "chain" }
599
+ { auth: "Public", service: "chain" }
600
600
  );
601
601
  }
602
602
  getContractTransaction(contractAddress, txHash) {
603
603
  return this.req(
604
604
  "GET",
605
605
  `/v5/contracts/${encodeURIComponent(contractAddress)}/transactions/${encodeURIComponent(txHash)}`,
606
- { auth: "BearerJWT", service: "chain" }
606
+ { auth: "Public", service: "chain" }
607
607
  );
608
608
  }
609
609
  getTransaction(txHash) {
610
610
  return this.req(
611
611
  "GET",
612
612
  `/v5/transactions/${encodeURIComponent(txHash)}`,
613
- { auth: "BearerJWT", service: "chain" }
613
+ { auth: "Public", service: "chain" }
614
614
  );
615
615
  }
616
616
  getWalletBalance(address) {
617
617
  return this.req(
618
618
  "GET",
619
619
  `/v5/wallets/${encodeURIComponent(address)}/balance`,
620
- { auth: "BearerJWT", service: "chain" }
620
+ { auth: "Public", service: "chain" }
621
621
  );
622
622
  }
623
623
  getDashboardContract() {
@@ -697,8 +697,9 @@ var WitniumchainClient = class {
697
697
  url.searchParams.set("code_challenge", challenge);
698
698
  url.searchParams.set("code_challenge_method", "S256");
699
699
  if (args.prompt) url.searchParams.set("prompt", args.prompt);
700
- this.verifierStorageOrDefault().set(state, verifier);
701
- this.pendingLogins.set(state, { redirectUri: args.redirectUri });
700
+ const storage = this.verifierStorageOrDefault();
701
+ storage.set(state, verifier);
702
+ storage.set(redirectUriKey(state), args.redirectUri);
702
703
  return { authorizationUrl: url.toString(), state };
703
704
  }
704
705
  /**
@@ -751,24 +752,24 @@ var WitniumchainClient = class {
751
752
  message: "WitniumchainClient: no PKCE verifier stored for this `state`. The login flow either was not started in this tab, was already completed, or the sessionStorage entry was cleared."
752
753
  });
753
754
  }
754
- const pending = this.pendingLogins.get(state);
755
- if (!pending) {
755
+ const redirectUri = storage.get(redirectUriKey(state));
756
+ if (!redirectUri) {
756
757
  storage.remove(state);
757
758
  throw new WitniumchainApiError({
758
759
  status: 0,
759
- message: "WitniumchainClient: PKCE verifier exists but the redirectUri for this `state` was not found in client memory (the client instance was replaced between beginOAuthLogin and completeOAuthLogin)."
760
+ message: "WitniumchainClient: PKCE verifier exists but the redirectUri stored with it for this `state` is missing."
760
761
  });
761
762
  }
762
763
  const discovery = await this.fetchDiscovery();
763
764
  const form = new URLSearchParams();
764
765
  form.set("grant_type", "authorization_code");
765
766
  form.set("code", code);
766
- form.set("redirect_uri", pending.redirectUri);
767
+ form.set("redirect_uri", redirectUri);
767
768
  form.set("client_id", this.oauthClientId);
768
769
  form.set("code_verifier", verifier);
769
770
  const tokens = await this.postTokenEndpoint(discovery.token_endpoint, form);
770
771
  storage.remove(state);
771
- this.pendingLogins.delete(state);
772
+ storage.remove(redirectUriKey(state));
772
773
  return this.persistTokenResponse(tokens);
773
774
  }
774
775
  /**
@@ -789,14 +790,16 @@ var WitniumchainClient = class {
789
790
  */
790
791
  async refreshAccessToken() {
791
792
  if (this.refreshInFlight) return this.refreshInFlight;
792
- this.refreshInFlight = (async () => {
793
+ let flight = void 0;
794
+ flight = (async () => {
793
795
  try {
794
796
  return await this.refreshAccessTokenInternal();
795
797
  } finally {
796
- this.refreshInFlight = void 0;
798
+ if (this.refreshInFlight === flight) this.refreshInFlight = void 0;
797
799
  }
798
800
  })();
799
- return this.refreshInFlight;
801
+ this.refreshInFlight = flight;
802
+ return flight;
800
803
  }
801
804
  /**
802
805
  * Exchange the logged-in session (the `wac_session` cookie) for a short-lived
@@ -821,9 +824,12 @@ var WitniumchainClient = class {
821
824
  */
822
825
  async exchangeSessionForToken() {
823
826
  if (this.sessionExchangeInFlight) return this.sessionExchangeInFlight;
824
- this.sessionExchangeInFlight = (async () => {
827
+ const generation = this.credentialGeneration;
828
+ let flight = void 0;
829
+ flight = (async () => {
825
830
  try {
826
831
  const res = await this.req("POST", "/v1/auth/access-token", { auth: "SessionCookie" });
832
+ this.assertCredentialGeneration(generation);
827
833
  this.accessToken = res.accessToken;
828
834
  this.accessTokenFromSession = true;
829
835
  this.refreshToken = void 0;
@@ -833,10 +839,32 @@ var WitniumchainClient = class {
833
839
  expiresAt: Math.floor(new Date(res.expiresAt).getTime() / 1e3)
834
840
  };
835
841
  } finally {
836
- this.sessionExchangeInFlight = void 0;
842
+ if (this.sessionExchangeInFlight === flight) this.sessionExchangeInFlight = void 0;
837
843
  }
838
844
  })();
839
- return this.sessionExchangeInFlight;
845
+ this.sessionExchangeInFlight = flight;
846
+ return flight;
847
+ }
848
+ assertCredentialGeneration(generation) {
849
+ if (generation !== this.credentialGeneration) {
850
+ throw new WitniumchainApiError({
851
+ status: 0,
852
+ message: "WitniumchainClient: the session changed (sign-out or MFA confirmation) while a token request was in flight; its token was discarded."
853
+ });
854
+ }
855
+ }
856
+ /**
857
+ * Forget every cached credential and orphan in-flight mints/refreshes, so
858
+ * none of them can store a token afterwards.
859
+ */
860
+ invalidateCredentials() {
861
+ this.credentialGeneration++;
862
+ this.accessToken = void 0;
863
+ this.accessTokenFromSession = false;
864
+ this.refreshToken = void 0;
865
+ this.hasRefreshCookie = false;
866
+ this.sessionExchangeInFlight = void 0;
867
+ this.refreshInFlight = void 0;
840
868
  }
841
869
  /**
842
870
  * Best-effort transparent mint for the `BearerJWT` path: if no access token
@@ -848,7 +876,8 @@ var WitniumchainClient = class {
848
876
  if (this.accessToken) return;
849
877
  try {
850
878
  await this.exchangeSessionForToken();
851
- } catch {
879
+ } catch (err) {
880
+ if (!(err instanceof WitniumchainApiError && err.status === 401)) throw err;
852
881
  }
853
882
  }
854
883
  /**
@@ -889,11 +918,7 @@ var WitniumchainClient = class {
889
918
  * dropped — which it is, here.
890
919
  */
891
920
  signOut() {
892
- this.accessToken = void 0;
893
- this.accessTokenFromSession = false;
894
- this.refreshToken = void 0;
895
- this.hasRefreshCookie = false;
896
- this.pendingLogins.clear();
921
+ this.invalidateCredentials();
897
922
  }
898
923
  // ────────────────────────────────────────────────────────────────────────
899
924
  // Internal: OAuth flow helpers
@@ -1012,6 +1037,7 @@ var WitniumchainClient = class {
1012
1037
  message: "WitniumchainClient: no refresh credential available. Call `completeOAuthLogin` first \u2014 the server delivers the refresh token either in the response body (Node) or as an HttpOnly cookie (browser)."
1013
1038
  });
1014
1039
  }
1040
+ const generation = this.credentialGeneration;
1015
1041
  const discovery = await this.fetchDiscovery();
1016
1042
  const form = new URLSearchParams();
1017
1043
  form.set("grant_type", "refresh_token");
@@ -1019,15 +1045,18 @@ var WitniumchainClient = class {
1019
1045
  if (this.refreshToken) {
1020
1046
  form.set("refresh_token", this.refreshToken);
1021
1047
  }
1048
+ let tokens;
1022
1049
  try {
1023
- const tokens = await this.postTokenEndpoint(discovery.token_endpoint, form);
1024
- return this.persistTokenResponse(tokens);
1050
+ tokens = await this.postTokenEndpoint(discovery.token_endpoint, form);
1025
1051
  } catch (err) {
1052
+ this.assertCredentialGeneration(generation);
1026
1053
  this.accessToken = void 0;
1027
1054
  this.refreshToken = void 0;
1028
1055
  this.hasRefreshCookie = false;
1029
1056
  throw err;
1030
1057
  }
1058
+ this.assertCredentialGeneration(generation);
1059
+ return this.persistTokenResponse(tokens);
1031
1060
  }
1032
1061
  // ────────────────────────────────────────────────────────────────────────
1033
1062
  // Internal: fetch wrapper that maps non-2xx → WitniumchainApiError
@@ -1470,6 +1499,9 @@ var MfaRecoveryCodes = class {
1470
1499
  return this.client.regenerateRecoveryCodes(proof);
1471
1500
  }
1472
1501
  };
1502
+ function redirectUriKey(state) {
1503
+ return `${state}.redirect_uri`;
1504
+ }
1473
1505
  function sleep(ms) {
1474
1506
  return new Promise((resolve) => setTimeout(resolve, ms));
1475
1507
  }