najm-auth 3.3.0 → 3.3.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.
@@ -110,6 +110,7 @@ declare class NajmAuthClient {
110
110
  private _refreshWithCircuit;
111
111
  private _doRefresh;
112
112
  private handleUnauthorized;
113
+ private requestServerLogout;
113
114
  private applyTokens;
114
115
  private scheduleRefresh;
115
116
  private clearRefreshTimer;
@@ -1,4 +1,4 @@
1
- export { F as FetchClient, H as HydrateSession, N as NajmAuthClient, c as createAuthClient } from '../NajmAuthClient-ZtXTIUSF.js';
1
+ export { F as FetchClient, H as HydrateSession, N as NajmAuthClient, c as createAuthClient } from '../NajmAuthClient-BBl-GRis.js';
2
2
  import { D as DecodedToken, T as TabSyncMessage, S as SyncPayload } from '../types-BaSfgxqE.js';
3
3
  export { b as AuthClientConfig, h as AuthError, f as AuthEvent, g as AuthEventHandler, i as AuthEventMap, e as AuthState, A as AuthUser, j as AuthenticatedLogin, C as CredentialSetupPending, L as LoginCredentials, c as LoginResult, d as OAuthLoginOptions, O as OAuthProvider, a as RequestOptions, R as RetryConfig, k as ServerResponse, l as TokenPair } from '../types-BaSfgxqE.js';
4
4
 
@@ -364,7 +364,7 @@ var NajmAuthClient = class _NajmAuthClient {
364
364
  this.emit("logout", null);
365
365
  try {
366
366
  await pendingRefresh?.catch(() => void 0);
367
- await this.api.post(`${this.prefix}/logout`, { skipAuth: true });
367
+ await this.requestServerLogout();
368
368
  } catch (err) {
369
369
  this.emit("logoutError", err);
370
370
  }
@@ -549,6 +549,7 @@ var NajmAuthClient = class _NajmAuthClient {
549
549
  if (generation !== this.authGeneration) return;
550
550
  const shouldOpenCircuit = this.registerRefreshFailure(err);
551
551
  if (shouldOpenCircuit) {
552
+ await this.requestServerLogout().catch(() => void 0);
552
553
  this.resetState();
553
554
  this.emit("sessionExpired", null);
554
555
  if (err instanceof AuthError && err.status === 401) {
@@ -578,6 +579,9 @@ var NajmAuthClient = class _NajmAuthClient {
578
579
  return null;
579
580
  }
580
581
  }
582
+ async requestServerLogout() {
583
+ await this.api.post(`${this.prefix}/logout`, { skipAuth: true });
584
+ }
581
585
  applyTokens(tokens) {
582
586
  this.resetRefreshFailures();
583
587
  const decoded = decodeToken(tokens.accessToken);
@@ -618,7 +622,7 @@ var NajmAuthClient = class _NajmAuthClient {
618
622
  this.clearRefreshCircuitTimer();
619
623
  }
620
624
  registerRefreshFailure(err) {
621
- if (err instanceof AuthError && err.status === 401) {
625
+ if (err instanceof AuthError && [401, 403, 429].includes(err.status)) {
622
626
  this.refreshFailures = _NajmAuthClient.MAX_REFRESH_FAILURES;
623
627
  } else {
624
628
  this.refreshFailures += 1;
@@ -1,6 +1,6 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode, CSSProperties, ReactElement } from 'react';
3
- import { N as NajmAuthClient, H as HydrateSession } from '../../NajmAuthClient-ZtXTIUSF.js';
3
+ import { N as NajmAuthClient, H as HydrateSession } from '../../NajmAuthClient-BBl-GRis.js';
4
4
  import { e as AuthState, A as AuthUser, c as LoginResult, h as AuthError, L as LoginCredentials, d as OAuthLoginOptions, f as AuthEvent, i as AuthEventMap } from '../../types-BaSfgxqE.js';
5
5
 
6
6
  interface AuthProviderProps {
@@ -1,5 +1,5 @@
1
1
  import { A as AuthUser, R as RetryConfig } from '../../types-BaSfgxqE.js';
2
- import { F as FetchClient, N as NajmAuthClient } from '../../NajmAuthClient-ZtXTIUSF.js';
2
+ import { F as FetchClient, N as NajmAuthClient } from '../../NajmAuthClient-BBl-GRis.js';
3
3
  import { ProxySessionMode } from '../edge.js';
4
4
  export { withAuthMiddleware } from '../edge.js';
5
5
  import { G as GetSessionConfig, S as ServerSession$1 } from '../../getSession-BthP85UA.js';
@@ -1075,7 +1075,7 @@ var NajmAuthClient = class _NajmAuthClient {
1075
1075
  this.emit("logout", null);
1076
1076
  try {
1077
1077
  await pendingRefresh?.catch(() => void 0);
1078
- await this.api.post(`${this.prefix}/logout`, { skipAuth: true });
1078
+ await this.requestServerLogout();
1079
1079
  } catch (err) {
1080
1080
  this.emit("logoutError", err);
1081
1081
  }
@@ -1260,6 +1260,7 @@ var NajmAuthClient = class _NajmAuthClient {
1260
1260
  if (generation !== this.authGeneration) return;
1261
1261
  const shouldOpenCircuit = this.registerRefreshFailure(err);
1262
1262
  if (shouldOpenCircuit) {
1263
+ await this.requestServerLogout().catch(() => void 0);
1263
1264
  this.resetState();
1264
1265
  this.emit("sessionExpired", null);
1265
1266
  if (err instanceof AuthError && err.status === 401) {
@@ -1289,6 +1290,9 @@ var NajmAuthClient = class _NajmAuthClient {
1289
1290
  return null;
1290
1291
  }
1291
1292
  }
1293
+ async requestServerLogout() {
1294
+ await this.api.post(`${this.prefix}/logout`, { skipAuth: true });
1295
+ }
1292
1296
  applyTokens(tokens) {
1293
1297
  this.resetRefreshFailures();
1294
1298
  const decoded = decodeToken(tokens.accessToken);
@@ -1329,7 +1333,7 @@ var NajmAuthClient = class _NajmAuthClient {
1329
1333
  this.clearRefreshCircuitTimer();
1330
1334
  }
1331
1335
  registerRefreshFailure(err) {
1332
- if (err instanceof AuthError && err.status === 401) {
1336
+ if (err instanceof AuthError && [401, 403, 429].includes(err.status)) {
1333
1337
  this.refreshFailures = _NajmAuthClient.MAX_REFRESH_FAILURES;
1334
1338
  } else {
1335
1339
  this.refreshFailures += 1;
package/dist/index.d.ts CHANGED
@@ -1084,16 +1084,19 @@ declare class TokenService {
1084
1084
  tokenFamily: string;
1085
1085
  };
1086
1086
  private static readonly PREVIOUS_GRACE_SECONDS;
1087
+ private clearRefreshSessionCookies;
1088
+ private rejectRefreshSession;
1089
+ private readRefreshSessionCookie;
1087
1090
  /**
1088
1091
  * Read the refresh cookie and return the userId it belongs to.
1089
1092
  * Validates against current/previous token state with bounded recovery.
1090
1093
  * Throws if the cookie is missing, invalid, or outside the grace window.
1091
1094
  *
1092
- * Intentionally side-effect-free: unlike refreshTokens(), a mismatch here
1093
- * does NOT revoke the suspect family. This is a read path (e.g. GET
1094
- * /auth/me) a stray stale cookie on a read must not be able to destroy
1095
- * the active session. Reuse detection and revocation belong to the
1096
- * rotation path only.
1095
+ * Intentionally does not revoke on a mismatch: unlike refreshTokens(), this
1096
+ * is a read path (e.g. GET /auth/me), so a stray stale cookie must not be
1097
+ * able to destroy server-side session state. Definitively invalid browser
1098
+ * cookies are still cleared so a signed snapshot cannot cause a redirect
1099
+ * loop.
1097
1100
  */
1098
1101
  private resolveRefreshSessionFromCookie;
1099
1102
  resolveUserFromCookie(): Promise<string>;
@@ -1623,7 +1626,7 @@ declare class AuthService {
1623
1626
  recoverSession(): Promise<{
1624
1627
  recovered: true;
1625
1628
  }>;
1626
- logoutUser(userId: string, authorization?: string): Promise<{
1629
+ logoutUser(userId: string | undefined, authorization?: string): Promise<{
1627
1630
  data: any;
1628
1631
  message: string;
1629
1632
  }>;
@@ -1700,7 +1703,7 @@ declare class AuthController {
1700
1703
  recoverSession(recoveryRequest: string | undefined, ctx: Context): Promise<{
1701
1704
  recovered: true;
1702
1705
  }>;
1703
- logoutUser(userId: string, authorization?: string): Promise<{
1706
+ logoutUser(userId: string | undefined, authorization?: string): Promise<{
1704
1707
  data: any;
1705
1708
  message: string;
1706
1709
  }>;
package/dist/index.js CHANGED
@@ -1914,26 +1914,42 @@ var TokenService = class TokenService2 {
1914
1914
  return { userId: decoded.userId, tokenFamily: decoded.tokenFamily };
1915
1915
  }
1916
1916
  static PREVIOUS_GRACE_SECONDS = 120;
1917
+ clearRefreshSessionCookies() {
1918
+ this.cookieManager.clearRefreshToken();
1919
+ this.cookieManager.clearSessionCookie();
1920
+ }
1921
+ rejectRefreshSession(messageKey = "errors.refreshTokenInvalid") {
1922
+ this.clearRefreshSessionCookies();
1923
+ Err7(this.t(messageKey), 401);
1924
+ }
1925
+ readRefreshSessionCookie() {
1926
+ const refreshToken = this.cookieManager.getRefreshToken();
1927
+ if (!refreshToken) {
1928
+ this.rejectRefreshSession("errors.refreshTokenMissing");
1929
+ }
1930
+ try {
1931
+ return { refreshToken, ...this.verifyRefreshToken(refreshToken) };
1932
+ } catch (error) {
1933
+ this.clearRefreshSessionCookies();
1934
+ throw error;
1935
+ }
1936
+ }
1917
1937
  /**
1918
1938
  * Read the refresh cookie and return the userId it belongs to.
1919
1939
  * Validates against current/previous token state with bounded recovery.
1920
1940
  * Throws if the cookie is missing, invalid, or outside the grace window.
1921
1941
  *
1922
- * Intentionally side-effect-free: unlike refreshTokens(), a mismatch here
1923
- * does NOT revoke the suspect family. This is a read path (e.g. GET
1924
- * /auth/me) a stray stale cookie on a read must not be able to destroy
1925
- * the active session. Reuse detection and revocation belong to the
1926
- * rotation path only.
1942
+ * Intentionally does not revoke on a mismatch: unlike refreshTokens(), this
1943
+ * is a read path (e.g. GET /auth/me), so a stray stale cookie must not be
1944
+ * able to destroy server-side session state. Definitively invalid browser
1945
+ * cookies are still cleared so a signed snapshot cannot cause a redirect
1946
+ * loop.
1927
1947
  */
1928
1948
  async resolveRefreshSessionFromCookie() {
1929
- const refreshToken = this.cookieManager.getRefreshToken();
1930
- if (!refreshToken) {
1931
- Err7(this.t("errors.refreshTokenMissing"), 401);
1932
- }
1933
- const { userId, tokenFamily } = this.verifyRefreshToken(refreshToken);
1949
+ const { refreshToken, userId, tokenFamily } = this.readRefreshSessionCookie();
1934
1950
  const stored = await this.tokenRepository.getByFamily(tokenFamily);
1935
1951
  if (!stored || stored.userId !== userId) {
1936
- Err7(this.t("errors.refreshTokenInvalid"), 401);
1952
+ this.rejectRefreshSession();
1937
1953
  }
1938
1954
  const presentedHash = this.hashToken(refreshToken);
1939
1955
  if (presentedHash === stored.token) {
@@ -1943,7 +1959,7 @@ var TokenService = class TokenService2 {
1943
1959
  if (canRecover) {
1944
1960
  return { userId, tokenFamily };
1945
1961
  }
1946
- Err7(this.t("errors.refreshTokenInvalid"), 401);
1962
+ this.rejectRefreshSession();
1947
1963
  }
1948
1964
  async resolveUserFromCookie() {
1949
1965
  return (await this.resolveRefreshSessionFromCookie()).userId;
@@ -2143,14 +2159,10 @@ var TokenService = class TokenService2 {
2143
2159
  * Compares provided token with hashed version in database
2144
2160
  */
2145
2161
  async refreshTokens() {
2146
- const refreshToken = this.cookieManager.getRefreshToken();
2147
- if (!refreshToken) {
2148
- Err7(this.t("errors.refreshTokenMissing"), 401);
2149
- }
2150
- const { userId, tokenFamily } = this.verifyRefreshToken(refreshToken);
2162
+ const { refreshToken, userId, tokenFamily } = this.readRefreshSessionCookie();
2151
2163
  const stored = await this.tokenRepository.getByFamily(tokenFamily);
2152
2164
  if (!stored || stored.userId !== userId) {
2153
- Err7(this.t("errors.refreshTokenInvalid"), 401);
2165
+ this.rejectRefreshSession();
2154
2166
  }
2155
2167
  const presentedHash = this.hashToken(refreshToken);
2156
2168
  await this.requireActiveRefreshUser(userId, tokenFamily);
@@ -2166,19 +2178,18 @@ var TokenService = class TokenService2 {
2166
2178
  return this.rotateTokens(userId, tokenFamily, stored.token);
2167
2179
  }
2168
2180
  await this.revokeSuspectRefreshFamily(userId, tokenFamily);
2169
- Err7(this.t("errors.refreshTokenInvalid"), 401);
2181
+ this.rejectRefreshSession();
2170
2182
  }
2171
2183
  async requireActiveRefreshUser(userId, tokenFamily) {
2172
2184
  const user = await this.tokenRepository.getUser(userId);
2173
2185
  if (!user || user.status !== "active") {
2174
2186
  await this.revokeFamily(tokenFamily);
2175
- Err7(this.t("errors.refreshTokenInvalid"), 401);
2187
+ this.rejectRefreshSession();
2176
2188
  }
2177
2189
  const requirement = await this.credentialSetupRequirements?.find(userId, PASSWORD_SETUP_PURPOSE);
2178
2190
  if (requirement?.required) {
2179
2191
  await this.revokeFamily(tokenFamily);
2180
- this.cookieManager.clearRefreshToken();
2181
- this.cookieManager.clearSessionCookie();
2192
+ this.clearRefreshSessionCookies();
2182
2193
  Err7(this.t("errors.credentialSetupRequired"), 403);
2183
2194
  }
2184
2195
  return user;
@@ -3350,9 +3361,14 @@ var AuthService = class AuthService2 {
3350
3361
  return { recovered: true };
3351
3362
  }
3352
3363
  async logoutUser(userId, authorization) {
3353
- await this.tokenService.logout(userId, authorization);
3354
- this.cookieManager.clearRefreshToken();
3355
- this.cookieManager.clearSessionCookie();
3364
+ try {
3365
+ if (userId) {
3366
+ await this.tokenService.logout(userId, authorization);
3367
+ }
3368
+ } finally {
3369
+ this.cookieManager.clearRefreshToken();
3370
+ this.cookieManager.clearSessionCookie();
3371
+ }
3356
3372
  return { data: null, message: this.t("auth.success.logout") };
3357
3373
  }
3358
3374
  /**
@@ -3846,8 +3862,6 @@ __decorate20([
3846
3862
  ], AuthController.prototype, "recoverSession", null);
3847
3863
  __decorate20([
3848
3864
  Post("/logout"),
3849
- isAuth(),
3850
- RateLimit({ limit: 10, window: "15m", key: "user" }),
3851
3865
  __param5(0, User2("id")),
3852
3866
  __param5(1, Headers("authorization")),
3853
3867
  __metadata20("design:type", Function),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "najm-auth",
3
- "version": "3.3.0",
3
+ "version": "3.3.2",
4
4
  "description": "Authentication and authorization library for najm framework",
5
5
  "type": "module",
6
6
  "files": [
@@ -91,13 +91,13 @@
91
91
  "dependencies": {
92
92
  "bcryptjs": "^3.0.2",
93
93
  "najm-cookies": "^2.0.2",
94
- "najm-core": "^2.0.5",
95
- "najm-database": "^2.0.3",
94
+ "najm-core": "^2.0.6",
95
+ "najm-database": "^2.0.4",
96
96
  "najm-guard": "^2.0.2",
97
- "najm-i18n": "^2.0.3",
98
- "najm-cache": "^2.1.0",
97
+ "najm-i18n": "^2.1.1",
98
+ "najm-cache": "^2.1.2",
99
99
  "najm-email": "^2.0.2",
100
- "najm-rate": "^2.1.0",
100
+ "najm-rate": "^2.1.1",
101
101
  "najm-validation": "^2.0.2",
102
102
  "jsonwebtoken": "^9.0.3",
103
103
  "jose": "^6.1.3",