@payez/next-mvp 4.8.0 → 4.8.1
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.
|
@@ -198,16 +198,17 @@ function serviceUnavailableResponse(request, check = 'viability', unavailablePag
|
|
|
198
198
|
// callback, and no Set-Cookie (nothing here touches the session). No redirect, so no 404 is possible.
|
|
199
199
|
// `unavailablePage` is accepted only as a PLAIN path ([A-Za-z0-9_-/]; no query, fragment, %-encoding, dot segment or
|
|
200
200
|
// scheme), so the self-check below compares like with like and a mis-set value falls back to the inline page (Scout 74668).
|
|
201
|
-
//
|
|
201
|
+
// PAY-2345: the viability-FAILURE arm (500 / timeout / breaker) takes the same path, so an app with no page of its own
|
|
202
|
+
// gets the inline 503 instead of a 404 during an outage. The two arms differ only in `check` (JSON body for /api/*). If the
|
|
202
203
|
// request is already FOR the named page (an app that did not register it as public), answer inline: redirecting to
|
|
203
204
|
// it again would loop with a longer callbackUrl each time (QAPert 74651).
|
|
204
|
-
const named =
|
|
205
|
-
if (
|
|
205
|
+
const named = unavailablePage && /^\/[A-Za-z0-9_\-\/]*$/.test(unavailablePage) && !unavailablePage.includes('//') ? unavailablePage : null;
|
|
206
|
+
if (!named || pathname.replace(/\/+$/, '') === named.replace(/\/+$/, '')) {
|
|
206
207
|
const href = callback.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<');
|
|
207
208
|
return new server_1.NextResponse('<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Temporarily unavailable</title>' +
|
|
208
209
|
'<main role="status" style="font-family:system-ui,sans-serif;max-width:28rem;margin:20vh auto;padding:0 1rem;text-align:center">' +
|
|
209
210
|
'<h1 style="font-size:1.25rem">Temporarily unavailable</h1>' +
|
|
210
|
-
'<p>We could not check your session just now. You are still signed in
|
|
211
|
+
'<p>' + (check === 'revocation' ? 'We could not check your session just now. You are still signed in.' : 'We could not reach the sign-in service just now. Please try again.') + '</p>' +
|
|
211
212
|
'<p><a href="' + href + '">Try again</a></p></main>', { status: 503, headers: { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store', 'retry-after': '30' } });
|
|
212
213
|
}
|
|
213
214
|
const target = new URL(named || '/service-unavailable', request.url);
|
|
@@ -766,7 +767,7 @@ async function handleRefresh(request, safeCallback, opts) {
|
|
|
766
767
|
log.warn('Circuit breaker preventing refresh attempt');
|
|
767
768
|
// PAY-2147: a breaker that merely declines to TRY a refresh has learned nothing about the session; wiping the
|
|
768
769
|
// cookies here logged out every user on the first request after a blip (HALF_OPEN refuses all but one probe).
|
|
769
|
-
return serviceUnavailableResponse(request);
|
|
770
|
+
return serviceUnavailableResponse(request, 'viability', opts.unavailablePage);
|
|
770
771
|
}
|
|
771
772
|
try {
|
|
772
773
|
const baseUrl = (0, internal_api_url_1.getInternalApiUrl)(request);
|
package/package.json
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* PAY-2339 (BAPert 74633 gap 1): the two revocation fields every viability producer emits, in ONE place so the
|
|
3
|
-
* producers (api-handlers/session/viability.ts and routes/auth/viability.ts, four return sites) cannot drift.
|
|
4
|
-
*
|
|
5
|
-
* forceInvalid - true ONLY for 'revoked'. An 'unknown' answer is no longer a sign-out verdict.
|
|
6
|
-
* unavailable - true when the revocation backend could not answer: 'unknown' and ANY string that is not one of the
|
|
7
|
-
* three recognised states. A consumer that reads this JSON (useSessionCheck, makeAuthDecision) holds
|
|
8
|
-
* instead of signing the member out. `revocationStatus` itself is emitted unchanged.
|
|
9
|
-
*
|
|
10
|
-
* 'not_configured' is a configuration fault, not an outage: neither field is set for it (unchanged).
|
|
11
|
-
*/
|
|
12
|
-
export function revocationFields(status: unknown): { forceInvalid: boolean; unavailable: boolean; revoked: boolean } {
|
|
13
|
-
const revoked = status === 'revoked';
|
|
14
|
-
const unavailable = !(status === 'revoked' || status === 'active' || status === 'not_configured');
|
|
15
|
-
return { forceInvalid: revoked, unavailable, revoked };
|
|
16
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* PAY-2339 (BAPert 74633 gap 1): the two revocation fields every viability producer emits, in ONE place so the
|
|
3
|
+
* producers (api-handlers/session/viability.ts and routes/auth/viability.ts, four return sites) cannot drift.
|
|
4
|
+
*
|
|
5
|
+
* forceInvalid - true ONLY for 'revoked'. An 'unknown' answer is no longer a sign-out verdict.
|
|
6
|
+
* unavailable - true when the revocation backend could not answer: 'unknown' and ANY string that is not one of the
|
|
7
|
+
* three recognised states. A consumer that reads this JSON (useSessionCheck, makeAuthDecision) holds
|
|
8
|
+
* instead of signing the member out. `revocationStatus` itself is emitted unchanged.
|
|
9
|
+
*
|
|
10
|
+
* 'not_configured' is a configuration fault, not an outage: neither field is set for it (unchanged).
|
|
11
|
+
*/
|
|
12
|
+
export function revocationFields(status: unknown): { forceInvalid: boolean; unavailable: boolean; revoked: boolean } {
|
|
13
|
+
const revoked = status === 'revoked';
|
|
14
|
+
const unavailable = !(status === 'revoked' || status === 'active' || status === 'not_configured');
|
|
15
|
+
return { forceInvalid: revoked, unavailable, revoked };
|
|
16
|
+
}
|
|
@@ -381,19 +381,20 @@ function serviceUnavailableResponse(request: NextRequest, check: UnavailableChec
|
|
|
381
381
|
// PAY-2339 (BAPert 74640, P13): a HELD revocation answer needs no page in the consumer. Unless the app names its own
|
|
382
382
|
// page (`unavailablePage`), the middleware answers inline: a minimal 503 HTML, a "try again" link to the safe
|
|
383
383
|
// callback, and no Set-Cookie (nothing here touches the session). No redirect, so no 404 is possible.
|
|
384
|
-
// `unavailablePage` is accepted only as a PLAIN path ([A-Za-z0-9_-/]; no query, fragment, %-encoding, dot segment or
|
|
385
|
-
// scheme), so the self-check below compares like with like and a mis-set value falls back to the inline page (Scout 74668).
|
|
386
|
-
//
|
|
384
|
+
// `unavailablePage` is accepted only as a PLAIN path ([A-Za-z0-9_-/]; no query, fragment, %-encoding, dot segment or
|
|
385
|
+
// scheme), so the self-check below compares like with like and a mis-set value falls back to the inline page (Scout 74668).
|
|
386
|
+
// PAY-2345: the viability-FAILURE arm (500 / timeout / breaker) takes the same path, so an app with no page of its own
|
|
387
|
+
// gets the inline 503 instead of a 404 during an outage. The two arms differ only in `check` (JSON body for /api/*). If the
|
|
387
388
|
// request is already FOR the named page (an app that did not register it as public), answer inline: redirecting to
|
|
388
389
|
// it again would loop with a longer callbackUrl each time (QAPert 74651).
|
|
389
|
-
const named =
|
|
390
|
-
if (
|
|
390
|
+
const named = unavailablePage && /^\/[A-Za-z0-9_\-\/]*$/.test(unavailablePage) && !unavailablePage.includes('//') ? unavailablePage : null;
|
|
391
|
+
if (!named || pathname.replace(/\/+$/, '') === named.replace(/\/+$/, '')) {
|
|
391
392
|
const href = callback.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<');
|
|
392
393
|
return new NextResponse(
|
|
393
394
|
'<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Temporarily unavailable</title>' +
|
|
394
395
|
'<main role="status" style="font-family:system-ui,sans-serif;max-width:28rem;margin:20vh auto;padding:0 1rem;text-align:center">' +
|
|
395
396
|
'<h1 style="font-size:1.25rem">Temporarily unavailable</h1>' +
|
|
396
|
-
'<p>We could not check your session just now. You are still signed in
|
|
397
|
+
'<p>' + (check === 'revocation' ? 'We could not check your session just now. You are still signed in.' : 'We could not reach the sign-in service just now. Please try again.') + '</p>' +
|
|
397
398
|
'<p><a href="' + href + '">Try again</a></p></main>',
|
|
398
399
|
{ status: 503, headers: { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store', 'retry-after': '30' } },
|
|
399
400
|
);
|
|
@@ -1064,7 +1065,7 @@ async function handleRefresh(
|
|
|
1064
1065
|
log.warn('Circuit breaker preventing refresh attempt');
|
|
1065
1066
|
// PAY-2147: a breaker that merely declines to TRY a refresh has learned nothing about the session; wiping the
|
|
1066
1067
|
// cookies here logged out every user on the first request after a blip (HALF_OPEN refuses all but one probe).
|
|
1067
|
-
return serviceUnavailableResponse(request);
|
|
1068
|
+
return serviceUnavailableResponse(request, 'viability', opts.unavailablePage);
|
|
1068
1069
|
}
|
|
1069
1070
|
|
|
1070
1071
|
try {
|
|
@@ -1,63 +1,63 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import React, { Suspense } from 'react';
|
|
4
|
-
import { useSearchParams } from 'next/navigation';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* PAY-2339 (option C): the default "temporarily unavailable" page the middleware sends a member to when the revocation
|
|
8
|
-
* check (or the viability read) could not answer. Mount it in one line in the consuming app:
|
|
9
|
-
*
|
|
10
|
-
* // app/service-unavailable/page.tsx
|
|
11
|
-
* export { default } from '@payez/next-mvp/pages/service-unavailable';
|
|
12
|
-
*
|
|
13
|
-
* It signs nobody out and clears nothing: "Try again" goes back to `callbackUrl`, and the member is still signed in. A
|
|
14
|
-
* consumer may replace the page (and the wording) with its own at the same route.
|
|
15
|
-
*
|
|
16
|
-
* `callbackUrl` is untrusted input. Only a same-origin PATH is followed ("/dashboard"); anything else (an absolute URL,
|
|
17
|
-
* a protocol-relative "//host", a "javascript:" value, a backslash trick, nothing at all) falls back to "/".
|
|
18
|
-
*/
|
|
19
|
-
export function safeReturnPath(raw: string | null | undefined): string {
|
|
20
|
-
if (!raw || typeof raw !== 'string') return '/';
|
|
21
|
-
if (!raw.startsWith('/') || raw.startsWith('//') || raw.includes('\\')) return '/';
|
|
22
|
-
if (/[\u0000-\u001f]/.test(raw)) return '/';
|
|
23
|
-
return raw;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function ServiceUnavailableContent() {
|
|
27
|
-
const params = useSearchParams();
|
|
28
|
-
const back = safeReturnPath(params?.get('callbackUrl'));
|
|
29
|
-
return (
|
|
30
|
-
<div className="min-h-screen flex items-center justify-center p-4">
|
|
31
|
-
<div
|
|
32
|
-
role="status"
|
|
33
|
-
className="max-w-md w-full text-center rounded-xl p-8 shadow-lg border"
|
|
34
|
-
style={{
|
|
35
|
-
backgroundColor: 'var(--bg-card, #ffffff)',
|
|
36
|
-
borderColor: 'var(--border-default, #e5e7eb)',
|
|
37
|
-
}}
|
|
38
|
-
>
|
|
39
|
-
<h1 className="text-xl font-semibold mb-2" style={{ color: 'var(--text-primary, #111827)' }}>
|
|
40
|
-
Temporarily unavailable
|
|
41
|
-
</h1>
|
|
42
|
-
<p className="mb-6" style={{ color: 'var(--text-secondary, #6b7280)' }}>
|
|
43
|
-
We could not check your session just now. You are still signed in. Please try again in a moment.
|
|
44
|
-
</p>
|
|
45
|
-
<a
|
|
46
|
-
href={back}
|
|
47
|
-
className="inline-block rounded-lg px-4 py-2 font-medium"
|
|
48
|
-
style={{ background: 'var(--color-primary, #10b981)', color: 'white' }}
|
|
49
|
-
>
|
|
50
|
-
Try again
|
|
51
|
-
</a>
|
|
52
|
-
</div>
|
|
53
|
-
</div>
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export default function ServiceUnavailablePage() {
|
|
58
|
-
return (
|
|
59
|
-
<Suspense fallback={null}>
|
|
60
|
-
<ServiceUnavailableContent />
|
|
61
|
-
</Suspense>
|
|
62
|
-
);
|
|
63
|
-
}
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { Suspense } from 'react';
|
|
4
|
+
import { useSearchParams } from 'next/navigation';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* PAY-2339 (option C): the default "temporarily unavailable" page the middleware sends a member to when the revocation
|
|
8
|
+
* check (or the viability read) could not answer. Mount it in one line in the consuming app:
|
|
9
|
+
*
|
|
10
|
+
* // app/service-unavailable/page.tsx
|
|
11
|
+
* export { default } from '@payez/next-mvp/pages/service-unavailable';
|
|
12
|
+
*
|
|
13
|
+
* It signs nobody out and clears nothing: "Try again" goes back to `callbackUrl`, and the member is still signed in. A
|
|
14
|
+
* consumer may replace the page (and the wording) with its own at the same route.
|
|
15
|
+
*
|
|
16
|
+
* `callbackUrl` is untrusted input. Only a same-origin PATH is followed ("/dashboard"); anything else (an absolute URL,
|
|
17
|
+
* a protocol-relative "//host", a "javascript:" value, a backslash trick, nothing at all) falls back to "/".
|
|
18
|
+
*/
|
|
19
|
+
export function safeReturnPath(raw: string | null | undefined): string {
|
|
20
|
+
if (!raw || typeof raw !== 'string') return '/';
|
|
21
|
+
if (!raw.startsWith('/') || raw.startsWith('//') || raw.includes('\\')) return '/';
|
|
22
|
+
if (/[\u0000-\u001f]/.test(raw)) return '/';
|
|
23
|
+
return raw;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function ServiceUnavailableContent() {
|
|
27
|
+
const params = useSearchParams();
|
|
28
|
+
const back = safeReturnPath(params?.get('callbackUrl'));
|
|
29
|
+
return (
|
|
30
|
+
<div className="min-h-screen flex items-center justify-center p-4">
|
|
31
|
+
<div
|
|
32
|
+
role="status"
|
|
33
|
+
className="max-w-md w-full text-center rounded-xl p-8 shadow-lg border"
|
|
34
|
+
style={{
|
|
35
|
+
backgroundColor: 'var(--bg-card, #ffffff)',
|
|
36
|
+
borderColor: 'var(--border-default, #e5e7eb)',
|
|
37
|
+
}}
|
|
38
|
+
>
|
|
39
|
+
<h1 className="text-xl font-semibold mb-2" style={{ color: 'var(--text-primary, #111827)' }}>
|
|
40
|
+
Temporarily unavailable
|
|
41
|
+
</h1>
|
|
42
|
+
<p className="mb-6" style={{ color: 'var(--text-secondary, #6b7280)' }}>
|
|
43
|
+
We could not check your session just now. You are still signed in. Please try again in a moment.
|
|
44
|
+
</p>
|
|
45
|
+
<a
|
|
46
|
+
href={back}
|
|
47
|
+
className="inline-block rounded-lg px-4 py-2 font-medium"
|
|
48
|
+
style={{ background: 'var(--color-primary, #10b981)', color: 'white' }}
|
|
49
|
+
>
|
|
50
|
+
Try again
|
|
51
|
+
</a>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export default function ServiceUnavailablePage() {
|
|
58
|
+
return (
|
|
59
|
+
<Suspense fallback={null}>
|
|
60
|
+
<ServiceUnavailableContent />
|
|
61
|
+
</Suspense>
|
|
62
|
+
);
|
|
63
|
+
}
|