@payez/next-mvp 4.5.3 → 4.5.4

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.
@@ -741,7 +741,16 @@ async function checkViability(
741
741
  // booleans for unknown/not_configured. The booleans are read ONLY when
742
742
  // revocationStatus is ABSENT (idealresume.online's older boolean route).
743
743
  sessionStatus.forceInvalid = decideForceInvalid(data, revocationFailMode);
744
- sessionPointer.exists = sessionStatus.exists ?? false;
744
+ // PAY-2216: a STALE session - the cookie resolved to a token, but no store holds the session - used to set
745
+ // pointer.exists=false, so makeAuthDecision took no_session (login, cookie NOT cleared) and its stale_session
746
+ // arm (clearCookies) was unreachable: a dead cookie lingered and re-ran viability on every request.
747
+ // Stale is a POSITIVE signal and needs ALL of: this 2xx read, exists === false, a sessionTokenId (every
748
+ // producer sets it only when a cookie resolved to a token: routes/auth/viability.ts, api-handlers/session/
749
+ // viability.ts, refresh-viability.ts 'session_not_found'), and a session cookie on THIS request (plain or
750
+ // __Secure-). A FAILED read never gets here (exists=null -> service_error, PAY-2147); a store error throws
751
+ // inside every producer and answers 5xx or 'unknown' WITHOUT a sessionTokenId; no cookie -> no_session.
752
+ const stale = sessionStatus.exists === false && !!data.sessionTokenId && hasSessionCookie(request);
753
+ sessionPointer.exists = stale ? true : (sessionStatus.exists ?? false);
745
754
  sessionPointer.sessionTokenId = data.sessionTokenId ?? null;
746
755
  sessionPointer.expired = data.accessTokenExpired || false;
747
756
  sessionPointer.hasRefreshToken = data.hasRefreshToken ?? data.canRefresh ?? false;
@@ -815,7 +824,12 @@ async function handleAllow(
815
824
  ): Promise<NextResponse> {
816
825
  const isPublic = isUnauthenticatedRoute(pathname);
817
826
 
818
- if (isRBACEnabled() && !isPublic && sessionPointer.exists) {
827
+ // PAY-2216: RBAC needs a session that viability CONFIRMED. A stale /api request (pointer.exists only says the
828
+ // cookie is present, status.exists is false) is allowed through so the route handler answers its own 401 - not an
829
+ // RBAC 401 here. TRUTHY, not `=== true` (NightHawk 71960): status.exists carries the producer's JSON value as-is,
830
+ // and a consumer route may answer authenticated: 1 / "true"; `=== true` skipped RBAC for those healthy sessions
831
+ // (fail-OPEN). Truthy keeps 4.5.3's RBAC coverage exactly and still excludes stale (false) and a failed read (null).
832
+ if (isRBACEnabled() && !isPublic && sessionPointer.exists && sessionStatus.exists) {
819
833
  // Skip RBAC for error/fallback pages (prevent redirect loops) and app-configured exempt paths
820
834
  if (RBAC_EXEMPT_PATHS.some(p => pathname.startsWith(p)) ||
821
835
  rbacExemptPaths.some(p => pathname.startsWith(p))) {