@witnium-tech/witniumchain 0.11.0 → 0.12.1

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.js CHANGED
@@ -121,6 +121,11 @@ var WitniumchainClient = class {
121
121
  // first BearerJWT call (and any concurrent fan-out) awaits one in-flight
122
122
  // mint rather than minting N tokens.
123
123
  sessionExchangeInFlight;
124
+ // Bumped whenever the cached credentials stop being valid (sign-out, the
125
+ // TOTP-confirm epoch bump). A mint or refresh that started under an older
126
+ // generation must not store its result: it could be a previous user's
127
+ // token, or one the server has already ended.
128
+ credentialGeneration = 0;
124
129
  /** Subscriptions / billing helpers. See {@link Subscriptions}. */
125
130
  subscriptions;
126
131
  /** Delegated-key namespace including the one-call {@link DelegatedKeys.provision} flow. */
@@ -177,9 +182,10 @@ var WitniumchainClient = class {
177
182
  * the contract with the caller's client-generated `ownerPublicKey` +
178
183
  * `initialSigningPublicKey`. Witnium never sees the private bytes.
179
184
  *
180
- * Returns `{ orgId, userId, contractAddress, contractVersion,
181
- * emailVerifyToken }`. The verify token is also emailed; it's echoed here
182
- * for UIs that prefer to render the verify link themselves.
185
+ * Returns `{ orgId, userId, contractAddress, contractVersion }`. The
186
+ * email-verify link is mailed to `adminEmail` and never returned, so there
187
+ * is nothing to render. If the mail cannot be sent, nothing is created and
188
+ * the call fails with a 502; retry it.
183
189
  */
184
190
  createOrg(body) {
185
191
  return this.req("POST", "/v1/orgs", { auth: "Public", body });
@@ -237,11 +243,36 @@ var WitniumchainClient = class {
237
243
  query: { token }
238
244
  });
239
245
  }
246
+ /**
247
+ * Password login. Check `status`: `'authenticated'` means the session
248
+ * cookie is set. `'mfa_required'` means NO session exists yet: the server
249
+ * set a short-lived challenge cookie, and {@link loginMfa} must follow with
250
+ * the user's TOTP or recovery code.
251
+ */
240
252
  login(body) {
241
253
  return this.req("POST", "/v1/auth/login", { auth: "Public", body });
242
254
  }
243
- logout() {
244
- return this.req("POST", "/v1/auth/logout", { auth: "Public" });
255
+ /**
256
+ * Second step of an MFA login, after `login()` returned `'mfa_required'`.
257
+ * Relies on the challenge cookie from that call (`credentials: 'include'`).
258
+ * 401 `mfa_code_invalid`: wrong or reused code, the user may retry.
259
+ * 401 `mfa_challenge_invalid`: expired or used up, start again at `login()`.
260
+ * 429 `mfa_rate_limited`: the body carries `retryAfterSeconds`.
261
+ */
262
+ loginMfa(body) {
263
+ return this.req("POST", "/v1/auth/login/mfa", { auth: "Public", body });
264
+ }
265
+ /**
266
+ * End the first-party session. The cached bearer token is dropped even when
267
+ * the request fails, so a logged-out tab never keeps the previous user's
268
+ * token.
269
+ */
270
+ async logout() {
271
+ try {
272
+ return await this.req("POST", "/v1/auth/logout", { auth: "Public" });
273
+ } finally {
274
+ this.signOut();
275
+ }
245
276
  }
246
277
  forgotPassword(body) {
247
278
  return this.req("POST", "/v1/auth/forgot-password", { auth: "Public", body });
@@ -474,20 +505,32 @@ var WitniumchainClient = class {
474
505
  auth: "SessionCookie"
475
506
  });
476
507
  }
477
- confirmTotp(body) {
478
- return this.req("POST", "/v1/account/mfa/totp/confirm", {
508
+ /**
509
+ * Confirming bumps the user's security epoch server-side, which ends every
510
+ * session-minted access token and OAuth artifact of the user. Cached and
511
+ * in-flight tokens are dropped so the next bearer call mints a fresh one
512
+ * from the (surviving) session.
513
+ */
514
+ async confirmTotp(body) {
515
+ const res = await this.req("POST", "/v1/account/mfa/totp/confirm", {
479
516
  auth: "SessionCookie",
480
517
  body
481
518
  });
519
+ this.invalidateCredentials();
520
+ return res;
482
521
  }
483
- disableTotp() {
522
+ /** Step-up: 403 `mfa_step_up_failed` on a wrong proof, 429 `mfa_rate_limited`. */
523
+ disableTotp(body) {
484
524
  return this.req("DELETE", "/v1/account/mfa/totp", {
485
- auth: "SessionCookie"
525
+ auth: "SessionCookie",
526
+ body
486
527
  });
487
528
  }
488
- regenerateRecoveryCodes() {
529
+ /** Step-up: 403 `mfa_step_up_failed` on a wrong proof, 429 `mfa_rate_limited`. */
530
+ regenerateRecoveryCodes(body) {
489
531
  return this.req("POST", "/v1/account/mfa/recovery-codes/regenerate", {
490
- auth: "SessionCookie"
532
+ auth: "SessionCookie",
533
+ body
491
534
  });
492
535
  }
493
536
  // ────────────────────────────────────────────────────────────────────────
@@ -751,14 +794,16 @@ var WitniumchainClient = class {
751
794
  */
752
795
  async refreshAccessToken() {
753
796
  if (this.refreshInFlight) return this.refreshInFlight;
754
- this.refreshInFlight = (async () => {
797
+ let flight = void 0;
798
+ flight = (async () => {
755
799
  try {
756
800
  return await this.refreshAccessTokenInternal();
757
801
  } finally {
758
- this.refreshInFlight = void 0;
802
+ if (this.refreshInFlight === flight) this.refreshInFlight = void 0;
759
803
  }
760
804
  })();
761
- return this.refreshInFlight;
805
+ this.refreshInFlight = flight;
806
+ return flight;
762
807
  }
763
808
  /**
764
809
  * Exchange the logged-in session (the `wac_session` cookie) for a short-lived
@@ -783,9 +828,12 @@ var WitniumchainClient = class {
783
828
  */
784
829
  async exchangeSessionForToken() {
785
830
  if (this.sessionExchangeInFlight) return this.sessionExchangeInFlight;
786
- this.sessionExchangeInFlight = (async () => {
831
+ const generation = this.credentialGeneration;
832
+ let flight = void 0;
833
+ flight = (async () => {
787
834
  try {
788
835
  const res = await this.req("POST", "/v1/auth/access-token", { auth: "SessionCookie" });
836
+ this.assertCredentialGeneration(generation);
789
837
  this.accessToken = res.accessToken;
790
838
  this.accessTokenFromSession = true;
791
839
  this.refreshToken = void 0;
@@ -795,10 +843,32 @@ var WitniumchainClient = class {
795
843
  expiresAt: Math.floor(new Date(res.expiresAt).getTime() / 1e3)
796
844
  };
797
845
  } finally {
798
- this.sessionExchangeInFlight = void 0;
846
+ if (this.sessionExchangeInFlight === flight) this.sessionExchangeInFlight = void 0;
799
847
  }
800
848
  })();
801
- return this.sessionExchangeInFlight;
849
+ this.sessionExchangeInFlight = flight;
850
+ return flight;
851
+ }
852
+ assertCredentialGeneration(generation) {
853
+ if (generation !== this.credentialGeneration) {
854
+ throw new WitniumchainApiError({
855
+ status: 0,
856
+ message: "WitniumchainClient: the session changed (sign-out or MFA confirmation) while a token request was in flight; its token was discarded."
857
+ });
858
+ }
859
+ }
860
+ /**
861
+ * Forget every cached credential and orphan in-flight mints/refreshes, so
862
+ * none of them can store a token afterwards.
863
+ */
864
+ invalidateCredentials() {
865
+ this.credentialGeneration++;
866
+ this.accessToken = void 0;
867
+ this.accessTokenFromSession = false;
868
+ this.refreshToken = void 0;
869
+ this.hasRefreshCookie = false;
870
+ this.sessionExchangeInFlight = void 0;
871
+ this.refreshInFlight = void 0;
802
872
  }
803
873
  /**
804
874
  * Best-effort transparent mint for the `BearerJWT` path: if no access token
@@ -810,7 +880,8 @@ var WitniumchainClient = class {
810
880
  if (this.accessToken) return;
811
881
  try {
812
882
  await this.exchangeSessionForToken();
813
- } catch {
883
+ } catch (err) {
884
+ if (!(err instanceof WitniumchainApiError && err.status === 401)) throw err;
814
885
  }
815
886
  }
816
887
  /**
@@ -851,10 +922,7 @@ var WitniumchainClient = class {
851
922
  * dropped — which it is, here.
852
923
  */
853
924
  signOut() {
854
- this.accessToken = void 0;
855
- this.accessTokenFromSession = false;
856
- this.refreshToken = void 0;
857
- this.hasRefreshCookie = false;
925
+ this.invalidateCredentials();
858
926
  this.pendingLogins.clear();
859
927
  }
860
928
  // ────────────────────────────────────────────────────────────────────────
@@ -974,6 +1042,7 @@ var WitniumchainClient = class {
974
1042
  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)."
975
1043
  });
976
1044
  }
1045
+ const generation = this.credentialGeneration;
977
1046
  const discovery = await this.fetchDiscovery();
978
1047
  const form = new URLSearchParams();
979
1048
  form.set("grant_type", "refresh_token");
@@ -981,15 +1050,18 @@ var WitniumchainClient = class {
981
1050
  if (this.refreshToken) {
982
1051
  form.set("refresh_token", this.refreshToken);
983
1052
  }
1053
+ let tokens;
984
1054
  try {
985
- const tokens = await this.postTokenEndpoint(discovery.token_endpoint, form);
986
- return this.persistTokenResponse(tokens);
1055
+ tokens = await this.postTokenEndpoint(discovery.token_endpoint, form);
987
1056
  } catch (err) {
1057
+ this.assertCredentialGeneration(generation);
988
1058
  this.accessToken = void 0;
989
1059
  this.refreshToken = void 0;
990
1060
  this.hasRefreshCookie = false;
991
1061
  throw err;
992
1062
  }
1063
+ this.assertCredentialGeneration(generation);
1064
+ return this.persistTokenResponse(tokens);
993
1065
  }
994
1066
  // ────────────────────────────────────────────────────────────────────────
995
1067
  // Internal: fetch wrapper that maps non-2xx → WitniumchainApiError
@@ -1352,7 +1424,12 @@ var OrgMembers = class {
1352
1424
  list() {
1353
1425
  return this.client.listMembers();
1354
1426
  }
1355
- /** Invite a member by email (default role `member`). */
1427
+ /**
1428
+ * Invite a member by email (default role `member`). `inviteEmailSent` says
1429
+ * whether the invitation (with the verify link) was mailed; the token is
1430
+ * never returned. A 502 means the membership was recorded but the mail
1431
+ * failed; retry.
1432
+ */
1356
1433
  invite(body) {
1357
1434
  return this.client.inviteMember(body);
1358
1435
  }
@@ -1372,7 +1449,10 @@ var OrgMembers = class {
1372
1449
  remove(userId) {
1373
1450
  return this.client.removeMember(userId);
1374
1451
  }
1375
- /** Re-mint an email-verify token for a pending member. */
1452
+ /**
1453
+ * Re-send the invitation email to a pending member. Returns `{ email }`;
1454
+ * the fresh verify token is mailed, never returned.
1455
+ */
1376
1456
  resendInvite(userId) {
1377
1457
  return this.client.resendMemberInvite(userId);
1378
1458
  }
@@ -1396,14 +1476,18 @@ var MfaTotp = class {
1396
1476
  * Confirm enrolment with the first user-supplied code. Returns the 10
1397
1477
  * single-use recovery codes — show them to the user ONCE; the server never
1398
1478
  * returns them again. Throws `WitniumchainApiError` with status 400 when
1399
- * the code is invalid or the enrolment is already confirmed.
1479
+ * the code is invalid or the enrolment is already confirmed. Signs the
1480
+ * user out of every other device and connected app.
1400
1481
  */
1401
1482
  confirm(code) {
1402
1483
  return this.client.confirmTotp({ code });
1403
1484
  }
1404
- /** Disable TOTP, wiping both the secret and all recovery codes. */
1405
- disable() {
1406
- return this.client.disableTotp();
1485
+ /**
1486
+ * Disable TOTP, wiping both the secret and all recovery codes. Requires the
1487
+ * user's current code or a recovery code.
1488
+ */
1489
+ disable(proof) {
1490
+ return this.client.disableTotp(proof);
1407
1491
  }
1408
1492
  };
1409
1493
  var MfaRecoveryCodes = class {
@@ -1414,9 +1498,10 @@ var MfaRecoveryCodes = class {
1414
1498
  /**
1415
1499
  * Issue a fresh set of 10 recovery codes, invalidating the prior ones.
1416
1500
  * Same as `confirm` — codes are returned ONCE and never readable again.
1501
+ * Requires the user's current code or a recovery code.
1417
1502
  */
1418
- regenerate() {
1419
- return this.client.regenerateRecoveryCodes();
1503
+ regenerate(proof) {
1504
+ return this.client.regenerateRecoveryCodes(proof);
1420
1505
  }
1421
1506
  };
1422
1507
  function sleep(ms) {