@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.
Files changed (177) hide show
  1. package/POST-INSTALL.md +408 -0
  2. package/README.md +7 -0
  3. package/dist/api-handlers/account/recovery/initiate.js +35 -2
  4. package/dist/api-handlers/account/recovery/send-code.d.ts +0 -33
  5. package/dist/api-handlers/account/recovery/send-code.js +35 -2
  6. package/dist/api-handlers/account/recovery/verify-code.d.ts +0 -33
  7. package/dist/api-handlers/account/recovery/verify-code.js +35 -2
  8. package/dist/api-handlers/account/reset-password.js +35 -2
  9. package/dist/api-handlers/account/validate-password.js +21 -3
  10. package/dist/api-handlers/admin/analytics.js +2 -1
  11. package/dist/api-handlers/admin/audit.js +2 -1
  12. package/dist/api-handlers/admin/sessions.js +2 -1
  13. package/dist/api-handlers/admin/stats.js +2 -1
  14. package/dist/api-handlers/admin/users.js +2 -1
  15. package/dist/api-handlers/admin/vibe-data.js +2 -1
  16. package/dist/api-handlers/auth/jwks.js +10 -0
  17. package/dist/api-handlers/auth/login.js +5 -2
  18. package/dist/api-handlers/auth/passkey-signin.js +16 -1
  19. package/dist/api-handlers/auth/refresh.d.ts +23 -3
  20. package/dist/api-handlers/auth/refresh.js +107 -16
  21. package/dist/api-handlers/auth/signout.js +3 -2
  22. package/dist/api-handlers/auth/verify-code.js +5 -2
  23. package/dist/api-handlers/session/viability.js +27 -1
  24. package/dist/api-handlers/test/force-expire.d.ts +29 -15
  25. package/dist/api-handlers/test/force-expire.js +109 -14
  26. package/dist/auth/better-auth.d.ts +263 -6
  27. package/dist/auth/better-auth.js +437 -59
  28. package/dist/auth/utils/idp-client.js +1 -1
  29. package/dist/client/AuthContext.js +49 -17
  30. package/dist/components/account/UserAvatarMenu.js +12 -0
  31. package/dist/components/auth/ModeAwareLoginPage.js +10 -8
  32. package/dist/components/auth/ModeAwareSignupPage.js +5 -2
  33. package/dist/components/auth/TraditionalAuthSection.js +5 -1
  34. package/dist/hooks/useAuthSettings.js +14 -2
  35. package/dist/hooks/usePublicAuthSettings.d.ts +2 -2
  36. package/dist/hooks/usePublicAuthSettings.js +28 -14
  37. package/dist/lib/client-key.d.ts +61 -0
  38. package/dist/lib/client-key.js +72 -0
  39. package/dist/lib/demo-mode.js +2 -1
  40. package/dist/lib/ensure-fresh-access-token.js +12 -1
  41. package/dist/lib/idp-client-config.d.ts +25 -2
  42. package/dist/lib/idp-client-config.js +381 -36
  43. package/dist/lib/idp-fetch.d.ts +2 -0
  44. package/dist/lib/idp-fetch.js +31 -1
  45. package/dist/lib/idp-url-guard.d.ts +29 -0
  46. package/dist/lib/idp-url-guard.js +38 -0
  47. package/dist/lib/redis.js +77 -5
  48. package/dist/lib/session-cookie-shape.d.ts +69 -0
  49. package/dist/lib/session-cookie-shape.js +126 -0
  50. package/dist/lib/session-store.d.ts +35 -2
  51. package/dist/lib/session-store.js +135 -4
  52. package/dist/lib/single-flight.d.ts +35 -0
  53. package/dist/lib/single-flight.js +30 -0
  54. package/dist/lib/token-lifecycle.js +12 -0
  55. package/dist/middleware/create-middleware.js +12 -0
  56. package/dist/models/SessionModel.d.ts +41 -0
  57. package/dist/models/SessionModel.js +9 -0
  58. package/dist/pages/admin-login/page.d.ts +1 -1
  59. package/dist/pages/admin-login/page.js +5 -41
  60. package/dist/pages/security/EnhancedSecurityPage.d.ts +35 -1
  61. package/dist/pages/security/EnhancedSecurityPage.js +42 -2
  62. package/dist/pages/security/index.d.ts +1 -0
  63. package/dist/pages/security/index.js +3 -0
  64. package/dist/pages/test-env/EmergencyLogoutPage.d.ts +17 -5
  65. package/dist/pages/test-env/EmergencyLogoutPage.js +17 -5
  66. package/dist/pages/test-env/JwtInspectPage.d.ts +30 -5
  67. package/dist/pages/test-env/JwtInspectPage.js +166 -7
  68. package/dist/pages/test-env/RefreshTokenPage.d.ts +22 -6
  69. package/dist/pages/test-env/RefreshTokenPage.js +22 -6
  70. package/dist/pages/test-env/TestEnvPage.d.ts +16 -4
  71. package/dist/pages/test-env/TestEnvPage.js +16 -4
  72. package/dist/pages/test-env/guarded.d.ts +4 -0
  73. package/dist/pages/test-env/guarded.js +86 -0
  74. package/dist/pages/test-env/index.d.ts +50 -7
  75. package/dist/pages/test-env/index.js +58 -12
  76. package/dist/routes/auth/logout.js +3 -2
  77. package/dist/routes/auth/refresh.js +27 -1
  78. package/dist/routes/auth/session.d.ts +12 -3
  79. package/dist/routes/auth/session.js +92 -9
  80. package/dist/routes/auth/settings.d.ts +34 -4
  81. package/dist/routes/auth/settings.js +87 -33
  82. package/dist/routes/auth/viability.d.ts +1 -1
  83. package/dist/routes/auth/viability.js +122 -11
  84. package/dist/server/auth-guard.js +22 -15
  85. package/dist/server/auth.d.ts +170 -4
  86. package/dist/server/auth.js +42 -75
  87. package/dist/server/decode-session.js +84 -10
  88. package/dist/types/auth.d.ts +18 -0
  89. package/package.json +969 -958
  90. package/scripts/check-internal-url-usage.sh +73 -73
  91. package/scripts/check-publish-health.mjs +120 -0
  92. package/scripts/dev-broker.ps1 +71 -6
  93. package/scripts/print-client-config.mjs +162 -0
  94. package/scripts/server-only-stub.cjs +4 -0
  95. package/src/api-handlers/account/recovery/initiate.ts +35 -2
  96. package/src/api-handlers/account/recovery/send-code.ts +36 -2
  97. package/src/api-handlers/account/recovery/verify-code.ts +36 -2
  98. package/src/api-handlers/account/reset-password.ts +35 -2
  99. package/src/api-handlers/account/validate-password.ts +22 -3
  100. package/src/api-handlers/admin/analytics.ts +2 -1
  101. package/src/api-handlers/admin/audit.ts +2 -1
  102. package/src/api-handlers/admin/sessions.ts +2 -1
  103. package/src/api-handlers/admin/stats.ts +2 -1
  104. package/src/api-handlers/admin/users.ts +2 -1
  105. package/src/api-handlers/admin/vibe-data.ts +2 -1
  106. package/src/api-handlers/auth/jwks.ts +10 -0
  107. package/src/api-handlers/auth/login.ts +5 -2
  108. package/src/api-handlers/auth/passkey-signin.ts +112 -98
  109. package/src/api-handlers/auth/refresh.ts +112 -15
  110. package/src/api-handlers/auth/signout.ts +4 -3
  111. package/src/api-handlers/auth/verify-code.ts +5 -2
  112. package/src/api-handlers/mfa/backup-code-verify.ts +175 -175
  113. package/src/api-handlers/mfa/backup-codes.ts +41 -41
  114. package/src/api-handlers/mfa/methods.ts +46 -46
  115. package/src/api-handlers/mfa/totp-confirm.ts +42 -42
  116. package/src/api-handlers/mfa/totp-enroll.ts +42 -42
  117. package/src/api-handlers/passkeys/list.ts +31 -31
  118. package/src/api-handlers/passkeys/manage.ts +54 -54
  119. package/src/api-handlers/passkeys/register-options.ts +42 -42
  120. package/src/api-handlers/passkeys/register-verify.ts +41 -41
  121. package/src/api-handlers/session/viability.ts +29 -1
  122. package/src/api-handlers/test/force-expire.ts +118 -13
  123. package/src/auth/better-auth.ts +486 -51
  124. package/src/auth/utils/idp-client.ts +1 -1
  125. package/src/client/AuthContext.tsx +55 -19
  126. package/src/components/account/UserAvatarMenu.tsx +11 -0
  127. package/src/components/auth/ModeAwareLoginPage.tsx +40 -49
  128. package/src/components/auth/ModeAwareSignupPage.tsx +5 -2
  129. package/src/components/auth/TraditionalAuthSection.tsx +5 -1
  130. package/src/hooks/useAuthSettings.ts +14 -2
  131. package/src/hooks/usePublicAuthSettings.ts +38 -16
  132. package/src/lib/client-key.ts +88 -0
  133. package/src/lib/demo-mode.ts +2 -1
  134. package/src/lib/ensure-fresh-access-token.ts +12 -1
  135. package/src/lib/idp-client-config.ts +399 -36
  136. package/src/lib/idp-fetch.ts +36 -1
  137. package/src/lib/idp-url-guard.ts +37 -0
  138. package/src/lib/redis.ts +84 -6
  139. package/src/lib/session-cookie-shape.ts +184 -0
  140. package/src/lib/session-store.ts +152 -6
  141. package/src/lib/single-flight.ts +64 -0
  142. package/src/lib/token-lifecycle.ts +12 -0
  143. package/src/middleware/create-middleware.ts +11 -0
  144. package/src/models/SessionModel.ts +49 -0
  145. package/src/pages/admin-login/page.tsx +17 -127
  146. package/src/pages/security/EnhancedSecurityPage.tsx +95 -9
  147. package/src/pages/security/index.ts +3 -0
  148. package/src/pages/test-env/EmergencyLogoutPage.tsx +17 -5
  149. package/src/pages/test-env/JwtInspectPage.tsx +235 -20
  150. package/src/pages/test-env/RefreshTokenPage.tsx +22 -6
  151. package/src/pages/test-env/TestEnvPage.tsx +16 -4
  152. package/src/pages/test-env/guarded.tsx +81 -0
  153. package/src/pages/test-env/index.ts +59 -7
  154. package/src/routes/auth/logout.ts +4 -3
  155. package/src/routes/auth/passkey-signin.ts +13 -13
  156. package/src/routes/auth/refresh.ts +30 -1
  157. package/src/routes/auth/session.ts +98 -9
  158. package/src/routes/auth/settings.ts +144 -42
  159. package/src/routes/auth/viability.ts +129 -11
  160. package/src/routes/mfa/backup-code-verify.ts +1 -1
  161. package/src/routes/mfa/backup-codes.ts +10 -10
  162. package/src/routes/mfa/methods.ts +22 -22
  163. package/src/routes/mfa/totp-confirm.ts +10 -10
  164. package/src/routes/mfa/totp-enroll.ts +10 -10
  165. package/src/routes/mfa/totp-verify.ts +14 -14
  166. package/src/routes/passkeys/list.ts +10 -10
  167. package/src/routes/passkeys/manage.ts +23 -23
  168. package/src/routes/passkeys/register-options.ts +10 -10
  169. package/src/routes/passkeys/register-verify.ts +10 -10
  170. package/src/server/auth-guard.ts +24 -17
  171. package/src/server/auth.ts +54 -74
  172. package/src/server/decode-session.ts +95 -11
  173. package/src/templates/instrumentation.ts +21 -2
  174. package/src/types/auth.ts +20 -0
  175. package/dist/lib/auth-secret.d.ts +0 -17
  176. package/dist/lib/auth-secret.js +0 -116
  177. package/src/lib/auth-secret.ts +0 -137
@@ -1,46 +1,46 @@
1
- import { NextRequest, NextResponse } from 'next/server';
2
- import { idpFetchJSON } from '../../lib/idp-fetch';
3
- import { ENV_CONFIG } from '../../config/env';
4
-
5
- /**
6
- * MFA methods (GET list / DELETE one, /api/Account/mfa/methods[/{id}],
7
- * user JWT). Drives the method picker: an EMPTY list must route the user to
8
- * ENROLMENT, never to a verify screen for a factor they do not hold.
9
- */
10
-
11
- function upstreamError(result: { status: number; json: unknown; attemptedRefresh: boolean }) {
12
- return NextResponse.json(
13
- {
14
- success: false,
15
- message: 'Upstream error',
16
- error: { code: 'UPSTREAM_SERVICE_ERROR', status: result.status, details: result.json },
17
- meta: { attemptedRefresh: result.attemptedRefresh },
18
- },
19
- { status: result.status },
20
- );
21
- }
22
-
23
- function passthrough(json: unknown) {
24
- const bodyJson: any = json;
25
- if (bodyJson && typeof bodyJson === 'object' && 'success' in bodyJson && 'data' in bodyJson) {
26
- if (bodyJson.success === true) return NextResponse.json(bodyJson.data ?? { success: true }, { status: 200 });
27
- return NextResponse.json(bodyJson, { status: 200 });
28
- }
29
- return NextResponse.json(bodyJson ?? { success: true }, { status: 200 });
30
- }
31
-
32
- export async function GET(req: NextRequest) {
33
- const result = await idpFetchJSON(req, `${ENV_CONFIG.IDP_URL}/api/Account/mfa/methods`, { method: 'GET' });
34
- if (!result.ok) return upstreamError(result);
35
- return passthrough(result.json);
36
- }
37
-
38
- export async function DELETE(req: NextRequest, id: string) {
39
- const result = await idpFetchJSON(
40
- req,
41
- `${ENV_CONFIG.IDP_URL}/api/Account/mfa/methods/${encodeURIComponent(id)}`,
42
- { method: 'DELETE' },
43
- );
44
- if (!result.ok) return upstreamError(result);
45
- return passthrough(result.json);
46
- }
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { idpFetchJSON } from '../../lib/idp-fetch';
3
+ import { ENV_CONFIG } from '../../config/env';
4
+
5
+ /**
6
+ * MFA methods (GET list / DELETE one, /api/Account/mfa/methods[/{id}],
7
+ * user JWT). Drives the method picker: an EMPTY list must route the user to
8
+ * ENROLMENT, never to a verify screen for a factor they do not hold.
9
+ */
10
+
11
+ function upstreamError(result: { status: number; json: unknown; attemptedRefresh: boolean }) {
12
+ return NextResponse.json(
13
+ {
14
+ success: false,
15
+ message: 'Upstream error',
16
+ error: { code: 'UPSTREAM_SERVICE_ERROR', status: result.status, details: result.json },
17
+ meta: { attemptedRefresh: result.attemptedRefresh },
18
+ },
19
+ { status: result.status },
20
+ );
21
+ }
22
+
23
+ function passthrough(json: unknown) {
24
+ const bodyJson: any = json;
25
+ if (bodyJson && typeof bodyJson === 'object' && 'success' in bodyJson && 'data' in bodyJson) {
26
+ if (bodyJson.success === true) return NextResponse.json(bodyJson.data ?? { success: true }, { status: 200 });
27
+ return NextResponse.json(bodyJson, { status: 200 });
28
+ }
29
+ return NextResponse.json(bodyJson ?? { success: true }, { status: 200 });
30
+ }
31
+
32
+ export async function GET(req: NextRequest) {
33
+ const result = await idpFetchJSON(req, `${ENV_CONFIG.IDP_URL}/api/Account/mfa/methods`, { method: 'GET' });
34
+ if (!result.ok) return upstreamError(result);
35
+ return passthrough(result.json);
36
+ }
37
+
38
+ export async function DELETE(req: NextRequest, id: string) {
39
+ const result = await idpFetchJSON(
40
+ req,
41
+ `${ENV_CONFIG.IDP_URL}/api/Account/mfa/methods/${encodeURIComponent(id)}`,
42
+ { method: 'DELETE' },
43
+ );
44
+ if (!result.ok) return upstreamError(result);
45
+ return passthrough(result.json);
46
+ }
@@ -1,42 +1,42 @@
1
- import { NextRequest, NextResponse } from 'next/server';
2
- import { idpFetchJSON } from '../../lib/idp-fetch';
3
- import { ENV_CONFIG } from '../../config/env';
4
-
5
- /**
6
- * Confirm TOTP enrolment with the first authenticator code
7
- * (POST /api/Account/mfa/totp/confirm, user JWT). Body forwarded verbatim —
8
- * it carries the enrolment handle plus the code. Only after this succeeds is
9
- * the method ACTIVE.
10
- */
11
- export async function POST(req: NextRequest) {
12
- let body = '{}';
13
- try {
14
- const raw = await req.text();
15
- if (raw && raw.trim().length > 0) body = raw;
16
- } catch {}
17
-
18
- const result = await idpFetchJSON(req, `${ENV_CONFIG.IDP_URL}/api/Account/mfa/totp/confirm`, {
19
- method: 'POST',
20
- headers: { 'Content-Type': 'application/json' },
21
- body,
22
- });
23
-
24
- if (!result.ok) {
25
- return NextResponse.json(
26
- {
27
- success: false,
28
- message: 'Upstream error',
29
- error: { code: 'UPSTREAM_SERVICE_ERROR', status: result.status, details: result.json },
30
- meta: { attemptedRefresh: result.attemptedRefresh },
31
- },
32
- { status: result.status },
33
- );
34
- }
35
-
36
- const bodyJson: any = result.json;
37
- if (bodyJson && typeof bodyJson === 'object' && 'success' in bodyJson && 'data' in bodyJson) {
38
- if (bodyJson.success === true) return NextResponse.json(bodyJson.data, { status: 200 });
39
- return NextResponse.json(bodyJson, { status: 200 });
40
- }
41
- return NextResponse.json(bodyJson ?? {}, { status: 200 });
42
- }
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { idpFetchJSON } from '../../lib/idp-fetch';
3
+ import { ENV_CONFIG } from '../../config/env';
4
+
5
+ /**
6
+ * Confirm TOTP enrolment with the first authenticator code
7
+ * (POST /api/Account/mfa/totp/confirm, user JWT). Body forwarded verbatim —
8
+ * it carries the enrolment handle plus the code. Only after this succeeds is
9
+ * the method ACTIVE.
10
+ */
11
+ export async function POST(req: NextRequest) {
12
+ let body = '{}';
13
+ try {
14
+ const raw = await req.text();
15
+ if (raw && raw.trim().length > 0) body = raw;
16
+ } catch {}
17
+
18
+ const result = await idpFetchJSON(req, `${ENV_CONFIG.IDP_URL}/api/Account/mfa/totp/confirm`, {
19
+ method: 'POST',
20
+ headers: { 'Content-Type': 'application/json' },
21
+ body,
22
+ });
23
+
24
+ if (!result.ok) {
25
+ return NextResponse.json(
26
+ {
27
+ success: false,
28
+ message: 'Upstream error',
29
+ error: { code: 'UPSTREAM_SERVICE_ERROR', status: result.status, details: result.json },
30
+ meta: { attemptedRefresh: result.attemptedRefresh },
31
+ },
32
+ { status: result.status },
33
+ );
34
+ }
35
+
36
+ const bodyJson: any = result.json;
37
+ if (bodyJson && typeof bodyJson === 'object' && 'success' in bodyJson && 'data' in bodyJson) {
38
+ if (bodyJson.success === true) return NextResponse.json(bodyJson.data, { status: 200 });
39
+ return NextResponse.json(bodyJson, { status: 200 });
40
+ }
41
+ return NextResponse.json(bodyJson ?? {}, { status: 200 });
42
+ }
@@ -1,42 +1,42 @@
1
- import { NextRequest, NextResponse } from 'next/server';
2
- import { idpFetchJSON } from '../../lib/idp-fetch';
3
- import { ENV_CONFIG } from '../../config/env';
4
-
5
- /**
6
- * Begin TOTP enrolment (POST /api/Account/mfa/totp/enroll, user JWT).
7
- * Returns the shared secret / otpauth URI for the QR. An enrolment left
8
- * UNCONFIRMED is pending — it can only be RESTARTED, and must never render
9
- * as "Active" (MFA honesty fixes, 2026-08-01).
10
- */
11
- export async function POST(req: NextRequest) {
12
- let body = '{}';
13
- try {
14
- const raw = await req.text();
15
- if (raw && raw.trim().length > 0) body = raw;
16
- } catch {}
17
-
18
- const result = await idpFetchJSON(req, `${ENV_CONFIG.IDP_URL}/api/Account/mfa/totp/enroll`, {
19
- method: 'POST',
20
- headers: { 'Content-Type': 'application/json' },
21
- body,
22
- });
23
-
24
- if (!result.ok) {
25
- return NextResponse.json(
26
- {
27
- success: false,
28
- message: 'Upstream error',
29
- error: { code: 'UPSTREAM_SERVICE_ERROR', status: result.status, details: result.json },
30
- meta: { attemptedRefresh: result.attemptedRefresh },
31
- },
32
- { status: result.status },
33
- );
34
- }
35
-
36
- const bodyJson: any = result.json;
37
- if (bodyJson && typeof bodyJson === 'object' && 'success' in bodyJson && 'data' in bodyJson) {
38
- if (bodyJson.success === true) return NextResponse.json(bodyJson.data, { status: 200 });
39
- return NextResponse.json(bodyJson, { status: 200 });
40
- }
41
- return NextResponse.json(bodyJson ?? {}, { status: 200 });
42
- }
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { idpFetchJSON } from '../../lib/idp-fetch';
3
+ import { ENV_CONFIG } from '../../config/env';
4
+
5
+ /**
6
+ * Begin TOTP enrolment (POST /api/Account/mfa/totp/enroll, user JWT).
7
+ * Returns the shared secret / otpauth URI for the QR. An enrolment left
8
+ * UNCONFIRMED is pending — it can only be RESTARTED, and must never render
9
+ * as "Active" (MFA honesty fixes, 2026-08-01).
10
+ */
11
+ export async function POST(req: NextRequest) {
12
+ let body = '{}';
13
+ try {
14
+ const raw = await req.text();
15
+ if (raw && raw.trim().length > 0) body = raw;
16
+ } catch {}
17
+
18
+ const result = await idpFetchJSON(req, `${ENV_CONFIG.IDP_URL}/api/Account/mfa/totp/enroll`, {
19
+ method: 'POST',
20
+ headers: { 'Content-Type': 'application/json' },
21
+ body,
22
+ });
23
+
24
+ if (!result.ok) {
25
+ return NextResponse.json(
26
+ {
27
+ success: false,
28
+ message: 'Upstream error',
29
+ error: { code: 'UPSTREAM_SERVICE_ERROR', status: result.status, details: result.json },
30
+ meta: { attemptedRefresh: result.attemptedRefresh },
31
+ },
32
+ { status: result.status },
33
+ );
34
+ }
35
+
36
+ const bodyJson: any = result.json;
37
+ if (bodyJson && typeof bodyJson === 'object' && 'success' in bodyJson && 'data' in bodyJson) {
38
+ if (bodyJson.success === true) return NextResponse.json(bodyJson.data, { status: 200 });
39
+ return NextResponse.json(bodyJson, { status: 200 });
40
+ }
41
+ return NextResponse.json(bodyJson ?? {}, { status: 200 });
42
+ }
@@ -1,31 +1,31 @@
1
- import { NextRequest, NextResponse } from 'next/server';
2
- import { idpFetchJSON } from '../../lib/idp-fetch';
3
- import { ENV_CONFIG } from '../../config/env';
4
-
5
- /**
6
- * List the signed-in user's passkeys (GET /api/Passkeys, user JWT).
7
- * Probe-verified live 2026-08-01 (anon -> 401 = routed).
8
- * No session -> upstream 401, passed through, never invented locally.
9
- */
10
- export async function GET(req: NextRequest) {
11
- const result = await idpFetchJSON(req, `${ENV_CONFIG.IDP_URL}/api/Passkeys`, { method: 'GET' });
12
-
13
- if (!result.ok) {
14
- return NextResponse.json(
15
- {
16
- success: false,
17
- message: 'Upstream error',
18
- error: { code: 'UPSTREAM_SERVICE_ERROR', status: result.status, details: result.json },
19
- meta: { attemptedRefresh: result.attemptedRefresh },
20
- },
21
- { status: result.status },
22
- );
23
- }
24
-
25
- const bodyJson: any = result.json;
26
- if (bodyJson && typeof bodyJson === 'object' && 'success' in bodyJson && 'data' in bodyJson) {
27
- if (bodyJson.success === true) return NextResponse.json(bodyJson.data, { status: 200 });
28
- return NextResponse.json(bodyJson, { status: 200 });
29
- }
30
- return NextResponse.json(bodyJson ?? {}, { status: 200 });
31
- }
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { idpFetchJSON } from '../../lib/idp-fetch';
3
+ import { ENV_CONFIG } from '../../config/env';
4
+
5
+ /**
6
+ * List the signed-in user's passkeys (GET /api/Passkeys, user JWT).
7
+ * Probe-verified live 2026-08-01 (anon -> 401 = routed).
8
+ * No session -> upstream 401, passed through, never invented locally.
9
+ */
10
+ export async function GET(req: NextRequest) {
11
+ const result = await idpFetchJSON(req, `${ENV_CONFIG.IDP_URL}/api/Passkeys`, { method: 'GET' });
12
+
13
+ if (!result.ok) {
14
+ return NextResponse.json(
15
+ {
16
+ success: false,
17
+ message: 'Upstream error',
18
+ error: { code: 'UPSTREAM_SERVICE_ERROR', status: result.status, details: result.json },
19
+ meta: { attemptedRefresh: result.attemptedRefresh },
20
+ },
21
+ { status: result.status },
22
+ );
23
+ }
24
+
25
+ const bodyJson: any = result.json;
26
+ if (bodyJson && typeof bodyJson === 'object' && 'success' in bodyJson && 'data' in bodyJson) {
27
+ if (bodyJson.success === true) return NextResponse.json(bodyJson.data, { status: 200 });
28
+ return NextResponse.json(bodyJson, { status: 200 });
29
+ }
30
+ return NextResponse.json(bodyJson ?? {}, { status: 200 });
31
+ }
@@ -1,54 +1,54 @@
1
- import { NextRequest, NextResponse } from 'next/server';
2
- import { idpFetchJSON } from '../../lib/idp-fetch';
3
- import { ENV_CONFIG } from '../../config/env';
4
-
5
- /**
6
- * Manage ONE passkey (PATCH rename / DELETE revoke, /api/Passkeys/{id},
7
- * user JWT). The id arrives from the Next dynamic segment via the routes
8
- * wrapper — it is never read from the body.
9
- */
10
-
11
- function upstreamError(result: { status: number; json: unknown; attemptedRefresh: boolean }) {
12
- return NextResponse.json(
13
- {
14
- success: false,
15
- message: 'Upstream error',
16
- error: { code: 'UPSTREAM_SERVICE_ERROR', status: result.status, details: result.json },
17
- meta: { attemptedRefresh: result.attemptedRefresh },
18
- },
19
- { status: result.status },
20
- );
21
- }
22
-
23
- function passthrough(json: unknown) {
24
- const bodyJson: any = json;
25
- if (bodyJson && typeof bodyJson === 'object' && 'success' in bodyJson && 'data' in bodyJson) {
26
- if (bodyJson.success === true) return NextResponse.json(bodyJson.data ?? { success: true }, { status: 200 });
27
- return NextResponse.json(bodyJson, { status: 200 });
28
- }
29
- return NextResponse.json(bodyJson ?? { success: true }, { status: 200 });
30
- }
31
-
32
- export async function PATCH(req: NextRequest, id: string) {
33
- let body = '{}';
34
- try {
35
- const raw = await req.text();
36
- if (raw && raw.trim().length > 0) body = raw;
37
- } catch {}
38
-
39
- const result = await idpFetchJSON(req, `${ENV_CONFIG.IDP_URL}/api/Passkeys/${encodeURIComponent(id)}`, {
40
- method: 'PATCH',
41
- headers: { 'Content-Type': 'application/json' },
42
- body,
43
- });
44
- if (!result.ok) return upstreamError(result);
45
- return passthrough(result.json);
46
- }
47
-
48
- export async function DELETE(req: NextRequest, id: string) {
49
- const result = await idpFetchJSON(req, `${ENV_CONFIG.IDP_URL}/api/Passkeys/${encodeURIComponent(id)}`, {
50
- method: 'DELETE',
51
- });
52
- if (!result.ok) return upstreamError(result);
53
- return passthrough(result.json);
54
- }
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { idpFetchJSON } from '../../lib/idp-fetch';
3
+ import { ENV_CONFIG } from '../../config/env';
4
+
5
+ /**
6
+ * Manage ONE passkey (PATCH rename / DELETE revoke, /api/Passkeys/{id},
7
+ * user JWT). The id arrives from the Next dynamic segment via the routes
8
+ * wrapper — it is never read from the body.
9
+ */
10
+
11
+ function upstreamError(result: { status: number; json: unknown; attemptedRefresh: boolean }) {
12
+ return NextResponse.json(
13
+ {
14
+ success: false,
15
+ message: 'Upstream error',
16
+ error: { code: 'UPSTREAM_SERVICE_ERROR', status: result.status, details: result.json },
17
+ meta: { attemptedRefresh: result.attemptedRefresh },
18
+ },
19
+ { status: result.status },
20
+ );
21
+ }
22
+
23
+ function passthrough(json: unknown) {
24
+ const bodyJson: any = json;
25
+ if (bodyJson && typeof bodyJson === 'object' && 'success' in bodyJson && 'data' in bodyJson) {
26
+ if (bodyJson.success === true) return NextResponse.json(bodyJson.data ?? { success: true }, { status: 200 });
27
+ return NextResponse.json(bodyJson, { status: 200 });
28
+ }
29
+ return NextResponse.json(bodyJson ?? { success: true }, { status: 200 });
30
+ }
31
+
32
+ export async function PATCH(req: NextRequest, id: string) {
33
+ let body = '{}';
34
+ try {
35
+ const raw = await req.text();
36
+ if (raw && raw.trim().length > 0) body = raw;
37
+ } catch {}
38
+
39
+ const result = await idpFetchJSON(req, `${ENV_CONFIG.IDP_URL}/api/Passkeys/${encodeURIComponent(id)}`, {
40
+ method: 'PATCH',
41
+ headers: { 'Content-Type': 'application/json' },
42
+ body,
43
+ });
44
+ if (!result.ok) return upstreamError(result);
45
+ return passthrough(result.json);
46
+ }
47
+
48
+ export async function DELETE(req: NextRequest, id: string) {
49
+ const result = await idpFetchJSON(req, `${ENV_CONFIG.IDP_URL}/api/Passkeys/${encodeURIComponent(id)}`, {
50
+ method: 'DELETE',
51
+ });
52
+ if (!result.ok) return upstreamError(result);
53
+ return passthrough(result.json);
54
+ }
@@ -1,42 +1,42 @@
1
- import { NextRequest, NextResponse } from 'next/server';
2
- import { idpFetchJSON } from '../../lib/idp-fetch';
3
- import { ENV_CONFIG } from '../../config/env';
4
-
5
- /**
6
- * Begin the passkey REGISTRATION ceremony (POST /api/Passkeys/register/options,
7
- * user JWT — a credential change; the IDP may demand step-up, surfaced as-is).
8
- * Returns the WebAuthn PublicKeyCredentialCreationOptions (ride the envelope
9
- * `data`, per the archived hand-roll contract).
10
- */
11
- export async function POST(req: NextRequest) {
12
- let body = '{}';
13
- try {
14
- const raw = await req.text();
15
- if (raw && raw.trim().length > 0) body = raw;
16
- } catch {}
17
-
18
- const result = await idpFetchJSON(req, `${ENV_CONFIG.IDP_URL}/api/Passkeys/register/options`, {
19
- method: 'POST',
20
- headers: { 'Content-Type': 'application/json' },
21
- body,
22
- });
23
-
24
- if (!result.ok) {
25
- return NextResponse.json(
26
- {
27
- success: false,
28
- message: 'Upstream error',
29
- error: { code: 'UPSTREAM_SERVICE_ERROR', status: result.status, details: result.json },
30
- meta: { attemptedRefresh: result.attemptedRefresh },
31
- },
32
- { status: result.status },
33
- );
34
- }
35
-
36
- const bodyJson: any = result.json;
37
- if (bodyJson && typeof bodyJson === 'object' && 'success' in bodyJson && 'data' in bodyJson) {
38
- if (bodyJson.success === true) return NextResponse.json(bodyJson.data, { status: 200 });
39
- return NextResponse.json(bodyJson, { status: 200 });
40
- }
41
- return NextResponse.json(bodyJson ?? {}, { status: 200 });
42
- }
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { idpFetchJSON } from '../../lib/idp-fetch';
3
+ import { ENV_CONFIG } from '../../config/env';
4
+
5
+ /**
6
+ * Begin the passkey REGISTRATION ceremony (POST /api/Passkeys/register/options,
7
+ * user JWT — a credential change; the IDP may demand step-up, surfaced as-is).
8
+ * Returns the WebAuthn PublicKeyCredentialCreationOptions (ride the envelope
9
+ * `data`, per the archived hand-roll contract).
10
+ */
11
+ export async function POST(req: NextRequest) {
12
+ let body = '{}';
13
+ try {
14
+ const raw = await req.text();
15
+ if (raw && raw.trim().length > 0) body = raw;
16
+ } catch {}
17
+
18
+ const result = await idpFetchJSON(req, `${ENV_CONFIG.IDP_URL}/api/Passkeys/register/options`, {
19
+ method: 'POST',
20
+ headers: { 'Content-Type': 'application/json' },
21
+ body,
22
+ });
23
+
24
+ if (!result.ok) {
25
+ return NextResponse.json(
26
+ {
27
+ success: false,
28
+ message: 'Upstream error',
29
+ error: { code: 'UPSTREAM_SERVICE_ERROR', status: result.status, details: result.json },
30
+ meta: { attemptedRefresh: result.attemptedRefresh },
31
+ },
32
+ { status: result.status },
33
+ );
34
+ }
35
+
36
+ const bodyJson: any = result.json;
37
+ if (bodyJson && typeof bodyJson === 'object' && 'success' in bodyJson && 'data' in bodyJson) {
38
+ if (bodyJson.success === true) return NextResponse.json(bodyJson.data, { status: 200 });
39
+ return NextResponse.json(bodyJson, { status: 200 });
40
+ }
41
+ return NextResponse.json(bodyJson ?? {}, { status: 200 });
42
+ }
@@ -1,41 +1,41 @@
1
- import { NextRequest, NextResponse } from 'next/server';
2
- import { idpFetchJSON } from '../../lib/idp-fetch';
3
- import { ENV_CONFIG } from '../../config/env';
4
-
5
- /**
6
- * Finish the passkey REGISTRATION ceremony (POST /api/Passkeys/register/verify,
7
- * user JWT). Body forwarded VERBATIM — it carries the enrolment handle plus the
8
- * authenticator attestation produced by navigator.credentials.create().
9
- */
10
- export async function POST(req: NextRequest) {
11
- let body = '{}';
12
- try {
13
- const raw = await req.text();
14
- if (raw && raw.trim().length > 0) body = raw;
15
- } catch {}
16
-
17
- const result = await idpFetchJSON(req, `${ENV_CONFIG.IDP_URL}/api/Passkeys/register/verify`, {
18
- method: 'POST',
19
- headers: { 'Content-Type': 'application/json' },
20
- body,
21
- });
22
-
23
- if (!result.ok) {
24
- return NextResponse.json(
25
- {
26
- success: false,
27
- message: 'Upstream error',
28
- error: { code: 'UPSTREAM_SERVICE_ERROR', status: result.status, details: result.json },
29
- meta: { attemptedRefresh: result.attemptedRefresh },
30
- },
31
- { status: result.status },
32
- );
33
- }
34
-
35
- const bodyJson: any = result.json;
36
- if (bodyJson && typeof bodyJson === 'object' && 'success' in bodyJson && 'data' in bodyJson) {
37
- if (bodyJson.success === true) return NextResponse.json(bodyJson.data, { status: 200 });
38
- return NextResponse.json(bodyJson, { status: 200 });
39
- }
40
- return NextResponse.json(bodyJson ?? {}, { status: 200 });
41
- }
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { idpFetchJSON } from '../../lib/idp-fetch';
3
+ import { ENV_CONFIG } from '../../config/env';
4
+
5
+ /**
6
+ * Finish the passkey REGISTRATION ceremony (POST /api/Passkeys/register/verify,
7
+ * user JWT). Body forwarded VERBATIM — it carries the enrolment handle plus the
8
+ * authenticator attestation produced by navigator.credentials.create().
9
+ */
10
+ export async function POST(req: NextRequest) {
11
+ let body = '{}';
12
+ try {
13
+ const raw = await req.text();
14
+ if (raw && raw.trim().length > 0) body = raw;
15
+ } catch {}
16
+
17
+ const result = await idpFetchJSON(req, `${ENV_CONFIG.IDP_URL}/api/Passkeys/register/verify`, {
18
+ method: 'POST',
19
+ headers: { 'Content-Type': 'application/json' },
20
+ body,
21
+ });
22
+
23
+ if (!result.ok) {
24
+ return NextResponse.json(
25
+ {
26
+ success: false,
27
+ message: 'Upstream error',
28
+ error: { code: 'UPSTREAM_SERVICE_ERROR', status: result.status, details: result.json },
29
+ meta: { attemptedRefresh: result.attemptedRefresh },
30
+ },
31
+ { status: result.status },
32
+ );
33
+ }
34
+
35
+ const bodyJson: any = result.json;
36
+ if (bodyJson && typeof bodyJson === 'object' && 'success' in bodyJson && 'data' in bodyJson) {
37
+ if (bodyJson.success === true) return NextResponse.json(bodyJson.data, { status: 200 });
38
+ return NextResponse.json(bodyJson, { status: 200 });
39
+ }
40
+ return NextResponse.json(bodyJson ?? {}, { status: 200 });
41
+ }
@@ -81,7 +81,35 @@ export async function GET(req: NextRequest) {
81
81
  // CRITICAL: OAuth sessions without refresh token always need 2FA completion
82
82
  // to obtain a refresh token from the IDP. The OAuth callback sets mfaVerified=false
83
83
  // when no refresh_token is present. This must override the client config.
84
- if (sessionData.mfaVerified === false && !sessionData.idpRefreshToken) {
84
+ //
85
+ // *** R807 (BAPert, card 362550) — SECOND OF THE TWO SITES THE SIGNAL IS FOR. ***
86
+ // The `=== false` below reads as strict but had stopped behaving strictly: every
87
+ // construction site collapses an ABSENT mfaVerified to `false` (`?? false`), so by
88
+ // the time it is read here "the callback said false" and "we never knew" are the
89
+ // same value. This override therefore fired on EVERY session, not on OAuth sessions
90
+ // that genuinely lack a refresh token.
91
+ //
92
+ // The comment at line ~122 of this file already warned that fixing only ONE of the
93
+ // two viability implementations leaves an app mounting the other one blind. That
94
+ // applies here verbatim: routes/auth/viability.ts carries the same guard, and both
95
+ // are publicly exported entry points, so both must gate on the signal or the defect
96
+ // survives one export over.
97
+ //
98
+ // mfaSignalPresent === true -> the producer really emitted a value, so the `false`
99
+ // is a MEASUREMENT. Override.
100
+ // absent / false -> LEGACY session (minted before the signal existed) or
101
+ // a path with no IDP tokens. NOT a signal. Do not
102
+ // force a re-challenge.
103
+ //
104
+ // *** CONFINED, BY RULING. *** This signal is permissive by construction — it can
105
+ // only ever SUPPRESS this override, never grant access. Anything that GRANTS must
106
+ // keep gating on `mfaVerified`, which stays fail-closed at all five sites. R807 is
107
+ // void if this signal reaches an auth decision.
108
+ if (
109
+ sessionData.mfaSignalPresent === true &&
110
+ sessionData.mfaVerified === false &&
111
+ !sessionData.idpRefreshToken
112
+ ) {
85
113
  requires2FA = true;
86
114
  }
87
115