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