@payez/next-mvp 4.5.1 → 4.5.2
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.
- package/dist/auth/auth-decision.d.ts +5 -0
- package/dist/auth/auth-decision.js +12 -0
- package/dist/lib/idp-fetch.d.ts +8 -1
- package/dist/lib/idp-fetch.js +5 -2
- package/dist/lib/retry-after.d.ts +10 -0
- package/dist/lib/retry-after.js +30 -0
- package/dist/lib/two-factor-verify-failure.d.ts +8 -0
- package/dist/lib/two-factor-verify-failure.js +35 -3
- package/package.json +1042 -1042
- package/scripts/check-publish-health.mjs +12 -3
- package/scripts/publish-health-tag.mjs +27 -0
- package/src/auth/auth-decision.ts +15 -0
- package/src/lib/idp-fetch.ts +10 -2
- package/src/lib/retry-after.ts +24 -0
- package/src/lib/two-factor-verify-failure.ts +277 -248
|
@@ -39,5 +39,10 @@ export type AuthAction = {
|
|
|
39
39
|
* Main decision function - pure, testable, deterministic.
|
|
40
40
|
* PAY-2136: when `context.isPrefetch` is set, no redirect it returns asks for cookies to be cleared; the
|
|
41
41
|
* redirect itself is kept, and the next real navigation reaches the same decision and clears then.
|
|
42
|
+
*
|
|
43
|
+
* @deprecated PAY-2180: this exported engine is NOT the one the middleware runs. `createMvpMiddleware` decides with
|
|
44
|
+
* its own private engine (middleware/create-middleware.ts), so calling this answers a question nothing enforces.
|
|
45
|
+
* A sweep found no code consumer in the portal, idealvibe, idealresume or cryptaply. It will be REMOVED in the next
|
|
46
|
+
* major version, together with its re-exports from the package root and `/edge`. Use `createMvpMiddleware`.
|
|
42
47
|
*/
|
|
43
48
|
export declare function makeAuthDecision(context: AuthContext): AuthAction;
|
|
@@ -9,12 +9,24 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
9
9
|
exports.makeAuthDecision = makeAuthDecision;
|
|
10
10
|
const safe_callback_1 = require("./safe-callback");
|
|
11
11
|
const token_log_id_1 = require("../lib/token-log-id");
|
|
12
|
+
/** PAY-2180: warn once per process, not per request. */
|
|
13
|
+
let deprecationWarned = false;
|
|
12
14
|
/**
|
|
13
15
|
* Main decision function - pure, testable, deterministic.
|
|
14
16
|
* PAY-2136: when `context.isPrefetch` is set, no redirect it returns asks for cookies to be cleared; the
|
|
15
17
|
* redirect itself is kept, and the next real navigation reaches the same decision and clears then.
|
|
18
|
+
*
|
|
19
|
+
* @deprecated PAY-2180: this exported engine is NOT the one the middleware runs. `createMvpMiddleware` decides with
|
|
20
|
+
* its own private engine (middleware/create-middleware.ts), so calling this answers a question nothing enforces.
|
|
21
|
+
* A sweep found no code consumer in the portal, idealvibe, idealresume or cryptaply. It will be REMOVED in the next
|
|
22
|
+
* major version, together with its re-exports from the package root and `/edge`. Use `createMvpMiddleware`.
|
|
16
23
|
*/
|
|
17
24
|
function makeAuthDecision(context) {
|
|
25
|
+
if (!deprecationWarned) {
|
|
26
|
+
deprecationWarned = true;
|
|
27
|
+
console.warn('[next-mvp] makeAuthDecision (auth/auth-decision) is deprecated (PAY-2180) and will be removed in the next major: ' +
|
|
28
|
+
'createMvpMiddleware does not use it. Use createMvpMiddleware.');
|
|
29
|
+
}
|
|
18
30
|
const action = decide(context);
|
|
19
31
|
if (context.isPrefetch && action.type === 'redirect' && action.clearCookies) {
|
|
20
32
|
const { clearCookies: _dropped, ...rest } = action;
|
package/dist/lib/idp-fetch.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { NextRequest } from 'next/server';
|
|
2
2
|
import { assertAbsoluteIdpUrl } from './idp-url-guard';
|
|
3
|
-
|
|
3
|
+
import { parseRetryAfterSeconds } from './retry-after';
|
|
4
|
+
export { assertAbsoluteIdpUrl, parseRetryAfterSeconds };
|
|
4
5
|
/**
|
|
5
6
|
* Centralized IDP fetch helper
|
|
6
7
|
* - Injects Bearer from Redis session
|
|
@@ -12,5 +13,11 @@ export interface IdpFetchResult<T = any> {
|
|
|
12
13
|
status: number;
|
|
13
14
|
json: T | null;
|
|
14
15
|
attemptedRefresh: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* PAY-1608: the upstream Retry-After in whole seconds, when the IDP sent a usable one. Core sends it on every
|
|
18
|
+
* 2FA lock (MfaController.SecondFactorLocked: 429 + Retry-After 900; VerifyTotp: what is left of the lock).
|
|
19
|
+
* It used to be dropped here, so no BFF could state the real lock window. Absent otherwise.
|
|
20
|
+
*/
|
|
21
|
+
retryAfterSeconds?: number;
|
|
15
22
|
}
|
|
16
23
|
export declare function idpFetchJSON<T = any>(req: NextRequest, targetUrl: string, init?: RequestInit): Promise<IdpFetchResult<T>>;
|
package/dist/lib/idp-fetch.js
CHANGED
|
@@ -33,13 +33,15 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.assertAbsoluteIdpUrl = void 0;
|
|
36
|
+
exports.parseRetryAfterSeconds = exports.assertAbsoluteIdpUrl = void 0;
|
|
37
37
|
exports.idpFetchJSON = idpFetchJSON;
|
|
38
38
|
const test_aware_get_token_1 = require("./test-aware-get-token");
|
|
39
39
|
const session_store_1 = require("./session-store");
|
|
40
40
|
const internal_api_1 = require("./internal-api");
|
|
41
41
|
const idp_url_guard_1 = require("./idp-url-guard");
|
|
42
42
|
Object.defineProperty(exports, "assertAbsoluteIdpUrl", { enumerable: true, get: function () { return idp_url_guard_1.assertAbsoluteIdpUrl; } });
|
|
43
|
+
const retry_after_1 = require("./retry-after");
|
|
44
|
+
Object.defineProperty(exports, "parseRetryAfterSeconds", { enumerable: true, get: function () { return retry_after_1.parseRetryAfterSeconds; } });
|
|
43
45
|
function buildHeaders(req, bearer, extra) {
|
|
44
46
|
const headers = {
|
|
45
47
|
Accept: 'application/json',
|
|
@@ -177,5 +179,6 @@ async function idpFetchJSON(req, targetUrl, init = {}) {
|
|
|
177
179
|
}
|
|
178
180
|
catch { }
|
|
179
181
|
}
|
|
180
|
-
|
|
182
|
+
const retryAfterSeconds = (0, retry_after_1.parseRetryAfterSeconds)(res.headers?.get?.('Retry-After'));
|
|
183
|
+
return { ok: res.ok, status: res.status, json: json, attemptedRefresh, ...(retryAfterSeconds ? { retryAfterSeconds } : {}) };
|
|
181
184
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PAY-1608: parse an HTTP Retry-After header to whole seconds.
|
|
3
|
+
*
|
|
4
|
+
* Accepts delta-seconds ("900") and an HTTP date in the future. Answers undefined for absent, zero, negative,
|
|
5
|
+
* fractional, past or malformed values - a caller that gets undefined states no window rather than a guessed one.
|
|
6
|
+
*
|
|
7
|
+
* A dependency-free leaf on purpose: idp-fetch re-exports it, but a test can load it without the session/redis
|
|
8
|
+
* graph idp-fetch pulls in.
|
|
9
|
+
*/
|
|
10
|
+
export declare function parseRetryAfterSeconds(value: string | null | undefined, nowMs?: number): number | undefined;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseRetryAfterSeconds = parseRetryAfterSeconds;
|
|
4
|
+
/**
|
|
5
|
+
* PAY-1608: parse an HTTP Retry-After header to whole seconds.
|
|
6
|
+
*
|
|
7
|
+
* Accepts delta-seconds ("900") and an HTTP date in the future. Answers undefined for absent, zero, negative,
|
|
8
|
+
* fractional, past or malformed values - a caller that gets undefined states no window rather than a guessed one.
|
|
9
|
+
*
|
|
10
|
+
* A dependency-free leaf on purpose: idp-fetch re-exports it, but a test can load it without the session/redis
|
|
11
|
+
* graph idp-fetch pulls in.
|
|
12
|
+
*/
|
|
13
|
+
function parseRetryAfterSeconds(value, nowMs = Date.now()) {
|
|
14
|
+
if (typeof value !== 'string')
|
|
15
|
+
return undefined;
|
|
16
|
+
const v = value.trim();
|
|
17
|
+
if (/^\d+$/.test(v)) {
|
|
18
|
+
const n = parseInt(v, 10);
|
|
19
|
+
return n > 0 ? n : undefined;
|
|
20
|
+
}
|
|
21
|
+
// Only something that looks like an HTTP date (it has letters: "Sat, 10 Oct 2026 12:01:30 GMT") is parsed as
|
|
22
|
+
// one; "-5" and "1.5" must not reach Date.parse, which would read them as years.
|
|
23
|
+
if (!/[a-z]/i.test(v))
|
|
24
|
+
return undefined;
|
|
25
|
+
const at = Date.parse(v);
|
|
26
|
+
if (!Number.isFinite(at))
|
|
27
|
+
return undefined;
|
|
28
|
+
const secs = Math.ceil((at - nowMs) / 1000);
|
|
29
|
+
return secs > 0 ? secs : undefined;
|
|
30
|
+
}
|
|
@@ -56,10 +56,18 @@ export declare function stepUpRefusal(status: number, json: unknown): StepUpRefu
|
|
|
56
56
|
*/
|
|
57
57
|
export declare function upstreamErrorTexts(json: unknown): string[];
|
|
58
58
|
export declare function classifyTwoFactorVerifyFailure(status: number, json: unknown): TwoFactorVerifyFailureCode;
|
|
59
|
+
/**
|
|
60
|
+
* PAY-1608: the window as words - "15 minutes", "1 minute", "45 seconds", "2 hours". Rounded UP, so the user is never
|
|
61
|
+
* told a shorter wait than the IDP enforces. Undefined in, undefined out.
|
|
62
|
+
*/
|
|
63
|
+
export declare function formatLockWindow(seconds: number | undefined): string | undefined;
|
|
64
|
+
/** PAY-1608: the LOCKED sentence with the IDP's real window, or the number-free one when none was sent. */
|
|
65
|
+
export declare function lockedText(retryAfterSeconds?: number): string;
|
|
59
66
|
export declare function twoFactorVerifyFailure(result: {
|
|
60
67
|
status: number;
|
|
61
68
|
json: unknown;
|
|
62
69
|
attemptedRefresh: boolean;
|
|
70
|
+
retryAfterSeconds?: number;
|
|
63
71
|
}, method: TwoFactorVerifyMethod): NextResponse;
|
|
64
72
|
/**
|
|
65
73
|
* PAY-1443 residual: account/send-code on the SAME verify-code screen. A send has no
|
|
@@ -4,6 +4,8 @@ exports.STEP_UP_REFUSAL_CODE = void 0;
|
|
|
4
4
|
exports.stepUpRefusal = stepUpRefusal;
|
|
5
5
|
exports.upstreamErrorTexts = upstreamErrorTexts;
|
|
6
6
|
exports.classifyTwoFactorVerifyFailure = classifyTwoFactorVerifyFailure;
|
|
7
|
+
exports.formatLockWindow = formatLockWindow;
|
|
8
|
+
exports.lockedText = lockedText;
|
|
7
9
|
exports.twoFactorVerifyFailure = twoFactorVerifyFailure;
|
|
8
10
|
exports.classifyTwoFactorSendFailure = classifyTwoFactorSendFailure;
|
|
9
11
|
exports.twoFactorSendCodeFailure = twoFactorSendCodeFailure;
|
|
@@ -33,7 +35,9 @@ const INVALID_CODE_TEXT = {
|
|
|
33
35
|
backup_code: 'Invalid backup code. Please try again.',
|
|
34
36
|
};
|
|
35
37
|
const FIXED_TEXT = {
|
|
36
|
-
|
|
38
|
+
// PAY-1608: NO NUMBER HERE. This is the sentence for a lock whose window the IDP did not send; lockedText()
|
|
39
|
+
// states the real window when Retry-After arrives. A hard-coded "15 minutes" was true only while every lock was 15.
|
|
40
|
+
LOCKED: 'Too many incorrect codes. Verification is locked for now - even a correct code is refused until the lock ends. Wait before entering another code.',
|
|
37
41
|
RATE_LIMITED: 'Too many attempts. Please wait a moment and try again.',
|
|
38
42
|
SESSION_REJECTED: 'We could not verify the code for this session. Please try again.',
|
|
39
43
|
VERIFY_UNAVAILABLE: 'We could not verify the code right now. Please try again.',
|
|
@@ -186,13 +190,41 @@ function classifyTwoFactorVerifyFailure(status, json) {
|
|
|
186
190
|
return 'VERIFY_UNAVAILABLE';
|
|
187
191
|
return 'VERIFY_FAILED';
|
|
188
192
|
}
|
|
193
|
+
/**
|
|
194
|
+
* PAY-1608: the window as words - "15 minutes", "1 minute", "45 seconds", "2 hours". Rounded UP, so the user is never
|
|
195
|
+
* told a shorter wait than the IDP enforces. Undefined in, undefined out.
|
|
196
|
+
*/
|
|
197
|
+
function formatLockWindow(seconds) {
|
|
198
|
+
if (typeof seconds !== 'number' || !Number.isFinite(seconds) || seconds <= 0)
|
|
199
|
+
return undefined;
|
|
200
|
+
const plural = (n, unit) => `${n} ${unit}${n === 1 ? '' : 's'}`;
|
|
201
|
+
if (seconds < 60)
|
|
202
|
+
return plural(Math.ceil(seconds), 'second');
|
|
203
|
+
const minutes = Math.ceil(seconds / 60);
|
|
204
|
+
if (minutes < 120)
|
|
205
|
+
return plural(minutes, 'minute');
|
|
206
|
+
return plural(Math.ceil(minutes / 60), 'hour');
|
|
207
|
+
}
|
|
208
|
+
/** PAY-1608: the LOCKED sentence with the IDP's real window, or the number-free one when none was sent. */
|
|
209
|
+
function lockedText(retryAfterSeconds) {
|
|
210
|
+
const window = formatLockWindow(retryAfterSeconds);
|
|
211
|
+
return window
|
|
212
|
+
? `Too many incorrect codes. Verification is locked for ${window} - wait before entering another code.`
|
|
213
|
+
: FIXED_TEXT.LOCKED;
|
|
214
|
+
}
|
|
189
215
|
function twoFactorVerifyFailure(result, method) {
|
|
190
216
|
const refusal = stepUpRefusal(result.status, result.json);
|
|
191
217
|
if (refusal)
|
|
192
218
|
return stepUpResponse(refusal, result.attemptedRefresh);
|
|
193
219
|
const code = classifyTwoFactorVerifyFailure(result.status, result.json);
|
|
194
|
-
const error = code === 'INVALID_CODE' ? INVALID_CODE_TEXT[method]
|
|
195
|
-
|
|
220
|
+
const error = code === 'INVALID_CODE' ? INVALID_CODE_TEXT[method]
|
|
221
|
+
: code === 'LOCKED' ? lockedText(result.retryAfterSeconds)
|
|
222
|
+
: FIXED_TEXT[code];
|
|
223
|
+
// PAY-1608: a lock or a throttle relays the IDP's window, as a number (for a client that words it itself) and as the
|
|
224
|
+
// standard header. Never on other failures, and never invented: absent upstream means absent here.
|
|
225
|
+
const wait = (code === 'LOCKED' || code === 'RATE_LIMITED') && formatLockWindow(result.retryAfterSeconds)
|
|
226
|
+
? Math.ceil(result.retryAfterSeconds) : undefined;
|
|
227
|
+
return server_1.NextResponse.json({ success: false, error, code, ...(wait ? { retryAfterSeconds: wait } : {}), meta: { attemptedRefresh: result.attemptedRefresh } }, { status: result.status, ...(wait ? { headers: { 'Retry-After': String(wait) } } : {}) });
|
|
196
228
|
}
|
|
197
229
|
const SEND_LABEL = { sms: 'text message', email: 'email' };
|
|
198
230
|
function classifyTwoFactorSendFailure(status, json) {
|