@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.
@@ -575,7 +575,16 @@ async function checkViability(request, endpoint, log, revocationFailMode) {
575
575
  // booleans for unknown/not_configured. The booleans are read ONLY when
576
576
  // revocationStatus is ABSENT (idealresume.online's older boolean route).
577
577
  sessionStatus.forceInvalid = decideForceInvalid(data, revocationFailMode);
578
- sessionPointer.exists = sessionStatus.exists ?? false;
578
+ // PAY-2216: a STALE session - the cookie resolved to a token, but no store holds the session - used to set
579
+ // pointer.exists=false, so makeAuthDecision took no_session (login, cookie NOT cleared) and its stale_session
580
+ // arm (clearCookies) was unreachable: a dead cookie lingered and re-ran viability on every request.
581
+ // Stale is a POSITIVE signal and needs ALL of: this 2xx read, exists === false, a sessionTokenId (every
582
+ // producer sets it only when a cookie resolved to a token: routes/auth/viability.ts, api-handlers/session/
583
+ // viability.ts, refresh-viability.ts 'session_not_found'), and a session cookie on THIS request (plain or
584
+ // __Secure-). A FAILED read never gets here (exists=null -> service_error, PAY-2147); a store error throws
585
+ // inside every producer and answers 5xx or 'unknown' WITHOUT a sessionTokenId; no cookie -> no_session.
586
+ const stale = sessionStatus.exists === false && !!data.sessionTokenId && hasSessionCookie(request);
587
+ sessionPointer.exists = stale ? true : (sessionStatus.exists ?? false);
579
588
  sessionPointer.sessionTokenId = data.sessionTokenId ?? null;
580
589
  sessionPointer.expired = data.accessTokenExpired || false;
581
590
  sessionPointer.hasRefreshToken = data.hasRefreshToken ?? data.canRefresh ?? false;
@@ -620,7 +629,12 @@ const RBAC_EXEMPT_PATHS = ['/error', '/unauthorized', '/service-unavailable'];
620
629
  /** Handle 'allow' decision - run RBAC if enabled */
621
630
  async function handleAllow(request, pathname, sessionPointer, sessionStatus, rbacExemptPaths = []) {
622
631
  const isPublic = (0, route_config_1.isUnauthenticatedRoute)(pathname);
623
- if ((0, rbac_check_1.isRBACEnabled)() && !isPublic && sessionPointer.exists) {
632
+ // PAY-2216: RBAC needs a session that viability CONFIRMED. A stale /api request (pointer.exists only says the
633
+ // cookie is present, status.exists is false) is allowed through so the route handler answers its own 401 - not an
634
+ // RBAC 401 here. TRUTHY, not `=== true` (NightHawk 71960): status.exists carries the producer's JSON value as-is,
635
+ // and a consumer route may answer authenticated: 1 / "true"; `=== true` skipped RBAC for those healthy sessions
636
+ // (fail-OPEN). Truthy keeps 4.5.3's RBAC coverage exactly and still excludes stale (false) and a failed read (null).
637
+ if ((0, rbac_check_1.isRBACEnabled)() && !isPublic && sessionPointer.exists && sessionStatus.exists) {
624
638
  // Skip RBAC for error/fallback pages (prevent redirect loops) and app-configured exempt paths
625
639
  if (RBAC_EXEMPT_PATHS.some(p => pathname.startsWith(p)) ||
626
640
  rbacExemptPaths.some(p => pathname.startsWith(p))) {