@payez/next-mvp 4.5.2 → 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.
|
@@ -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 = {
|
|
@@ -540,7 +575,16 @@ async function checkViability(request, endpoint, log, revocationFailMode) {
|
|
|
540
575
|
// booleans for unknown/not_configured. The booleans are read ONLY when
|
|
541
576
|
// revocationStatus is ABSENT (idealresume.online's older boolean route).
|
|
542
577
|
sessionStatus.forceInvalid = decideForceInvalid(data, revocationFailMode);
|
|
543
|
-
|
|
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);
|
|
544
588
|
sessionPointer.sessionTokenId = data.sessionTokenId ?? null;
|
|
545
589
|
sessionPointer.expired = data.accessTokenExpired || false;
|
|
546
590
|
sessionPointer.hasRefreshToken = data.hasRefreshToken ?? data.canRefresh ?? false;
|
|
@@ -551,12 +595,14 @@ async function checkViability(request, endpoint, log, revocationFailMode) {
|
|
|
551
595
|
log.error('Viability check failed', { status: response.status });
|
|
552
596
|
sessionStatus.exists = null;
|
|
553
597
|
sessionStatus.forceInvalid = null;
|
|
598
|
+
sessionPointer.exists = hasSessionCookie(request); // PAY-2147: see hasSessionCookie
|
|
554
599
|
}
|
|
555
600
|
}
|
|
556
601
|
catch (error) {
|
|
557
602
|
log.error('Viability check error', { error: error instanceof Error ? error.message : String(error) });
|
|
558
603
|
sessionStatus.exists = null;
|
|
559
604
|
sessionStatus.forceInvalid = null;
|
|
605
|
+
sessionPointer.exists = hasSessionCookie(request); // PAY-2147: see hasSessionCookie
|
|
560
606
|
}
|
|
561
607
|
return { sessionStatus, sessionPointer };
|
|
562
608
|
}
|
|
@@ -569,7 +615,7 @@ async function executeDecision(request, decision, pathname, sessionPointer, sess
|
|
|
569
615
|
case 'redirect':
|
|
570
616
|
return redirectTo(request, decision.location, decision.clearCookies);
|
|
571
617
|
case 'service_error':
|
|
572
|
-
return
|
|
618
|
+
return serviceUnavailableResponse(request);
|
|
573
619
|
case 'refresh_needed':
|
|
574
620
|
// PAY-2136: a speculative request must not spend (rotate) a single-use refresh token or, on a 401/403,
|
|
575
621
|
// clear the session. Let it through; the real navigation refreshes.
|
|
@@ -583,7 +629,12 @@ const RBAC_EXEMPT_PATHS = ['/error', '/unauthorized', '/service-unavailable'];
|
|
|
583
629
|
/** Handle 'allow' decision - run RBAC if enabled */
|
|
584
630
|
async function handleAllow(request, pathname, sessionPointer, sessionStatus, rbacExemptPaths = []) {
|
|
585
631
|
const isPublic = (0, route_config_1.isUnauthenticatedRoute)(pathname);
|
|
586
|
-
|
|
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) {
|
|
587
638
|
// Skip RBAC for error/fallback pages (prevent redirect loops) and app-configured exempt paths
|
|
588
639
|
if (RBAC_EXEMPT_PATHS.some(p => pathname.startsWith(p)) ||
|
|
589
640
|
rbacExemptPaths.some(p => pathname.startsWith(p))) {
|
|
@@ -629,7 +680,9 @@ async function handleRefresh(request, safeCallback, opts) {
|
|
|
629
680
|
// Check if circuit breaker allows refresh attempt
|
|
630
681
|
if (!cb.canAttemptRefresh()) {
|
|
631
682
|
log.warn('Circuit breaker preventing refresh attempt');
|
|
632
|
-
|
|
683
|
+
// PAY-2147: a breaker that merely declines to TRY a refresh has learned nothing about the session; wiping the
|
|
684
|
+
// cookies here logged out every user on the first request after a blip (HALF_OPEN refuses all but one probe).
|
|
685
|
+
return serviceUnavailableResponse(request);
|
|
633
686
|
}
|
|
634
687
|
try {
|
|
635
688
|
const baseUrl = (0, internal_api_url_1.getInternalApiUrl)(request);
|
package/package.json
CHANGED
|
@@ -103,8 +103,10 @@ function decide(context: AuthContext): AuthAction {
|
|
|
103
103
|
return {
|
|
104
104
|
type: 'redirect',
|
|
105
105
|
location: '/account-auth/login?error=ServiceUnavailable&reason=CircuitBreakerOpen',
|
|
106
|
-
reason: 'CircuitBreakerOpen'
|
|
107
|
-
|
|
106
|
+
reason: 'CircuitBreakerOpen'
|
|
107
|
+
// PAY-2147: NO clearCookies. The breaker opening is not a verdict on the session; clearing here logged out every
|
|
108
|
+
// user who made a request during a blip. (Deprecated twin: it cannot express a 503 or the /service-unavailable
|
|
109
|
+
// page - createMvpMiddleware does, see serviceUnavailableResponse.)
|
|
108
110
|
};
|
|
109
111
|
}
|
|
110
112
|
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
import { NextRequest, NextResponse } from 'next/server';
|
|
21
21
|
import { isUnauthenticatedRoute, should2FABypass } from '../auth/route-config';
|
|
22
|
-
import { resolveCallbackDestination } from '../auth/safe-callback';
|
|
22
|
+
import { resolveCallbackDestination, safeCallbackPath } from '../auth/safe-callback';
|
|
23
23
|
import { getInternalApiUrl } from '../edge/internal-api-url';
|
|
24
24
|
import { getSessionCookieName, getSecureSessionCookieName } from '../lib/app-slug';
|
|
25
25
|
import { checkPagePermission, isRBACEnabled } from './rbac-check';
|
|
@@ -290,6 +290,31 @@ function refreshFailureResponse(
|
|
|
290
290
|
return redirectTo(request, location, clearCookies);
|
|
291
291
|
}
|
|
292
292
|
|
|
293
|
+
/**
|
|
294
|
+
* PAY-2147: the terminal answer when the middleware CANNOT TELL whether the session is good
|
|
295
|
+
* (circuit breaker open, a half-open breaker refusing a refresh attempt, or the viability read failed).
|
|
296
|
+
*
|
|
297
|
+
* "I could not check" is not a verdict on the session, so this NEVER touches a cookie: a transient blip
|
|
298
|
+
* in the IdP / viability path used to wipe the session of every user who made any request while the
|
|
299
|
+
* breaker was open. Cookies are cleared only on a POSITIVE signal (force_invalidated, stale_session,
|
|
300
|
+
* pre_2fa_token_expired, refresh 401/403, explicit sign-out) - never from here.
|
|
301
|
+
*
|
|
302
|
+
* - /api/* -> 503 JSON a caller can branch on (a redirect would be followed by fetch() into a 200 HTML page).
|
|
303
|
+
* - pages -> /service-unavailable (public + allowDuringCircuitBreakerOpen, so it cannot loop), carrying the
|
|
304
|
+
* requested path as `callbackUrl` so "Try again" returns the user to it still signed in. The path goes
|
|
305
|
+
* through safeCallbackPath (URL-parser origin check): an off-origin or auth-surface value is dropped.
|
|
306
|
+
*/
|
|
307
|
+
function serviceUnavailableResponse(request: NextRequest): NextResponse {
|
|
308
|
+
const { pathname, search } = request.nextUrl;
|
|
309
|
+
if (pathname.startsWith('/api/')) {
|
|
310
|
+
return NextResponse.json({ error: 'ServiceUnavailable' }, { status: 503 });
|
|
311
|
+
}
|
|
312
|
+
const target = new URL('/service-unavailable', request.url);
|
|
313
|
+
const callback = safeCallbackPath(pathname + search);
|
|
314
|
+
if (callback !== '/') target.searchParams.set('callbackUrl', callback);
|
|
315
|
+
return NextResponse.redirect(target);
|
|
316
|
+
}
|
|
317
|
+
|
|
293
318
|
// =============================================================================
|
|
294
319
|
// AUTH DECISION ENGINE
|
|
295
320
|
// =============================================================================
|
|
@@ -334,14 +359,11 @@ function makeAuthDecision(ctx: AuthContext): AuthAction {
|
|
|
334
359
|
}
|
|
335
360
|
|
|
336
361
|
// --- Circuit breaker open: Service degraded ---
|
|
362
|
+
// PAY-2147: the breaker opening says the IdP/viability path is unreachable, NOT that the session is bad.
|
|
363
|
+
// Answer "service unavailable" (503 for /api, /service-unavailable for pages) and leave every cookie alone.
|
|
337
364
|
if (circuitBreakerOpen) {
|
|
338
365
|
if (onLoginPage) return { type: 'allow' };
|
|
339
|
-
return {
|
|
340
|
-
type: 'redirect',
|
|
341
|
-
location: `${LOGIN_PAGE}?error=ServiceUnavailable`,
|
|
342
|
-
reason: 'circuit_breaker_open',
|
|
343
|
-
clearCookies: true
|
|
344
|
-
};
|
|
366
|
+
return { type: 'service_error', reason: 'circuit_breaker_open' };
|
|
345
367
|
}
|
|
346
368
|
|
|
347
369
|
// --- No session cookie: Need to login ---
|
|
@@ -363,6 +385,7 @@ function makeAuthDecision(ctx: AuthContext): AuthAction {
|
|
|
363
385
|
|
|
364
386
|
// --- Service error: Can't verify session ---
|
|
365
387
|
if (sessionStatus.exists === null || sessionStatus.forceInvalid === null) {
|
|
388
|
+
if (onLoginPage) return { type: 'allow' }; // the login page must stay reachable while the check is down
|
|
366
389
|
return { type: 'service_error', reason: 'viability_check_failed' };
|
|
367
390
|
}
|
|
368
391
|
|
|
@@ -622,6 +645,18 @@ export function decideForceInvalid(
|
|
|
622
645
|
return data.forceInvalid === true || data.revoked === true;
|
|
623
646
|
}
|
|
624
647
|
|
|
648
|
+
/**
|
|
649
|
+
* PAY-2147: does the request carry a session cookie? Used ONLY when the viability read itself FAILED.
|
|
650
|
+
* Before this, a failed read left sessionPointer.exists=false, so the decision engine took its `no_session` arm
|
|
651
|
+
* (pages -> the LOGIN page, /api -> straight through) and its `exists === null` -> service_error arm below it was
|
|
652
|
+
* unreachable dead code: a user holding a perfectly good session was told to sign in during an outage. With a
|
|
653
|
+
* cookie present and the check down we do not know the session is bad, so the engine must say "service
|
|
654
|
+
* unavailable", not "sign in". No cookie -> unchanged (no_session).
|
|
655
|
+
*/
|
|
656
|
+
function hasSessionCookie(request: NextRequest): boolean {
|
|
657
|
+
return !!(request.cookies.get(getSessionCookieName())?.value || request.cookies.get(getSecureSessionCookieName())?.value);
|
|
658
|
+
}
|
|
659
|
+
|
|
625
660
|
/** Check session viability via internal API */
|
|
626
661
|
async function checkViability(
|
|
627
662
|
request: NextRequest,
|
|
@@ -706,7 +741,16 @@ async function checkViability(
|
|
|
706
741
|
// booleans for unknown/not_configured. The booleans are read ONLY when
|
|
707
742
|
// revocationStatus is ABSENT (idealresume.online's older boolean route).
|
|
708
743
|
sessionStatus.forceInvalid = decideForceInvalid(data, revocationFailMode);
|
|
709
|
-
|
|
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);
|
|
710
754
|
sessionPointer.sessionTokenId = data.sessionTokenId ?? null;
|
|
711
755
|
sessionPointer.expired = data.accessTokenExpired || false;
|
|
712
756
|
sessionPointer.hasRefreshToken = data.hasRefreshToken ?? data.canRefresh ?? false;
|
|
@@ -716,11 +760,13 @@ async function checkViability(
|
|
|
716
760
|
log.error('Viability check failed', { status: response.status });
|
|
717
761
|
sessionStatus.exists = null;
|
|
718
762
|
sessionStatus.forceInvalid = null;
|
|
763
|
+
sessionPointer.exists = hasSessionCookie(request); // PAY-2147: see hasSessionCookie
|
|
719
764
|
}
|
|
720
765
|
} catch (error) {
|
|
721
766
|
log.error('Viability check error', { error: error instanceof Error ? error.message : String(error) });
|
|
722
767
|
sessionStatus.exists = null;
|
|
723
768
|
sessionStatus.forceInvalid = null;
|
|
769
|
+
sessionPointer.exists = hasSessionCookie(request); // PAY-2147: see hasSessionCookie
|
|
724
770
|
}
|
|
725
771
|
|
|
726
772
|
return { sessionStatus, sessionPointer };
|
|
@@ -755,7 +801,7 @@ async function executeDecision(
|
|
|
755
801
|
return redirectTo(request, decision.location, decision.clearCookies);
|
|
756
802
|
|
|
757
803
|
case 'service_error':
|
|
758
|
-
return
|
|
804
|
+
return serviceUnavailableResponse(request);
|
|
759
805
|
|
|
760
806
|
case 'refresh_needed':
|
|
761
807
|
// PAY-2136: a speculative request must not spend (rotate) a single-use refresh token or, on a 401/403,
|
|
@@ -778,7 +824,12 @@ async function handleAllow(
|
|
|
778
824
|
): Promise<NextResponse> {
|
|
779
825
|
const isPublic = isUnauthenticatedRoute(pathname);
|
|
780
826
|
|
|
781
|
-
|
|
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) {
|
|
782
833
|
// Skip RBAC for error/fallback pages (prevent redirect loops) and app-configured exempt paths
|
|
783
834
|
if (RBAC_EXEMPT_PATHS.some(p => pathname.startsWith(p)) ||
|
|
784
835
|
rbacExemptPaths.some(p => pathname.startsWith(p))) {
|
|
@@ -843,12 +894,9 @@ async function handleRefresh(
|
|
|
843
894
|
// Check if circuit breaker allows refresh attempt
|
|
844
895
|
if (!cb.canAttemptRefresh()) {
|
|
845
896
|
log.warn('Circuit breaker preventing refresh attempt');
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
'ServiceUnavailable',
|
|
850
|
-
true
|
|
851
|
-
);
|
|
897
|
+
// PAY-2147: a breaker that merely declines to TRY a refresh has learned nothing about the session; wiping the
|
|
898
|
+
// cookies here logged out every user on the first request after a blip (HALF_OPEN refuses all but one probe).
|
|
899
|
+
return serviceUnavailableResponse(request);
|
|
852
900
|
}
|
|
853
901
|
|
|
854
902
|
try {
|