@payez/next-mvp 4.8.0 → 4.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (301) hide show
  1. package/POST-INSTALL.md +408 -408
  2. package/README.md +859 -859
  3. package/dist/components/account/MobileNavDrawer.js +18 -18
  4. package/dist/components/account/UserAvatarMenu.js +1 -1
  5. package/dist/components/admin/AlertSettingsTab.js +40 -40
  6. package/dist/components/admin/LoggingSettingsTab.js +28 -28
  7. package/dist/lib/session-store.js +21 -21
  8. package/dist/lib/startup-init.js +19 -19
  9. package/dist/middleware/create-middleware.js +6 -5
  10. package/dist/pages/login/page.js +28 -28
  11. package/dist/pages/settings/EnhancedSettingsPage.js +17 -17
  12. package/dist/vibe/sessions.js +28 -3
  13. package/package.json +2 -2
  14. package/scripts/check-publish-eol.mjs +33 -15
  15. package/src/api/auth-handler.ts +550 -550
  16. package/src/api/index.ts +17 -17
  17. package/src/api-handlers/account/change-password.ts +170 -170
  18. package/src/api-handlers/account/masked-info.ts +46 -46
  19. package/src/api-handlers/account/profile.ts +99 -99
  20. package/src/api-handlers/account/recovery/forward.ts +117 -117
  21. package/src/api-handlers/account/recovery/initiate.ts +59 -59
  22. package/src/api-handlers/account/recovery/send-code.ts +21 -21
  23. package/src/api-handlers/account/recovery/verify-code.ts +20 -20
  24. package/src/api-handlers/account/reset-password.ts +87 -87
  25. package/src/api-handlers/account/send-code.ts +70 -70
  26. package/src/api-handlers/account/update-phone.ts +84 -84
  27. package/src/api-handlers/account/validate-password.ts +139 -139
  28. package/src/api-handlers/account/verify-email.ts +134 -134
  29. package/src/api-handlers/account/verify-sms.ts +134 -134
  30. package/src/api-handlers/admin/analytics.ts +452 -452
  31. package/src/api-handlers/admin/audit.ts +232 -232
  32. package/src/api-handlers/admin/index.ts +63 -63
  33. package/src/api-handlers/admin/redis-sessions.ts +259 -259
  34. package/src/api-handlers/admin/sessions.ts +327 -327
  35. package/src/api-handlers/admin/site-logs.ts +388 -388
  36. package/src/api-handlers/admin/stats.ts +260 -260
  37. package/src/api-handlers/admin/users.ts +251 -251
  38. package/src/api-handlers/admin/vibe-data.ts +333 -333
  39. package/src/api-handlers/anon/preferences.ts +123 -123
  40. package/src/api-handlers/auth/credential-login-email-code.ts +92 -92
  41. package/src/api-handlers/auth/credential-login.ts +948 -948
  42. package/src/api-handlers/auth/jwks.ts +31 -31
  43. package/src/api-handlers/auth/login.ts +252 -252
  44. package/src/api-handlers/auth/passkey-signin.ts +116 -116
  45. package/src/api-handlers/auth/refresh.ts +829 -829
  46. package/src/api-handlers/auth/signout.ts +201 -201
  47. package/src/api-handlers/auth/status.ts +23 -23
  48. package/src/api-handlers/auth/update-session.ts +144 -144
  49. package/src/api-handlers/auth/validate.ts +45 -45
  50. package/src/api-handlers/auth/verify-code.ts +116 -116
  51. package/src/api-handlers/mfa/backup-code-verify.ts +170 -170
  52. package/src/api-handlers/mfa/backup-codes.ts +42 -42
  53. package/src/api-handlers/mfa/methods.ts +47 -47
  54. package/src/api-handlers/mfa/totp-confirm.ts +43 -43
  55. package/src/api-handlers/mfa/totp-enroll.ts +43 -43
  56. package/src/api-handlers/mfa/totp-verify.ts +172 -172
  57. package/src/api-handlers/oauth/resume.ts +182 -182
  58. package/src/api-handlers/onboarding/send-email-code.ts +18 -18
  59. package/src/api-handlers/onboarding/set-password.ts +21 -21
  60. package/src/api-handlers/onboarding/verify-code.ts +20 -20
  61. package/src/api-handlers/onboarding/verify-welcome-email.ts +17 -17
  62. package/src/api-handlers/passkeys/list.ts +32 -32
  63. package/src/api-handlers/passkeys/manage.ts +55 -55
  64. package/src/api-handlers/passkeys/register-options.ts +43 -43
  65. package/src/api-handlers/passkeys/register-verify.ts +42 -42
  66. package/src/api-handlers/session/refresh-viability.ts +56 -56
  67. package/src/api-handlers/session/viability.ts +238 -238
  68. package/src/api-handlers/test/force-expire.ts +167 -167
  69. package/src/auth/auth-decision.ts +292 -292
  70. package/src/auth/better-auth.ts +1688 -1688
  71. package/src/auth/idp-oauth.ts +310 -310
  72. package/src/auth/password-reset-bridge.ts +98 -98
  73. package/src/auth/route-config.ts +236 -236
  74. package/src/auth/safe-callback.ts +199 -199
  75. package/src/auth/session-cookie-clear.ts +30 -30
  76. package/src/auth/session-reset-link.ts +159 -159
  77. package/src/auth/session-response-sanitizer.ts +151 -151
  78. package/src/auth/types/auth-types.ts +555 -555
  79. package/src/auth/types/index.ts +7 -7
  80. package/src/auth/unauthenticated-routes.ts +2 -2
  81. package/src/auth/utils/idp-client.ts +364 -364
  82. package/src/auth/utils/index.ts +6 -6
  83. package/src/auth/utils/token-utils.ts +243 -243
  84. package/src/client/AuthContext.tsx +192 -192
  85. package/src/client/better-auth-client.ts +70 -70
  86. package/src/client/fetch-with-auth.ts +47 -47
  87. package/src/client/fetchWithSession.ts +21 -21
  88. package/src/client/index.ts +13 -13
  89. package/src/client/useAnonSession.ts +131 -131
  90. package/src/components/SessionSync.tsx +138 -138
  91. package/src/components/SignalRHealthCheck.tsx +131 -131
  92. package/src/components/account/MobileNavDrawer.tsx +305 -305
  93. package/src/components/account/UserAvatarMenu.tsx +277 -277
  94. package/src/components/account/index.ts +13 -13
  95. package/src/components/admin/AlertSettingsTab.tsx +728 -728
  96. package/src/components/admin/AnalyticsTab.tsx +703 -703
  97. package/src/components/admin/DataBrowserTab.tsx +508 -508
  98. package/src/components/admin/LoggingSettingsTab.tsx +665 -665
  99. package/src/components/admin/SessionsTab.tsx +421 -421
  100. package/src/components/admin/StatsTab.tsx +379 -379
  101. package/src/components/admin/VibeAdminContext.tsx +87 -87
  102. package/src/components/admin/VibeAdminLayout.tsx +187 -187
  103. package/src/components/admin/index.ts +59 -59
  104. package/src/components/auth/CredentialSignInForm.tsx +240 -240
  105. package/src/components/auth/FederatedAuthSection.tsx +129 -129
  106. package/src/components/auth/ModeAwareLoginPage.tsx +146 -146
  107. package/src/components/auth/ModeAwareSignupPage.tsx +200 -200
  108. package/src/components/auth/PasswordResetBridge.tsx +30 -30
  109. package/src/components/auth/TraditionalAuthSection.tsx +103 -103
  110. package/src/components/recovery/CompleteStep.tsx +36 -36
  111. package/src/components/recovery/InitiateRecoveryStep.tsx +68 -68
  112. package/src/components/recovery/RequestedStep.tsx +30 -30
  113. package/src/components/recovery/SelectMethodStep.tsx +73 -73
  114. package/src/components/recovery/SetPasswordStep.tsx +118 -118
  115. package/src/components/recovery/VerifyCodeStep.tsx +90 -90
  116. package/src/components/reserved/ReservedRecoveryWarning.tsx +160 -160
  117. package/src/components/reserved/ReservedStatusBox.tsx +118 -118
  118. package/src/components/ui/BetaBadge.tsx +58 -58
  119. package/src/components/ui/Footer.tsx +93 -93
  120. package/src/config/env.ts +55 -55
  121. package/src/config/logger.ts +62 -62
  122. package/src/config/logging-config.ts +82 -82
  123. package/src/config/unauthenticated-routes.ts +19 -19
  124. package/src/config/vibe-log-transport.ts +264 -264
  125. package/src/edge/internal-api-url.ts +65 -65
  126. package/src/edge/middleware.ts +42 -42
  127. package/src/hooks/useAuth.ts +117 -117
  128. package/src/hooks/useAuthSettings.ts +109 -109
  129. package/src/hooks/useAvailableProviders.ts +143 -143
  130. package/src/hooks/usePasswordValidation.ts +127 -127
  131. package/src/hooks/useProfile.ts +82 -82
  132. package/src/hooks/usePublicAuthSettings.ts +171 -171
  133. package/src/hooks/useSessionExpiration.ts +101 -101
  134. package/src/hooks/useViabilitySession.ts +337 -337
  135. package/src/index.ts +64 -64
  136. package/src/lib/anon-session.ts +213 -213
  137. package/src/lib/api-handler.ts +643 -643
  138. package/src/lib/app-slug.ts +228 -228
  139. package/src/lib/authorize-resume.ts +172 -172
  140. package/src/lib/bearer-fetch.ts +31 -31
  141. package/src/lib/client-key.ts +88 -88
  142. package/src/lib/code-step-copy.ts +83 -83
  143. package/src/lib/credential-sign-in-client.ts +101 -101
  144. package/src/lib/demo-mode.ts +18 -18
  145. package/src/lib/device-id.ts +67 -67
  146. package/src/lib/ensure-fresh-access-token.ts +509 -509
  147. package/src/lib/geolocation.ts +265 -265
  148. package/src/lib/idp-client-config.ts +1005 -1005
  149. package/src/lib/idp-fetch.ts +169 -169
  150. package/src/lib/idp-url-guard.ts +37 -37
  151. package/src/lib/internal-api.ts +171 -171
  152. package/src/lib/jwt-decode-client.ts +45 -45
  153. package/src/lib/jwt-decode.ts +82 -82
  154. package/src/lib/lock-window.ts +49 -49
  155. package/src/lib/login-error-message.ts +41 -41
  156. package/src/lib/never-in-response.ts +64 -64
  157. package/src/lib/nextauth-secret.ts +168 -168
  158. package/src/lib/onboarding-proxy.ts +57 -57
  159. package/src/lib/password-rules.ts +50 -50
  160. package/src/lib/rate-limit-service.ts +9 -9
  161. package/src/lib/redis.ts +105 -105
  162. package/src/lib/refresh-token-validator.ts +64 -64
  163. package/src/lib/roles.ts +177 -177
  164. package/src/lib/safe-error-response.ts +149 -149
  165. package/src/lib/secret-validation.ts +8 -8
  166. package/src/lib/session-cookie-shape.ts +185 -185
  167. package/src/lib/session-store.ts +1092 -1092
  168. package/src/lib/session.ts +34 -34
  169. package/src/lib/single-flight.ts +64 -64
  170. package/src/lib/site-logger.ts +245 -245
  171. package/src/lib/standardized-client-api.ts +901 -901
  172. package/src/lib/startup-init.ts +276 -276
  173. package/src/lib/test-aware-get-token.ts +41 -41
  174. package/src/lib/token-expiry.ts +40 -40
  175. package/src/lib/token-lifecycle.ts +546 -546
  176. package/src/lib/token-log-id.ts +69 -69
  177. package/src/lib/twofa-rejection.ts +91 -91
  178. package/src/lib/types/api-responses.ts +336 -336
  179. package/src/lib/user-agent-parser.ts +252 -252
  180. package/src/logging/api/admin-analytics.ts +51 -51
  181. package/src/logging/api/audit-log.ts +53 -53
  182. package/src/logging/components/AdminAnalyticsLayout.tsx +49 -49
  183. package/src/logging/components/AuditLogViewer.tsx +125 -125
  184. package/src/logging/components/ErrorMetricsCard.tsx +98 -98
  185. package/src/logging/components/HealthMetricsCard.tsx +70 -70
  186. package/src/logging/hooks/useAdminAnalytics.ts +22 -22
  187. package/src/logging/hooks/useAuditLog.ts +24 -24
  188. package/src/logging/hooks/useErrorMetrics.ts +40 -40
  189. package/src/logging/hooks/useHealthMetrics.ts +44 -44
  190. package/src/logging/index.ts +18 -18
  191. package/src/logging/types/analytics.ts +81 -81
  192. package/src/logging/types/audit.ts +31 -31
  193. package/src/logging/types/index.ts +3 -3
  194. package/src/middleware/create-middleware.ts +1139 -1138
  195. package/src/middleware/rbac-check.ts +279 -279
  196. package/src/middleware/twofa-presets.ts +224 -224
  197. package/src/models/DecodedAccessToken.ts +16 -16
  198. package/src/models/SessionModel.ts +331 -331
  199. package/src/pages/admin-login/page.tsx +111 -111
  200. package/src/pages/admin-page-permissions/PagePermissionsAdminPage.tsx +527 -527
  201. package/src/pages/admin-page-permissions/index.ts +7 -7
  202. package/src/pages/admin-roles/RolesAdminPage.tsx +533 -533
  203. package/src/pages/admin-roles/index.ts +9 -9
  204. package/src/pages/admin-roles/modals.tsx +469 -469
  205. package/src/pages/client-admin/ClientSiteAdminPage.tsx +382 -382
  206. package/src/pages/client-admin/index.ts +33 -33
  207. package/src/pages/coming-soon/page.tsx +83 -83
  208. package/src/pages/login/page.tsx +338 -338
  209. package/src/pages/onboarding/index.ts +11 -11
  210. package/src/pages/onboarding/page.tsx +212 -212
  211. package/src/pages/profile/EnhancedProfilePage.tsx +514 -514
  212. package/src/pages/profile/index.ts +9 -9
  213. package/src/pages/profile/page.tsx +166 -166
  214. package/src/pages/recovery/page.tsx +267 -267
  215. package/src/pages/roles/MyRolesPage.tsx +211 -211
  216. package/src/pages/roles/components.tsx +294 -294
  217. package/src/pages/roles/index.ts +17 -17
  218. package/src/pages/security/EnhancedSecurityPage.tsx +660 -660
  219. package/src/pages/security/index.ts +12 -12
  220. package/src/pages/security/page.tsx +507 -507
  221. package/src/pages/settings/EnhancedSettingsPage.tsx +642 -642
  222. package/src/pages/settings/index.ts +9 -9
  223. package/src/pages/settings/page.tsx +47 -47
  224. package/src/pages/showcase/ShowcasePage.tsx +532 -532
  225. package/src/pages/showcase/index.ts +13 -13
  226. package/src/pages/test-env/EmergencyLogoutPage.tsx +192 -192
  227. package/src/pages/test-env/JwtInspectPage.tsx +449 -449
  228. package/src/pages/test-env/RefreshTokenPage.tsx +178 -178
  229. package/src/pages/test-env/TestEnvPage.tsx +130 -130
  230. package/src/pages/test-env/guarded.tsx +81 -81
  231. package/src/pages/test-env/index.ts +77 -77
  232. package/src/pages/test-env/redact-credentials.ts +96 -96
  233. package/src/pages/verify-code/page.tsx +746 -746
  234. package/src/routes/account/index.ts +31 -31
  235. package/src/routes/account/masked-info.ts +36 -36
  236. package/src/routes/account/send-code.ts +39 -39
  237. package/src/routes/account/update-phone.ts +13 -13
  238. package/src/routes/account/verify-email.ts +40 -40
  239. package/src/routes/account/verify-sms.ts +40 -40
  240. package/src/routes/auth/credential-login-email-code.ts +6 -6
  241. package/src/routes/auth/credential-login.ts +6 -6
  242. package/src/routes/auth/index.ts +22 -22
  243. package/src/routes/auth/logout.ts +111 -111
  244. package/src/routes/auth/nextauth.ts +45 -45
  245. package/src/routes/auth/passkey-signin.ts +13 -13
  246. package/src/routes/auth/refresh.ts +77 -77
  247. package/src/routes/auth/session-reset.ts +37 -37
  248. package/src/routes/auth/session.ts +261 -261
  249. package/src/routes/auth/settings.ts +181 -181
  250. package/src/routes/auth/viability.ts +519 -519
  251. package/src/routes/index.ts +17 -17
  252. package/src/routes/mfa/backup-code-verify.ts +1 -1
  253. package/src/routes/mfa/backup-codes.ts +10 -10
  254. package/src/routes/mfa/methods.ts +22 -22
  255. package/src/routes/mfa/totp-confirm.ts +10 -10
  256. package/src/routes/mfa/totp-enroll.ts +10 -10
  257. package/src/routes/mfa/totp-verify.ts +14 -14
  258. package/src/routes/onboarding/index.ts +15 -15
  259. package/src/routes/onboarding/send-email-code.ts +2 -2
  260. package/src/routes/onboarding/set-password.ts +2 -2
  261. package/src/routes/onboarding/verify-code.ts +2 -2
  262. package/src/routes/onboarding/verify-welcome-email.ts +2 -2
  263. package/src/routes/passkeys/list.ts +10 -10
  264. package/src/routes/passkeys/manage.ts +23 -23
  265. package/src/routes/passkeys/register-options.ts +10 -10
  266. package/src/routes/passkeys/register-verify.ts +10 -10
  267. package/src/routes/session/index.ts +7 -7
  268. package/src/routes/session/refresh-viability.ts +17 -17
  269. package/src/server/auth-guard.ts +203 -203
  270. package/src/server/auth.ts +486 -486
  271. package/src/server/decode-session.ts +309 -309
  272. package/src/server/slim-middleware.ts +117 -117
  273. package/src/server/verified-session-token.ts +103 -103
  274. package/src/server/with-auth.ts +93 -93
  275. package/src/services/signalrActivityService.ts +258 -258
  276. package/src/stores/authStore.ts +1900 -1900
  277. package/src/templates/instrumentation.ts +60 -60
  278. package/src/theme/ThemeProvider.tsx +39 -39
  279. package/src/theme/default.ts +33 -33
  280. package/src/theme/index.ts +31 -31
  281. package/src/theme/types.ts +69 -69
  282. package/src/theme/useTheme.ts +57 -57
  283. package/src/theme/utils.ts +40 -40
  284. package/src/types/api.ts +12 -12
  285. package/src/types/auth.d.ts +23 -23
  286. package/src/types/auth.ts +48 -48
  287. package/src/types/logging.ts +11 -11
  288. package/src/types/recovery.ts +54 -54
  289. package/src/utils/api.ts +401 -401
  290. package/src/utils/circuitBreaker.ts +40 -40
  291. package/src/utils/error-message.ts +108 -108
  292. package/src/utils/layout/reservedSpace.ts +124 -124
  293. package/src/utils/logout.ts +30 -30
  294. package/src/vibe/client.ts +877 -877
  295. package/src/vibe/enterprise-auth.ts +208 -208
  296. package/src/vibe/errors.ts +209 -209
  297. package/src/vibe/generic.ts +339 -339
  298. package/src/vibe/hooks/index.ts +367 -367
  299. package/src/vibe/index.ts +133 -133
  300. package/src/vibe/sessions.ts +925 -899
  301. package/src/vibe/types.ts +577 -577
@@ -1,99 +1,99 @@
1
- import { NextRequest, NextResponse } from 'next/server';
2
- import { idpFetchJSON } from '../../lib/idp-fetch';
3
- import { ENV_CONFIG } from '../../config/env';
4
-
5
- /**
6
- * Account Profile API Handler
7
- *
8
- * GET /api/account/profile - Get user profile
9
- * PUT /api/account/profile - Update user profile
10
- *
11
- * WHY THIS WAS REWRITTEN (2026-08-02, QAPert defect #7)
12
- * Reported as "/api/account/profile 401s on the same cookie masked-info
13
- * accepts (200)". Two endpoints disagreeing about one cookie meant two auth
14
- * implementations, and they were:
15
- *
16
- * masked-info.ts idpFetchJSON(req, ...) resolves the caller's token
17
- * from the server-side session
18
- * profile.ts req.headers.get('authorization') required an INBOUND bearer
19
- *
20
- * A browser sends a session COOKIE and no Authorization header, so the old
21
- * profile.ts 401'd EVERY browser request by construction. Nothing was revoked
22
- * and no token was stale — the handler demanded a credential the app does not
23
- * use.
24
- *
25
- * THE OLD AUTH CHECK WAS A VENEER. It tested that the header was PRESENT, never
26
- * that it was valid, then forwarded it upstream unexamined. Any string at all
27
- * got past it and the IDP did the real work. It looked like enforcement and
28
- * enforced nothing.
29
- *
30
- * WHAT THE WORKAROUND COST US: consumers could not call this with a cookie, so
31
- * the graft's useProfile hook fetched the raw IDP access token from
32
- * /api/session/idp-token INTO BROWSER JAVASCRIPT and re-sent it as a bearer.
33
- * That defeats the reason idpFetchJSON exists — it keeps the token server-side
34
- * precisely so client-side script can never hold it. The bearer-only handler
35
- * did not merely fail; it pushed its callers into exposing the token.
36
- *
37
- * Switching to idpFetchJSON makes the cookie work, so the hook's token round
38
- * trip can be deleted and /api/session/idp-token drops to a single deliberate
39
- * caller: the jwt-inspect debug page.
40
- *
41
- * @package @payez/next-mvp
42
- */
43
-
44
- export async function GET(req: NextRequest) {
45
- const result = await idpFetchJSON(req, `${ENV_CONFIG.IDP_URL}/api/Account/profile`, {
46
- method: 'GET',
47
- headers: { 'Content-Type': 'application/json' },
48
- });
49
-
50
- if (!result.ok) {
51
- return NextResponse.json(
52
- {
53
- success: false,
54
- error: {
55
- code: 'UPSTREAM_SERVICE_ERROR',
56
- message: result.json?.message || 'Failed to fetch profile',
57
- status: result.status,
58
- },
59
- meta: { attemptedRefresh: result.attemptedRefresh },
60
- },
61
- { status: result.status }
62
- );
63
- }
64
-
65
- return NextResponse.json(result.json, { status: 200 });
66
- }
67
-
68
- export async function PUT(req: NextRequest) {
69
- let body = '{}';
70
- try {
71
- const raw = await req.text();
72
- if (raw && raw.trim().length > 0) body = raw;
73
- } catch {
74
- // Empty body is valid here; the IDP validates the payload.
75
- }
76
-
77
- const result = await idpFetchJSON(req, `${ENV_CONFIG.IDP_URL}/api/Account/profile`, {
78
- method: 'PUT',
79
- headers: { 'Content-Type': 'application/json' },
80
- body,
81
- });
82
-
83
- if (!result.ok) {
84
- return NextResponse.json(
85
- {
86
- success: false,
87
- error: {
88
- code: 'UPSTREAM_SERVICE_ERROR',
89
- message: result.json?.message || 'Failed to update profile',
90
- status: result.status,
91
- },
92
- meta: { attemptedRefresh: result.attemptedRefresh },
93
- },
94
- { status: result.status }
95
- );
96
- }
97
-
98
- return NextResponse.json(result.json, { status: 200 });
99
- }
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { idpFetchJSON } from '../../lib/idp-fetch';
3
+ import { ENV_CONFIG } from '../../config/env';
4
+
5
+ /**
6
+ * Account Profile API Handler
7
+ *
8
+ * GET /api/account/profile - Get user profile
9
+ * PUT /api/account/profile - Update user profile
10
+ *
11
+ * WHY THIS WAS REWRITTEN (2026-08-02, QAPert defect #7)
12
+ * Reported as "/api/account/profile 401s on the same cookie masked-info
13
+ * accepts (200)". Two endpoints disagreeing about one cookie meant two auth
14
+ * implementations, and they were:
15
+ *
16
+ * masked-info.ts idpFetchJSON(req, ...) resolves the caller's token
17
+ * from the server-side session
18
+ * profile.ts req.headers.get('authorization') required an INBOUND bearer
19
+ *
20
+ * A browser sends a session COOKIE and no Authorization header, so the old
21
+ * profile.ts 401'd EVERY browser request by construction. Nothing was revoked
22
+ * and no token was stale — the handler demanded a credential the app does not
23
+ * use.
24
+ *
25
+ * THE OLD AUTH CHECK WAS A VENEER. It tested that the header was PRESENT, never
26
+ * that it was valid, then forwarded it upstream unexamined. Any string at all
27
+ * got past it and the IDP did the real work. It looked like enforcement and
28
+ * enforced nothing.
29
+ *
30
+ * WHAT THE WORKAROUND COST US: consumers could not call this with a cookie, so
31
+ * the graft's useProfile hook fetched the raw IDP access token from
32
+ * /api/session/idp-token INTO BROWSER JAVASCRIPT and re-sent it as a bearer.
33
+ * That defeats the reason idpFetchJSON exists — it keeps the token server-side
34
+ * precisely so client-side script can never hold it. The bearer-only handler
35
+ * did not merely fail; it pushed its callers into exposing the token.
36
+ *
37
+ * Switching to idpFetchJSON makes the cookie work, so the hook's token round
38
+ * trip can be deleted and /api/session/idp-token drops to a single deliberate
39
+ * caller: the jwt-inspect debug page.
40
+ *
41
+ * @package @payez/next-mvp
42
+ */
43
+
44
+ export async function GET(req: NextRequest) {
45
+ const result = await idpFetchJSON(req, `${ENV_CONFIG.IDP_URL}/api/Account/profile`, {
46
+ method: 'GET',
47
+ headers: { 'Content-Type': 'application/json' },
48
+ });
49
+
50
+ if (!result.ok) {
51
+ return NextResponse.json(
52
+ {
53
+ success: false,
54
+ error: {
55
+ code: 'UPSTREAM_SERVICE_ERROR',
56
+ message: result.json?.message || 'Failed to fetch profile',
57
+ status: result.status,
58
+ },
59
+ meta: { attemptedRefresh: result.attemptedRefresh },
60
+ },
61
+ { status: result.status }
62
+ );
63
+ }
64
+
65
+ return NextResponse.json(result.json, { status: 200 });
66
+ }
67
+
68
+ export async function PUT(req: NextRequest) {
69
+ let body = '{}';
70
+ try {
71
+ const raw = await req.text();
72
+ if (raw && raw.trim().length > 0) body = raw;
73
+ } catch {
74
+ // Empty body is valid here; the IDP validates the payload.
75
+ }
76
+
77
+ const result = await idpFetchJSON(req, `${ENV_CONFIG.IDP_URL}/api/Account/profile`, {
78
+ method: 'PUT',
79
+ headers: { 'Content-Type': 'application/json' },
80
+ body,
81
+ });
82
+
83
+ if (!result.ok) {
84
+ return NextResponse.json(
85
+ {
86
+ success: false,
87
+ error: {
88
+ code: 'UPSTREAM_SERVICE_ERROR',
89
+ message: result.json?.message || 'Failed to update profile',
90
+ status: result.status,
91
+ },
92
+ meta: { attemptedRefresh: result.attemptedRefresh },
93
+ },
94
+ { status: result.status }
95
+ );
96
+ }
97
+
98
+ return NextResponse.json(result.json, { status: 200 });
99
+ }
@@ -1,117 +1,117 @@
1
- import { NextRequest, NextResponse } from 'next/server';
2
- import { ENV_CONFIG } from '../../../config/env';
3
- import { getIDPClientConfig } from '../../../lib/idp-client-config';
4
- import { resolveClientKey } from '../../../lib/client-key';
5
- import { idpErrorEnvelope } from '../../../lib/safe-error-response';
6
-
7
- /**
8
- * Account recovery - the ONE forwarder behind /recovery/send-code and /recovery/verify-code (PAY-1588).
9
- *
10
- * THE DEFECT (QAPert, W1 walk on :4797 -> dev-93 IdP 90f766bd0, 2026-09-19): portal /account-auth/recovery,
11
- * initiate -> 200, send-code -> 401 "Upstream error". The browser sends `Authorization: Bearer <recovery session
12
- * token>` (the token `initiate` minted); these handlers went through idpFetchJSON, which sets Authorization ONLY from
13
- * the server-side SESSION and never forwards the inbound header, so the IdP got no Bearer at all
14
- * (AccountController.cs:872-885 = 401 MISSING_TOKEN). Positive control direct to the IdP: without Authorization 401,
15
- * with it 200 and the email is sent.
16
- *
17
- * WHY THE 2026-08-02 REWRITE GOT IT WRONG. It reasoned "browsers send a session COOKIE and no Authorization header"
18
- * and moved both handlers onto idpFetchJSON. That is true of every OTHER authenticated route. It is false of this
19
- * flow: the caller is LOCKED OUT, so there is no session to resolve - the recovery session token IS the credential,
20
- * and the browser is the only party that holds it. Resolving a token from a session that does not exist is why the
21
- * entire recovery path, both halves, has been dead: it fails exactly when it is the only thing left that could help.
22
- *
23
- * WHAT THIS DOES INSTEAD
24
- * - Forwards the inbound `Authorization: Bearer <token>` to the IdP and nothing else from the caller's headers.
25
- * - Uses a direct fetch, NOT idpFetchJSON: a stray session belonging to someone else on the same browser must not
26
- * be able to override the recovery credential with its own bearer (idpFetchJSON's session bearer wins over any
27
- * caller-supplied Authorization).
28
- * - FAILS LOUD when there is no usable bearer: 401 `missing_recovery_token`, without calling upstream. The old
29
- * behaviour returned the IdP's opaque rejection, so the user saw recovery "not working" and the log showed an
30
- * upstream error rather than the auth fault it was.
31
- * - Shape-checks the token (a bearer scheme, one opaque token, sane length) before forwarding, so the header cannot
32
- * be used to smuggle anything but a credential to the IdP. It is never logged.
33
- * - Anonymous route (PAY-1438): on failure only the whitelisted envelope, never the IdP body.
34
- *
35
- * SERVER VALUE WINS for client_id (cards 268012 / 268410): applied AFTER the caller's payload spread, because these are
36
- * unauthenticated endpoints and a browser-supplied client_id would be a tenant-targeting lever on a password-reset path.
37
- *
38
- * @package @payez/next-mvp
39
- */
40
-
41
- /** One opaque token: JWT / base64url / opaque-hex alphabet, no whitespace or control characters, bounded length. */
42
- const TOKEN_SHAPE = /^[A-Za-z0-9\-._~+/]+=*$/;
43
-
44
- /** The recovery bearer from the inbound request, or null when absent / malformed. Never throws. */
45
- export function inboundRecoveryBearer(req: Pick<NextRequest, 'headers'>): string | null {
46
- const raw = req.headers.get('authorization');
47
- if (!raw) return null;
48
- const m = /^Bearer[ ]+(\S+)$/i.exec(raw.trim());
49
- if (!m) return null;
50
- const token = m[1];
51
- if (token.length < 16 || token.length > 4096) return null;
52
- return TOKEN_SHAPE.test(token) ? token : null;
53
- }
54
-
55
- async function recoveryClientKey(where: string): Promise<string | null> {
56
- try {
57
- const config = await getIDPClientConfig();
58
- return resolveClientKey(config, where).key;
59
- } catch (e) {
60
- console.error(`[${where}] could not resolve IDP client config for client_id:`, e);
61
- return null;
62
- }
63
- }
64
-
65
- export interface RecoveryForward {
66
- /** IdP path after the host, e.g. /api/Account/recovery/send-code */
67
- upstreamPath: string;
68
- /** log/route label used for the client-key resolver */
69
- where: string;
70
- /** stable `error` code for an upstream that could not be reached */
71
- failureCode: string;
72
- /** user-facing fallback when the IdP gave no message */
73
- fallbackMessage: string;
74
- }
75
-
76
- export async function forwardRecoveryCall(req: NextRequest, spec: RecoveryForward): Promise<NextResponse> {
77
- let payload: unknown = {};
78
- try {
79
- payload = await req.json();
80
- } catch {
81
- return NextResponse.json({ success: false, error: 'invalid_request', message: 'A JSON body is required' }, { status: 400 });
82
- }
83
-
84
- const bearer = inboundRecoveryBearer(req);
85
- if (!bearer) {
86
- return NextResponse.json(
87
- {
88
- success: false,
89
- error: 'missing_recovery_token',
90
- message: 'Your recovery session could not be found. Start the recovery again from the beginning.',
91
- },
92
- { status: 401 },
93
- );
94
- }
95
-
96
- const clientKey = await recoveryClientKey(spec.where);
97
-
98
- let upstream: Response;
99
- try {
100
- upstream = await fetch(`${ENV_CONFIG.IDP_URL}${spec.upstreamPath}`, {
101
- method: 'POST',
102
- headers: {
103
- 'Content-Type': 'application/json',
104
- Accept: 'application/json',
105
- Authorization: `Bearer ${bearer}`,
106
- ...(clientKey ? { 'X-Client-Id': clientKey } : {}),
107
- },
108
- body: JSON.stringify({ ...(payload as Record<string, unknown>), ...(clientKey ? { client_id: clientKey } : {}) }),
109
- });
110
- } catch {
111
- return NextResponse.json({ success: false, error: spec.failureCode }, { status: 502 });
112
- }
113
-
114
- const data = await upstream.json().catch(() => ({}));
115
- if (!upstream.ok) return NextResponse.json(idpErrorEnvelope(data, spec.fallbackMessage), { status: upstream.status });
116
- return NextResponse.json(data, { status: 200 });
117
- }
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { ENV_CONFIG } from '../../../config/env';
3
+ import { getIDPClientConfig } from '../../../lib/idp-client-config';
4
+ import { resolveClientKey } from '../../../lib/client-key';
5
+ import { idpErrorEnvelope } from '../../../lib/safe-error-response';
6
+
7
+ /**
8
+ * Account recovery - the ONE forwarder behind /recovery/send-code and /recovery/verify-code (PAY-1588).
9
+ *
10
+ * THE DEFECT (QAPert, W1 walk on :4797 -> dev-93 IdP 90f766bd0, 2026-09-19): portal /account-auth/recovery,
11
+ * initiate -> 200, send-code -> 401 "Upstream error". The browser sends `Authorization: Bearer <recovery session
12
+ * token>` (the token `initiate` minted); these handlers went through idpFetchJSON, which sets Authorization ONLY from
13
+ * the server-side SESSION and never forwards the inbound header, so the IdP got no Bearer at all
14
+ * (AccountController.cs:872-885 = 401 MISSING_TOKEN). Positive control direct to the IdP: without Authorization 401,
15
+ * with it 200 and the email is sent.
16
+ *
17
+ * WHY THE 2026-08-02 REWRITE GOT IT WRONG. It reasoned "browsers send a session COOKIE and no Authorization header"
18
+ * and moved both handlers onto idpFetchJSON. That is true of every OTHER authenticated route. It is false of this
19
+ * flow: the caller is LOCKED OUT, so there is no session to resolve - the recovery session token IS the credential,
20
+ * and the browser is the only party that holds it. Resolving a token from a session that does not exist is why the
21
+ * entire recovery path, both halves, has been dead: it fails exactly when it is the only thing left that could help.
22
+ *
23
+ * WHAT THIS DOES INSTEAD
24
+ * - Forwards the inbound `Authorization: Bearer <token>` to the IdP and nothing else from the caller's headers.
25
+ * - Uses a direct fetch, NOT idpFetchJSON: a stray session belonging to someone else on the same browser must not
26
+ * be able to override the recovery credential with its own bearer (idpFetchJSON's session bearer wins over any
27
+ * caller-supplied Authorization).
28
+ * - FAILS LOUD when there is no usable bearer: 401 `missing_recovery_token`, without calling upstream. The old
29
+ * behaviour returned the IdP's opaque rejection, so the user saw recovery "not working" and the log showed an
30
+ * upstream error rather than the auth fault it was.
31
+ * - Shape-checks the token (a bearer scheme, one opaque token, sane length) before forwarding, so the header cannot
32
+ * be used to smuggle anything but a credential to the IdP. It is never logged.
33
+ * - Anonymous route (PAY-1438): on failure only the whitelisted envelope, never the IdP body.
34
+ *
35
+ * SERVER VALUE WINS for client_id (cards 268012 / 268410): applied AFTER the caller's payload spread, because these are
36
+ * unauthenticated endpoints and a browser-supplied client_id would be a tenant-targeting lever on a password-reset path.
37
+ *
38
+ * @package @payez/next-mvp
39
+ */
40
+
41
+ /** One opaque token: JWT / base64url / opaque-hex alphabet, no whitespace or control characters, bounded length. */
42
+ const TOKEN_SHAPE = /^[A-Za-z0-9\-._~+/]+=*$/;
43
+
44
+ /** The recovery bearer from the inbound request, or null when absent / malformed. Never throws. */
45
+ export function inboundRecoveryBearer(req: Pick<NextRequest, 'headers'>): string | null {
46
+ const raw = req.headers.get('authorization');
47
+ if (!raw) return null;
48
+ const m = /^Bearer[ ]+(\S+)$/i.exec(raw.trim());
49
+ if (!m) return null;
50
+ const token = m[1];
51
+ if (token.length < 16 || token.length > 4096) return null;
52
+ return TOKEN_SHAPE.test(token) ? token : null;
53
+ }
54
+
55
+ async function recoveryClientKey(where: string): Promise<string | null> {
56
+ try {
57
+ const config = await getIDPClientConfig();
58
+ return resolveClientKey(config, where).key;
59
+ } catch (e) {
60
+ console.error(`[${where}] could not resolve IDP client config for client_id:`, e);
61
+ return null;
62
+ }
63
+ }
64
+
65
+ export interface RecoveryForward {
66
+ /** IdP path after the host, e.g. /api/Account/recovery/send-code */
67
+ upstreamPath: string;
68
+ /** log/route label used for the client-key resolver */
69
+ where: string;
70
+ /** stable `error` code for an upstream that could not be reached */
71
+ failureCode: string;
72
+ /** user-facing fallback when the IdP gave no message */
73
+ fallbackMessage: string;
74
+ }
75
+
76
+ export async function forwardRecoveryCall(req: NextRequest, spec: RecoveryForward): Promise<NextResponse> {
77
+ let payload: unknown = {};
78
+ try {
79
+ payload = await req.json();
80
+ } catch {
81
+ return NextResponse.json({ success: false, error: 'invalid_request', message: 'A JSON body is required' }, { status: 400 });
82
+ }
83
+
84
+ const bearer = inboundRecoveryBearer(req);
85
+ if (!bearer) {
86
+ return NextResponse.json(
87
+ {
88
+ success: false,
89
+ error: 'missing_recovery_token',
90
+ message: 'Your recovery session could not be found. Start the recovery again from the beginning.',
91
+ },
92
+ { status: 401 },
93
+ );
94
+ }
95
+
96
+ const clientKey = await recoveryClientKey(spec.where);
97
+
98
+ let upstream: Response;
99
+ try {
100
+ upstream = await fetch(`${ENV_CONFIG.IDP_URL}${spec.upstreamPath}`, {
101
+ method: 'POST',
102
+ headers: {
103
+ 'Content-Type': 'application/json',
104
+ Accept: 'application/json',
105
+ Authorization: `Bearer ${bearer}`,
106
+ ...(clientKey ? { 'X-Client-Id': clientKey } : {}),
107
+ },
108
+ body: JSON.stringify({ ...(payload as Record<string, unknown>), ...(clientKey ? { client_id: clientKey } : {}) }),
109
+ });
110
+ } catch {
111
+ return NextResponse.json({ success: false, error: spec.failureCode }, { status: 502 });
112
+ }
113
+
114
+ const data = await upstream.json().catch(() => ({}));
115
+ if (!upstream.ok) return NextResponse.json(idpErrorEnvelope(data, spec.fallbackMessage), { status: upstream.status });
116
+ return NextResponse.json(data, { status: 200 });
117
+ }
@@ -1,59 +1,59 @@
1
- import { NextRequest, NextResponse } from 'next/server';
2
- import { getIDPClientConfig } from '../../../lib/idp-client-config';
3
- import { resolveClientKey } from '../../../lib/client-key';
4
- import { idpErrorEnvelope } from '../../../lib/safe-error-response';
5
-
6
- function getIdpUrl(): string {
7
- const url = process.env.IDP_URL;
8
- if (!url) throw new Error('[IDP_URL] FATAL: IDP_URL environment variable is REQUIRED.');
9
- return url;
10
- }
11
-
12
- /**
13
- * RECOVERY CLIENT KEY — cards 268012 (S12) and 268410.
14
- *
15
- * These four proxies populated NONE of PayEzClientIdResolver's six slots, so
16
- * RequirePayEzUnAuthorized 404'd the request BEFORE the action ran and no
17
- * recovery token was ever minted. That is why S12 failed at step 1.
18
- *
19
- * NOT `ENV_CONFIG.CLIENT_ID`, which is what card 268012 originally prescribed.
20
- * That variable is the ENVIRONMENT-SPECIFIC NUMERIC id, and the IDP accepts a
21
- * numeric with NO existence check (RequirePayEzUnAuthorizedAttribute.cs:31-33)
22
- * while it VALIDATES a slug (:41-47). Hardcoding the numeric here would have
23
- * added four more sites to the 268410 family and made a stale id answer for
24
- * the WRONG TENANT on a password-reset path, silently. Slug-preferring, with
25
- * the numeric fallback logged, is the same resolver the other nine sites use.
26
- *
27
- * SERVER VALUE WINS: client_id is applied AFTER the caller's payload spread.
28
- * These are unauthenticated recovery endpoints, so a browser-supplied
29
- * client_id would be a tenant-targeting lever. The caller does not get to
30
- * choose which tenant their password reset is addressed to.
31
- */
32
- async function recoveryClientKey(): Promise<string | null> {
33
- try {
34
- const config = await getIDPClientConfig();
35
- return resolveClientKey(config, 'account/recovery/initiate').key;
36
- } catch (e) {
37
- console.error('[account/recovery/initiate] could not resolve IDP client config for client_id:', e);
38
- return null;
39
- }
40
- }
41
-
42
- export async function POST(req: NextRequest) {
43
- const IDP_URL = getIdpUrl();
44
- try {
45
- const { email } = await req.json();
46
- const clientKey = await recoveryClientKey();
47
- const upstream = await fetch(`${IDP_URL}/api/Account/recovery/initiate`, {
48
- method: 'POST',
49
- headers: { 'Content-Type': 'application/json', ...(clientKey ? { 'X-Client-Id': clientKey } : {}) },
50
- body: JSON.stringify({ email, ...(clientKey ? { client_id: clientKey } : {}) }),
51
- });
52
- const data = await upstream.json().catch(() => ({}));
53
- // PAY-1438: anonymous route - on failure only the whitelisted envelope, never the IDP body.
54
- if (!upstream.ok) return NextResponse.json(idpErrorEnvelope(data, 'Failed to initiate recovery'), { status: upstream.status });
55
- return NextResponse.json(data, { status: upstream.status });
56
- } catch (e) {
57
- return NextResponse.json({ success: false, error: 'initiate_failed' }, { status: 500 });
58
- }
59
- }
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { getIDPClientConfig } from '../../../lib/idp-client-config';
3
+ import { resolveClientKey } from '../../../lib/client-key';
4
+ import { idpErrorEnvelope } from '../../../lib/safe-error-response';
5
+
6
+ function getIdpUrl(): string {
7
+ const url = process.env.IDP_URL;
8
+ if (!url) throw new Error('[IDP_URL] FATAL: IDP_URL environment variable is REQUIRED.');
9
+ return url;
10
+ }
11
+
12
+ /**
13
+ * RECOVERY CLIENT KEY — cards 268012 (S12) and 268410.
14
+ *
15
+ * These four proxies populated NONE of PayEzClientIdResolver's six slots, so
16
+ * RequirePayEzUnAuthorized 404'd the request BEFORE the action ran and no
17
+ * recovery token was ever minted. That is why S12 failed at step 1.
18
+ *
19
+ * NOT `ENV_CONFIG.CLIENT_ID`, which is what card 268012 originally prescribed.
20
+ * That variable is the ENVIRONMENT-SPECIFIC NUMERIC id, and the IDP accepts a
21
+ * numeric with NO existence check (RequirePayEzUnAuthorizedAttribute.cs:31-33)
22
+ * while it VALIDATES a slug (:41-47). Hardcoding the numeric here would have
23
+ * added four more sites to the 268410 family and made a stale id answer for
24
+ * the WRONG TENANT on a password-reset path, silently. Slug-preferring, with
25
+ * the numeric fallback logged, is the same resolver the other nine sites use.
26
+ *
27
+ * SERVER VALUE WINS: client_id is applied AFTER the caller's payload spread.
28
+ * These are unauthenticated recovery endpoints, so a browser-supplied
29
+ * client_id would be a tenant-targeting lever. The caller does not get to
30
+ * choose which tenant their password reset is addressed to.
31
+ */
32
+ async function recoveryClientKey(): Promise<string | null> {
33
+ try {
34
+ const config = await getIDPClientConfig();
35
+ return resolveClientKey(config, 'account/recovery/initiate').key;
36
+ } catch (e) {
37
+ console.error('[account/recovery/initiate] could not resolve IDP client config for client_id:', e);
38
+ return null;
39
+ }
40
+ }
41
+
42
+ export async function POST(req: NextRequest) {
43
+ const IDP_URL = getIdpUrl();
44
+ try {
45
+ const { email } = await req.json();
46
+ const clientKey = await recoveryClientKey();
47
+ const upstream = await fetch(`${IDP_URL}/api/Account/recovery/initiate`, {
48
+ method: 'POST',
49
+ headers: { 'Content-Type': 'application/json', ...(clientKey ? { 'X-Client-Id': clientKey } : {}) },
50
+ body: JSON.stringify({ email, ...(clientKey ? { client_id: clientKey } : {}) }),
51
+ });
52
+ const data = await upstream.json().catch(() => ({}));
53
+ // PAY-1438: anonymous route - on failure only the whitelisted envelope, never the IDP body.
54
+ if (!upstream.ok) return NextResponse.json(idpErrorEnvelope(data, 'Failed to initiate recovery'), { status: upstream.status });
55
+ return NextResponse.json(data, { status: upstream.status });
56
+ } catch (e) {
57
+ return NextResponse.json({ success: false, error: 'initiate_failed' }, { status: 500 });
58
+ }
59
+ }
@@ -1,21 +1,21 @@
1
- import { NextRequest } from 'next/server';
2
- import { forwardRecoveryCall } from './forward';
3
-
4
- /**
5
- * Account recovery - send-code.
6
- *
7
- * PAY-1588: the recovery session token minted by `initiate` is the caller's only credential (they are locked out; there is
8
- * no session), so this forwards the inbound `Authorization: Bearer <token>` to the IdP and fails loud (401
9
- * `missing_recovery_token`) when it is absent. The whole reasoning, and why the 2026-08-02 idpFetchJSON rewrite killed
10
- * this flow, is in ./forward.ts. send-code and verify-code share that ONE forwarder so they cannot drift apart again.
11
- *
12
- * @package @payez/next-mvp
13
- */
14
- export async function POST(req: NextRequest) {
15
- return forwardRecoveryCall(req, {
16
- upstreamPath: '/api/Account/recovery/send-code',
17
- where: 'account/recovery/send-code',
18
- failureCode: 'send_code_failed',
19
- fallbackMessage: 'Failed to send the verification code',
20
- });
21
- }
1
+ import { NextRequest } from 'next/server';
2
+ import { forwardRecoveryCall } from './forward';
3
+
4
+ /**
5
+ * Account recovery - send-code.
6
+ *
7
+ * PAY-1588: the recovery session token minted by `initiate` is the caller's only credential (they are locked out; there is
8
+ * no session), so this forwards the inbound `Authorization: Bearer <token>` to the IdP and fails loud (401
9
+ * `missing_recovery_token`) when it is absent. The whole reasoning, and why the 2026-08-02 idpFetchJSON rewrite killed
10
+ * this flow, is in ./forward.ts. send-code and verify-code share that ONE forwarder so they cannot drift apart again.
11
+ *
12
+ * @package @payez/next-mvp
13
+ */
14
+ export async function POST(req: NextRequest) {
15
+ return forwardRecoveryCall(req, {
16
+ upstreamPath: '/api/Account/recovery/send-code',
17
+ where: 'account/recovery/send-code',
18
+ failureCode: 'send_code_failed',
19
+ fallbackMessage: 'Failed to send the verification code',
20
+ });
21
+ }
@@ -1,20 +1,20 @@
1
- import { NextRequest } from 'next/server';
2
- import { forwardRecoveryCall } from './forward';
3
-
4
- /**
5
- * Account recovery - verify-code.
6
- *
7
- * PAY-1588: forwards the inbound recovery `Authorization: Bearer <token>` to the IdP and fails loud (401
8
- * `missing_recovery_token`) when it is absent. See ./forward.ts for the full reasoning; send-code and verify-code share
9
- * that ONE forwarder so they cannot drift apart again.
10
- *
11
- * @package @payez/next-mvp
12
- */
13
- export async function POST(req: NextRequest) {
14
- return forwardRecoveryCall(req, {
15
- upstreamPath: '/api/Account/recovery/verify-code',
16
- where: 'account/recovery/verify-code',
17
- failureCode: 'verify_code_failed',
18
- fallbackMessage: 'Failed to verify the code',
19
- });
20
- }
1
+ import { NextRequest } from 'next/server';
2
+ import { forwardRecoveryCall } from './forward';
3
+
4
+ /**
5
+ * Account recovery - verify-code.
6
+ *
7
+ * PAY-1588: forwards the inbound recovery `Authorization: Bearer <token>` to the IdP and fails loud (401
8
+ * `missing_recovery_token`) when it is absent. See ./forward.ts for the full reasoning; send-code and verify-code share
9
+ * that ONE forwarder so they cannot drift apart again.
10
+ *
11
+ * @package @payez/next-mvp
12
+ */
13
+ export async function POST(req: NextRequest) {
14
+ return forwardRecoveryCall(req, {
15
+ upstreamPath: '/api/Account/recovery/verify-code',
16
+ where: 'account/recovery/verify-code',
17
+ failureCode: 'verify_code_failed',
18
+ fallbackMessage: 'Failed to verify the code',
19
+ });
20
+ }