@witnium-tech/witniumchain 0.9.0 → 0.10.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.
@@ -118,6 +118,36 @@ await client.login({ email, password }); // sets wac_session cookie
118
118
  In a browser the `wac_session` cookie is set automatically (the SDK uses
119
119
  `credentials: 'include'`). Server-side callers must capture + replay the cookie.
120
120
 
121
+ ### Cookie-only dashboard → bearer calls (automatic, no OAuth needed)
122
+
123
+ You do **not** need to run the OAuth code flow to call the operational API
124
+ (delegated keys, witness writes, chain reads). After `login()`, just call them:
125
+
126
+ ```ts
127
+ await client.login({ email, password }); // sets wac_session
128
+ await client.listDelegatedKeys(); // ← works; SDK mints a bearer token
129
+ ```
130
+
131
+ How: those endpoints are bearer-only (they never accept the ambient cookie, so
132
+ there's **no CSRF surface** even with `SameSite=None`). On the first such call
133
+ the SDK transparently exchanges the session cookie for a short-lived access
134
+ token via `POST /v1/auth/access-token`, caches it, and re-mints on a 401. You
135
+ never see the token unless you want it:
136
+
137
+ ```ts
138
+ const { accessToken, expiresAt } = await client.exchangeSessionForToken(); // optional pre-warm
139
+ ```
140
+
141
+ Requirements: in the browser the `wac_session` cookie rides automatically
142
+ (`credentials: 'include'`). Node/SSR callers pass `sessionCookie: '<value>'` to
143
+ the constructor. If there's no valid session, the call throws a descriptive
144
+ "requires an OAuth access token … or provide `sessionCookie`" error — that's
145
+ your cue to send the user back to login.
146
+
147
+ This is the recommended path for a first-party dashboard. Reach for the full
148
+ OAuth flow below only when the dashboard is itself a registered OAuth client
149
+ (e.g. third-party apps, or you want long-lived refresh tokens).
150
+
121
151
  ### Magic-link sign-in
122
152
 
123
153
  `beginOAuthLogin` / passwordless paths exist; see the SDK's `OauthNamespace`
package/dist/index.d.mts CHANGED
@@ -10580,6 +10580,8 @@ declare class WitniumchainClient {
10580
10580
  private readonly verifierStorage;
10581
10581
  private discoveryCache;
10582
10582
  private refreshInFlight;
10583
+ private accessTokenFromSession;
10584
+ private sessionExchangeInFlight;
10583
10585
  /** Subscriptions / billing helpers. See {@link Subscriptions}. */
10584
10586
  readonly subscriptions: Subscriptions;
10585
10587
  /** Delegated-key namespace including the one-call {@link DelegatedKeys.provision} flow. */
@@ -10754,6 +10756,41 @@ declare class WitniumchainClient {
10754
10756
  * instead of retrying with a now-invalid token.
10755
10757
  */
10756
10758
  refreshAccessToken(): Promise<OAuthTokenSnapshot>;
10759
+ /**
10760
+ * Exchange the logged-in session (the `wac_session` cookie) for a short-lived
10761
+ * bearer access token and cache it as the client's `accessToken`.
10762
+ *
10763
+ * This is the SDK side of the session→token model (accounts'
10764
+ * `POST /v1/auth/access-token`): the dashboard authenticates with the
10765
+ * cookie, but the operational API (delegated keys, witness writes, chain
10766
+ * reads) is bearer-only — no endpoint accepts the ambient cookie, so there's
10767
+ * no CSRF surface even with `SameSite=None`. The minted token is
10768
+ * byte-compatible with an OAuth user token (same signing key, `iss`, `aud`,
10769
+ * scope + org/role/contracts claims), so every `BearerJWT` route accepts it.
10770
+ *
10771
+ * Callers rarely invoke this directly — {@link applyAuth} mints transparently
10772
+ * on the first `BearerJWT` call when no token is set and re-mints on a 401.
10773
+ * It's exposed so a dashboard can pre-warm the token right after login.
10774
+ *
10775
+ * Requires a session: either `sessionCookie` on the constructor (Node) or an
10776
+ * ambient `wac_session` cookie carried by the browser (`credentials:
10777
+ * 'include'`). Throws {@link WitniumchainApiError} (401) when there's no
10778
+ * valid session.
10779
+ */
10780
+ exchangeSessionForToken(): Promise<OAuthTokenSnapshot>;
10781
+ /**
10782
+ * Best-effort transparent mint for the `BearerJWT` path: if no access token
10783
+ * is cached, try the session exchange (single-flight). Swallows failure so
10784
+ * the caller in {@link applyAuth} surfaces the actionable "no token / no
10785
+ * session" guidance rather than a raw 401 from the exchange endpoint.
10786
+ */
10787
+ private ensureSessionAccessToken;
10788
+ /**
10789
+ * Obtain a fresh access token for a 401-retry, picking the right mechanism:
10790
+ * an OAuth refresh when a refresh credential exists, else a session re-mint
10791
+ * for a session-exchanged token. Returns true when a fresh token is in hand.
10792
+ */
10793
+ private reauthenticateForRetry;
10757
10794
  /**
10758
10795
  * End the OAuth session.
10759
10796
  *
package/dist/index.d.ts CHANGED
@@ -10580,6 +10580,8 @@ declare class WitniumchainClient {
10580
10580
  private readonly verifierStorage;
10581
10581
  private discoveryCache;
10582
10582
  private refreshInFlight;
10583
+ private accessTokenFromSession;
10584
+ private sessionExchangeInFlight;
10583
10585
  /** Subscriptions / billing helpers. See {@link Subscriptions}. */
10584
10586
  readonly subscriptions: Subscriptions;
10585
10587
  /** Delegated-key namespace including the one-call {@link DelegatedKeys.provision} flow. */
@@ -10754,6 +10756,41 @@ declare class WitniumchainClient {
10754
10756
  * instead of retrying with a now-invalid token.
10755
10757
  */
10756
10758
  refreshAccessToken(): Promise<OAuthTokenSnapshot>;
10759
+ /**
10760
+ * Exchange the logged-in session (the `wac_session` cookie) for a short-lived
10761
+ * bearer access token and cache it as the client's `accessToken`.
10762
+ *
10763
+ * This is the SDK side of the session→token model (accounts'
10764
+ * `POST /v1/auth/access-token`): the dashboard authenticates with the
10765
+ * cookie, but the operational API (delegated keys, witness writes, chain
10766
+ * reads) is bearer-only — no endpoint accepts the ambient cookie, so there's
10767
+ * no CSRF surface even with `SameSite=None`. The minted token is
10768
+ * byte-compatible with an OAuth user token (same signing key, `iss`, `aud`,
10769
+ * scope + org/role/contracts claims), so every `BearerJWT` route accepts it.
10770
+ *
10771
+ * Callers rarely invoke this directly — {@link applyAuth} mints transparently
10772
+ * on the first `BearerJWT` call when no token is set and re-mints on a 401.
10773
+ * It's exposed so a dashboard can pre-warm the token right after login.
10774
+ *
10775
+ * Requires a session: either `sessionCookie` on the constructor (Node) or an
10776
+ * ambient `wac_session` cookie carried by the browser (`credentials:
10777
+ * 'include'`). Throws {@link WitniumchainApiError} (401) when there's no
10778
+ * valid session.
10779
+ */
10780
+ exchangeSessionForToken(): Promise<OAuthTokenSnapshot>;
10781
+ /**
10782
+ * Best-effort transparent mint for the `BearerJWT` path: if no access token
10783
+ * is cached, try the session exchange (single-flight). Swallows failure so
10784
+ * the caller in {@link applyAuth} surfaces the actionable "no token / no
10785
+ * session" guidance rather than a raw 401 from the exchange endpoint.
10786
+ */
10787
+ private ensureSessionAccessToken;
10788
+ /**
10789
+ * Obtain a fresh access token for a 401-retry, picking the right mechanism:
10790
+ * an OAuth refresh when a refresh credential exists, else a session re-mint
10791
+ * for a session-exchanged token. Returns true when a fresh token is in hand.
10792
+ */
10793
+ private reauthenticateForRetry;
10757
10794
  /**
10758
10795
  * End the OAuth session.
10759
10796
  *
package/dist/index.js CHANGED
@@ -111,6 +111,16 @@ var WitniumchainClient = class {
111
111
  // tokens rotate on use (D10), so a second concurrent refresh would race the
112
112
  // first and one of them would 401.
113
113
  refreshInFlight;
114
+ // True when the current `accessToken` was minted by the session→token
115
+ // exchange (`exchangeSessionForToken`) rather than the OAuth code/refresh
116
+ // flow. Drives the 401-retry: a session-exchanged token has no refresh
117
+ // credential, so on expiry the SDK re-mints from the cookie instead of
118
+ // refreshing. Reset to false whenever an OAuth /token response lands.
119
+ accessTokenFromSession = false;
120
+ // Single-flight gate for the session→token exchange. A cookie-only client's
121
+ // first BearerJWT call (and any concurrent fan-out) awaits one in-flight
122
+ // mint rather than minting N tokens.
123
+ sessionExchangeInFlight;
114
124
  /** Subscriptions / billing helpers. See {@link Subscriptions}. */
115
125
  subscriptions;
116
126
  /** Delegated-key namespace including the one-call {@link DelegatedKeys.provision} flow. */
@@ -787,6 +797,84 @@ var WitniumchainClient = class {
787
797
  })();
788
798
  return this.refreshInFlight;
789
799
  }
800
+ /**
801
+ * Exchange the logged-in session (the `wac_session` cookie) for a short-lived
802
+ * bearer access token and cache it as the client's `accessToken`.
803
+ *
804
+ * This is the SDK side of the session→token model (accounts'
805
+ * `POST /v1/auth/access-token`): the dashboard authenticates with the
806
+ * cookie, but the operational API (delegated keys, witness writes, chain
807
+ * reads) is bearer-only — no endpoint accepts the ambient cookie, so there's
808
+ * no CSRF surface even with `SameSite=None`. The minted token is
809
+ * byte-compatible with an OAuth user token (same signing key, `iss`, `aud`,
810
+ * scope + org/role/contracts claims), so every `BearerJWT` route accepts it.
811
+ *
812
+ * Callers rarely invoke this directly — {@link applyAuth} mints transparently
813
+ * on the first `BearerJWT` call when no token is set and re-mints on a 401.
814
+ * It's exposed so a dashboard can pre-warm the token right after login.
815
+ *
816
+ * Requires a session: either `sessionCookie` on the constructor (Node) or an
817
+ * ambient `wac_session` cookie carried by the browser (`credentials:
818
+ * 'include'`). Throws {@link WitniumchainApiError} (401) when there's no
819
+ * valid session.
820
+ */
821
+ async exchangeSessionForToken() {
822
+ if (this.sessionExchangeInFlight) return this.sessionExchangeInFlight;
823
+ this.sessionExchangeInFlight = (async () => {
824
+ try {
825
+ const res = await this.req("POST", "/v1/auth/access-token", { auth: "SessionCookie" });
826
+ this.accessToken = res.accessToken;
827
+ this.accessTokenFromSession = true;
828
+ this.refreshToken = void 0;
829
+ this.hasRefreshCookie = false;
830
+ return {
831
+ accessToken: res.accessToken,
832
+ expiresAt: Math.floor(new Date(res.expiresAt).getTime() / 1e3)
833
+ };
834
+ } finally {
835
+ this.sessionExchangeInFlight = void 0;
836
+ }
837
+ })();
838
+ return this.sessionExchangeInFlight;
839
+ }
840
+ /**
841
+ * Best-effort transparent mint for the `BearerJWT` path: if no access token
842
+ * is cached, try the session exchange (single-flight). Swallows failure so
843
+ * the caller in {@link applyAuth} surfaces the actionable "no token / no
844
+ * session" guidance rather than a raw 401 from the exchange endpoint.
845
+ */
846
+ async ensureSessionAccessToken() {
847
+ if (this.accessToken) return;
848
+ try {
849
+ await this.exchangeSessionForToken();
850
+ } catch {
851
+ }
852
+ }
853
+ /**
854
+ * Obtain a fresh access token for a 401-retry, picking the right mechanism:
855
+ * an OAuth refresh when a refresh credential exists, else a session re-mint
856
+ * for a session-exchanged token. Returns true when a fresh token is in hand.
857
+ */
858
+ async reauthenticateForRetry() {
859
+ if (this.refreshToken || this.hasRefreshCookie) {
860
+ try {
861
+ await this.refreshAccessToken();
862
+ return true;
863
+ } catch {
864
+ return false;
865
+ }
866
+ }
867
+ if (this.accessTokenFromSession) {
868
+ this.accessToken = void 0;
869
+ try {
870
+ await this.exchangeSessionForToken();
871
+ return true;
872
+ } catch {
873
+ return false;
874
+ }
875
+ }
876
+ return false;
877
+ }
790
878
  /**
791
879
  * End the OAuth session.
792
880
  *
@@ -801,6 +889,7 @@ var WitniumchainClient = class {
801
889
  */
802
890
  signOut() {
803
891
  this.accessToken = void 0;
892
+ this.accessTokenFromSession = false;
804
893
  this.refreshToken = void 0;
805
894
  this.hasRefreshCookie = false;
806
895
  this.pendingLogins.clear();
@@ -898,6 +987,7 @@ var WitniumchainClient = class {
898
987
  });
899
988
  }
900
989
  this.accessToken = tokens.access_token;
990
+ this.accessTokenFromSession = false;
901
991
  if (tokens.refresh_token) {
902
992
  this.refreshToken = tokens.refresh_token;
903
993
  } else {
@@ -1028,13 +1118,9 @@ var WitniumchainClient = class {
1028
1118
  if (opts._isRetry) return false;
1029
1119
  if (opts.auth !== "BearerJWT") return false;
1030
1120
  if (res.status !== 401) return false;
1031
- if (!this.refreshToken && !this.hasRefreshCookie) return false;
1032
- try {
1033
- await this.refreshAccessToken();
1034
- return true;
1035
- } catch {
1121
+ if (!this.refreshToken && !this.hasRefreshCookie && !this.accessTokenFromSession)
1036
1122
  return false;
1037
- }
1123
+ return this.reauthenticateForRetry();
1038
1124
  }
1039
1125
  /**
1040
1126
  * Same decision as `shouldRefreshAndRetry` but for the JSON-parsed path,
@@ -1048,18 +1134,14 @@ var WitniumchainClient = class {
1048
1134
  if (opts._isRetry) return false;
1049
1135
  if (opts.auth !== "BearerJWT") return false;
1050
1136
  if (status !== 401) return false;
1051
- if (!this.refreshToken && !this.hasRefreshCookie) return false;
1137
+ if (!this.refreshToken && !this.hasRefreshCookie && !this.accessTokenFromSession)
1138
+ return false;
1052
1139
  const body = parsed;
1053
1140
  const label = typeof body?.error === "string" ? body.error : void 0;
1054
1141
  if (label !== void 0 && label !== "token_expired" && label !== "invalid_token") {
1055
1142
  return false;
1056
1143
  }
1057
- try {
1058
- await this.refreshAccessToken();
1059
- return true;
1060
- } catch {
1061
- return false;
1062
- }
1144
+ return this.reauthenticateForRetry();
1063
1145
  }
1064
1146
  /**
1065
1147
  * Resolve which base URL to call. Default is accounts (`baseUrl`). When a
@@ -1099,9 +1181,12 @@ var WitniumchainClient = class {
1099
1181
  return;
1100
1182
  }
1101
1183
  case "BearerJWT": {
1184
+ if (!this.accessToken) {
1185
+ await this.ensureSessionAccessToken();
1186
+ }
1102
1187
  if (!this.accessToken) {
1103
1188
  throw new Error(
1104
- `WitniumchainClient: ${method} ${path} requires an OAuth access token. Pass \`accessToken\` to the constructor, or call \`beginOAuthLogin\`/\`completeOAuthLogin\` to obtain one.`
1189
+ `WitniumchainClient: ${method} ${path} requires an OAuth access token. Pass \`accessToken\` to the constructor, call \`beginOAuthLogin\`/\`completeOAuthLogin\`, or \u2014 for a logged-in dashboard \u2014 provide \`sessionCookie\` (Node) or rely on the browser \`wac_session\` cookie so the SDK can mint one.`
1105
1190
  );
1106
1191
  }
1107
1192
  headers["authorization"] = `Bearer ${this.accessToken}`;