@payez/next-mvp 4.13.0 → 4.14.0
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/CHANGELOG.md +21 -0
- package/dist/api-handlers/account/profile.d.ts +2 -39
- package/dist/api-handlers/account/profile.js +5 -2
- package/dist/api-handlers/anon/preferences.d.ts +1 -14
- package/dist/api-handlers/anon/preferences.js +5 -5
- package/dist/api-handlers/auth/credential-login.js +46 -0
- package/dist/api-handlers/health.d.ts +5 -0
- package/dist/api-handlers/health.js +48 -0
- package/dist/api-handlers/mfa/first-enrol.d.ts +5 -0
- package/dist/api-handlers/mfa/first-enrol.js +58 -0
- package/dist/api-handlers/session/refresh-viability.d.ts +3 -15
- package/dist/api-handlers/session/refresh-viability.js +5 -2
- package/dist/api-handlers/session/viability.d.ts +3 -8
- package/dist/api-handlers/session/viability.js +5 -2
- package/dist/auth/better-auth-exposure.d.ts +11 -0
- package/dist/auth/better-auth-exposure.js +73 -0
- package/dist/auth/better-auth.js +13 -1
- package/dist/auth/route-config.js +9 -0
- package/dist/components/auth/CredentialSignInForm.d.ts +6 -1
- package/dist/components/auth/CredentialSignInForm.js +15 -1
- package/dist/components/mfa/FirstEnrolPanel.d.ts +21 -0
- package/dist/components/mfa/FirstEnrolPanel.js +115 -0
- package/dist/lib/credential-sign-in-client.d.ts +4 -0
- package/dist/lib/credential-sign-in-client.js +13 -1
- package/dist/lib/first-enrol.d.ts +66 -0
- package/dist/lib/first-enrol.js +188 -0
- package/dist/lib/no-store.d.ts +13 -0
- package/dist/lib/no-store.js +35 -0
- package/dist/pages/first-enroll/page.d.ts +7 -0
- package/dist/pages/first-enroll/page.js +15 -0
- package/dist/routes/auth/session.d.ts +2 -9
- package/dist/routes/auth/session.js +5 -2
- package/dist/routes/auth/viability.d.ts +3 -54
- package/dist/routes/auth/viability.js +5 -2
- package/dist/routes/health.d.ts +1 -0
- package/dist/routes/health.js +16 -0
- package/dist/routes/mfa/first-enrol-confirm.d.ts +10 -0
- package/dist/routes/mfa/first-enrol-confirm.js +14 -0
- package/dist/routes/mfa/first-enrol-enroll.d.ts +10 -0
- package/dist/routes/mfa/first-enrol-enroll.js +14 -0
- package/dist/routes/mfa/methods.d.ts +2 -2
- package/dist/routes/mfa/methods.js +3 -1
- package/dist/routes/passkeys/list.d.ts +2 -10
- package/dist/routes/passkeys/list.js +4 -2
- package/dist/routes/session/refresh-viability.d.ts +2 -1
- package/dist/routes/session/refresh-viability.js +4 -2
- package/dist/utils/api.d.ts +33 -6
- package/dist/utils/api.js +35 -2
- package/package.json +26 -1
- package/src/api-handlers/account/profile.ts +5 -1
- package/src/api-handlers/anon/preferences.ts +5 -4
- package/src/api-handlers/auth/credential-login.ts +49 -0
- package/src/api-handlers/health.ts +53 -0
- package/src/api-handlers/mfa/first-enrol.ts +55 -0
- package/src/api-handlers/session/refresh-viability.ts +5 -1
- package/src/api-handlers/session/viability.ts +6 -2
- package/src/auth/better-auth-exposure.ts +69 -0
- package/src/auth/better-auth.ts +15 -1
- package/src/auth/route-config.ts +9 -0
- package/src/components/auth/CredentialSignInForm.tsx +18 -1
- package/src/components/mfa/FirstEnrolPanel.tsx +189 -0
- package/src/lib/credential-sign-in-client.ts +13 -1
- package/src/lib/first-enrol.ts +180 -0
- package/src/lib/no-store.ts +30 -0
- package/src/pages/first-enroll/page.tsx +19 -0
- package/src/routes/auth/session.ts +5 -1
- package/src/routes/auth/viability.ts +6 -2
- package/src/routes/health.ts +14 -0
- package/src/routes/mfa/first-enrol-confirm.ts +10 -0
- package/src/routes/mfa/first-enrol-enroll.ts +10 -0
- package/src/routes/mfa/methods.ts +3 -1
- package/src/routes/passkeys/list.ts +5 -1
- package/src/routes/session/refresh-viability.ts +5 -1
- package/src/utils/api.ts +50 -8
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* @since auth-ready-v2
|
|
16
16
|
*/
|
|
17
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
-
exports.GET =
|
|
18
|
+
exports.GET = void 0;
|
|
19
19
|
const revocation_fields_1 = require("../../auth/revocation-fields");
|
|
20
20
|
const server_1 = require("next/server");
|
|
21
21
|
const auth_1 = require("../../server/auth");
|
|
@@ -23,6 +23,7 @@ const session_store_1 = require("../../lib/session-store");
|
|
|
23
23
|
const idp_client_config_1 = require("../../lib/idp-client-config");
|
|
24
24
|
const safe_error_response_1 = require("../../lib/safe-error-response");
|
|
25
25
|
const token_log_id_1 = require("../../lib/token-log-id");
|
|
26
|
+
const no_store_1 = require("../../lib/no-store");
|
|
26
27
|
const sessions_1 = require("../../vibe/sessions");
|
|
27
28
|
/**
|
|
28
29
|
* Get tenant-wide 2FA requirement from cached client config (from broker handshake)
|
|
@@ -135,7 +136,7 @@ function resolveClientId(sessionClientId) {
|
|
|
135
136
|
* - needsRefresh: boolean - Whether a refresh is recommended
|
|
136
137
|
* - expiresIn: number - Seconds until token expires
|
|
137
138
|
*/
|
|
138
|
-
async function
|
|
139
|
+
async function getHandler(req) {
|
|
139
140
|
try {
|
|
140
141
|
// PAY-2216: ask for the session's STATE, not a session-or-null. A null used to cover a store outage, a deleted
|
|
141
142
|
// session and no cookie alike, so this route answered 'No session found' to all three and the middleware could
|
|
@@ -495,3 +496,5 @@ async function GET(req) {
|
|
|
495
496
|
}, { status: 500 });
|
|
496
497
|
}
|
|
497
498
|
}
|
|
499
|
+
/** PAY-2994: session state is per-user and some answers carry tokens; never cached (noStore). */
|
|
500
|
+
exports.GET = (0, no_store_1.noStore)(getHandler);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const GET: () => Promise<Response>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GET = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Ready-to-use /api/health route: reports Redis reachability (PAY-2804).
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* // app/api/health/route.ts
|
|
10
|
+
* export const runtime = 'nodejs';
|
|
11
|
+
* export { GET } from '@payez/next-mvp/routes/health';
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
const health_1 = require("../api-handlers/health");
|
|
15
|
+
const redis_1 = require("../lib/redis");
|
|
16
|
+
exports.GET = (0, health_1.createHealthHandler)(() => (0, redis_1.getRedis)());
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AUTH-045 first-enrolment: confirm the first authenticator code, clear the enrolment cookie, return the recovery codes. Mints no session.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```typescript
|
|
6
|
+
* // app/api/auth/first-enrol/confirm/route.ts
|
|
7
|
+
* export { POST } from '@payez/next-mvp/routes/mfa/first-enrol-confirm';
|
|
8
|
+
* ```
|
|
9
|
+
*/
|
|
10
|
+
export { POST_CONFIRM as POST } from '../../api-handlers/mfa/first-enrol';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.POST = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* AUTH-045 first-enrolment: confirm the first authenticator code, clear the enrolment cookie, return the recovery codes. Mints no session.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* // app/api/auth/first-enrol/confirm/route.ts
|
|
10
|
+
* export { POST } from '@payez/next-mvp/routes/mfa/first-enrol-confirm';
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
var first_enrol_1 = require("../../api-handlers/mfa/first-enrol");
|
|
14
|
+
Object.defineProperty(exports, "POST", { enumerable: true, get: function () { return first_enrol_1.POST_CONFIRM; } });
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AUTH-045 first-enrolment: begin TOTP enrolment for a user the IdP told to enrol (uses ONLY the httpOnly enrolment cookie the credential door parked).
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```typescript
|
|
6
|
+
* // app/api/auth/first-enrol/enroll/route.ts
|
|
7
|
+
* export { POST } from '@payez/next-mvp/routes/mfa/first-enrol-enroll';
|
|
8
|
+
* ```
|
|
9
|
+
*/
|
|
10
|
+
export { POST_ENROLL as POST } from '../../api-handlers/mfa/first-enrol';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.POST = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* AUTH-045 first-enrolment: begin TOTP enrolment for a user the IdP told to enrol (uses ONLY the httpOnly enrolment cookie the credential door parked).
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* // app/api/auth/first-enrol/enroll/route.ts
|
|
10
|
+
* export { POST } from '@payez/next-mvp/routes/mfa/first-enrol-enroll';
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
var first_enrol_1 = require("../../api-handlers/mfa/first-enrol");
|
|
14
|
+
Object.defineProperty(exports, "POST", { enumerable: true, get: function () { return first_enrol_1.POST_ENROLL; } });
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
* ```
|
|
11
11
|
*/
|
|
12
12
|
import { NextRequest } from 'next/server';
|
|
13
|
-
|
|
14
|
-
export declare const GET:
|
|
13
|
+
/** PAY-2994: the user's MFA methods; never cached (noStore). */
|
|
14
|
+
export declare const GET: (req: NextRequest) => Promise<Response>;
|
|
15
15
|
type Ctx = {
|
|
16
16
|
params: Promise<{
|
|
17
17
|
id: string;
|
|
@@ -3,7 +3,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.GET = void 0;
|
|
4
4
|
exports.DELETE = DELETE;
|
|
5
5
|
const methods_1 = require("../../api-handlers/mfa/methods");
|
|
6
|
-
|
|
6
|
+
const no_store_1 = require("../../lib/no-store");
|
|
7
|
+
/** PAY-2994: the user's MFA methods; never cached (noStore). */
|
|
8
|
+
exports.GET = (0, no_store_1.noStore)(methods_1.GET);
|
|
7
9
|
async function DELETE(req, ctx) {
|
|
8
10
|
const { id } = await ctx.params;
|
|
9
11
|
return (0, methods_1.DELETE)(req, id);
|
|
@@ -1,10 +1,2 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
* @example
|
|
5
|
-
* ```typescript
|
|
6
|
-
* // app/api/passkeys/route.ts
|
|
7
|
-
* export { GET } from '@payez/next-mvp/routes/passkeys/list';
|
|
8
|
-
* ```
|
|
9
|
-
*/
|
|
10
|
-
export { GET } from '../../api-handlers/passkeys/list';
|
|
1
|
+
/** PAY-2994: the signed-in user's passkeys; never cached (noStore). */
|
|
2
|
+
export declare const GET: (req: import("next/server").NextRequest) => Promise<Response>;
|
|
@@ -10,5 +10,7 @@ exports.GET = void 0;
|
|
|
10
10
|
* export { GET } from '@payez/next-mvp/routes/passkeys/list';
|
|
11
11
|
* ```
|
|
12
12
|
*/
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
const list_1 = require("../../api-handlers/passkeys/list");
|
|
14
|
+
const no_store_1 = require("../../lib/no-store");
|
|
15
|
+
/** PAY-2994: the signed-in user's passkeys; never cached (noStore). */
|
|
16
|
+
exports.GET = (0, no_store_1.noStore)(list_1.GET);
|
|
@@ -13,4 +13,5 @@
|
|
|
13
13
|
* @version 2.0.0
|
|
14
14
|
* @since auth-ready-v2
|
|
15
15
|
*/
|
|
16
|
-
|
|
16
|
+
/** PAY-2994: refresh-token viability of this session; never cached (noStore). */
|
|
17
|
+
export declare const GET: (req: import("next/server").NextRequest) => Promise<Response>;
|
|
@@ -16,5 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
18
|
exports.GET = void 0;
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
const refresh_viability_1 = require("../../api-handlers/session/refresh-viability");
|
|
20
|
+
const no_store_1 = require("../../lib/no-store");
|
|
21
|
+
/** PAY-2994: refresh-token viability of this session; never cached (noStore). */
|
|
22
|
+
exports.GET = (0, no_store_1.noStore)(refresh_viability_1.GET);
|
package/dist/utils/api.d.ts
CHANGED
|
@@ -49,6 +49,38 @@ export declare const RESET_BREACHED_TEXT = "This password appears in a known dat
|
|
|
49
49
|
* they can retry.
|
|
50
50
|
*/
|
|
51
51
|
export declare function resetFailureText(status: number, rules?: string[], policy?: boolean, tokenInvalid?: boolean): string;
|
|
52
|
+
/**
|
|
53
|
+
* SMS-first sign-up (door 3b): when the phone code is right but the email is still unconfirmed, the server answers 200 success with
|
|
54
|
+
* `next_step = EMAIL_CONFIRMATION_REQUIRED` and NO reset token. `verifyOnboardingSmsCode` surfaces that as `nextStep`;
|
|
55
|
+
* `interpretSmsVerification` turns a result into ONE outcome, testing `nextStep` BEFORE `success`, so a consumer that uses it can
|
|
56
|
+
* never reach "verified" without a reset token or with an unfinished step. The sentences are the package's own, never the server's text.
|
|
57
|
+
*/
|
|
58
|
+
export declare const SMS_NEXT_STEP_EMAIL_CONFIRMATION_REQUIRED = "EMAIL_CONFIRMATION_REQUIRED";
|
|
59
|
+
export declare const SMS_EMAIL_CONFIRMATION_TEXT = "Your phone is verified. Confirm your email to finish signing up.";
|
|
60
|
+
export declare const SMS_MORE_STEPS_TEXT = "One more step is needed to finish signing up.";
|
|
61
|
+
export interface SmsCodeVerification {
|
|
62
|
+
success: boolean;
|
|
63
|
+
resetToken: string;
|
|
64
|
+
email: string;
|
|
65
|
+
clientKey: string;
|
|
66
|
+
/** Present when the server says more steps remain (an UNRECOGNISED value is kept as a non-empty string, never dropped). */
|
|
67
|
+
nextStep?: string;
|
|
68
|
+
}
|
|
69
|
+
export type SmsVerificationOutcome = {
|
|
70
|
+
kind: 'verified';
|
|
71
|
+
resetToken: string;
|
|
72
|
+
email: string;
|
|
73
|
+
clientKey: string;
|
|
74
|
+
} | {
|
|
75
|
+
kind: 'email_confirmation_required';
|
|
76
|
+
message: string;
|
|
77
|
+
} | {
|
|
78
|
+
kind: 'more_steps';
|
|
79
|
+
message: string;
|
|
80
|
+
} | {
|
|
81
|
+
kind: 'not_verified';
|
|
82
|
+
};
|
|
83
|
+
export declare function interpretSmsVerification(result: SmsCodeVerification): SmsVerificationOutcome;
|
|
52
84
|
export declare class AccountApi {
|
|
53
85
|
/**
|
|
54
86
|
* Get masked user info (email, phone, authenticator status)
|
|
@@ -90,12 +122,7 @@ export declare class AccountApi {
|
|
|
90
122
|
email: string;
|
|
91
123
|
clientKey: string;
|
|
92
124
|
}>;
|
|
93
|
-
verifyOnboardingSmsCode(onboardToken: string, code: string, clientKey?: string): Promise<
|
|
94
|
-
success: boolean;
|
|
95
|
-
resetToken: string;
|
|
96
|
-
email: string;
|
|
97
|
-
clientKey: string;
|
|
98
|
-
}>;
|
|
125
|
+
verifyOnboardingSmsCode(onboardToken: string, code: string, clientKey?: string): Promise<SmsCodeVerification>;
|
|
99
126
|
setOnboardingPassword(data: {
|
|
100
127
|
onboardToken: string;
|
|
101
128
|
email: string;
|
package/dist/utils/api.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.accountApi = exports.AccountApi = exports.RESET_BREACHED_TEXT = exports.RESET_NEUTRAL_TEXT = exports.RECOVERY_EXPIRED_TEXT = exports.ApiError = void 0;
|
|
3
|
+
exports.accountApi = exports.AccountApi = exports.SMS_MORE_STEPS_TEXT = exports.SMS_EMAIL_CONFIRMATION_TEXT = exports.SMS_NEXT_STEP_EMAIL_CONFIRMATION_REQUIRED = exports.RESET_BREACHED_TEXT = exports.RESET_NEUTRAL_TEXT = exports.RECOVERY_EXPIRED_TEXT = exports.ApiError = void 0;
|
|
4
4
|
exports.apiFetch = apiFetch;
|
|
5
5
|
exports.resetFailureText = resetFailureText;
|
|
6
|
+
exports.interpretSmsVerification = interpretSmsVerification;
|
|
6
7
|
const standardized_client_api_1 = require("../lib/standardized-client-api");
|
|
7
8
|
const password_rules_1 = require("../lib/password-rules");
|
|
8
9
|
class ApiError extends Error {
|
|
@@ -110,6 +111,30 @@ function resetFailureText(status, rules = [], policy = false, tokenInvalid = fal
|
|
|
110
111
|
}
|
|
111
112
|
return "We couldn't reset your password just now. Please try again.";
|
|
112
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* SMS-first sign-up (door 3b): when the phone code is right but the email is still unconfirmed, the server answers 200 success with
|
|
116
|
+
* `next_step = EMAIL_CONFIRMATION_REQUIRED` and NO reset token. `verifyOnboardingSmsCode` surfaces that as `nextStep`;
|
|
117
|
+
* `interpretSmsVerification` turns a result into ONE outcome, testing `nextStep` BEFORE `success`, so a consumer that uses it can
|
|
118
|
+
* never reach "verified" without a reset token or with an unfinished step. The sentences are the package's own, never the server's text.
|
|
119
|
+
*/
|
|
120
|
+
exports.SMS_NEXT_STEP_EMAIL_CONFIRMATION_REQUIRED = 'EMAIL_CONFIRMATION_REQUIRED';
|
|
121
|
+
exports.SMS_EMAIL_CONFIRMATION_TEXT = 'Your phone is verified. Confirm your email to finish signing up.';
|
|
122
|
+
exports.SMS_MORE_STEPS_TEXT = 'One more step is needed to finish signing up.';
|
|
123
|
+
function interpretSmsVerification(result) {
|
|
124
|
+
// success:false WINS over any step (BAPert 88508): a failed answer must never say "Your phone is verified".
|
|
125
|
+
if (!result.success)
|
|
126
|
+
return { kind: 'not_verified' };
|
|
127
|
+
// For every answer that is not a failure, nextStep comes BEFORE the token test: a step that is not finished is never "verified".
|
|
128
|
+
if (result.nextStep) {
|
|
129
|
+
return result.nextStep === exports.SMS_NEXT_STEP_EMAIL_CONFIRMATION_REQUIRED
|
|
130
|
+
? { kind: 'email_confirmation_required', message: exports.SMS_EMAIL_CONFIRMATION_TEXT }
|
|
131
|
+
: { kind: 'more_steps', message: exports.SMS_MORE_STEPS_TEXT };
|
|
132
|
+
}
|
|
133
|
+
// success with an EMPTY reset token is the shape that is dangerous (set-password would be called with ''): not verified.
|
|
134
|
+
if (!result.resetToken)
|
|
135
|
+
return { kind: 'more_steps', message: exports.SMS_MORE_STEPS_TEXT };
|
|
136
|
+
return { kind: 'verified', resetToken: result.resetToken, email: result.email, clientKey: result.clientKey };
|
|
137
|
+
}
|
|
113
138
|
class AccountApi {
|
|
114
139
|
/**
|
|
115
140
|
* Get masked user info (email, phone, authenticator status)
|
|
@@ -303,11 +328,19 @@ class AccountApi {
|
|
|
303
328
|
}
|
|
304
329
|
// Extract from nested data structure
|
|
305
330
|
const data = result.data?.data || result.data || {};
|
|
331
|
+
// next_step (door 3b): read from the nested shapes AND a flat body. Absent / null / '' = no step; a non-string value is an
|
|
332
|
+
// UNRECOGNISED step and is kept as a non-empty string (never dropped, never mistaken for the known one). That includes false and 0:
|
|
333
|
+
// a server that means "no step" must send nothing or null (or '').
|
|
334
|
+
const rawStep = data.next_step ?? result.next_step;
|
|
335
|
+
const nextStep = rawStep === undefined || rawStep === null || rawStep === ''
|
|
336
|
+
? undefined
|
|
337
|
+
: typeof rawStep === 'string' ? rawStep : 'UNRECOGNISED_NEXT_STEP';
|
|
306
338
|
return {
|
|
307
339
|
success: data.success !== false,
|
|
308
340
|
resetToken: data.reset_token || '',
|
|
309
341
|
email: data.email || '',
|
|
310
|
-
clientKey: data.client_key || ''
|
|
342
|
+
clientKey: data.client_key || '',
|
|
343
|
+
...(nextStep !== undefined ? { nextStep } : {})
|
|
311
344
|
};
|
|
312
345
|
}
|
|
313
346
|
async setOnboardingPassword(data, clientKey) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@payez/next-mvp",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.14.0",
|
|
4
4
|
"sideEffects": false,
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -505,6 +505,11 @@
|
|
|
505
505
|
"require": "./dist/api-handlers/auth/validate.js",
|
|
506
506
|
"default": "./api-handlers/auth/validate.js"
|
|
507
507
|
},
|
|
508
|
+
"./api-handlers/health": {
|
|
509
|
+
"types": "./dist/api-handlers/health.d.ts",
|
|
510
|
+
"require": "./dist/api-handlers/health.js",
|
|
511
|
+
"default": "./dist/api-handlers/health.js"
|
|
512
|
+
},
|
|
508
513
|
"./api-handlers/session/refresh-viability": {
|
|
509
514
|
"types": "./dist/api-handlers/session/refresh-viability.d.ts",
|
|
510
515
|
"require": "./dist/api-handlers/session/refresh-viability.js",
|
|
@@ -705,6 +710,11 @@
|
|
|
705
710
|
"require": "./dist/routes/auth/viability.js",
|
|
706
711
|
"default": "./dist/routes/auth/viability.js"
|
|
707
712
|
},
|
|
713
|
+
"./routes/health": {
|
|
714
|
+
"types": "./dist/routes/health.d.ts",
|
|
715
|
+
"require": "./dist/routes/health.js",
|
|
716
|
+
"default": "./dist/routes/health.js"
|
|
717
|
+
},
|
|
708
718
|
"./routes/session/refresh-viability": {
|
|
709
719
|
"types": "./dist/routes/session/refresh-viability.d.ts",
|
|
710
720
|
"require": "./dist/routes/session/refresh-viability.js",
|
|
@@ -980,6 +990,16 @@
|
|
|
980
990
|
"require": "./dist/routes/mfa/totp-confirm.js",
|
|
981
991
|
"default": "./dist/routes/mfa/totp-confirm.js"
|
|
982
992
|
},
|
|
993
|
+
"./routes/mfa/first-enrol-enroll": {
|
|
994
|
+
"types": "./dist/routes/mfa/first-enrol-enroll.d.ts",
|
|
995
|
+
"require": "./dist/routes/mfa/first-enrol-enroll.js",
|
|
996
|
+
"default": "./dist/routes/mfa/first-enrol-enroll.js"
|
|
997
|
+
},
|
|
998
|
+
"./routes/mfa/first-enrol-confirm": {
|
|
999
|
+
"types": "./dist/routes/mfa/first-enrol-confirm.d.ts",
|
|
1000
|
+
"require": "./dist/routes/mfa/first-enrol-confirm.js",
|
|
1001
|
+
"default": "./dist/routes/mfa/first-enrol-confirm.js"
|
|
1002
|
+
},
|
|
983
1003
|
"./routes/mfa/totp-verify": {
|
|
984
1004
|
"types": "./dist/routes/mfa/totp-verify.d.ts",
|
|
985
1005
|
"require": "./dist/routes/mfa/totp-verify.js",
|
|
@@ -1090,6 +1110,11 @@
|
|
|
1090
1110
|
"require": "./dist/components/mfa/TotpEnrollPanel.js",
|
|
1091
1111
|
"default": "./dist/components/mfa/TotpEnrollPanel.js"
|
|
1092
1112
|
},
|
|
1113
|
+
"./dist/pages/first-enroll": {
|
|
1114
|
+
"types": "./dist/pages/first-enroll/page.d.ts",
|
|
1115
|
+
"require": "./dist/pages/first-enroll/page.js",
|
|
1116
|
+
"default": "./dist/pages/first-enroll/page.js"
|
|
1117
|
+
},
|
|
1093
1118
|
"./dist/pages/enroll": {
|
|
1094
1119
|
"types": "./dist/pages/enroll/page.d.ts",
|
|
1095
1120
|
"require": "./dist/pages/enroll/page.js",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from 'next/server';
|
|
2
2
|
import { idpFetchJSON } from '../../lib/idp-fetch';
|
|
3
3
|
import { ENV_CONFIG } from '../../config/env';
|
|
4
|
+
import { noStore } from '../../lib/no-store';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Account Profile API Handler
|
|
@@ -41,7 +42,7 @@ import { ENV_CONFIG } from '../../config/env';
|
|
|
41
42
|
* @package @payez/next-mvp
|
|
42
43
|
*/
|
|
43
44
|
|
|
44
|
-
|
|
45
|
+
async function getProfile(req: NextRequest) {
|
|
45
46
|
const result = await idpFetchJSON(req, `${ENV_CONFIG.IDP_URL}/api/Account/profile`, {
|
|
46
47
|
method: 'GET',
|
|
47
48
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -65,6 +66,9 @@ export async function GET(req: NextRequest) {
|
|
|
65
66
|
return NextResponse.json(result.json, { status: 200 });
|
|
66
67
|
}
|
|
67
68
|
|
|
69
|
+
/** PAY-2994: the profile is one person's data; never cached (noStore). */
|
|
70
|
+
export const GET = noStore(getProfile);
|
|
71
|
+
|
|
68
72
|
export async function PUT(req: NextRequest) {
|
|
69
73
|
let body = '{}';
|
|
70
74
|
try {
|
|
@@ -16,13 +16,12 @@ import {
|
|
|
16
16
|
AnonSessionPreferences,
|
|
17
17
|
} from '../../lib/anon-session';
|
|
18
18
|
import { errForLog } from '../../lib/log-safe';
|
|
19
|
+
import { noStore } from '../../lib/no-store';
|
|
19
20
|
|
|
20
21
|
const COOKIE_MAX_AGE = 90 * 24 * 60 * 60; // 90 days in seconds
|
|
21
22
|
|
|
22
|
-
/**
|
|
23
|
-
|
|
24
|
-
*/
|
|
25
|
-
export async function GET(request: NextRequest) {
|
|
23
|
+
/** Retrieves anonymous session preferences. Exported as GET below, wrapped in noStore (PAY-2994). */
|
|
24
|
+
async function getPreferences(request: NextRequest) {
|
|
26
25
|
try {
|
|
27
26
|
const cookieStore = await cookies();
|
|
28
27
|
let anonId = cookieStore.get(ANON_COOKIE_NAME)?.value;
|
|
@@ -64,6 +63,8 @@ export async function GET(request: NextRequest) {
|
|
|
64
63
|
/**
|
|
65
64
|
* POST handler - updates anonymous session preferences
|
|
66
65
|
*/
|
|
66
|
+
export const GET = noStore(getPreferences);
|
|
67
|
+
|
|
67
68
|
export async function POST(request: NextRequest) {
|
|
68
69
|
try {
|
|
69
70
|
const cookieStore = await cookies();
|
|
@@ -49,6 +49,8 @@ import { DEVICE_ID_HEADER, isValidDeviceId } from '../../lib/device-id';
|
|
|
49
49
|
import { classifyTwofaRejection, type TwofaLaneResult } from '../../lib/twofa-rejection';
|
|
50
50
|
import { bearerFetch } from '../../lib/bearer-fetch';
|
|
51
51
|
import { errForLog } from '../../lib/log-safe';
|
|
52
|
+
// AUTH-045: the httpOnly cookie that carries the enrolment-scoped token from the code turn to the two enrolment routes (ONE name, defined in lib/first-enrol).
|
|
53
|
+
import { ENROL_COOKIE, ENROL_COOKIE_MAX_AGE } from '../../lib/first-enrol';
|
|
52
54
|
|
|
53
55
|
// `export const runtime = 'nodejs'` must be written LITERALLY in the app's route file (Next ignores a re-exported segment config).
|
|
54
56
|
|
|
@@ -348,6 +350,53 @@ export async function POST(req: NextRequest) {
|
|
|
348
350
|
const vresult = verifyData?.data?.result ?? verifyData?.data ?? verifyData;
|
|
349
351
|
const vAccess: string | undefined = vresult?.access_token;
|
|
350
352
|
const vRefresh: string | undefined = vresult?.refresh_token;
|
|
353
|
+
// PAY-3083 (AUTH-045): ACCESS WITHOUT REFRESH AT THE CODE TURN IS NOT A FAILURE - IT IS ENROLMENT.
|
|
354
|
+
//
|
|
355
|
+
// The IdP withholds the refresh token for a user who has no usable second factor on THIS client.
|
|
356
|
+
// TOTP enrolment is PER CLIENT (DotNetPert 99446 sec A, client 90717 / user 1175): the password turn
|
|
357
|
+
// answered Require2FA=true, IssueRefreshToken=false, step=enroll_mfa, and this verify turn then
|
|
358
|
+
// accepted the emailed code as CONTACT PROOF and issued an ENROLMENT-SCOPED access token, again with
|
|
359
|
+
// no refresh token. That is exactly what PAY-2798 requires of the IdP - the IdP is RIGHT.
|
|
360
|
+
//
|
|
361
|
+
// This branch used to read "access, no refresh" as an incomplete response and answer 502, so the
|
|
362
|
+
// developer who had just proved their contact was told the server was broken and could never reach
|
|
363
|
+
// the enrolment page: the 502 the field saw as
|
|
364
|
+
// '[ADMIN-LOGIN] verify response missing token { hasAccess: true, hasRefresh: false }'.
|
|
365
|
+
//
|
|
366
|
+
// Send them to ENROLMENT instead, in the ONE contract the door and the form already share for it
|
|
367
|
+
// (PAY-2630): requiresTwoFactor + nextStep 'enroll_mfa'. No session is minted - an enrolment-scoped
|
|
368
|
+
// token is not a signed-in session - and the token value is NEVER logged nor echoed (BAPert 99521 #2).
|
|
369
|
+
//
|
|
370
|
+
// Guard kept: no access token AT ALL (a refresh-only or empty body) is still a broken success and
|
|
371
|
+
// still 502 below - this widening is only for a genuine access-with-no-refresh answer.
|
|
372
|
+
if (vAccess && !vRefresh) {
|
|
373
|
+
const step = vresult?.next_step === 'enroll_mfa' || vresult?.next_step === 'mfa_verify' ? vresult.next_step : 'enroll_mfa';
|
|
374
|
+
const enrolling = step === 'enroll_mfa';
|
|
375
|
+
const answer = NextResponse.json(
|
|
376
|
+
{
|
|
377
|
+
success: false,
|
|
378
|
+
requiresTwoFactor: true,
|
|
379
|
+
acceptedFactors: acceptedFactorsFor('email'),
|
|
380
|
+
sentVia: null,
|
|
381
|
+
codeRequested: false,
|
|
382
|
+
emailSent: false,
|
|
383
|
+
nextStep: step,
|
|
384
|
+
// AUTH-045 (walk 14 Nov 2026): requiresTwoFactor alone is also the password turn's "a code was sent", so the form re-drew the code box
|
|
385
|
+
// and looped. This flag is the ONLY signal that means "go enrol"; it is set on this answer and nowhere else.
|
|
386
|
+
...(enrolling ? { enrolmentRequired: true } : {}),
|
|
387
|
+
},
|
|
388
|
+
{ status: 200 },
|
|
389
|
+
);
|
|
390
|
+
if (enrolling) {
|
|
391
|
+
// No session exists for an enrolment-scoped token, and the enrolment page's actions need one, so the token rides in this httpOnly
|
|
392
|
+
// cookie to the enrolment routes (the portal's PAY-2798 part 2 pattern): never in a body, a URL or a log; strict; 600 s.
|
|
393
|
+
answer.cookies.set(ENROL_COOKIE, vAccess, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'strict', path: '/', maxAge: ENROL_COOKIE_MAX_AGE });
|
|
394
|
+
// The provisional bearer is spent: the enrolment token replaces it.
|
|
395
|
+
answer.cookies.set('adm2fa', '', { path: '/', maxAge: 0 });
|
|
396
|
+
answer.cookies.set('adm2fa_lane', '', { path: '/', maxAge: 0 });
|
|
397
|
+
}
|
|
398
|
+
return answer;
|
|
399
|
+
}
|
|
351
400
|
if (!vAccess || !vRefresh) {
|
|
352
401
|
console.error('[ADMIN-LOGIN] verify response missing tokens', { hasAccess: !!vAccess, hasRefresh: !!vRefresh });
|
|
353
402
|
return NextResponse.json({ success: false, error: 'Authentication service returned an incomplete response' }, { status: 502 });
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /api/health handler (PAY-2804, AUTH-045; BAPert ruling 88065).
|
|
3
|
+
*
|
|
4
|
+
* Sessions are Redis-only. The body reports state ONLY: `redis: 'up' | 'down' | 'unknown'` and
|
|
5
|
+
* `status: 'ok' | 'degraded'`. No REDIS_URL, host, port, error text or stack, to any caller (anonymous route).
|
|
6
|
+
*
|
|
7
|
+
* The HTTP status is 200 even when Redis is down: this route is the ALB target-group check, and a 503
|
|
8
|
+
* there would make the ALB pull every task for an outage that replacing tasks cannot fix.
|
|
9
|
+
* HEALTH_REDIS_FAILS_STATUS=1 turns Redis-down into 503 as a READINESS setting for a deployer's OWN probe;
|
|
10
|
+
* it is default off and must never be set on a load-balancer health check.
|
|
11
|
+
*/
|
|
12
|
+
import { NextResponse } from 'next/server';
|
|
13
|
+
|
|
14
|
+
export interface HealthRedis {
|
|
15
|
+
ping(): Promise<unknown>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type RedisState = 'up' | 'down' | 'unknown';
|
|
19
|
+
|
|
20
|
+
const PING_BUDGET_MS = 2000;
|
|
21
|
+
|
|
22
|
+
async function probe(redis: HealthRedis): Promise<RedisState> {
|
|
23
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
24
|
+
try {
|
|
25
|
+
const timeout = new Promise<never>((_, reject) => {
|
|
26
|
+
timer = setTimeout(() => reject(new Error('ping timeout')), PING_BUDGET_MS);
|
|
27
|
+
});
|
|
28
|
+
await Promise.race([redis.ping(), timeout]);
|
|
29
|
+
return 'up';
|
|
30
|
+
} catch {
|
|
31
|
+
return 'down';
|
|
32
|
+
} finally {
|
|
33
|
+
if (timer) clearTimeout(timer);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function createHealthHandler(getRedis: () => HealthRedis) {
|
|
38
|
+
return async function GET(): Promise<Response> {
|
|
39
|
+
let redis: RedisState;
|
|
40
|
+
let client: HealthRedis | undefined;
|
|
41
|
+
try {
|
|
42
|
+
client = getRedis();
|
|
43
|
+
} catch {
|
|
44
|
+
client = undefined;
|
|
45
|
+
}
|
|
46
|
+
redis = client ? await probe(client) : 'unknown';
|
|
47
|
+
const failsStatus = process.env.HEALTH_REDIS_FAILS_STATUS === '1';
|
|
48
|
+
return NextResponse.json(
|
|
49
|
+
{ status: redis === 'up' ? 'ok' : 'degraded', redis },
|
|
50
|
+
{ status: redis !== 'up' && failsStatus ? 503 : 200, headers: { 'Cache-Control': 'no-store' } },
|
|
51
|
+
);
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
3
|
+
import { getIDPClientConfig } from '../../lib/idp-client-config';
|
|
4
|
+
import { resolveClientKey } from '../../lib/client-key';
|
|
5
|
+
import { NO_STORE, refuseNonJson } from '../../lib/mfa-json-guard';
|
|
6
|
+
import { ENROL_COOKIE, LIMIT_PER_COOKIE, attemptAllowed, runEnrolStep, type EnrolStep } from '../../lib/first-enrol';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* AUTH-045 first-enrolment carry (PAY-2938, BAPert 100969 rows 3-5): the TWO routes a user the IdP told to enrol uses, built on lib/first-enrol.
|
|
10
|
+
*
|
|
11
|
+
* WHO MAY CALL: the browser that holds the httpOnly enrolment cookie the credential door parked (ENROL_COOKIE). This is the ONLY module besides the
|
|
12
|
+
* door that reads that cookie, and it serves ONLY these two operations; every other package route stays session-gated and answers 401 to a caller
|
|
13
|
+
* holding nothing but this cookie (a gate row holds that). No cookie -> 401 SIGN_IN_AGAIN and no IdP call.
|
|
14
|
+
*
|
|
15
|
+
* What follows a confirm (BAPert 100969 row 5): the cookie is cleared and the answer carries the recovery codes; NO session is minted here and no new
|
|
16
|
+
* way to mint one is added. The person signs in again with their password and the authenticator, through the same credential door as before.
|
|
17
|
+
*
|
|
18
|
+
* The token never leaves the server: not in a body, a URL, a log or a Referer. The IdP's prose is never relayed.
|
|
19
|
+
*/
|
|
20
|
+
const digest = (s: string) => createHash('sha256').update(s).digest('hex');
|
|
21
|
+
|
|
22
|
+
const cookieOpts = (maxAge: number) => ({ httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'strict' as const, path: '/', maxAge });
|
|
23
|
+
|
|
24
|
+
function reply(status: number, body: Record<string, unknown>, clear: boolean): NextResponse {
|
|
25
|
+
const res = NextResponse.json(body, { status, headers: NO_STORE });
|
|
26
|
+
if (clear) res.cookies.set(ENROL_COOKIE, '', cookieOpts(0));
|
|
27
|
+
return res;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function enrolStepPOST(step: EnrolStep) {
|
|
31
|
+
return async function POST(req: NextRequest): Promise<NextResponse> {
|
|
32
|
+
const refused = refuseNonJson(req);
|
|
33
|
+
if (refused) return refused;
|
|
34
|
+
const token = req.cookies.get(ENROL_COOKIE)?.value;
|
|
35
|
+
if (!token) return reply(401, { success: false, code: 'SIGN_IN_AGAIN' }, false);
|
|
36
|
+
if (!attemptAllowed(`${step}|ck|${digest(token)}`, LIMIT_PER_COOKIE)) return reply(429, { success: false, code: 'TOO_MANY', error: 'Too many attempts. Wait a few minutes and try again.' }, true);
|
|
37
|
+
|
|
38
|
+
const idp = process.env.IDP_URL;
|
|
39
|
+
let clientKey = '';
|
|
40
|
+
try {
|
|
41
|
+
const config = await getIDPClientConfig();
|
|
42
|
+
clientKey = resolveClientKey(config, 'first-enrol').key ?? '';
|
|
43
|
+
} catch {
|
|
44
|
+
return reply(503, { success: false, code: 'IDP_CONFIG_UNAVAILABLE' }, false);
|
|
45
|
+
}
|
|
46
|
+
if (!idp || !clientKey) return reply(503, { success: false, code: 'IDP_CONFIG_UNAVAILABLE' }, false);
|
|
47
|
+
|
|
48
|
+
const body = await req.json().catch(() => null);
|
|
49
|
+
const r = await runEnrolStep({ fetchImpl: (u, i) => fetch(u, i), idp, clientKey }, { step, token, body });
|
|
50
|
+
return reply(r.status, r.body, r.cookie === 'clear');
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const POST_ENROLL = enrolStepPOST('enroll');
|
|
55
|
+
export const POST_CONFIRM = enrolStepPOST('confirm');
|
|
@@ -5,9 +5,10 @@ import { getTokenTestAware } from '../../lib/test-aware-get-token';
|
|
|
5
5
|
import { logger } from '../../config/logger';
|
|
6
6
|
import { tokenLogId } from '../../lib/token-log-id';
|
|
7
7
|
import { errForLog } from '../../lib/log-safe';
|
|
8
|
+
import { noStore } from '../../lib/no-store';
|
|
8
9
|
import { resolveSessionState } from '../../server/auth';
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
async function getRefreshViability(req: NextRequest) {
|
|
11
12
|
try {
|
|
12
13
|
const sessionToken = req.nextUrl.searchParams.get('token');
|
|
13
14
|
let finalSessionToken = sessionToken;
|
|
@@ -55,3 +56,6 @@ export async function GET(req: NextRequest) {
|
|
|
55
56
|
return NextResponse.json({ canRefresh: false, reason: 'check_error', error: 'Failed to check refresh viability', code: 'INTERNAL_ERROR', correlationId }, { status: 500 });
|
|
56
57
|
}
|
|
57
58
|
}
|
|
59
|
+
|
|
60
|
+
/** PAY-2994 (NightHawk 92964 Low 1): the api-handlers/session/* path is also in the exports map; wrap here too so a consumer importing it is covered. */
|
|
61
|
+
export const GET = noStore(getRefreshViability);
|
|
@@ -13,8 +13,9 @@ import { getIDPClientConfig } from '../../lib/idp-client-config';
|
|
|
13
13
|
import { tokenLogId } from '../../lib/token-log-id';
|
|
14
14
|
import { getRevocationStatus, warnNonNumericUserId, resolveNumericIdpUserId, type RevocationStatus } from '../../vibe/sessions';
|
|
15
15
|
import { errForLog } from '../../lib/log-safe';
|
|
16
|
+
import { noStore } from '../../lib/no-store';
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
async function getViability(req: NextRequest) {
|
|
18
19
|
try {
|
|
19
20
|
// Ensure initialization is complete (auth signing secret resolved from IDP)
|
|
20
21
|
if (!process.env.BETTER_AUTH_SECRET && !process.env.NEXTAUTH_SECRET) {
|
|
@@ -241,4 +242,7 @@ export async function GET(req: NextRequest) {
|
|
|
241
242
|
console.error('[API Viability] Error checking session viability:', errForLog(error));
|
|
242
243
|
return NextResponse.json({ authenticated: false }, { status: 500 });
|
|
243
244
|
}
|
|
244
|
-
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** PAY-2994 (NightHawk 92964 Low 1): the api-handlers/session/* path is also in the exports map; wrap here too so a consumer importing it is covered. */
|
|
248
|
+
export const GET = noStore(getViability);
|