@payez/next-mvp 4.5.2 → 4.5.3
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.
|
@@ -61,8 +61,10 @@ function decide(context) {
|
|
|
61
61
|
return {
|
|
62
62
|
type: 'redirect',
|
|
63
63
|
location: '/account-auth/login?error=ServiceUnavailable&reason=CircuitBreakerOpen',
|
|
64
|
-
reason: 'CircuitBreakerOpen'
|
|
65
|
-
|
|
64
|
+
reason: 'CircuitBreakerOpen'
|
|
65
|
+
// PAY-2147: NO clearCookies. The breaker opening is not a verdict on the session; clearing here logged out every
|
|
66
|
+
// user who made a request during a blip. (Deprecated twin: it cannot express a 503 or the /service-unavailable
|
|
67
|
+
// page - createMvpMiddleware does, see serviceUnavailableResponse.)
|
|
66
68
|
};
|
|
67
69
|
}
|
|
68
70
|
// No session pointer (JWT cookie) - need to login
|
|
@@ -160,6 +160,31 @@ function refreshFailureResponse(request, location, errorCode, clearCookies = fal
|
|
|
160
160
|
}
|
|
161
161
|
return redirectTo(request, location, clearCookies);
|
|
162
162
|
}
|
|
163
|
+
/**
|
|
164
|
+
* PAY-2147: the terminal answer when the middleware CANNOT TELL whether the session is good
|
|
165
|
+
* (circuit breaker open, a half-open breaker refusing a refresh attempt, or the viability read failed).
|
|
166
|
+
*
|
|
167
|
+
* "I could not check" is not a verdict on the session, so this NEVER touches a cookie: a transient blip
|
|
168
|
+
* in the IdP / viability path used to wipe the session of every user who made any request while the
|
|
169
|
+
* breaker was open. Cookies are cleared only on a POSITIVE signal (force_invalidated, stale_session,
|
|
170
|
+
* pre_2fa_token_expired, refresh 401/403, explicit sign-out) - never from here.
|
|
171
|
+
*
|
|
172
|
+
* - /api/* -> 503 JSON a caller can branch on (a redirect would be followed by fetch() into a 200 HTML page).
|
|
173
|
+
* - pages -> /service-unavailable (public + allowDuringCircuitBreakerOpen, so it cannot loop), carrying the
|
|
174
|
+
* requested path as `callbackUrl` so "Try again" returns the user to it still signed in. The path goes
|
|
175
|
+
* through safeCallbackPath (URL-parser origin check): an off-origin or auth-surface value is dropped.
|
|
176
|
+
*/
|
|
177
|
+
function serviceUnavailableResponse(request) {
|
|
178
|
+
const { pathname, search } = request.nextUrl;
|
|
179
|
+
if (pathname.startsWith('/api/')) {
|
|
180
|
+
return server_1.NextResponse.json({ error: 'ServiceUnavailable' }, { status: 503 });
|
|
181
|
+
}
|
|
182
|
+
const target = new URL('/service-unavailable', request.url);
|
|
183
|
+
const callback = (0, safe_callback_1.safeCallbackPath)(pathname + search);
|
|
184
|
+
if (callback !== '/')
|
|
185
|
+
target.searchParams.set('callbackUrl', callback);
|
|
186
|
+
return server_1.NextResponse.redirect(target);
|
|
187
|
+
}
|
|
163
188
|
// =============================================================================
|
|
164
189
|
// AUTH DECISION ENGINE
|
|
165
190
|
// =============================================================================
|
|
@@ -198,15 +223,12 @@ function makeAuthDecision(ctx) {
|
|
|
198
223
|
return { type: 'allow' };
|
|
199
224
|
}
|
|
200
225
|
// --- Circuit breaker open: Service degraded ---
|
|
226
|
+
// PAY-2147: the breaker opening says the IdP/viability path is unreachable, NOT that the session is bad.
|
|
227
|
+
// Answer "service unavailable" (503 for /api, /service-unavailable for pages) and leave every cookie alone.
|
|
201
228
|
if (circuitBreakerOpen) {
|
|
202
229
|
if (onLoginPage)
|
|
203
230
|
return { type: 'allow' };
|
|
204
|
-
return {
|
|
205
|
-
type: 'redirect',
|
|
206
|
-
location: `${LOGIN_PAGE}?error=ServiceUnavailable`,
|
|
207
|
-
reason: 'circuit_breaker_open',
|
|
208
|
-
clearCookies: true
|
|
209
|
-
};
|
|
231
|
+
return { type: 'service_error', reason: 'circuit_breaker_open' };
|
|
210
232
|
}
|
|
211
233
|
// --- No session cookie: Need to login ---
|
|
212
234
|
if (!sessionPointer.exists) {
|
|
@@ -227,6 +249,8 @@ function makeAuthDecision(ctx) {
|
|
|
227
249
|
}
|
|
228
250
|
// --- Service error: Can't verify session ---
|
|
229
251
|
if (sessionStatus.exists === null || sessionStatus.forceInvalid === null) {
|
|
252
|
+
if (onLoginPage)
|
|
253
|
+
return { type: 'allow' }; // the login page must stay reachable while the check is down
|
|
230
254
|
return { type: 'service_error', reason: 'viability_check_failed' };
|
|
231
255
|
}
|
|
232
256
|
// --- Session force-invalidated (admin action, password change, etc.) ---
|
|
@@ -466,6 +490,17 @@ function decideForceInvalid(data, revocationFailMode) {
|
|
|
466
490
|
// No revocationStatus: an older boolean producer. Absence of both stays false.
|
|
467
491
|
return data.forceInvalid === true || data.revoked === true;
|
|
468
492
|
}
|
|
493
|
+
/**
|
|
494
|
+
* PAY-2147: does the request carry a session cookie? Used ONLY when the viability read itself FAILED.
|
|
495
|
+
* Before this, a failed read left sessionPointer.exists=false, so the decision engine took its `no_session` arm
|
|
496
|
+
* (pages -> the LOGIN page, /api -> straight through) and its `exists === null` -> service_error arm below it was
|
|
497
|
+
* unreachable dead code: a user holding a perfectly good session was told to sign in during an outage. With a
|
|
498
|
+
* cookie present and the check down we do not know the session is bad, so the engine must say "service
|
|
499
|
+
* unavailable", not "sign in". No cookie -> unchanged (no_session).
|
|
500
|
+
*/
|
|
501
|
+
function hasSessionCookie(request) {
|
|
502
|
+
return !!(request.cookies.get((0, app_slug_1.getSessionCookieName)())?.value || request.cookies.get((0, app_slug_1.getSecureSessionCookieName)())?.value);
|
|
503
|
+
}
|
|
469
504
|
/** Check session viability via internal API */
|
|
470
505
|
async function checkViability(request, endpoint, log, revocationFailMode) {
|
|
471
506
|
const sessionStatus = {
|
|
@@ -551,12 +586,14 @@ async function checkViability(request, endpoint, log, revocationFailMode) {
|
|
|
551
586
|
log.error('Viability check failed', { status: response.status });
|
|
552
587
|
sessionStatus.exists = null;
|
|
553
588
|
sessionStatus.forceInvalid = null;
|
|
589
|
+
sessionPointer.exists = hasSessionCookie(request); // PAY-2147: see hasSessionCookie
|
|
554
590
|
}
|
|
555
591
|
}
|
|
556
592
|
catch (error) {
|
|
557
593
|
log.error('Viability check error', { error: error instanceof Error ? error.message : String(error) });
|
|
558
594
|
sessionStatus.exists = null;
|
|
559
595
|
sessionStatus.forceInvalid = null;
|
|
596
|
+
sessionPointer.exists = hasSessionCookie(request); // PAY-2147: see hasSessionCookie
|
|
560
597
|
}
|
|
561
598
|
return { sessionStatus, sessionPointer };
|
|
562
599
|
}
|
|
@@ -569,7 +606,7 @@ async function executeDecision(request, decision, pathname, sessionPointer, sess
|
|
|
569
606
|
case 'redirect':
|
|
570
607
|
return redirectTo(request, decision.location, decision.clearCookies);
|
|
571
608
|
case 'service_error':
|
|
572
|
-
return
|
|
609
|
+
return serviceUnavailableResponse(request);
|
|
573
610
|
case 'refresh_needed':
|
|
574
611
|
// PAY-2136: a speculative request must not spend (rotate) a single-use refresh token or, on a 401/403,
|
|
575
612
|
// clear the session. Let it through; the real navigation refreshes.
|
|
@@ -629,7 +666,9 @@ async function handleRefresh(request, safeCallback, opts) {
|
|
|
629
666
|
// Check if circuit breaker allows refresh attempt
|
|
630
667
|
if (!cb.canAttemptRefresh()) {
|
|
631
668
|
log.warn('Circuit breaker preventing refresh attempt');
|
|
632
|
-
|
|
669
|
+
// PAY-2147: a breaker that merely declines to TRY a refresh has learned nothing about the session; wiping the
|
|
670
|
+
// cookies here logged out every user on the first request after a blip (HALF_OPEN refuses all but one probe).
|
|
671
|
+
return serviceUnavailableResponse(request);
|
|
633
672
|
}
|
|
634
673
|
try {
|
|
635
674
|
const baseUrl = (0, internal_api_url_1.getInternalApiUrl)(request);
|