@payez/next-mvp 4.1.11 → 4.1.20
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/api-handlers/account/profile.d.ts +38 -0
- package/dist/api-handlers/account/profile.js +73 -45
- package/dist/api-handlers/account/recovery/send-code.d.ts +33 -0
- package/dist/api-handlers/account/recovery/send-code.js +52 -18
- package/dist/api-handlers/account/recovery/verify-code.d.ts +33 -0
- package/dist/api-handlers/account/recovery/verify-code.js +52 -18
- package/dist/api-handlers/account/verify-email.js +15 -1
- package/dist/api-handlers/account/verify-sms.js +15 -1
- package/dist/api-handlers/auth/passkey-signin.d.ts +29 -0
- package/dist/api-handlers/auth/passkey-signin.js +97 -0
- package/dist/api-handlers/mfa/backup-code-verify.d.ts +73 -0
- package/dist/api-handlers/mfa/backup-code-verify.js +153 -0
- package/dist/api-handlers/mfa/backup-codes.d.ts +7 -0
- package/dist/api-handlers/mfa/backup-codes.js +40 -0
- package/dist/api-handlers/mfa/methods.d.ts +3 -0
- package/dist/api-handlers/mfa/methods.js +41 -0
- package/dist/api-handlers/mfa/totp-confirm.d.ts +8 -0
- package/dist/api-handlers/mfa/totp-confirm.js +41 -0
- package/dist/api-handlers/mfa/totp-enroll.d.ts +8 -0
- package/dist/api-handlers/mfa/totp-enroll.js +41 -0
- package/dist/api-handlers/mfa/totp-verify.d.ts +47 -0
- package/dist/api-handlers/mfa/totp-verify.js +134 -0
- package/dist/api-handlers/passkeys/list.d.ts +7 -0
- package/dist/api-handlers/passkeys/list.js +29 -0
- package/dist/api-handlers/passkeys/manage.d.ts +3 -0
- package/dist/api-handlers/passkeys/manage.js +54 -0
- package/dist/api-handlers/passkeys/register-options.d.ts +8 -0
- package/dist/api-handlers/passkeys/register-options.js +41 -0
- package/dist/api-handlers/passkeys/register-verify.d.ts +7 -0
- package/dist/api-handlers/passkeys/register-verify.js +40 -0
- package/dist/api-handlers/session/viability.js +19 -0
- package/dist/auth/better-auth.d.ts +68 -5
- package/dist/auth/better-auth.js +256 -42
- package/dist/auth/utils/idp-client.d.ts +13 -22
- package/dist/auth/utils/idp-client.js +12 -82
- package/dist/client/better-auth-client.d.ts +14 -2
- package/dist/lib/session-store.js +28 -21
- package/dist/lib/startup-init.js +19 -19
- package/dist/lib/token-lifecycle.js +40 -4
- package/dist/models/SessionModel.d.ts +17 -0
- package/dist/pages/login/page.d.ts +10 -1
- package/dist/pages/login/page.js +69 -80
- package/dist/pages/verify-code/page.d.ts +10 -1
- package/dist/pages/verify-code/page.js +72 -4
- package/dist/routes/auth/passkey-signin.d.ts +13 -0
- package/dist/routes/auth/passkey-signin.js +18 -0
- package/dist/routes/auth/viability.d.ts +2 -0
- package/dist/routes/auth/viability.js +88 -0
- package/dist/routes/mfa/backup-code-verify.d.ts +1 -0
- package/dist/routes/mfa/backup-code-verify.js +5 -0
- package/dist/routes/mfa/backup-codes.d.ts +10 -0
- package/dist/routes/mfa/backup-codes.js +14 -0
- package/dist/routes/mfa/methods.d.ts +21 -0
- package/dist/routes/mfa/methods.js +10 -0
- package/dist/routes/mfa/totp-confirm.d.ts +10 -0
- package/dist/routes/mfa/totp-confirm.js +14 -0
- package/dist/routes/mfa/totp-enroll.d.ts +10 -0
- package/dist/routes/mfa/totp-enroll.js +14 -0
- package/dist/routes/mfa/totp-verify.d.ts +14 -0
- package/dist/routes/mfa/totp-verify.js +18 -0
- package/dist/routes/passkeys/list.d.ts +10 -0
- package/dist/routes/passkeys/list.js +14 -0
- package/dist/routes/passkeys/manage.d.ts +18 -0
- package/dist/routes/passkeys/manage.js +13 -0
- package/dist/routes/passkeys/register-options.d.ts +10 -0
- package/dist/routes/passkeys/register-options.js +14 -0
- package/dist/routes/passkeys/register-verify.d.ts +10 -0
- package/dist/routes/passkeys/register-verify.js +14 -0
- package/dist/server/auth.d.ts +2 -2
- package/dist/server/decode-session.js +37 -3
- package/package.json +958 -893
- package/scripts/check-internal-url-usage.sh +73 -73
- package/src/api/index.ts +17 -17
- package/src/api-handlers/account/masked-info.ts +45 -45
- package/src/api-handlers/account/profile.ts +99 -80
- package/src/api-handlers/account/recovery/initiate.ts +23 -23
- package/src/api-handlers/account/recovery/send-code.ts +73 -25
- package/src/api-handlers/account/recovery/verify-code.ts +73 -25
- package/src/api-handlers/account/reset-password.ts +23 -23
- package/src/api-handlers/account/send-code.ts +76 -76
- package/src/api-handlers/account/validate-password.ts +118 -118
- package/src/api-handlers/account/verify-email.ts +15 -1
- package/src/api-handlers/account/verify-sms.ts +15 -1
- package/src/api-handlers/admin/stats.ts +249 -249
- package/src/api-handlers/anon/preferences.ts +123 -123
- package/src/api-handlers/auth/jwks.ts +20 -20
- package/src/api-handlers/auth/login.ts +240 -240
- package/src/api-handlers/auth/passkey-signin.ts +98 -0
- package/src/api-handlers/auth/validate.ts +43 -43
- package/src/api-handlers/mfa/backup-code-verify.ts +175 -0
- package/src/api-handlers/mfa/backup-codes.ts +41 -0
- package/src/api-handlers/mfa/methods.ts +46 -0
- package/src/api-handlers/mfa/totp-confirm.ts +42 -0
- package/src/api-handlers/mfa/totp-enroll.ts +42 -0
- package/src/api-handlers/mfa/totp-verify.ts +156 -0
- package/src/api-handlers/passkeys/list.ts +31 -0
- package/src/api-handlers/passkeys/manage.ts +54 -0
- package/src/api-handlers/passkeys/register-options.ts +42 -0
- package/src/api-handlers/passkeys/register-verify.ts +41 -0
- package/src/api-handlers/session/viability.ts +161 -142
- package/src/auth/better-auth.ts +247 -6
- package/src/auth/unauthenticated-routes.ts +2 -2
- package/src/auth/utils/idp-client.ts +13 -95
- package/src/client/better-auth-client.ts +70 -70
- package/src/client/fetchWithSession.ts +21 -21
- package/src/client/index.ts +13 -13
- package/src/client/useAnonSession.ts +131 -131
- package/src/components/admin/VibeAdminContext.tsx +87 -87
- package/src/config/logging-config.ts +82 -82
- package/src/config/unauthenticated-routes.ts +19 -19
- package/src/edge/internal-api-url.ts +65 -65
- package/src/edge/middleware.ts +42 -42
- package/src/hooks/useProfile.ts +75 -75
- package/src/lib/anon-session.ts +213 -213
- package/src/lib/auth-secret.ts +137 -137
- package/src/lib/demo-mode.ts +17 -17
- package/src/lib/idp-client-config.ts +559 -559
- package/src/lib/idp-fetch.ts +126 -126
- package/src/lib/internal-api.ts +171 -171
- package/src/lib/jwt-decode-client.ts +45 -45
- package/src/lib/rate-limit-service.ts +9 -9
- package/src/lib/redis.ts +26 -26
- package/src/lib/refresh-token-validator.ts +64 -64
- package/src/lib/secret-validation.ts +8 -8
- package/src/lib/session-store.ts +702 -695
- package/src/lib/session.ts +34 -34
- package/src/lib/startup-init.ts +251 -251
- package/src/lib/token-expiry.ts +40 -40
- package/src/lib/token-lifecycle.ts +533 -497
- package/src/lib/types/api-responses.ts +336 -336
- package/src/middleware/auth-decision.ts +43 -43
- package/src/models/DecodedAccessToken.ts +16 -16
- package/src/models/SessionModel.ts +282 -264
- package/src/pages/admin-roles/index.ts +9 -9
- package/src/pages/login/page.tsx +123 -228
- package/src/pages/security/EnhancedSecurityPage.tsx +9 -9
- package/src/pages/verify-code/page.tsx +71 -5
- package/src/routes/account/index.ts +31 -31
- package/src/routes/account/masked-info.ts +36 -36
- package/src/routes/account/send-code.ts +39 -39
- package/src/routes/account/verify-email.ts +40 -40
- package/src/routes/account/verify-sms.ts +40 -40
- package/src/routes/auth/index.ts +22 -22
- package/src/routes/auth/passkey-signin.ts +13 -0
- package/src/routes/auth/refresh.ts +48 -48
- package/src/routes/auth/session.ts +166 -166
- package/src/routes/auth/viability.ts +89 -0
- package/src/routes/index.ts +17 -17
- package/src/routes/mfa/backup-code-verify.ts +1 -0
- package/src/routes/mfa/backup-codes.ts +10 -0
- package/src/routes/mfa/methods.ts +22 -0
- package/src/routes/mfa/totp-confirm.ts +10 -0
- package/src/routes/mfa/totp-enroll.ts +10 -0
- package/src/routes/mfa/totp-verify.ts +14 -0
- package/src/routes/passkeys/list.ts +10 -0
- package/src/routes/passkeys/manage.ts +23 -0
- package/src/routes/passkeys/register-options.ts +10 -0
- package/src/routes/passkeys/register-verify.ts +10 -0
- package/src/routes/session/index.ts +7 -7
- package/src/routes/session/refresh-viability.ts +17 -17
- package/src/server/auth.ts +373 -373
- package/src/server/decode-session.ts +235 -202
- package/src/services/signalrActivityService.ts +258 -258
- package/src/theme/ThemeProvider.tsx +39 -39
- package/src/theme/default.ts +33 -33
- package/src/theme/index.ts +31 -31
- package/src/theme/types.ts +69 -69
- package/src/theme/useTheme.ts +57 -57
- package/src/theme/utils.ts +40 -40
- package/src/types/api.ts +12 -12
- package/src/types/auth.d.ts +23 -23
- package/src/types/auth.ts +22 -22
- package/src/types/logging.ts +11 -11
- package/src/types/recovery.ts +54 -54
- package/src/utils/api.ts +353 -353
- package/src/utils/circuitBreaker.ts +40 -40
- package/src/utils/error-message.ts +108 -108
- package/src/utils/layout/reservedSpace.ts +124 -124
|
@@ -1,3 +1,41 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
/**
|
|
3
|
+
* Account Profile API Handler
|
|
4
|
+
*
|
|
5
|
+
* GET /api/account/profile - Get user profile
|
|
6
|
+
* PUT /api/account/profile - Update user profile
|
|
7
|
+
*
|
|
8
|
+
* WHY THIS WAS REWRITTEN (2026-08-02, QAPert defect #7)
|
|
9
|
+
* Reported as "/api/account/profile 401s on the same cookie masked-info
|
|
10
|
+
* accepts (200)". Two endpoints disagreeing about one cookie meant two auth
|
|
11
|
+
* implementations, and they were:
|
|
12
|
+
*
|
|
13
|
+
* masked-info.ts idpFetchJSON(req, ...) resolves the caller's token
|
|
14
|
+
* from the server-side session
|
|
15
|
+
* profile.ts req.headers.get('authorization') required an INBOUND bearer
|
|
16
|
+
*
|
|
17
|
+
* A browser sends a session COOKIE and no Authorization header, so the old
|
|
18
|
+
* profile.ts 401'd EVERY browser request by construction. Nothing was revoked
|
|
19
|
+
* and no token was stale — the handler demanded a credential the app does not
|
|
20
|
+
* use.
|
|
21
|
+
*
|
|
22
|
+
* THE OLD AUTH CHECK WAS A VENEER. It tested that the header was PRESENT, never
|
|
23
|
+
* that it was valid, then forwarded it upstream unexamined. Any string at all
|
|
24
|
+
* got past it and the IDP did the real work. It looked like enforcement and
|
|
25
|
+
* enforced nothing.
|
|
26
|
+
*
|
|
27
|
+
* WHAT THE WORKAROUND COST US: consumers could not call this with a cookie, so
|
|
28
|
+
* the graft's useProfile hook fetched the raw IDP access token from
|
|
29
|
+
* /api/session/idp-token INTO BROWSER JAVASCRIPT and re-sent it as a bearer.
|
|
30
|
+
* That defeats the reason idpFetchJSON exists — it keeps the token server-side
|
|
31
|
+
* precisely so client-side script can never hold it. The bearer-only handler
|
|
32
|
+
* did not merely fail; it pushed its callers into exposing the token.
|
|
33
|
+
*
|
|
34
|
+
* Switching to idpFetchJSON makes the cookie work, so the hook's token round
|
|
35
|
+
* trip can be deleted and /api/session/idp-token drops to a single deliberate
|
|
36
|
+
* caller: the jwt-inspect debug page.
|
|
37
|
+
*
|
|
38
|
+
* @package @payez/next-mvp
|
|
39
|
+
*/
|
|
2
40
|
export declare function GET(req: NextRequest): Promise<NextResponse<any>>;
|
|
3
41
|
export declare function PUT(req: NextRequest): Promise<NextResponse<any>>;
|
|
@@ -3,61 +3,89 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.GET = GET;
|
|
4
4
|
exports.PUT = PUT;
|
|
5
5
|
const server_1 = require("next/server");
|
|
6
|
+
const idp_fetch_1 = require("../../lib/idp-fetch");
|
|
7
|
+
const env_1 = require("../../config/env");
|
|
6
8
|
/**
|
|
7
9
|
* Account Profile API Handler
|
|
8
|
-
* Simple proxy to IDP profile endpoint
|
|
9
10
|
*
|
|
10
|
-
* GET /api/account/profile
|
|
11
|
-
* PUT /api/account/profile
|
|
11
|
+
* GET /api/account/profile - Get user profile
|
|
12
|
+
* PUT /api/account/profile - Update user profile
|
|
13
|
+
*
|
|
14
|
+
* WHY THIS WAS REWRITTEN (2026-08-02, QAPert defect #7)
|
|
15
|
+
* Reported as "/api/account/profile 401s on the same cookie masked-info
|
|
16
|
+
* accepts (200)". Two endpoints disagreeing about one cookie meant two auth
|
|
17
|
+
* implementations, and they were:
|
|
18
|
+
*
|
|
19
|
+
* masked-info.ts idpFetchJSON(req, ...) resolves the caller's token
|
|
20
|
+
* from the server-side session
|
|
21
|
+
* profile.ts req.headers.get('authorization') required an INBOUND bearer
|
|
22
|
+
*
|
|
23
|
+
* A browser sends a session COOKIE and no Authorization header, so the old
|
|
24
|
+
* profile.ts 401'd EVERY browser request by construction. Nothing was revoked
|
|
25
|
+
* and no token was stale — the handler demanded a credential the app does not
|
|
26
|
+
* use.
|
|
27
|
+
*
|
|
28
|
+
* THE OLD AUTH CHECK WAS A VENEER. It tested that the header was PRESENT, never
|
|
29
|
+
* that it was valid, then forwarded it upstream unexamined. Any string at all
|
|
30
|
+
* got past it and the IDP did the real work. It looked like enforcement and
|
|
31
|
+
* enforced nothing.
|
|
32
|
+
*
|
|
33
|
+
* WHAT THE WORKAROUND COST US: consumers could not call this with a cookie, so
|
|
34
|
+
* the graft's useProfile hook fetched the raw IDP access token from
|
|
35
|
+
* /api/session/idp-token INTO BROWSER JAVASCRIPT and re-sent it as a bearer.
|
|
36
|
+
* That defeats the reason idpFetchJSON exists — it keeps the token server-side
|
|
37
|
+
* precisely so client-side script can never hold it. The bearer-only handler
|
|
38
|
+
* did not merely fail; it pushed its callers into exposing the token.
|
|
39
|
+
*
|
|
40
|
+
* Switching to idpFetchJSON makes the cookie work, so the hook's token round
|
|
41
|
+
* trip can be deleted and /api/session/idp-token drops to a single deliberate
|
|
42
|
+
* caller: the jwt-inspect debug page.
|
|
43
|
+
*
|
|
44
|
+
* @package @payez/next-mvp
|
|
12
45
|
*/
|
|
13
|
-
function getIdpUrl() {
|
|
14
|
-
const url = process.env.IDP_URL;
|
|
15
|
-
if (!url) {
|
|
16
|
-
throw new Error('[IDP_URL] FATAL: IDP_URL environment variable is REQUIRED.');
|
|
17
|
-
}
|
|
18
|
-
return url;
|
|
19
|
-
}
|
|
20
46
|
async function GET(req) {
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
47
|
+
const result = await (0, idp_fetch_1.idpFetchJSON)(req, `${env_1.ENV_CONFIG.IDP_URL}/api/Account/profile`, {
|
|
48
|
+
method: 'GET',
|
|
49
|
+
headers: { 'Content-Type': 'application/json' },
|
|
50
|
+
});
|
|
51
|
+
if (!result.ok) {
|
|
52
|
+
return server_1.NextResponse.json({
|
|
53
|
+
success: false,
|
|
54
|
+
error: {
|
|
55
|
+
code: 'UPSTREAM_SERVICE_ERROR',
|
|
56
|
+
message: result.json?.message || 'Failed to fetch profile',
|
|
57
|
+
status: result.status,
|
|
32
58
|
},
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
return server_1.NextResponse.json(data, { status: upstream.status });
|
|
36
|
-
}
|
|
37
|
-
catch (error) {
|
|
38
|
-
return server_1.NextResponse.json({ success: false, error: { code: 'UPSTREAM_ERROR', message: 'Failed to fetch profile' } }, { status: 500 });
|
|
59
|
+
meta: { attemptedRefresh: result.attemptedRefresh },
|
|
60
|
+
}, { status: result.status });
|
|
39
61
|
}
|
|
62
|
+
return server_1.NextResponse.json(result.json, { status: 200 });
|
|
40
63
|
}
|
|
41
64
|
async function PUT(req) {
|
|
42
|
-
|
|
43
|
-
const authHeader = req.headers.get('authorization');
|
|
44
|
-
if (!authHeader) {
|
|
45
|
-
return server_1.NextResponse.json({ success: false, error: { code: 'UNAUTHORIZED', message: 'Authentication required' } }, { status: 401 });
|
|
46
|
-
}
|
|
65
|
+
let body = '{}';
|
|
47
66
|
try {
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
headers: {
|
|
52
|
-
'Content-Type': 'application/json',
|
|
53
|
-
'Authorization': authHeader,
|
|
54
|
-
},
|
|
55
|
-
body,
|
|
56
|
-
});
|
|
57
|
-
const data = await upstream.json().catch(() => ({}));
|
|
58
|
-
return server_1.NextResponse.json(data, { status: upstream.status });
|
|
67
|
+
const raw = await req.text();
|
|
68
|
+
if (raw && raw.trim().length > 0)
|
|
69
|
+
body = raw;
|
|
59
70
|
}
|
|
60
|
-
catch
|
|
61
|
-
|
|
71
|
+
catch {
|
|
72
|
+
// Empty body is valid here; the IDP validates the payload.
|
|
73
|
+
}
|
|
74
|
+
const result = await (0, idp_fetch_1.idpFetchJSON)(req, `${env_1.ENV_CONFIG.IDP_URL}/api/Account/profile`, {
|
|
75
|
+
method: 'PUT',
|
|
76
|
+
headers: { 'Content-Type': 'application/json' },
|
|
77
|
+
body,
|
|
78
|
+
});
|
|
79
|
+
if (!result.ok) {
|
|
80
|
+
return server_1.NextResponse.json({
|
|
81
|
+
success: false,
|
|
82
|
+
error: {
|
|
83
|
+
code: 'UPSTREAM_SERVICE_ERROR',
|
|
84
|
+
message: result.json?.message || 'Failed to update profile',
|
|
85
|
+
status: result.status,
|
|
86
|
+
},
|
|
87
|
+
meta: { attemptedRefresh: result.attemptedRefresh },
|
|
88
|
+
}, { status: result.status });
|
|
62
89
|
}
|
|
90
|
+
return server_1.NextResponse.json(result.json, { status: 200 });
|
|
63
91
|
}
|
|
@@ -1,2 +1,35 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
/**
|
|
3
|
+
* Account recovery — send-code
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS WAS REWRITTEN (2026-08-02, swept out of QAPert defect #7)
|
|
6
|
+
* This handler read the caller's bearer straight off the inbound request:
|
|
7
|
+
*
|
|
8
|
+
* const auth = req.headers.get('authorization') || '';
|
|
9
|
+
* const token = auth.startsWith('Bearer ') ? auth.slice(7) : '';
|
|
10
|
+
* ... headers: { 'Authorization': `Bearer ${token}` }
|
|
11
|
+
*
|
|
12
|
+
* Browsers send a session COOKIE and no Authorization header, so `token` was
|
|
13
|
+
* ALWAYS the empty string and this always called the IDP with a literal
|
|
14
|
+
* "Bearer " and nothing after it.
|
|
15
|
+
*
|
|
16
|
+
* THAT IS WORSE THAN THE profile.ts VERSION OF THIS BUG, in two ways:
|
|
17
|
+
*
|
|
18
|
+
* 1. profile.ts at least 401'd when the header was missing. This one has no
|
|
19
|
+
* check at all — `|| ''` swallows the absence — so it proceeds to call
|
|
20
|
+
* upstream with an empty credential and returns the IDP's rejection as an
|
|
21
|
+
* opaque failure. The user sees recovery "not working", not "not
|
|
22
|
+
* authenticated", and the log shows an upstream error rather than an auth
|
|
23
|
+
* one. The defect hides itself.
|
|
24
|
+
* 2. It is BOTH HALVES of account recovery — send-code and verify-code — so
|
|
25
|
+
* the entire recovery path is unreachable, not merely degraded. This is
|
|
26
|
+
* the flow a locked-out user reaches for, i.e. it fails exactly when it is
|
|
27
|
+
* the only thing left that could help them.
|
|
28
|
+
*
|
|
29
|
+
* Fixed the same way as profile.ts and masked-info.ts: idpFetchJSON resolves
|
|
30
|
+
* the caller's token from the server-side session, so the cookie works and the
|
|
31
|
+
* token is never handed to browser JavaScript.
|
|
32
|
+
*
|
|
33
|
+
* @package @payez/next-mvp
|
|
34
|
+
*/
|
|
2
35
|
export declare function POST(req: NextRequest): Promise<NextResponse<any>>;
|
|
@@ -2,27 +2,61 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.POST = POST;
|
|
4
4
|
const server_1 = require("next/server");
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
const idp_fetch_1 = require("../../../lib/idp-fetch");
|
|
6
|
+
const env_1 = require("../../../config/env");
|
|
7
|
+
/**
|
|
8
|
+
* Account recovery — send-code
|
|
9
|
+
*
|
|
10
|
+
* WHY THIS WAS REWRITTEN (2026-08-02, swept out of QAPert defect #7)
|
|
11
|
+
* This handler read the caller's bearer straight off the inbound request:
|
|
12
|
+
*
|
|
13
|
+
* const auth = req.headers.get('authorization') || '';
|
|
14
|
+
* const token = auth.startsWith('Bearer ') ? auth.slice(7) : '';
|
|
15
|
+
* ... headers: { 'Authorization': `Bearer ${token}` }
|
|
16
|
+
*
|
|
17
|
+
* Browsers send a session COOKIE and no Authorization header, so `token` was
|
|
18
|
+
* ALWAYS the empty string and this always called the IDP with a literal
|
|
19
|
+
* "Bearer " and nothing after it.
|
|
20
|
+
*
|
|
21
|
+
* THAT IS WORSE THAN THE profile.ts VERSION OF THIS BUG, in two ways:
|
|
22
|
+
*
|
|
23
|
+
* 1. profile.ts at least 401'd when the header was missing. This one has no
|
|
24
|
+
* check at all — `|| ''` swallows the absence — so it proceeds to call
|
|
25
|
+
* upstream with an empty credential and returns the IDP's rejection as an
|
|
26
|
+
* opaque failure. The user sees recovery "not working", not "not
|
|
27
|
+
* authenticated", and the log shows an upstream error rather than an auth
|
|
28
|
+
* one. The defect hides itself.
|
|
29
|
+
* 2. It is BOTH HALVES of account recovery — send-code and verify-code — so
|
|
30
|
+
* the entire recovery path is unreachable, not merely degraded. This is
|
|
31
|
+
* the flow a locked-out user reaches for, i.e. it fails exactly when it is
|
|
32
|
+
* the only thing left that could help them.
|
|
33
|
+
*
|
|
34
|
+
* Fixed the same way as profile.ts and masked-info.ts: idpFetchJSON resolves
|
|
35
|
+
* the caller's token from the server-side session, so the cookie works and the
|
|
36
|
+
* token is never handed to browser JavaScript.
|
|
37
|
+
*
|
|
38
|
+
* @package @payez/next-mvp
|
|
39
|
+
*/
|
|
11
40
|
async function POST(req) {
|
|
12
|
-
|
|
41
|
+
let payload = {};
|
|
13
42
|
try {
|
|
14
|
-
|
|
15
|
-
const token = auth.startsWith('Bearer ') ? auth.slice(7) : '';
|
|
16
|
-
const { method } = await req.json();
|
|
17
|
-
const upstream = await fetch(`${IDP_URL}/api/Account/recovery/send-code`, {
|
|
18
|
-
method: 'POST',
|
|
19
|
-
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },
|
|
20
|
-
body: JSON.stringify({ method }),
|
|
21
|
-
});
|
|
22
|
-
const data = await upstream.json().catch(() => ({}));
|
|
23
|
-
return server_1.NextResponse.json(data, { status: upstream.status });
|
|
43
|
+
payload = await req.json();
|
|
24
44
|
}
|
|
25
45
|
catch {
|
|
26
|
-
return server_1.NextResponse.json({ success: false, error: '
|
|
46
|
+
return server_1.NextResponse.json({ success: false, error: 'invalid_request', message: 'A JSON body is required' }, { status: 400 });
|
|
47
|
+
}
|
|
48
|
+
const result = await (0, idp_fetch_1.idpFetchJSON)(req, `${env_1.ENV_CONFIG.IDP_URL}/api/Account/recovery/send-code`, {
|
|
49
|
+
method: 'POST',
|
|
50
|
+
headers: { 'Content-Type': 'application/json' },
|
|
51
|
+
body: JSON.stringify(payload),
|
|
52
|
+
});
|
|
53
|
+
if (!result.ok) {
|
|
54
|
+
return server_1.NextResponse.json({
|
|
55
|
+
success: false,
|
|
56
|
+
error: 'send_code_failed',
|
|
57
|
+
message: result.json?.message || 'Upstream error',
|
|
58
|
+
meta: { status: result.status, attemptedRefresh: result.attemptedRefresh },
|
|
59
|
+
}, { status: result.status });
|
|
27
60
|
}
|
|
61
|
+
return server_1.NextResponse.json(result.json, { status: 200 });
|
|
28
62
|
}
|
|
@@ -1,2 +1,35 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
/**
|
|
3
|
+
* Account recovery — verify-code
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS WAS REWRITTEN (2026-08-02, swept out of QAPert defect #7)
|
|
6
|
+
* This handler read the caller's bearer straight off the inbound request:
|
|
7
|
+
*
|
|
8
|
+
* const auth = req.headers.get('authorization') || '';
|
|
9
|
+
* const token = auth.startsWith('Bearer ') ? auth.slice(7) : '';
|
|
10
|
+
* ... headers: { 'Authorization': `Bearer ${token}` }
|
|
11
|
+
*
|
|
12
|
+
* Browsers send a session COOKIE and no Authorization header, so `token` was
|
|
13
|
+
* ALWAYS the empty string and this always called the IDP with a literal
|
|
14
|
+
* "Bearer " and nothing after it.
|
|
15
|
+
*
|
|
16
|
+
* THAT IS WORSE THAN THE profile.ts VERSION OF THIS BUG, in two ways:
|
|
17
|
+
*
|
|
18
|
+
* 1. profile.ts at least 401'd when the header was missing. This one has no
|
|
19
|
+
* check at all — `|| ''` swallows the absence — so it proceeds to call
|
|
20
|
+
* upstream with an empty credential and returns the IDP's rejection as an
|
|
21
|
+
* opaque failure. The user sees recovery "not working", not "not
|
|
22
|
+
* authenticated", and the log shows an upstream error rather than an auth
|
|
23
|
+
* one. The defect hides itself.
|
|
24
|
+
* 2. It is BOTH HALVES of account recovery — send-code and verify-code — so
|
|
25
|
+
* the entire recovery path is unreachable, not merely degraded. This is
|
|
26
|
+
* the flow a locked-out user reaches for, i.e. it fails exactly when it is
|
|
27
|
+
* the only thing left that could help them.
|
|
28
|
+
*
|
|
29
|
+
* Fixed the same way as profile.ts and masked-info.ts: idpFetchJSON resolves
|
|
30
|
+
* the caller's token from the server-side session, so the cookie works and the
|
|
31
|
+
* token is never handed to browser JavaScript.
|
|
32
|
+
*
|
|
33
|
+
* @package @payez/next-mvp
|
|
34
|
+
*/
|
|
2
35
|
export declare function POST(req: NextRequest): Promise<NextResponse<any>>;
|
|
@@ -2,27 +2,61 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.POST = POST;
|
|
4
4
|
const server_1 = require("next/server");
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
const idp_fetch_1 = require("../../../lib/idp-fetch");
|
|
6
|
+
const env_1 = require("../../../config/env");
|
|
7
|
+
/**
|
|
8
|
+
* Account recovery — verify-code
|
|
9
|
+
*
|
|
10
|
+
* WHY THIS WAS REWRITTEN (2026-08-02, swept out of QAPert defect #7)
|
|
11
|
+
* This handler read the caller's bearer straight off the inbound request:
|
|
12
|
+
*
|
|
13
|
+
* const auth = req.headers.get('authorization') || '';
|
|
14
|
+
* const token = auth.startsWith('Bearer ') ? auth.slice(7) : '';
|
|
15
|
+
* ... headers: { 'Authorization': `Bearer ${token}` }
|
|
16
|
+
*
|
|
17
|
+
* Browsers send a session COOKIE and no Authorization header, so `token` was
|
|
18
|
+
* ALWAYS the empty string and this always called the IDP with a literal
|
|
19
|
+
* "Bearer " and nothing after it.
|
|
20
|
+
*
|
|
21
|
+
* THAT IS WORSE THAN THE profile.ts VERSION OF THIS BUG, in two ways:
|
|
22
|
+
*
|
|
23
|
+
* 1. profile.ts at least 401'd when the header was missing. This one has no
|
|
24
|
+
* check at all — `|| ''` swallows the absence — so it proceeds to call
|
|
25
|
+
* upstream with an empty credential and returns the IDP's rejection as an
|
|
26
|
+
* opaque failure. The user sees recovery "not working", not "not
|
|
27
|
+
* authenticated", and the log shows an upstream error rather than an auth
|
|
28
|
+
* one. The defect hides itself.
|
|
29
|
+
* 2. It is BOTH HALVES of account recovery — send-code and verify-code — so
|
|
30
|
+
* the entire recovery path is unreachable, not merely degraded. This is
|
|
31
|
+
* the flow a locked-out user reaches for, i.e. it fails exactly when it is
|
|
32
|
+
* the only thing left that could help them.
|
|
33
|
+
*
|
|
34
|
+
* Fixed the same way as profile.ts and masked-info.ts: idpFetchJSON resolves
|
|
35
|
+
* the caller's token from the server-side session, so the cookie works and the
|
|
36
|
+
* token is never handed to browser JavaScript.
|
|
37
|
+
*
|
|
38
|
+
* @package @payez/next-mvp
|
|
39
|
+
*/
|
|
11
40
|
async function POST(req) {
|
|
12
|
-
|
|
41
|
+
let payload = {};
|
|
13
42
|
try {
|
|
14
|
-
|
|
15
|
-
const token = auth.startsWith('Bearer ') ? auth.slice(7) : '';
|
|
16
|
-
const { code, method } = await req.json();
|
|
17
|
-
const upstream = await fetch(`${IDP_URL}/api/Account/recovery/verify-code`, {
|
|
18
|
-
method: 'POST',
|
|
19
|
-
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },
|
|
20
|
-
body: JSON.stringify({ code, method }),
|
|
21
|
-
});
|
|
22
|
-
const data = await upstream.json().catch(() => ({}));
|
|
23
|
-
return server_1.NextResponse.json(data, { status: upstream.status });
|
|
43
|
+
payload = await req.json();
|
|
24
44
|
}
|
|
25
45
|
catch {
|
|
26
|
-
return server_1.NextResponse.json({ success: false, error: '
|
|
46
|
+
return server_1.NextResponse.json({ success: false, error: 'invalid_request', message: 'A JSON body is required' }, { status: 400 });
|
|
47
|
+
}
|
|
48
|
+
const result = await (0, idp_fetch_1.idpFetchJSON)(req, `${env_1.ENV_CONFIG.IDP_URL}/api/Account/recovery/verify-code`, {
|
|
49
|
+
method: 'POST',
|
|
50
|
+
headers: { 'Content-Type': 'application/json' },
|
|
51
|
+
body: JSON.stringify(payload),
|
|
52
|
+
});
|
|
53
|
+
if (!result.ok) {
|
|
54
|
+
return server_1.NextResponse.json({
|
|
55
|
+
success: false,
|
|
56
|
+
error: 'verify_code_failed',
|
|
57
|
+
message: result.json?.message || 'Upstream error',
|
|
58
|
+
meta: { status: result.status, attemptedRefresh: result.attemptedRefresh },
|
|
59
|
+
}, { status: result.status });
|
|
27
60
|
}
|
|
61
|
+
return server_1.NextResponse.json(result.json, { status: 200 });
|
|
28
62
|
}
|
|
@@ -44,6 +44,10 @@ async function POST(req) {
|
|
|
44
44
|
// Unwrap IDP envelope
|
|
45
45
|
const unwrappedData = result.json?.data || result.json;
|
|
46
46
|
// If we have new tokens, update the session to complete 2FA
|
|
47
|
+
// Reports whether the session ACTUALLY transitioned. Must not be assumed
|
|
48
|
+
// from a 2xx: the IDP can verify the code while we fail to transition
|
|
49
|
+
// (no tokens returned, or no resolvable session token).
|
|
50
|
+
let sessionTransitioned = false;
|
|
47
51
|
if (unwrappedData.access_token && unwrappedData.refresh_token) {
|
|
48
52
|
// Get session token from NextAuth
|
|
49
53
|
// Support both field names: sessionToken (auth.ts JWT) and redisSessionId (legacy)
|
|
@@ -81,17 +85,27 @@ async function POST(req) {
|
|
|
81
85
|
refreshTokenExpires
|
|
82
86
|
}, 'email' // Store 2FA method for refresh token flow
|
|
83
87
|
);
|
|
88
|
+
sessionTransitioned = true;
|
|
84
89
|
console.log('[VERIFY_EMAIL] Session updated successfully', {
|
|
85
90
|
accessTokenExpires: new Date(accessTokenExpires).toISOString(),
|
|
86
91
|
refreshTokenExpires: new Date(refreshTokenExpires).toISOString()
|
|
87
92
|
});
|
|
88
93
|
}
|
|
94
|
+
else {
|
|
95
|
+
console.warn('[VERIFY_EMAIL] IDP verified the code but no session token was resolvable — session NOT transitioned');
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
console.warn('[VERIFY_EMAIL] IDP returned 2xx without tokens — session NOT transitioned');
|
|
89
100
|
}
|
|
90
101
|
// Return simplified success response (don't expose tokens to client)
|
|
91
102
|
return server_1.NextResponse.json({
|
|
92
103
|
success: true,
|
|
93
104
|
verificationSuccessful: true,
|
|
94
|
-
|
|
105
|
+
// NOT a copy of `success` — reports what actually happened. A client that
|
|
106
|
+
// trusted a hardcoded true would show the user as MFA-verified while the
|
|
107
|
+
// session still says otherwise.
|
|
108
|
+
twoFactorSessionVerified: sessionTransitioned,
|
|
95
109
|
message: unwrappedData.message || 'Email code verified successfully'
|
|
96
110
|
}, { status: 200 });
|
|
97
111
|
}
|
|
@@ -44,6 +44,10 @@ async function POST(req) {
|
|
|
44
44
|
// Unwrap IDP envelope
|
|
45
45
|
const unwrappedData = result.json?.data || result.json;
|
|
46
46
|
// If we have new tokens, update the session to complete 2FA
|
|
47
|
+
// Reports whether the session ACTUALLY transitioned. Must not be assumed
|
|
48
|
+
// from a 2xx: the IDP can verify the code while we fail to transition
|
|
49
|
+
// (no tokens returned, or no resolvable session token).
|
|
50
|
+
let sessionTransitioned = false;
|
|
47
51
|
if (unwrappedData.access_token && unwrappedData.refresh_token) {
|
|
48
52
|
// Get session token from NextAuth
|
|
49
53
|
// Support both field names: sessionToken (auth.ts JWT) and redisSessionId (legacy)
|
|
@@ -81,17 +85,27 @@ async function POST(req) {
|
|
|
81
85
|
refreshTokenExpires
|
|
82
86
|
}, 'sms' // Store 2FA method for refresh token flow
|
|
83
87
|
);
|
|
88
|
+
sessionTransitioned = true;
|
|
84
89
|
console.log('[VERIFY_SMS] Session updated successfully', {
|
|
85
90
|
accessTokenExpires: new Date(accessTokenExpires).toISOString(),
|
|
86
91
|
refreshTokenExpires: new Date(refreshTokenExpires).toISOString()
|
|
87
92
|
});
|
|
88
93
|
}
|
|
94
|
+
else {
|
|
95
|
+
console.warn('[VERIFY_SMS] IDP verified the code but no session token was resolvable — session NOT transitioned');
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
console.warn('[VERIFY_SMS] IDP returned 2xx without tokens — session NOT transitioned');
|
|
89
100
|
}
|
|
90
101
|
// Return simplified success response (don't expose tokens to client)
|
|
91
102
|
return server_1.NextResponse.json({
|
|
92
103
|
success: true,
|
|
93
104
|
verificationSuccessful: true,
|
|
94
|
-
|
|
105
|
+
// NOT a copy of `success` — reports what actually happened. A client that
|
|
106
|
+
// trusted a hardcoded true would show the user as MFA-verified while the
|
|
107
|
+
// session still says otherwise.
|
|
108
|
+
twoFactorSessionVerified: sessionTransitioned,
|
|
95
109
|
message: unwrappedData.message || 'SMS code verified successfully'
|
|
96
110
|
}, { status: 200 });
|
|
97
111
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
export declare function optionsPOST(req: NextRequest): Promise<NextResponse<any>>;
|
|
3
|
+
/**
|
|
4
|
+
* VERIFY DOES NOT USE anonCall — IT MUST RESHAPE, NOT SPREAD.
|
|
5
|
+
*
|
|
6
|
+
* The IDP contract is three fields (PasskeyModels.cs:36 PasskeyAuthVerifyRequest):
|
|
7
|
+
* assertion_id : string
|
|
8
|
+
* response : the WHOLE AuthenticatorAssertionRawResponse from
|
|
9
|
+
* navigator.credentials.get()
|
|
10
|
+
* client_id : string
|
|
11
|
+
*
|
|
12
|
+
* anonCall SPREADS the caller's body (`{ client_id, ...parsed }`). For options
|
|
13
|
+
* that is harmless. For verify it is fatal: spreading an assertion hoists
|
|
14
|
+
* id/rawId/type to the top level and leaves `response` holding only the INNER
|
|
15
|
+
* {clientDataJSON, authenticatorData, signature, userHandle}. The IDP's guard
|
|
16
|
+
* then answers "response is required" in 4-12ms WITHOUT REACHING THE CRYPTO —
|
|
17
|
+
* a timing signature DotNetPert used to prove the caller was still spreading
|
|
18
|
+
* long after the graft-side plugin had been fixed.
|
|
19
|
+
*
|
|
20
|
+
* This route and the Better Auth plugin endpoint are TWO PATHS to the same IDP
|
|
21
|
+
* endpoint. Fixing only the plugin left this one live, spreading, and reachable
|
|
22
|
+
* on a URL one segment away from the correct one — which is what kept the
|
|
23
|
+
* sign-in leg red across several runs.
|
|
24
|
+
*
|
|
25
|
+
* Accepts the assertion either already nested (`{ assertion_id, response }`) or
|
|
26
|
+
* flat (`{ assertion_id, id, rawId, type, response }`) and normalises both, so a
|
|
27
|
+
* caller written against either shape works.
|
|
28
|
+
*/
|
|
29
|
+
export declare function verifyPOST(req: NextRequest): Promise<NextResponse<any>>;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.optionsPOST = optionsPOST;
|
|
4
|
+
exports.verifyPOST = verifyPOST;
|
|
5
|
+
const server_1 = require("next/server");
|
|
6
|
+
const env_1 = require("../../config/env");
|
|
7
|
+
const idp_client_config_1 = require("../../lib/idp-client-config");
|
|
8
|
+
/**
|
|
9
|
+
* Passkey SIGN-IN ceremony (ANONYMOUS — no session exists yet):
|
|
10
|
+
* optionsPOST -> POST /api/ExternalAuth/passwordless/passkey/options
|
|
11
|
+
* verifyPOST -> POST /api/ExternalAuth/passwordless/passkey/verify
|
|
12
|
+
*
|
|
13
|
+
* Anon idiom copied from auth/better-auth.ts (exchangeOAuthForIdpTokens):
|
|
14
|
+
* direct fetch to the IDP, client identified by client_id from the resolved
|
|
15
|
+
* IDP client config (slug preferred, numeric id fallback) — sent BOTH as the
|
|
16
|
+
* X-Client-Id header (the probe-verified routing key) and merged into the
|
|
17
|
+
* body when the caller did not supply one.
|
|
18
|
+
*
|
|
19
|
+
* The verify response is passed through WITHOUT unwrapping: this is a
|
|
20
|
+
* sign-in, its payload carries the grant the caller's session layer consumes.
|
|
21
|
+
*/
|
|
22
|
+
async function clientKey() {
|
|
23
|
+
const config = await (0, idp_client_config_1.getIDPClientConfig)();
|
|
24
|
+
return config.clientSlug || String(config.clientId);
|
|
25
|
+
}
|
|
26
|
+
async function anonCall(req, path) {
|
|
27
|
+
let parsed = {};
|
|
28
|
+
try {
|
|
29
|
+
const raw = await req.text();
|
|
30
|
+
if (raw && raw.trim().length > 0)
|
|
31
|
+
parsed = JSON.parse(raw);
|
|
32
|
+
}
|
|
33
|
+
catch { }
|
|
34
|
+
const key = await clientKey();
|
|
35
|
+
const res = await fetch(`${env_1.ENV_CONFIG.IDP_URL}${path}`, {
|
|
36
|
+
method: 'POST',
|
|
37
|
+
headers: { 'Content-Type': 'application/json', 'X-Client-Id': key },
|
|
38
|
+
body: JSON.stringify({ client_id: key, ...parsed }),
|
|
39
|
+
cache: 'no-store',
|
|
40
|
+
});
|
|
41
|
+
const json = await res.json().catch(() => null);
|
|
42
|
+
return { res, json };
|
|
43
|
+
}
|
|
44
|
+
async function optionsPOST(req) {
|
|
45
|
+
const { res, json } = await anonCall(req, '/api/ExternalAuth/passwordless/passkey/options');
|
|
46
|
+
return server_1.NextResponse.json(json ?? { success: false, message: 'Empty upstream response' }, { status: res.status });
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* VERIFY DOES NOT USE anonCall — IT MUST RESHAPE, NOT SPREAD.
|
|
50
|
+
*
|
|
51
|
+
* The IDP contract is three fields (PasskeyModels.cs:36 PasskeyAuthVerifyRequest):
|
|
52
|
+
* assertion_id : string
|
|
53
|
+
* response : the WHOLE AuthenticatorAssertionRawResponse from
|
|
54
|
+
* navigator.credentials.get()
|
|
55
|
+
* client_id : string
|
|
56
|
+
*
|
|
57
|
+
* anonCall SPREADS the caller's body (`{ client_id, ...parsed }`). For options
|
|
58
|
+
* that is harmless. For verify it is fatal: spreading an assertion hoists
|
|
59
|
+
* id/rawId/type to the top level and leaves `response` holding only the INNER
|
|
60
|
+
* {clientDataJSON, authenticatorData, signature, userHandle}. The IDP's guard
|
|
61
|
+
* then answers "response is required" in 4-12ms WITHOUT REACHING THE CRYPTO —
|
|
62
|
+
* a timing signature DotNetPert used to prove the caller was still spreading
|
|
63
|
+
* long after the graft-side plugin had been fixed.
|
|
64
|
+
*
|
|
65
|
+
* This route and the Better Auth plugin endpoint are TWO PATHS to the same IDP
|
|
66
|
+
* endpoint. Fixing only the plugin left this one live, spreading, and reachable
|
|
67
|
+
* on a URL one segment away from the correct one — which is what kept the
|
|
68
|
+
* sign-in leg red across several runs.
|
|
69
|
+
*
|
|
70
|
+
* Accepts the assertion either already nested (`{ assertion_id, response }`) or
|
|
71
|
+
* flat (`{ assertion_id, id, rawId, type, response }`) and normalises both, so a
|
|
72
|
+
* caller written against either shape works.
|
|
73
|
+
*/
|
|
74
|
+
async function verifyPOST(req) {
|
|
75
|
+
let parsed = {};
|
|
76
|
+
try {
|
|
77
|
+
const raw = await req.text();
|
|
78
|
+
if (raw && raw.trim().length > 0)
|
|
79
|
+
parsed = JSON.parse(raw);
|
|
80
|
+
}
|
|
81
|
+
catch { /* empty body -> the IDP's own validation answers */ }
|
|
82
|
+
const { assertion_id: assertionId, response, ...rest } = parsed;
|
|
83
|
+
// Already-nested callers send `response` holding a full assertion (it has an
|
|
84
|
+
// `id`). Flat callers send the assertion's own fields alongside assertion_id,
|
|
85
|
+
// and their `response` is the inner attestation half — not a whole assertion.
|
|
86
|
+
const nested = response && typeof response === 'object' && 'id' in response;
|
|
87
|
+
const assertion = nested ? response : { ...rest, response };
|
|
88
|
+
const key = await clientKey();
|
|
89
|
+
const res = await fetch(`${env_1.ENV_CONFIG.IDP_URL}/api/ExternalAuth/passwordless/passkey/verify`, {
|
|
90
|
+
method: 'POST',
|
|
91
|
+
headers: { 'Content-Type': 'application/json', 'X-Client-Id': key },
|
|
92
|
+
body: JSON.stringify({ client_id: key, assertion_id: assertionId, response: assertion }),
|
|
93
|
+
cache: 'no-store',
|
|
94
|
+
});
|
|
95
|
+
const json = await res.json().catch(() => null);
|
|
96
|
+
return server_1.NextResponse.json(json ?? { success: false, message: 'Empty upstream response' }, { status: res.status });
|
|
97
|
+
}
|