@payez/next-mvp 4.1.20 → 4.4.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/POST-INSTALL.md +408 -0
- package/README.md +7 -0
- package/dist/api-handlers/account/recovery/initiate.js +35 -2
- package/dist/api-handlers/account/recovery/send-code.d.ts +0 -33
- package/dist/api-handlers/account/recovery/send-code.js +35 -2
- package/dist/api-handlers/account/recovery/verify-code.d.ts +0 -33
- package/dist/api-handlers/account/recovery/verify-code.js +35 -2
- package/dist/api-handlers/account/reset-password.js +35 -2
- package/dist/api-handlers/account/validate-password.js +21 -3
- package/dist/api-handlers/admin/analytics.js +2 -1
- package/dist/api-handlers/admin/audit.js +2 -1
- package/dist/api-handlers/admin/sessions.js +2 -1
- package/dist/api-handlers/admin/stats.js +2 -1
- package/dist/api-handlers/admin/users.js +2 -1
- package/dist/api-handlers/admin/vibe-data.js +2 -1
- package/dist/api-handlers/auth/jwks.js +10 -0
- package/dist/api-handlers/auth/login.js +5 -2
- package/dist/api-handlers/auth/passkey-signin.js +16 -1
- package/dist/api-handlers/auth/refresh.d.ts +23 -3
- package/dist/api-handlers/auth/refresh.js +107 -16
- package/dist/api-handlers/auth/signout.js +3 -2
- package/dist/api-handlers/auth/verify-code.js +5 -2
- package/dist/api-handlers/session/viability.js +27 -1
- package/dist/api-handlers/test/force-expire.d.ts +29 -15
- package/dist/api-handlers/test/force-expire.js +109 -14
- package/dist/auth/better-auth.d.ts +263 -6
- package/dist/auth/better-auth.js +437 -59
- package/dist/auth/utils/idp-client.js +1 -1
- package/dist/client/AuthContext.js +49 -17
- package/dist/components/account/UserAvatarMenu.js +12 -0
- package/dist/components/auth/ModeAwareLoginPage.js +10 -8
- package/dist/components/auth/ModeAwareSignupPage.js +5 -2
- package/dist/components/auth/TraditionalAuthSection.js +5 -1
- package/dist/hooks/useAuthSettings.js +14 -2
- package/dist/hooks/usePublicAuthSettings.d.ts +2 -2
- package/dist/hooks/usePublicAuthSettings.js +28 -14
- package/dist/lib/client-key.d.ts +61 -0
- package/dist/lib/client-key.js +72 -0
- package/dist/lib/demo-mode.js +2 -1
- package/dist/lib/ensure-fresh-access-token.js +12 -1
- package/dist/lib/idp-client-config.d.ts +25 -2
- package/dist/lib/idp-client-config.js +381 -36
- package/dist/lib/idp-fetch.d.ts +2 -0
- package/dist/lib/idp-fetch.js +31 -1
- package/dist/lib/idp-url-guard.d.ts +29 -0
- package/dist/lib/idp-url-guard.js +38 -0
- package/dist/lib/redis.js +77 -5
- package/dist/lib/session-cookie-shape.d.ts +69 -0
- package/dist/lib/session-cookie-shape.js +126 -0
- package/dist/lib/session-store.d.ts +35 -2
- package/dist/lib/session-store.js +135 -4
- package/dist/lib/single-flight.d.ts +35 -0
- package/dist/lib/single-flight.js +30 -0
- package/dist/lib/token-lifecycle.js +12 -0
- package/dist/middleware/create-middleware.js +12 -0
- package/dist/models/SessionModel.d.ts +41 -0
- package/dist/models/SessionModel.js +9 -0
- package/dist/pages/admin-login/page.d.ts +1 -1
- package/dist/pages/admin-login/page.js +5 -41
- package/dist/pages/security/EnhancedSecurityPage.d.ts +35 -1
- package/dist/pages/security/EnhancedSecurityPage.js +42 -2
- package/dist/pages/security/index.d.ts +1 -0
- package/dist/pages/security/index.js +3 -0
- package/dist/pages/test-env/EmergencyLogoutPage.d.ts +17 -5
- package/dist/pages/test-env/EmergencyLogoutPage.js +17 -5
- package/dist/pages/test-env/JwtInspectPage.d.ts +30 -5
- package/dist/pages/test-env/JwtInspectPage.js +166 -7
- package/dist/pages/test-env/RefreshTokenPage.d.ts +22 -6
- package/dist/pages/test-env/RefreshTokenPage.js +22 -6
- package/dist/pages/test-env/TestEnvPage.d.ts +16 -4
- package/dist/pages/test-env/TestEnvPage.js +16 -4
- package/dist/pages/test-env/guarded.d.ts +4 -0
- package/dist/pages/test-env/guarded.js +86 -0
- package/dist/pages/test-env/index.d.ts +50 -7
- package/dist/pages/test-env/index.js +58 -12
- package/dist/routes/auth/logout.js +3 -2
- package/dist/routes/auth/refresh.js +27 -1
- package/dist/routes/auth/session.d.ts +12 -3
- package/dist/routes/auth/session.js +92 -9
- package/dist/routes/auth/settings.d.ts +34 -4
- package/dist/routes/auth/settings.js +87 -33
- package/dist/routes/auth/viability.d.ts +1 -1
- package/dist/routes/auth/viability.js +122 -11
- package/dist/server/auth-guard.js +22 -15
- package/dist/server/auth.d.ts +170 -4
- package/dist/server/auth.js +42 -75
- package/dist/server/decode-session.js +84 -10
- package/dist/types/auth.d.ts +18 -0
- package/package.json +969 -958
- package/scripts/check-internal-url-usage.sh +73 -73
- package/scripts/check-publish-health.mjs +120 -0
- package/scripts/dev-broker.ps1 +71 -6
- package/scripts/print-client-config.mjs +162 -0
- package/scripts/server-only-stub.cjs +4 -0
- package/src/api-handlers/account/recovery/initiate.ts +35 -2
- package/src/api-handlers/account/recovery/send-code.ts +36 -2
- package/src/api-handlers/account/recovery/verify-code.ts +36 -2
- package/src/api-handlers/account/reset-password.ts +35 -2
- package/src/api-handlers/account/validate-password.ts +22 -3
- package/src/api-handlers/admin/analytics.ts +2 -1
- package/src/api-handlers/admin/audit.ts +2 -1
- package/src/api-handlers/admin/sessions.ts +2 -1
- package/src/api-handlers/admin/stats.ts +2 -1
- package/src/api-handlers/admin/users.ts +2 -1
- package/src/api-handlers/admin/vibe-data.ts +2 -1
- package/src/api-handlers/auth/jwks.ts +10 -0
- package/src/api-handlers/auth/login.ts +5 -2
- package/src/api-handlers/auth/passkey-signin.ts +112 -98
- package/src/api-handlers/auth/refresh.ts +112 -15
- package/src/api-handlers/auth/signout.ts +4 -3
- package/src/api-handlers/auth/verify-code.ts +5 -2
- package/src/api-handlers/mfa/backup-code-verify.ts +175 -175
- package/src/api-handlers/mfa/backup-codes.ts +41 -41
- package/src/api-handlers/mfa/methods.ts +46 -46
- package/src/api-handlers/mfa/totp-confirm.ts +42 -42
- package/src/api-handlers/mfa/totp-enroll.ts +42 -42
- package/src/api-handlers/passkeys/list.ts +31 -31
- package/src/api-handlers/passkeys/manage.ts +54 -54
- package/src/api-handlers/passkeys/register-options.ts +42 -42
- package/src/api-handlers/passkeys/register-verify.ts +41 -41
- package/src/api-handlers/session/viability.ts +29 -1
- package/src/api-handlers/test/force-expire.ts +118 -13
- package/src/auth/better-auth.ts +486 -51
- package/src/auth/utils/idp-client.ts +1 -1
- package/src/client/AuthContext.tsx +55 -19
- package/src/components/account/UserAvatarMenu.tsx +11 -0
- package/src/components/auth/ModeAwareLoginPage.tsx +40 -49
- package/src/components/auth/ModeAwareSignupPage.tsx +5 -2
- package/src/components/auth/TraditionalAuthSection.tsx +5 -1
- package/src/hooks/useAuthSettings.ts +14 -2
- package/src/hooks/usePublicAuthSettings.ts +38 -16
- package/src/lib/client-key.ts +88 -0
- package/src/lib/demo-mode.ts +2 -1
- package/src/lib/ensure-fresh-access-token.ts +12 -1
- package/src/lib/idp-client-config.ts +399 -36
- package/src/lib/idp-fetch.ts +36 -1
- package/src/lib/idp-url-guard.ts +37 -0
- package/src/lib/redis.ts +84 -6
- package/src/lib/session-cookie-shape.ts +184 -0
- package/src/lib/session-store.ts +152 -6
- package/src/lib/single-flight.ts +64 -0
- package/src/lib/token-lifecycle.ts +12 -0
- package/src/middleware/create-middleware.ts +11 -0
- package/src/models/SessionModel.ts +49 -0
- package/src/pages/admin-login/page.tsx +17 -127
- package/src/pages/security/EnhancedSecurityPage.tsx +95 -9
- package/src/pages/security/index.ts +3 -0
- package/src/pages/test-env/EmergencyLogoutPage.tsx +17 -5
- package/src/pages/test-env/JwtInspectPage.tsx +235 -20
- package/src/pages/test-env/RefreshTokenPage.tsx +22 -6
- package/src/pages/test-env/TestEnvPage.tsx +16 -4
- package/src/pages/test-env/guarded.tsx +81 -0
- package/src/pages/test-env/index.ts +59 -7
- package/src/routes/auth/logout.ts +4 -3
- package/src/routes/auth/passkey-signin.ts +13 -13
- package/src/routes/auth/refresh.ts +30 -1
- package/src/routes/auth/session.ts +98 -9
- package/src/routes/auth/settings.ts +144 -42
- package/src/routes/auth/viability.ts +129 -11
- package/src/routes/mfa/backup-code-verify.ts +1 -1
- package/src/routes/mfa/backup-codes.ts +10 -10
- package/src/routes/mfa/methods.ts +22 -22
- package/src/routes/mfa/totp-confirm.ts +10 -10
- package/src/routes/mfa/totp-enroll.ts +10 -10
- package/src/routes/mfa/totp-verify.ts +14 -14
- package/src/routes/passkeys/list.ts +10 -10
- package/src/routes/passkeys/manage.ts +23 -23
- package/src/routes/passkeys/register-options.ts +10 -10
- package/src/routes/passkeys/register-verify.ts +10 -10
- package/src/server/auth-guard.ts +24 -17
- package/src/server/auth.ts +54 -74
- package/src/server/decode-session.ts +95 -11
- package/src/templates/instrumentation.ts +21 -2
- package/src/types/auth.ts +20 -0
- package/dist/lib/auth-secret.d.ts +0 -17
- package/dist/lib/auth-secret.js +0 -116
- package/src/lib/auth-secret.ts +0 -137
|
@@ -55,6 +55,27 @@ export async function GET(req: NextRequest) {
|
|
|
55
55
|
hasAccessToken: !!session?.idpAccessToken,
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
+
// A Better Auth cookie is NOT authorization. The Redis session is the
|
|
59
|
+
// authoritative record of what this user is allowed to do; without it we
|
|
60
|
+
// have no roles and no IDP access token, so there is nothing to authorize
|
|
61
|
+
// against. Returning a populated user here produces the worst failure mode
|
|
62
|
+
// available: a client that renders as signed-in and is not.
|
|
63
|
+
//
|
|
64
|
+
// MUST return empty {} for the same reason as line 34 — useSession() treats
|
|
65
|
+
// ANY non-empty response object as "authenticated".
|
|
66
|
+
//
|
|
67
|
+
// NOTE: an MFA-pending session IS present in Redis (it just has
|
|
68
|
+
// mfaVerified=false), so this gate does not break /verify-code. Gating on a
|
|
69
|
+
// missing accessToken instead WOULD break it.
|
|
70
|
+
if (!session) {
|
|
71
|
+
console.error('[SESSION_ROUTE] Redis session MISS — refusing to report authenticated', {
|
|
72
|
+
userId: authSession.user?.id,
|
|
73
|
+
redisSessionId: redisSessionId ? redisSessionId.substring(0, 8) + '...' : 'MISSING',
|
|
74
|
+
reason: redisSessionId ? 'session expired or evicted' : 'no session token on auth session',
|
|
75
|
+
});
|
|
76
|
+
return NextResponse.json({});
|
|
77
|
+
}
|
|
78
|
+
|
|
58
79
|
// Return session format with Redis data
|
|
59
80
|
// useSession() expects: { user: {...}, expires: "..." }
|
|
60
81
|
// We enrich with all session data from Redis
|
|
@@ -96,13 +117,62 @@ export async function GET(req: NextRequest) {
|
|
|
96
117
|
}
|
|
97
118
|
|
|
98
119
|
/**
|
|
99
|
-
*
|
|
120
|
+
* The fields this route USED to accept from the request body and must never accept
|
|
121
|
+
* again. Cards 308833 (critical) / 312092 (remediation spec) / 318272 / 311425.
|
|
122
|
+
*
|
|
123
|
+
* `twoFactorComplete` was written straight through to `mfaVerified`, so a caller
|
|
124
|
+
* holding any session supplied its OWN second-factor verdict - no factor presented,
|
|
125
|
+
* no comparison against `amr`, no server-side derivation. The caller can be PRE-2FA,
|
|
126
|
+
* which makes this a 2FA bypass rather than a user rewriting a flag he had earned.
|
|
100
127
|
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
128
|
+
* The package already states the invariant in four places, in its own voice:
|
|
129
|
+
* api-handlers/mfa/totp-verify.ts - "Enrolment must never set mfaVerified."
|
|
130
|
+
* api-handlers/auth/refresh.ts - "mfaVerified is derived from amrClaims"
|
|
131
|
+
* auth/better-auth.ts - "computed fresh ... re-derived fresh"
|
|
132
|
+
* (consumer) passkey-signin-plugin - "mfaVerified is DERIVED from amr, never asserted."
|
|
133
|
+
* Every other writer in the estate derives it. This one route took it from an HTTP body.
|
|
134
|
+
*
|
|
135
|
+
* PRESENCE, NOT TRUTHINESS, is what is refused - `twoFactorComplete: false` is rejected
|
|
136
|
+
* exactly like `true`. A caller must not be able to drive this field in EITHER direction,
|
|
137
|
+
* and per R486/R908 an absent signal is "cannot determine", never a negative assertion.
|
|
138
|
+
*
|
|
139
|
+
* REFUSED LOUDLY RATHER THAN IGNORED SILENTLY. Dropping the field would leave a caller
|
|
140
|
+
* believing it had recorded a 2FA completion that never happened - the same failure shape
|
|
141
|
+
* as the 4.1.14 -> 4.1.31 history below, where a write nobody read looked like it worked.
|
|
142
|
+
*
|
|
143
|
+
* *** DO NOT "FIX" A REGRESSION HERE BY REVERTING THE KEY NORMALIZATION (312092 s2). ***
|
|
144
|
+
* 4.1.14 had this same unvalidated write but stored bare `accessToken`/`twoFactorComplete`,
|
|
145
|
+
* keys no reader consulted, so the forgery landed where nothing looked. The 4.1.31
|
|
146
|
+
* normalization did not introduce the bug - IT ARMED IT. Reverting re-hides the hole and
|
|
147
|
+
* restores two real client-facing bugs. The write is authorized here instead.
|
|
148
|
+
*/
|
|
149
|
+
const CALLER_ASSERTED_FIELDS: ReadonlyArray<{ field: string; why: string }> = [
|
|
150
|
+
{ field: 'twoFactorComplete', why: 'the second-factor verdict is derived from the IDP token (amr), never asserted by the caller' },
|
|
151
|
+
{ field: 'mfaVerified', why: 'the second-factor verdict is derived from the IDP token (amr), never asserted by the caller' },
|
|
152
|
+
{ field: 'twoFactorMethod', why: 'the method is recorded by the server-side verification path that observed the factor' },
|
|
153
|
+
{ field: 'access_token', why: 'tokens are issued by the IDP; use POST /api/auth/refresh' },
|
|
154
|
+
{ field: 'refresh_token', why: 'tokens are issued by the IDP; use POST /api/auth/refresh' },
|
|
155
|
+
{ field: 'idpAccessToken', why: 'tokens are issued by the IDP; use POST /api/auth/refresh' },
|
|
156
|
+
{ field: 'idpRefreshToken', why: 'tokens are issued by the IDP; use POST /api/auth/refresh' },
|
|
157
|
+
];
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* POST /api/auth/session - Update session METADATA
|
|
161
|
+
*
|
|
162
|
+
* Updates session metadata only. This route holds no authority over authentication
|
|
163
|
+
* state: it cannot set the 2FA verdict and it cannot set tokens.
|
|
164
|
+
*
|
|
165
|
+
* Token refresh uses POST /api/auth/refresh. The 2FA verdict is written only by the
|
|
166
|
+
* server-side verification path that actually observed the factor.
|
|
103
167
|
*
|
|
104
168
|
* Body:
|
|
105
169
|
* - metadata: object - Custom metadata to store in session
|
|
170
|
+
*
|
|
171
|
+
* Any of CALLER_ASSERTED_FIELDS present in the body is a 400. The docstring above used
|
|
172
|
+
* to promise "metadata (not tokens)" while the handler wrote idpAccessToken,
|
|
173
|
+
* idpRefreshToken AND mfaVerified - that false parenthetical is the ONLY caution a
|
|
174
|
+
* mounter reads, and it is how this route came to be mounted (318272). The promise is
|
|
175
|
+
* now enforced by the code rather than asserted by the comment.
|
|
106
176
|
*/
|
|
107
177
|
export async function POST(req: NextRequest) {
|
|
108
178
|
try {
|
|
@@ -124,7 +194,25 @@ export async function POST(req: NextRequest) {
|
|
|
124
194
|
}
|
|
125
195
|
|
|
126
196
|
const body = await req.json();
|
|
127
|
-
|
|
197
|
+
|
|
198
|
+
// Refuse before reading the session: a caller must not learn whether its forged
|
|
199
|
+
// field would have landed, and nothing should be written on a rejected request.
|
|
200
|
+
const asserted = CALLER_ASSERTED_FIELDS.filter(
|
|
201
|
+
({ field }) => body != null && Object.prototype.hasOwnProperty.call(body, field)
|
|
202
|
+
);
|
|
203
|
+
if (asserted.length > 0) {
|
|
204
|
+
console.warn('[SESSION_ROUTE] REFUSED caller-asserted authentication state', {
|
|
205
|
+
fields: asserted.map(({ field }) => field),
|
|
206
|
+
userId: authSession.user?.id,
|
|
207
|
+
});
|
|
208
|
+
return NextResponse.json({
|
|
209
|
+
error: 'This route updates session metadata only. It cannot set authentication state.',
|
|
210
|
+
code: 'CALLER_ASSERTED_AUTH_STATE',
|
|
211
|
+
fields: asserted.map(({ field, why }) => ({ field, why })),
|
|
212
|
+
}, { status: 400 });
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const { metadata } = body;
|
|
128
216
|
|
|
129
217
|
// Get current session from Redis
|
|
130
218
|
const session = await getRedisSession(sessionToken);
|
|
@@ -135,13 +223,14 @@ export async function POST(req: NextRequest) {
|
|
|
135
223
|
}, { status: 404 });
|
|
136
224
|
}
|
|
137
225
|
|
|
138
|
-
// Update session with new data
|
|
226
|
+
// Update session with new data.
|
|
227
|
+
//
|
|
228
|
+
// METADATA ONLY. The token and 2FA-verdict writes that used to live here were
|
|
229
|
+
// removed, not merely guarded: see CALLER_ASSERTED_FIELDS above. Anything that
|
|
230
|
+
// needs to write authentication state must do so from a path that OBSERVED the
|
|
231
|
+
// fact - the IDP exchange for tokens, the verification path for the 2FA verdict.
|
|
139
232
|
const updatedSession = {
|
|
140
233
|
...session,
|
|
141
|
-
...(access_token ? { accessToken: access_token } : {}),
|
|
142
|
-
...(refresh_token ? { refreshToken: refresh_token } : {}),
|
|
143
|
-
...(typeof twoFactorComplete === 'boolean' ? { twoFactorComplete } : {}),
|
|
144
|
-
...(twoFactorMethod ? { twoFactorMethod } : {}),
|
|
145
234
|
...(metadata ? {
|
|
146
235
|
metadata: {
|
|
147
236
|
...(session.metadata || {}),
|
|
@@ -3,6 +3,28 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Returns client auth settings for pre-login pages (signup, login).
|
|
5
5
|
* Does NOT require authentication - these are public client settings.
|
|
6
|
+
*
|
|
7
|
+
* ── 257255: THIS ROUTE USED TO FABRICATE AN AUTHORIZATION POLICY ───────────
|
|
8
|
+
* It returned `success: true` with a hardcoded policy on ANY failure, so a
|
|
9
|
+
* caller could not distinguish "the IDP says X" from "I never reached the
|
|
10
|
+
* IDP". On 2026-08-24 it served require2FA=true / ["email","sms"] for hours
|
|
11
|
+
* while the database said require_2fa=false / ["emailcode","totp"] - wrong
|
|
12
|
+
* about WHETHER 2FA applied and WHICH factors existed, simultaneously and
|
|
13
|
+
* confidently. Six agents spent a night measuring policy with this instrument.
|
|
14
|
+
*
|
|
15
|
+
* Worse, the alarm it swallowed was CORRECT: idp-client-config throws
|
|
16
|
+
* specifically to stop an empty secret reaching betterAuth() and being
|
|
17
|
+
* silently replaced by the PUBLIC DEFAULT_SECRET. This route caught that and
|
|
18
|
+
* returned success.
|
|
19
|
+
*
|
|
20
|
+
* BAPert ruling v32: THE ENDPOINT HARD-FAILS, THE PAGE DEGRADES.
|
|
21
|
+
* QAPert's decisive measurement: the SAME client is require_2fa=TRUE on AKS
|
|
22
|
+
* and FALSE on dev-93, so ANY hardcoded fallback is provably wrong in one
|
|
23
|
+
* environment by construction. There is no safe default because the value is
|
|
24
|
+
* per-client per-environment DATA.
|
|
25
|
+
*
|
|
26
|
+
* The positive control already existed in this package: /api/session/viability
|
|
27
|
+
* surfaces this same fault honestly. These two routes now agree.
|
|
6
28
|
*/
|
|
7
29
|
|
|
8
30
|
import { NextResponse } from 'next/server';
|
|
@@ -11,65 +33,145 @@ import { getIDPClientConfig } from '../../lib/idp-client-config';
|
|
|
11
33
|
export interface PublicAuthSettings {
|
|
12
34
|
// OAuth providers enabled for this client
|
|
13
35
|
enabledProviders: string[];
|
|
14
|
-
|
|
15
|
-
// Registration settings
|
|
16
|
-
allowPublicRegistration: boolean;
|
|
17
36
|
allowFederatedLogin: boolean;
|
|
18
37
|
|
|
19
|
-
// Password settings
|
|
20
|
-
enablePasswordReset: boolean;
|
|
21
|
-
|
|
22
38
|
// 2FA settings (public - no secrets)
|
|
23
39
|
require2FA: boolean;
|
|
24
40
|
allowed2FAMethods: string[];
|
|
41
|
+
|
|
42
|
+
// allowPublicRegistration / enablePasswordReset: OPTIONAL, never invented.
|
|
43
|
+
//
|
|
44
|
+
// 257255 removed these outright on the measurement that AuthSettings
|
|
45
|
+
// "carries no such fields, so they cannot be read today", and left one
|
|
46
|
+
// instruction: "Re-add them ONLY when the config document actually carries
|
|
47
|
+
// them." That precondition is now MET. The 240447 fix landed on master
|
|
48
|
+
// after 257255 was written and taught the loader to read them - AuthSettings
|
|
49
|
+
// now declares allowPublicRegistration?/enablePasswordReset? and
|
|
50
|
+
// idp-client-config maps them from auth_settings.allow_public_registration /
|
|
51
|
+
// enable_password_reset at BOTH construction sites. Re-adding them here is
|
|
52
|
+
// that instruction being followed, not overridden: deleting them now would
|
|
53
|
+
// discard a real measurement the loader already performs.
|
|
54
|
+
//
|
|
55
|
+
// They are OPTIONAL rather than defaulted, which is the part 240447 got
|
|
56
|
+
// wrong. Its `?? true` was defended as "preserves today's behaviour ...
|
|
57
|
+
// cannot regress an older IDP build", but `true` is a PERMISSION CLAIM about
|
|
58
|
+
// a tenant we did not successfully ask - the same defect class this card
|
|
59
|
+
// exists to kill, and the exact literal named in its title
|
|
60
|
+
// (allowPublicRegistration:TRUE). Absent means the IDP did not say; a
|
|
61
|
+
// consumer must then decide, and cannot mistake a guess for a policy.
|
|
62
|
+
//
|
|
63
|
+
// They are NOT validated into a hard 503 like require2FA below, because an
|
|
64
|
+
// older IDP build may legitimately omit them. Absent-and-honest is correct
|
|
65
|
+
// there; absent-and-fabricated is not.
|
|
66
|
+
allowPublicRegistration?: boolean;
|
|
67
|
+
enablePasswordReset?: boolean;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Stable, machine-readable failure codes. Consumers branch on these. */
|
|
71
|
+
export type AuthSettingsErrorCode = 'IDP_UNREACHABLE' | 'IDP_CONFIG_INVALID';
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Fail closed and LOUD. 503 + success:false + a code, and CRUCIALLY no policy
|
|
75
|
+
* fields at all - not defaults, not partials. A consumer cannot mistake an
|
|
76
|
+
* absent body for an answer, which is exactly the mistake the old shape invited.
|
|
77
|
+
*/
|
|
78
|
+
function policyUnavailable(code: AuthSettingsErrorCode, message: string) {
|
|
79
|
+
return NextResponse.json(
|
|
80
|
+
{ success: false, error: { code, message } },
|
|
81
|
+
{ status: 503, headers: { 'Cache-Control': 'no-store' } },
|
|
82
|
+
);
|
|
25
83
|
}
|
|
26
84
|
|
|
27
85
|
/**
|
|
28
86
|
* GET /api/auth/settings
|
|
29
87
|
*
|
|
30
|
-
* Returns public auth settings for the current client
|
|
31
|
-
*
|
|
88
|
+
* Returns public auth settings for the current client, or 503 if the real
|
|
89
|
+
* policy could not be read. It never answers from a default.
|
|
32
90
|
*/
|
|
33
91
|
export async function GET() {
|
|
92
|
+
let config;
|
|
34
93
|
try {
|
|
35
|
-
|
|
94
|
+
config = await getIDPClientConfig();
|
|
95
|
+
} catch (error) {
|
|
96
|
+
// Keep the loud line - it is what finally unmasked this in production.
|
|
97
|
+
console.error('[AUTH_SETTINGS] Failed to get settings:', error);
|
|
98
|
+
// The `degraded: true` shape that used to live here (240447) is gone
|
|
99
|
+
// deliberately. It still answered `success: true` with a full invented
|
|
100
|
+
// policy and merely LABELLED the guess - which BAPert v32 rules out:
|
|
101
|
+
// THE ENDPOINT HARD-FAILS, THE PAGE DEGRADES. A flag a consumer may
|
|
102
|
+
// ignore is not a failure; 503 with no policy fields is.
|
|
103
|
+
return policyUnavailable(
|
|
104
|
+
'IDP_UNREACHABLE',
|
|
105
|
+
'Could not read client configuration from the identity service.',
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── THE CONDITION IS "DID I READ A VALIDATED POLICY", NOT "DID THE FETCH
|
|
110
|
+
// THROW". This distinction is load-bearing and is why the catch above is
|
|
111
|
+
// not sufficient on its own (QAPert, and card 257661).
|
|
112
|
+
//
|
|
113
|
+
// getIDPClientConfig caches BEFORE it validates, and the cache-hit paths
|
|
114
|
+
// do NOT re-validate. So it can RETURN NORMALLY - no throw, no log line -
|
|
115
|
+
// with a config that FAILED validation on the request that cached it.
|
|
116
|
+
// Measured on :4702 on 2026-08-24: the loader was throwing 53 times per
|
|
117
|
+
// 400 log lines while THIS endpoint served a well-formed policy from
|
|
118
|
+
// cache. A fix that only handles the throw is bypassed by the cache on
|
|
119
|
+
// every request after the first.
|
|
120
|
+
//
|
|
121
|
+
// authSecret is the invariant the loader itself enforces on the fetch
|
|
122
|
+
// path (idp-client-config: "LOAD-BEARING - empty must fail here, never
|
|
123
|
+
// downstream"). Re-checking it here is what makes a cache hit as safe as
|
|
124
|
+
// a fresh read.
|
|
125
|
+
if (!config?.authSecret) {
|
|
126
|
+
console.error(
|
|
127
|
+
'[AUTH_SETTINGS] Refusing to serve policy: config has no authSecret ' +
|
|
128
|
+
'(likely served from cache before validation - see card 257661).',
|
|
129
|
+
);
|
|
130
|
+
return policyUnavailable(
|
|
131
|
+
'IDP_CONFIG_INVALID',
|
|
132
|
+
'Client configuration is incomplete; refusing to report a policy.',
|
|
133
|
+
);
|
|
134
|
+
}
|
|
36
135
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
136
|
+
// No `??` defaults. If the policy section is absent or malformed we did not
|
|
137
|
+
// read a policy, and inventing one is the defect this card exists for.
|
|
138
|
+
const authSettings = config.authSettings;
|
|
139
|
+
if (
|
|
140
|
+
!authSettings ||
|
|
141
|
+
typeof authSettings.require2FA !== 'boolean' ||
|
|
142
|
+
!Array.isArray(authSettings.allowed2FAMethods)
|
|
143
|
+
) {
|
|
144
|
+
console.error(
|
|
145
|
+
'[AUTH_SETTINGS] Refusing to serve policy: authSettings absent or malformed.',
|
|
146
|
+
);
|
|
147
|
+
return policyUnavailable(
|
|
148
|
+
'IDP_CONFIG_INVALID',
|
|
149
|
+
'Client configuration carried no usable auth policy.',
|
|
150
|
+
);
|
|
151
|
+
}
|
|
42
152
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
allowFederatedLogin: config.oauthProviders?.some(p => p.enabled) ?? false,
|
|
153
|
+
const settings: PublicAuthSettings = {
|
|
154
|
+
enabledProviders: config.oauthProviders?.filter(p => p.enabled).map(p => p.provider) ?? [],
|
|
46
155
|
|
|
47
|
-
|
|
48
|
-
|
|
156
|
+
// Prefer the EXPLICIT policy field over inferring from the provider
|
|
157
|
+
// list (240447). Inferring is only a proxy: a tenant can have federated
|
|
158
|
+
// login switched off while enabled providers are still configured, and
|
|
159
|
+
// the inference reports `true` for it. Fall back to the inference only
|
|
160
|
+
// where the IDP does not state the field.
|
|
161
|
+
allowFederatedLogin:
|
|
162
|
+
authSettings.allowSocialLogin ??
|
|
163
|
+
(config.oauthProviders?.some(p => p.enabled) ?? false),
|
|
49
164
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
};
|
|
165
|
+
// Real reads, no `??` literal. Absent stays absent (see interface).
|
|
166
|
+
allowPublicRegistration: authSettings.allowPublicRegistration,
|
|
167
|
+
enablePasswordReset: authSettings.enablePasswordReset,
|
|
54
168
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
});
|
|
59
|
-
} catch (error) {
|
|
60
|
-
console.error('[AUTH_SETTINGS] Failed to get settings:', error);
|
|
169
|
+
require2FA: authSettings.require2FA,
|
|
170
|
+
allowed2FAMethods: authSettings.allowed2FAMethods,
|
|
171
|
+
};
|
|
61
172
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
enabledProviders: [],
|
|
67
|
-
allowPublicRegistration: true,
|
|
68
|
-
allowFederatedLogin: false,
|
|
69
|
-
enablePasswordReset: true,
|
|
70
|
-
require2FA: true,
|
|
71
|
-
allowed2FAMethods: ['email', 'sms'],
|
|
72
|
-
} as PublicAuthSettings,
|
|
73
|
-
});
|
|
74
|
-
}
|
|
173
|
+
return NextResponse.json(
|
|
174
|
+
{ success: true, data: settings },
|
|
175
|
+
{ headers: { 'Cache-Control': 'no-store' } },
|
|
176
|
+
);
|
|
75
177
|
}
|
|
@@ -74,21 +74,61 @@ async function getTenantRequiresTwoFactor(): Promise<boolean> {
|
|
|
74
74
|
* nullable require_2fa fix: "absent" has to be expressible, or callers cannot
|
|
75
75
|
* tell a real value from a default.
|
|
76
76
|
*
|
|
77
|
-
* No local default:
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
77
|
+
* No local default. 257255 UPDATE: allowed2FAMethods is now OPTIONAL on
|
|
78
|
+
* AuthSettings - the config mapper no longer substitutes ['email','sms'] when
|
|
79
|
+
* the document carries no policy, because that made "no policy" and "exactly
|
|
80
|
+
* these methods" indistinguishable. So absent now correctly reaches this
|
|
81
|
+
* function as undefined and is reported as UNKNOWN (null), which is what this
|
|
82
|
+
* function's own contract already said it wanted. Before the change it would
|
|
83
|
+
* have returned the mapper's fabrication AS IF IT WERE THE TENANT'S VALUE.
|
|
81
84
|
*/
|
|
82
85
|
async function getTenantAllowed2FAMethods(): Promise<string[] | null> {
|
|
83
86
|
try {
|
|
84
87
|
const config = await getIDPClientConfig();
|
|
85
|
-
|
|
88
|
+
// `?? null` is the whole point: undefined means the document carried no
|
|
89
|
+
// policy, and this function reports UNKNOWN rather than guessing.
|
|
90
|
+
return config.authSettings.allowed2FAMethods ?? null;
|
|
86
91
|
} catch {
|
|
87
92
|
console.warn('[VIABILITY] Could not get client config — reporting allowed 2FA methods as UNKNOWN (null), not guessing');
|
|
88
93
|
return null;
|
|
89
94
|
}
|
|
90
95
|
}
|
|
91
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Resolve the client id reported to the caller.
|
|
99
|
+
*
|
|
100
|
+
* Was: `session?.idpClientId || process.env.IDP_CLIENT_ID || ''`. Two defects,
|
|
101
|
+
* both of the "report a confident value where the truth is unknown" family that
|
|
102
|
+
* card 221076 was about:
|
|
103
|
+
*
|
|
104
|
+
* 1. `|| ''` made "we do not know this session's client" indistinguishable from
|
|
105
|
+
* "the client id is blank". Callers cannot tell absence from a known-empty
|
|
106
|
+
* value, so nothing can react to the former. Absence is now `null`, which is
|
|
107
|
+
* representable — and still falsy, so existing `if (!clientId)` guards behave
|
|
108
|
+
* exactly as before.
|
|
109
|
+
*
|
|
110
|
+
* 2. The env fallback SILENTLY substitutes the deployment's default client for a
|
|
111
|
+
* session whose own client is unknown. On a multi-tenant IDP that is a
|
|
112
|
+
* cross-tenant wrong answer, not a default. It is kept for single-tenant
|
|
113
|
+
* deployments that rely on it, but it now says so in the log rather than
|
|
114
|
+
* passing the substitution off as the session's own identity.
|
|
115
|
+
*/
|
|
116
|
+
function resolveClientId(sessionClientId: string | undefined): string | null {
|
|
117
|
+
if (sessionClientId) return sessionClientId;
|
|
118
|
+
|
|
119
|
+
const envClientId = process.env.IDP_CLIENT_ID;
|
|
120
|
+
if (envClientId) {
|
|
121
|
+
console.warn(
|
|
122
|
+
'[VIABILITY_ROUTE] session carries no idpClientId - substituting IDP_CLIENT_ID ' +
|
|
123
|
+
'from the environment. On a multi-tenant deployment this is a DIFFERENT client ' +
|
|
124
|
+
'than the session belongs to.'
|
|
125
|
+
);
|
|
126
|
+
return envClientId;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
92
132
|
/**
|
|
93
133
|
* GET /api/session/viability - Check if session is viable
|
|
94
134
|
*
|
|
@@ -165,9 +205,87 @@ export async function GET(req: NextRequest) {
|
|
|
165
205
|
// require_2fa case: absent has to stay expressible.
|
|
166
206
|
const nextStep = session?.nextStep;
|
|
167
207
|
|
|
208
|
+
/**
|
|
209
|
+
* 266007 — port the sibling's CRITICAL override (api-handlers/session/
|
|
210
|
+
* viability.ts:82-86) into this, the impl consumers actually MOUNT.
|
|
211
|
+
* An OAuth session whose callback set mfaVerified === false because no
|
|
212
|
+
* refresh_token was issued ALWAYS needs 2FA completion, whatever the
|
|
213
|
+
* tenant config says — completing 2FA is how it obtains one.
|
|
214
|
+
*
|
|
215
|
+
* STRICT === false, deliberately: an ABSENT mfaVerified (every session
|
|
216
|
+
* minted before that field existed) is not a signal, and treating it as
|
|
217
|
+
* one would force-rechallenge the entire existing session population.
|
|
218
|
+
*
|
|
219
|
+
* *** R807 (BAPert, card 362550) - WHY THE `=== false` TEST ALONE NO LONGER
|
|
220
|
+
* CARRIES THAT INTENT, AND WHAT RESTORES IT. ***
|
|
221
|
+
*
|
|
222
|
+
* The intent above is right, but by the time a session reaches here the
|
|
223
|
+
* distinction it depends on IS ALREADY GONE: every construction site
|
|
224
|
+
* (session-store, decode-session, token-lifecycle x2, SessionModel) collapses
|
|
225
|
+
* an absent mfaVerified to `false` via `?? false`. So `=== false` had stopped
|
|
226
|
+
* meaning "the callback affirmatively said false" and had come to mean
|
|
227
|
+
* "affirmatively false OR never knew" - i.e. the exact mass re-challenge this
|
|
228
|
+
* comment says it is preventing. The guard read as strict while behaving as
|
|
229
|
+
* permissive, because the collapse happens upstream and out of sight.
|
|
230
|
+
*
|
|
231
|
+
* R807 keeps `mfaVerified` fail-closed at all five sites (removing `?? false`
|
|
232
|
+
* would be an AUTH BYPASS - a session that never proved MFA read as having
|
|
233
|
+
* proved it) and gives THIS question its own signal instead. So the test is
|
|
234
|
+
* now two parts:
|
|
235
|
+
*
|
|
236
|
+
* mfaSignalPresent === true -> the producer really did emit a value, so the
|
|
237
|
+
* `false` below is a MEASUREMENT. Fire.
|
|
238
|
+
* absent / false -> LEGACY session (minted before the signal
|
|
239
|
+
* existed) or a path with no IDP tokens at all.
|
|
240
|
+
* NOT a signal. Do NOT force a re-challenge.
|
|
241
|
+
*
|
|
242
|
+
* That absent-means-do-not-fire read is the migration behaviour, and it is
|
|
243
|
+
* what preserves this comment's original guarantee verbatim through rollout:
|
|
244
|
+
* on the day this ships, EVERY existing session has no signal and none of them
|
|
245
|
+
* is re-challenged.
|
|
246
|
+
*
|
|
247
|
+
* *** AND IT IS SAFE ONLY BECAUSE IT IS CONFINED HERE. *** mfaSignalPresent is
|
|
248
|
+
* permissive by construction - it can only ever SUPPRESS this override. It must
|
|
249
|
+
* never reach a path that grants access; gate those on `mfaVerified`, which is
|
|
250
|
+
* still fail-closed everywhere. R807 is void if this signal escapes into an
|
|
251
|
+
* auth decision.
|
|
252
|
+
*
|
|
253
|
+
* Declared once here, like nextStep above, so the three authenticated
|
|
254
|
+
* responses cannot drift apart. Feeds the legacy `requires2FA` field the
|
|
255
|
+
* middleware actually gates on; `tenantRequiresTwoFactor` stays pure —
|
|
256
|
+
* it names TENANT policy, and this override is not tenant state.
|
|
257
|
+
*/
|
|
258
|
+
const oauthSessionNeedsTwoFactor =
|
|
259
|
+
session?.mfaSignalPresent === true &&
|
|
260
|
+
session?.mfaVerified === false &&
|
|
261
|
+
!(session.idpRefreshToken || (session as any)?.refreshToken);
|
|
262
|
+
|
|
168
263
|
// Check access token expiry
|
|
169
264
|
const now = Math.floor(Date.now() / 1000);
|
|
170
|
-
|
|
265
|
+
// 240654 addendum 2: fall back to the Redis session's own expiry.
|
|
266
|
+
//
|
|
267
|
+
// The first two reads come off the Better Auth session OBJECT. A Path-B
|
|
268
|
+
// synthetic session — the shape server/auth.ts builds for a legacy JWS
|
|
269
|
+
// session — carries NEITHER: it is `{session:{token}, user:{...}}` with no
|
|
270
|
+
// accessTokenExpires and no exp. So every legacy session fell into the
|
|
271
|
+
// "no expiry info" branch below and answered needsRefresh:true on EVERY
|
|
272
|
+
// viability read from the moment of login, while the token was in fact
|
|
273
|
+
// fresh (15-minute TTL measured). Benign for the viable/authenticated
|
|
274
|
+
// verdict, but it recommends a refresh constantly, which is pure perf
|
|
275
|
+
// noise and multiplies exposure to the empty-clientId refresh path (223542).
|
|
276
|
+
//
|
|
277
|
+
// lib/session-store puts the real value on the session as
|
|
278
|
+
// idpAccessTokenExpires, in MILLISECONDS (better-auth.ts:784 sets
|
|
279
|
+
// `Date.now() + expires_in * 1000`). The other two sources are in SECONDS.
|
|
280
|
+
// Mixing units here would be a silent disaster, so note that the
|
|
281
|
+
// normalisation below (`> 1e12 ? /1000 : as-is`) already discriminates
|
|
282
|
+
// them by magnitude: a seconds timestamp is ~1.7e9, a milliseconds one
|
|
283
|
+
// ~1.7e12. This fallback is therefore safe in either unit, and that
|
|
284
|
+
// existing normaliser is load-bearing for it -- do not remove it.
|
|
285
|
+
const accessTokenExpires =
|
|
286
|
+
(token as any).accessTokenExpires ||
|
|
287
|
+
token.exp ||
|
|
288
|
+
(session as any)?.idpAccessTokenExpires;
|
|
171
289
|
|
|
172
290
|
if (!accessTokenExpires) {
|
|
173
291
|
// No expiry info, assume viable but recommend refresh
|
|
@@ -199,7 +317,7 @@ export async function GET(req: NextRequest) {
|
|
|
199
317
|
tenantAllowed2FAMethods,
|
|
200
318
|
nextStep,
|
|
201
319
|
// Legacy field names for backwards compatibility
|
|
202
|
-
requires2FA: tenantRequiresTwoFactor,
|
|
320
|
+
requires2FA: tenantRequiresTwoFactor || oauthSessionNeedsTwoFactor,
|
|
203
321
|
twoFactorComplete: userHasCompletedTenantTwoFactorRequirements,
|
|
204
322
|
accessTokenExpired: false,
|
|
205
323
|
reason: 'No expiry information'
|
|
@@ -269,13 +387,13 @@ export async function GET(req: NextRequest) {
|
|
|
269
387
|
tenantAllowed2FAMethods,
|
|
270
388
|
nextStep,
|
|
271
389
|
// Legacy names for backwards compatibility
|
|
272
|
-
requires2FA: tenantRequiresTwoFactor,
|
|
390
|
+
requires2FA: tenantRequiresTwoFactor || oauthSessionNeedsTwoFactor,
|
|
273
391
|
twoFactorComplete: userHasCompletedTenantTwoFactorRequirements,
|
|
274
392
|
accessTokenExpired: true,
|
|
275
393
|
reason: 'Token expired',
|
|
276
394
|
// RBAC fields
|
|
277
395
|
roles: session?.roles || [],
|
|
278
|
-
clientId: session?.idpClientId
|
|
396
|
+
clientId: resolveClientId(session?.idpClientId),
|
|
279
397
|
});
|
|
280
398
|
}
|
|
281
399
|
|
|
@@ -293,13 +411,13 @@ export async function GET(req: NextRequest) {
|
|
|
293
411
|
tenantAllowed2FAMethods,
|
|
294
412
|
nextStep,
|
|
295
413
|
// Legacy names for backwards compatibility
|
|
296
|
-
requires2FA: tenantRequiresTwoFactor,
|
|
414
|
+
requires2FA: tenantRequiresTwoFactor || oauthSessionNeedsTwoFactor,
|
|
297
415
|
twoFactorComplete: userHasCompletedTenantTwoFactorRequirements,
|
|
298
416
|
accessTokenExpired: false,
|
|
299
417
|
expiresAt: new Date(expiryTime * 1000).toISOString(),
|
|
300
418
|
// RBAC fields
|
|
301
419
|
roles: session?.roles || [],
|
|
302
|
-
clientId: session?.idpClientId
|
|
420
|
+
clientId: resolveClientId(session?.idpClientId),
|
|
303
421
|
});
|
|
304
422
|
} catch (error) {
|
|
305
423
|
console.error('[VIABILITY_ROUTE] Error checking session viability:', error);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { POST } from '../../api-handlers/mfa/backup-code-verify';
|
|
1
|
+
export { POST } from '../../api-handlers/mfa/backup-code-verify';
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Ready-to-Use MFA Backup Codes Route
|
|
3
|
-
*
|
|
4
|
-
* @example
|
|
5
|
-
* ```typescript
|
|
6
|
-
* // app/api/mfa/backup-codes/route.ts
|
|
7
|
-
* export { POST } from '@payez/next-mvp/routes/mfa/backup-codes';
|
|
8
|
-
* ```
|
|
9
|
-
*/
|
|
10
|
-
export { POST } from '../../api-handlers/mfa/backup-codes';
|
|
1
|
+
/**
|
|
2
|
+
* Ready-to-Use MFA Backup Codes Route
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```typescript
|
|
6
|
+
* // app/api/mfa/backup-codes/route.ts
|
|
7
|
+
* export { POST } from '@payez/next-mvp/routes/mfa/backup-codes';
|
|
8
|
+
* ```
|
|
9
|
+
*/
|
|
10
|
+
export { POST } from '../../api-handlers/mfa/backup-codes';
|
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Ready-to-Use MFA Methods Route (list / remove)
|
|
3
|
-
*
|
|
4
|
-
* @example
|
|
5
|
-
* ```typescript
|
|
6
|
-
* // app/api/mfa/methods/route.ts
|
|
7
|
-
* export { GET } from '@payez/next-mvp/routes/mfa/methods';
|
|
8
|
-
* // app/api/mfa/methods/[id]/route.ts
|
|
9
|
-
* export { DELETE } from '@payez/next-mvp/routes/mfa/methods';
|
|
10
|
-
* ```
|
|
11
|
-
*/
|
|
12
|
-
import { NextRequest } from 'next/server';
|
|
13
|
-
import { GET as listMethods, DELETE as deleteMethod } from '../../api-handlers/mfa/methods';
|
|
14
|
-
|
|
15
|
-
export const GET = listMethods;
|
|
16
|
-
|
|
17
|
-
type Ctx = { params: Promise<{ id: string }> };
|
|
18
|
-
|
|
19
|
-
export async function DELETE(req: NextRequest, ctx: Ctx) {
|
|
20
|
-
const { id } = await ctx.params;
|
|
21
|
-
return deleteMethod(req, id);
|
|
22
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Ready-to-Use MFA Methods Route (list / remove)
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```typescript
|
|
6
|
+
* // app/api/mfa/methods/route.ts
|
|
7
|
+
* export { GET } from '@payez/next-mvp/routes/mfa/methods';
|
|
8
|
+
* // app/api/mfa/methods/[id]/route.ts
|
|
9
|
+
* export { DELETE } from '@payez/next-mvp/routes/mfa/methods';
|
|
10
|
+
* ```
|
|
11
|
+
*/
|
|
12
|
+
import { NextRequest } from 'next/server';
|
|
13
|
+
import { GET as listMethods, DELETE as deleteMethod } from '../../api-handlers/mfa/methods';
|
|
14
|
+
|
|
15
|
+
export const GET = listMethods;
|
|
16
|
+
|
|
17
|
+
type Ctx = { params: Promise<{ id: string }> };
|
|
18
|
+
|
|
19
|
+
export async function DELETE(req: NextRequest, ctx: Ctx) {
|
|
20
|
+
const { id } = await ctx.params;
|
|
21
|
+
return deleteMethod(req, id);
|
|
22
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Ready-to-Use TOTP Confirm Route
|
|
3
|
-
*
|
|
4
|
-
* @example
|
|
5
|
-
* ```typescript
|
|
6
|
-
* // app/api/mfa/totp/confirm/route.ts
|
|
7
|
-
* export { POST } from '@payez/next-mvp/routes/mfa/totp-confirm';
|
|
8
|
-
* ```
|
|
9
|
-
*/
|
|
10
|
-
export { POST } from '../../api-handlers/mfa/totp-confirm';
|
|
1
|
+
/**
|
|
2
|
+
* Ready-to-Use TOTP Confirm Route
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```typescript
|
|
6
|
+
* // app/api/mfa/totp/confirm/route.ts
|
|
7
|
+
* export { POST } from '@payez/next-mvp/routes/mfa/totp-confirm';
|
|
8
|
+
* ```
|
|
9
|
+
*/
|
|
10
|
+
export { POST } from '../../api-handlers/mfa/totp-confirm';
|