@payez/next-mvp 4.4.5 → 4.4.6

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 (189) hide show
  1. package/README.md +10 -3
  2. package/dist/api/auth-handler.js +5 -3
  3. package/dist/api-handlers/account/change-password.js +6 -3
  4. package/dist/api-handlers/account/masked-info.js +2 -1
  5. package/dist/api-handlers/account/recovery/forward.d.ts +14 -0
  6. package/dist/api-handlers/account/recovery/forward.js +105 -0
  7. package/dist/api-handlers/account/recovery/initiate.js +4 -0
  8. package/dist/api-handlers/account/recovery/send-code.d.ts +12 -2
  9. package/dist/api-handlers/account/recovery/send-code.js +11 -84
  10. package/dist/api-handlers/account/recovery/verify-code.d.ts +11 -2
  11. package/dist/api-handlers/account/recovery/verify-code.js +10 -84
  12. package/dist/api-handlers/account/reset-password.js +24 -1
  13. package/dist/api-handlers/account/send-code.d.ts +1 -15
  14. package/dist/api-handlers/account/send-code.js +3 -6
  15. package/dist/api-handlers/account/update-phone.js +6 -1
  16. package/dist/api-handlers/account/validate-password.js +3 -1
  17. package/dist/api-handlers/account/verify-email.d.ts +1 -17
  18. package/dist/api-handlers/account/verify-email.js +4 -6
  19. package/dist/api-handlers/account/verify-sms.d.ts +1 -17
  20. package/dist/api-handlers/account/verify-sms.js +4 -6
  21. package/dist/api-handlers/admin/analytics.js +12 -4
  22. package/dist/api-handlers/admin/audit.js +12 -4
  23. package/dist/api-handlers/admin/redis-sessions.js +16 -8
  24. package/dist/api-handlers/admin/sessions.js +14 -6
  25. package/dist/api-handlers/admin/site-logs.js +39 -16
  26. package/dist/api-handlers/admin/stats.js +14 -4
  27. package/dist/api-handlers/admin/users.js +14 -6
  28. package/dist/api-handlers/admin/vibe-data.js +19 -11
  29. package/dist/api-handlers/auth/jwks.js +2 -1
  30. package/dist/api-handlers/auth/login.js +13 -4
  31. package/dist/api-handlers/auth/passkey-signin.js +6 -0
  32. package/dist/api-handlers/auth/refresh.js +38 -11
  33. package/dist/api-handlers/auth/signout.js +2 -1
  34. package/dist/api-handlers/auth/status.js +4 -1
  35. package/dist/api-handlers/auth/update-session.d.ts +25 -4
  36. package/dist/api-handlers/auth/update-session.js +38 -11
  37. package/dist/api-handlers/auth/validate.js +4 -2
  38. package/dist/api-handlers/auth/verify-code.js +3 -2
  39. package/dist/api-handlers/mfa/backup-code-verify.d.ts +1 -18
  40. package/dist/api-handlers/mfa/backup-code-verify.js +4 -6
  41. package/dist/api-handlers/mfa/backup-codes.js +2 -1
  42. package/dist/api-handlers/mfa/methods.js +2 -1
  43. package/dist/api-handlers/mfa/totp-confirm.js +2 -1
  44. package/dist/api-handlers/mfa/totp-enroll.js +2 -1
  45. package/dist/api-handlers/mfa/totp-verify.d.ts +1 -19
  46. package/dist/api-handlers/mfa/totp-verify.js +4 -6
  47. package/dist/api-handlers/passkeys/list.js +2 -1
  48. package/dist/api-handlers/passkeys/manage.js +2 -1
  49. package/dist/api-handlers/passkeys/register-options.js +2 -1
  50. package/dist/api-handlers/passkeys/register-verify.js +2 -1
  51. package/dist/api-handlers/session/refresh-viability.d.ts +4 -2
  52. package/dist/api-handlers/session/refresh-viability.js +10 -7
  53. package/dist/api-handlers/session/viability.js +4 -3
  54. package/dist/api-handlers/test/force-expire.js +5 -3
  55. package/dist/auth/auth-decision.js +2 -1
  56. package/dist/auth/better-auth.d.ts +14 -0
  57. package/dist/auth/better-auth.js +35 -49
  58. package/dist/auth/utils/idp-client.js +2 -1
  59. package/dist/components/admin/DataBrowserTab.js +4 -1
  60. package/dist/components/admin/SessionsTab.js +9 -3
  61. package/dist/components/recovery/RequestedStep.d.ts +11 -0
  62. package/dist/components/recovery/RequestedStep.js +14 -0
  63. package/dist/components/recovery/SetPasswordStep.js +10 -1
  64. package/dist/lib/app-slug.d.ts +21 -0
  65. package/dist/lib/app-slug.js +51 -0
  66. package/dist/lib/ensure-fresh-access-token.d.ts +26 -0
  67. package/dist/lib/ensure-fresh-access-token.js +105 -4
  68. package/dist/lib/idp-client-config.d.ts +20 -0
  69. package/dist/lib/idp-client-config.js +66 -45
  70. package/dist/lib/idp-fetch.js +1 -1
  71. package/dist/lib/password-rules.d.ts +22 -0
  72. package/dist/lib/password-rules.js +43 -0
  73. package/dist/lib/safe-error-response.d.ts +67 -0
  74. package/dist/lib/safe-error-response.js +130 -0
  75. package/dist/lib/session-cookie-shape.d.ts +22 -19
  76. package/dist/lib/session-cookie-shape.js +14 -18
  77. package/dist/lib/session-store.d.ts +8 -1
  78. package/dist/lib/session-store.js +170 -24
  79. package/dist/lib/startup-init.d.ts +6 -0
  80. package/dist/lib/startup-init.js +30 -4
  81. package/dist/lib/token-lifecycle.js +2 -1
  82. package/dist/lib/token-log-id.d.ts +18 -0
  83. package/dist/lib/token-log-id.js +84 -0
  84. package/dist/lib/two-factor-verify-failure.d.ts +45 -0
  85. package/dist/lib/two-factor-verify-failure.js +143 -0
  86. package/dist/middleware/create-middleware.d.ts +2 -1
  87. package/dist/middleware/create-middleware.js +43 -8
  88. package/dist/pages/recovery/page.js +14 -4
  89. package/dist/pages/test-env/JwtInspectPage.js +7 -149
  90. package/dist/pages/test-env/RefreshTokenPage.js +4 -1
  91. package/dist/pages/test-env/redact-credentials.d.ts +6 -0
  92. package/dist/pages/test-env/redact-credentials.js +101 -0
  93. package/dist/pages/verify-code/page.js +2 -1
  94. package/dist/routes/auth/logout.d.ts +2 -1
  95. package/dist/routes/auth/logout.js +4 -2
  96. package/dist/routes/auth/session.d.ts +0 -3
  97. package/dist/routes/auth/session.js +16 -10
  98. package/dist/routes/auth/viability.d.ts +3 -2
  99. package/dist/routes/auth/viability.js +11 -7
  100. package/dist/server/auth.js +10 -6
  101. package/dist/server/decode-session.js +15 -26
  102. package/dist/server/verified-session-token.d.ts +13 -0
  103. package/dist/server/verified-session-token.js +103 -0
  104. package/dist/stores/authStore.js +14 -3
  105. package/dist/types/recovery.d.ts +1 -1
  106. package/dist/utils/api.d.ts +18 -0
  107. package/dist/utils/api.js +51 -9
  108. package/dist/vibe/sessions.js +2 -1
  109. package/package.json +2 -1
  110. package/scripts/verify-release-provenance.mjs +410 -0
  111. package/src/api/auth-handler.ts +5 -3
  112. package/src/api-handlers/account/change-password.ts +6 -3
  113. package/src/api-handlers/account/masked-info.ts +2 -1
  114. package/src/api-handlers/account/recovery/forward.ts +117 -0
  115. package/src/api-handlers/account/recovery/initiate.ts +3 -0
  116. package/src/api-handlers/account/recovery/send-code.ts +13 -99
  117. package/src/api-handlers/account/recovery/verify-code.ts +12 -99
  118. package/src/api-handlers/account/reset-password.ts +30 -1
  119. package/src/api-handlers/account/send-code.ts +3 -9
  120. package/src/api-handlers/account/update-phone.ts +6 -1
  121. package/src/api-handlers/account/validate-password.ts +3 -1
  122. package/src/api-handlers/account/verify-email.ts +4 -9
  123. package/src/api-handlers/account/verify-sms.ts +4 -9
  124. package/src/api-handlers/admin/analytics.ts +13 -5
  125. package/src/api-handlers/admin/audit.ts +13 -5
  126. package/src/api-handlers/admin/redis-sessions.ts +16 -8
  127. package/src/api-handlers/admin/sessions.ts +15 -7
  128. package/src/api-handlers/admin/site-logs.ts +39 -16
  129. package/src/api-handlers/admin/stats.ts +15 -5
  130. package/src/api-handlers/admin/users.ts +15 -7
  131. package/src/api-handlers/admin/vibe-data.ts +20 -12
  132. package/src/api-handlers/auth/jwks.ts +2 -1
  133. package/src/api-handlers/auth/login.ts +13 -4
  134. package/src/api-handlers/auth/passkey-signin.ts +4 -0
  135. package/src/api-handlers/auth/refresh.ts +38 -11
  136. package/src/api-handlers/auth/signout.ts +2 -1
  137. package/src/api-handlers/auth/status.ts +4 -1
  138. package/src/api-handlers/auth/update-session.ts +45 -11
  139. package/src/api-handlers/auth/validate.ts +4 -2
  140. package/src/api-handlers/auth/verify-code.ts +3 -2
  141. package/src/api-handlers/mfa/backup-code-verify.ts +4 -9
  142. package/src/api-handlers/mfa/backup-codes.ts +2 -1
  143. package/src/api-handlers/mfa/methods.ts +2 -1
  144. package/src/api-handlers/mfa/totp-confirm.ts +2 -1
  145. package/src/api-handlers/mfa/totp-enroll.ts +2 -1
  146. package/src/api-handlers/mfa/totp-verify.ts +4 -9
  147. package/src/api-handlers/passkeys/list.ts +2 -1
  148. package/src/api-handlers/passkeys/manage.ts +2 -1
  149. package/src/api-handlers/passkeys/register-options.ts +2 -1
  150. package/src/api-handlers/passkeys/register-verify.ts +2 -1
  151. package/src/api-handlers/session/refresh-viability.ts +10 -7
  152. package/src/api-handlers/session/viability.ts +4 -3
  153. package/src/api-handlers/test/force-expire.ts +5 -3
  154. package/src/auth/auth-decision.ts +2 -1
  155. package/src/auth/better-auth.ts +37 -55
  156. package/src/auth/utils/idp-client.ts +2 -1
  157. package/src/components/admin/DataBrowserTab.tsx +4 -1
  158. package/src/components/admin/SessionsTab.tsx +10 -3
  159. package/src/components/recovery/RequestedStep.tsx +30 -0
  160. package/src/components/recovery/SetPasswordStep.tsx +24 -3
  161. package/src/hooks/useViabilitySession.ts +2 -1
  162. package/src/lib/app-slug.ts +50 -0
  163. package/src/lib/ensure-fresh-access-token.ts +133 -4
  164. package/src/lib/idp-client-config.ts +91 -45
  165. package/src/lib/idp-fetch.ts +1 -1
  166. package/src/lib/password-rules.ts +50 -0
  167. package/src/lib/safe-error-response.ts +126 -0
  168. package/src/lib/session-cookie-shape.ts +23 -22
  169. package/src/lib/session-store.ts +167 -24
  170. package/src/lib/startup-init.ts +29 -4
  171. package/src/lib/token-lifecycle.ts +2 -1
  172. package/src/lib/token-log-id.ts +69 -0
  173. package/src/lib/two-factor-verify-failure.ts +167 -0
  174. package/src/middleware/create-middleware.ts +55 -9
  175. package/src/pages/recovery/page.tsx +21 -4
  176. package/src/pages/test-env/JwtInspectPage.tsx +11 -233
  177. package/src/pages/test-env/RefreshTokenPage.tsx +9 -4
  178. package/src/pages/test-env/redact-credentials.ts +96 -0
  179. package/src/pages/verify-code/page.tsx +2 -1
  180. package/src/routes/auth/logout.ts +4 -2
  181. package/src/routes/auth/session.ts +16 -10
  182. package/src/routes/auth/viability.ts +11 -7
  183. package/src/server/auth.ts +10 -6
  184. package/src/server/decode-session.ts +16 -26
  185. package/src/server/verified-session-token.ts +103 -0
  186. package/src/stores/authStore.ts +16 -5
  187. package/src/types/recovery.ts +1 -1
  188. package/src/utils/api.ts +47 -8
  189. package/src/vibe/sessions.ts +2 -1
@@ -46,12 +46,14 @@ var __importStar = (this && this.__importStar) || (function () {
46
46
  Object.defineProperty(exports, "__esModule", { value: true });
47
47
  exports.ensureInitialized = ensureInitialized;
48
48
  exports.logStartupStatus = logStartupStatus;
49
+ exports.getSlugMismatchReason = getSlugMismatchReason;
49
50
  exports.getStartupIDPConfig = getStartupIDPConfig;
50
51
  exports.isInitializationFailed = isInitializationFailed;
51
52
  exports.getInitializationError = getInitializationError;
52
53
  exports.isAppReady = isAppReady;
53
54
  require("server-only");
54
55
  const idp_client_config_1 = require("./idp-client-config");
56
+ const app_slug_1 = require("./app-slug");
55
57
  let initializationStarted = false;
56
58
  let initializationComplete = false;
57
59
  let initializationFailed = false;
@@ -90,9 +92,22 @@ async function ensureInitialized() {
90
92
  lastStartupAttemptTime = now;
91
93
  // Mark as started
92
94
  initializationStarted = true;
93
- // Start initialization
94
- initializationPromise = performInitialization();
95
- await initializationPromise;
95
+ // Start initialization.
96
+ //
97
+ // PAY-642: a FAILED attempt must not stay installed. This used to be
98
+ // `initializationPromise = performInitialization(); await initializationPromise;` with no reset, and the
99
+ // in-progress check above runs BEFORE the backoff check, so a pod that booted while the IdP was down replayed
100
+ // its first rejection to every caller forever: the 30 s backoff could never lead to a retry. Same latch, same
101
+ // fix, as createSingleFlight (lib/single-flight.ts): callers of the failing attempt still see the rejection
102
+ // (loud), then the attempt clears itself, identity-guarded so a slow failure never evicts a newer attempt.
103
+ // After a failure the backoff check above governs when the next attempt may start.
104
+ const attempt = performInitialization();
105
+ initializationPromise = attempt;
106
+ attempt.catch(() => {
107
+ if (initializationPromise === attempt)
108
+ initializationPromise = null;
109
+ });
110
+ await attempt;
96
111
  }
97
112
  /**
98
113
  * Synchronously log startup status
@@ -190,8 +205,11 @@ async function performInitialization() {
190
205
  // Step 3: Validate cookie name consistency
191
206
  // This catches bugs where getJwtCookieName() returns a different name than
192
207
  // what auth-options.ts configures, which causes sessions to fail in production
193
- const { validateCookieNameConsistency, getSessionCookieName } = await Promise.resolve().then(() => __importStar(require('./app-slug')));
208
+ const { validateCookieNameConsistency, getSessionCookieName, assertAppSlugMatchesIdp, assertIdpSlugKnown } = await Promise.resolve().then(() => __importStar(require('./app-slug')));
194
209
  validateCookieNameConsistency();
210
+ // rigpert 60743: refuse to run with two cookie-name sources (IdP clientSlug vs APP_SLUG/CLIENT_ID).
211
+ assertIdpSlugKnown(cachedIDPConfig?.clientSlug);
212
+ assertAppSlugMatchesIdp(cachedIDPConfig?.clientSlug);
195
213
  console.log('[STARTUP] Cookie name consistency validated:', getSessionCookieName());
196
214
  // All done
197
215
  console.log('');
@@ -241,6 +259,14 @@ ${connectionLine}║ Possible causes:
241
259
  * Get the cached IDP config after initialization.
242
260
  * Returns null if not yet initialized.
243
261
  */
262
+ /**
263
+ * For health endpoints (rigpert 60765): a guard that does not stop the process is an empty red unless the mismatch is visible
264
+ * where operators and promote-fe look. Returns `SLUG_MISMATCH: cookie=<idp slug> gate=<app slug>` while the drift holds, else null;
265
+ * a health route should answer non-200 with this as the reason. Null until the IdP config has been fetched.
266
+ */
267
+ function getSlugMismatchReason() {
268
+ return (0, app_slug_1.slugStateReason)(cachedIDPConfig?.clientSlug, initializationComplete || initializationFailed);
269
+ }
244
270
  function getStartupIDPConfig() {
245
271
  return cachedIDPConfig;
246
272
  }
@@ -27,6 +27,7 @@ const session_store_1 = require("./session-store");
27
27
  const auth_1 = require("../server/auth");
28
28
  const redis_1 = require("./redis");
29
29
  const app_slug_1 = require("./app-slug");
30
+ const token_log_id_1 = require("./token-log-id");
30
31
  // 5 minute threshold for "needs refresh" - matches refresh handler pattern
31
32
  const REFRESH_THRESHOLD_MS = 5 * 60 * 1000;
32
33
  // Concurrent refresh handling configuration
@@ -360,7 +361,7 @@ async function ensureFreshToken(request) {
360
361
  }
361
362
  }
362
363
  console.log('[TOKEN_LIFECYCLE] ensureFreshToken check:', {
363
- sessionToken: sessionToken.substring(0, 8) + '...',
364
+ sessionToken: (0, token_log_id_1.tokenLogId)(sessionToken),
364
365
  accessTokenExpires: tokenExpiresStr,
365
366
  now: new Date().toISOString(),
366
367
  needsRefresh: needsRefreshNow,
@@ -0,0 +1,18 @@
1
+ /**
2
+ * tokenLogId - a log-safe correlation id for a session token (PR 154 follow-up, BAPert ruling on PAY-1427:
3
+ * "no token material in logs, not even partial").
4
+ *
5
+ * The package used to log `sessionToken.substring(0, 8) + '...'` so lines about one session could be
6
+ * correlated. Those 8 characters ARE token material. This returns `h:` + the first 12 hex characters of
7
+ * SHA-256(token) instead: the same correlation (one token -> one stable id, across every log line and
8
+ * process), with nothing of the token itself.
9
+ *
10
+ * Synchronous and dependency-free on purpose: the call sites include Edge middleware
11
+ * (create-middleware, auth-decision) and browser code (authStore, SessionSync), where node:crypto is
12
+ * unavailable and Web Crypto's digest is async. SHA-256 below follows FIPS 180-4 and is verified
13
+ * against node:crypto in test/session-token-log-id.test.cjs.
14
+ */
15
+ /** SHA-256 of the UTF-8 encoding of `input`, as lowercase hex. */
16
+ export declare function sha256Hex(input: string): string;
17
+ /** `h:` + 12 hex of SHA-256(token); `none` when there is no token. Never any part of the token itself. */
18
+ export declare function tokenLogId(token: string | null | undefined): string;
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ /**
3
+ * tokenLogId - a log-safe correlation id for a session token (PR 154 follow-up, BAPert ruling on PAY-1427:
4
+ * "no token material in logs, not even partial").
5
+ *
6
+ * The package used to log `sessionToken.substring(0, 8) + '...'` so lines about one session could be
7
+ * correlated. Those 8 characters ARE token material. This returns `h:` + the first 12 hex characters of
8
+ * SHA-256(token) instead: the same correlation (one token -> one stable id, across every log line and
9
+ * process), with nothing of the token itself.
10
+ *
11
+ * Synchronous and dependency-free on purpose: the call sites include Edge middleware
12
+ * (create-middleware, auth-decision) and browser code (authStore, SessionSync), where node:crypto is
13
+ * unavailable and Web Crypto's digest is async. SHA-256 below follows FIPS 180-4 and is verified
14
+ * against node:crypto in test/session-token-log-id.test.cjs.
15
+ */
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.sha256Hex = sha256Hex;
18
+ exports.tokenLogId = tokenLogId;
19
+ const K = new Uint32Array([
20
+ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
21
+ 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
22
+ 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
23
+ 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
24
+ 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
25
+ 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
26
+ 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
27
+ 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
28
+ ]);
29
+ const rotr = (x, n) => (x >>> n) | (x << (32 - n));
30
+ /** SHA-256 of the UTF-8 encoding of `input`, as lowercase hex. */
31
+ function sha256Hex(input) {
32
+ const msg = new TextEncoder().encode(input);
33
+ const bitLen = msg.length * 8;
34
+ const padded = new Uint8Array(((msg.length + 9 + 63) >> 6) << 6);
35
+ padded.set(msg);
36
+ padded[msg.length] = 0x80;
37
+ const view = new DataView(padded.buffer);
38
+ view.setUint32(padded.length - 8, Math.floor(bitLen / 0x100000000));
39
+ view.setUint32(padded.length - 4, bitLen >>> 0);
40
+ const h = new Uint32Array([0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19]);
41
+ const w = new Uint32Array(64);
42
+ for (let off = 0; off < padded.length; off += 64) {
43
+ for (let i = 0; i < 16; i++)
44
+ w[i] = view.getUint32(off + i * 4);
45
+ for (let i = 16; i < 64; i++) {
46
+ const s15 = w[i - 15];
47
+ const s2 = w[i - 2];
48
+ const sigma0 = rotr(s15, 7) ^ rotr(s15, 18) ^ (s15 >>> 3);
49
+ const sigma1 = rotr(s2, 17) ^ rotr(s2, 19) ^ (s2 >>> 10);
50
+ w[i] = (w[i - 16] + sigma0 + w[i - 7] + sigma1) | 0;
51
+ }
52
+ let a = h[0], b = h[1], c = h[2], d = h[3], e = h[4], f = h[5], g = h[6], hh = h[7];
53
+ for (let i = 0; i < 64; i++) {
54
+ const t1 = (hh + (rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25)) + ((e & f) ^ (~e & g)) + K[i] + w[i]) | 0;
55
+ const t2 = ((rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) + ((a & b) ^ (a & c) ^ (b & c))) | 0;
56
+ hh = g;
57
+ g = f;
58
+ f = e;
59
+ e = (d + t1) | 0;
60
+ d = c;
61
+ c = b;
62
+ b = a;
63
+ a = (t1 + t2) | 0;
64
+ }
65
+ h[0] = (h[0] + a) | 0;
66
+ h[1] = (h[1] + b) | 0;
67
+ h[2] = (h[2] + c) | 0;
68
+ h[3] = (h[3] + d) | 0;
69
+ h[4] = (h[4] + e) | 0;
70
+ h[5] = (h[5] + f) | 0;
71
+ h[6] = (h[6] + g) | 0;
72
+ h[7] = (h[7] + hh) | 0;
73
+ }
74
+ let out = '';
75
+ for (let i = 0; i < 8; i++)
76
+ out += (h[i] >>> 0).toString(16).padStart(8, '0');
77
+ return out;
78
+ }
79
+ /** `h:` + 12 hex of SHA-256(token); `none` when there is no token. Never any part of the token itself. */
80
+ function tokenLogId(token) {
81
+ if (typeof token !== 'string' || token.length === 0)
82
+ return 'none';
83
+ return 'h:' + sha256Hex(token).slice(0, 12);
84
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * PAY-1443: the ONE way a 2FA verify handler answers an IDP rejection.
3
+ *
4
+ * Found live (portal.keelauth.com /account-auth/verify-code, 2026-09-17): a wrong
5
+ * authenticator code rendered as the banner text `{ message = Invalid authentication code }`.
6
+ * The handlers relayed `result.json?.message` verbatim, and the IDP's error text is not a
7
+ * user-facing string:
8
+ * - deployed IDP: the >=400 envelope sets Message = value.ToString() on the controller's
9
+ * `new { message = ... }`, producing C#'s anonymous-object ToString `{ message = X }`
10
+ * (or a DTO's TYPE NAME for a typed body);
11
+ * - IDP master (e03a0e449): no ToString, but the lookup is case-sensitive, so the envelope
12
+ * message is "An error occurred" and the real text sits only in error.details.message.
13
+ *
14
+ * So the upstream text is used ONLY TO CLASSIFY, never rendered. The caller gets:
15
+ * - the upstream HTTP status, unchanged (verify-code's 401 branch probes session liveness);
16
+ * - a stable `code` the UI can switch on;
17
+ * - one of the fixed sentences below.
18
+ */
19
+ import { NextResponse } from 'next/server';
20
+ export type TwoFactorVerifyMethod = 'totp' | 'sms' | 'email' | 'backup_code';
21
+ export type TwoFactorVerifyFailureCode = 'INVALID_CODE' | 'LOCKED' | 'RATE_LIMITED' | 'SESSION_REJECTED' | 'VERIFY_UNAVAILABLE' | 'VERIFY_FAILED';
22
+ /**
23
+ * Every string the IDP might have put anywhere in the error envelope, unwrapped from
24
+ * `{ message = X }` ToString form. Classification input only.
25
+ */
26
+ export declare function upstreamErrorTexts(json: unknown): string[];
27
+ export declare function classifyTwoFactorVerifyFailure(status: number, json: unknown): TwoFactorVerifyFailureCode;
28
+ export declare function twoFactorVerifyFailure(result: {
29
+ status: number;
30
+ json: unknown;
31
+ attemptedRefresh: boolean;
32
+ }, method: TwoFactorVerifyMethod): NextResponse;
33
+ /**
34
+ * PAY-1443 residual: account/send-code on the SAME verify-code screen. A send has no
35
+ * "wrong code" outcome, so it gets its own small classification, but the same rule applies:
36
+ * upstream text/code never reach the response. The portal renders `data.message || data.error`,
37
+ * so no `message` field is emitted on failure.
38
+ */
39
+ export type TwoFactorSendMethod = 'sms' | 'email';
40
+ export type TwoFactorSendFailureCode = 'RATE_LIMITED' | 'SESSION_REJECTED' | 'SEND_UNAVAILABLE' | 'SEND_FAILED';
41
+ export declare function classifyTwoFactorSendFailure(status: number): TwoFactorSendFailureCode;
42
+ export declare function twoFactorSendCodeFailure(result: {
43
+ status: number;
44
+ attemptedRefresh: boolean;
45
+ }, method: TwoFactorSendMethod): NextResponse;
@@ -0,0 +1,143 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.upstreamErrorTexts = upstreamErrorTexts;
4
+ exports.classifyTwoFactorVerifyFailure = classifyTwoFactorVerifyFailure;
5
+ exports.twoFactorVerifyFailure = twoFactorVerifyFailure;
6
+ exports.classifyTwoFactorSendFailure = classifyTwoFactorSendFailure;
7
+ exports.twoFactorSendCodeFailure = twoFactorSendCodeFailure;
8
+ /**
9
+ * PAY-1443: the ONE way a 2FA verify handler answers an IDP rejection.
10
+ *
11
+ * Found live (portal.keelauth.com /account-auth/verify-code, 2026-09-17): a wrong
12
+ * authenticator code rendered as the banner text `{ message = Invalid authentication code }`.
13
+ * The handlers relayed `result.json?.message` verbatim, and the IDP's error text is not a
14
+ * user-facing string:
15
+ * - deployed IDP: the >=400 envelope sets Message = value.ToString() on the controller's
16
+ * `new { message = ... }`, producing C#'s anonymous-object ToString `{ message = X }`
17
+ * (or a DTO's TYPE NAME for a typed body);
18
+ * - IDP master (e03a0e449): no ToString, but the lookup is case-sensitive, so the envelope
19
+ * message is "An error occurred" and the real text sits only in error.details.message.
20
+ *
21
+ * So the upstream text is used ONLY TO CLASSIFY, never rendered. The caller gets:
22
+ * - the upstream HTTP status, unchanged (verify-code's 401 branch probes session liveness);
23
+ * - a stable `code` the UI can switch on;
24
+ * - one of the fixed sentences below.
25
+ */
26
+ const server_1 = require("next/server");
27
+ const INVALID_CODE_TEXT = {
28
+ totp: 'Invalid authentication code. Please try again.',
29
+ sms: 'Invalid verification code. Please try again.',
30
+ email: 'Invalid verification code. Please try again.',
31
+ backup_code: 'Invalid backup code. Please try again.',
32
+ };
33
+ const FIXED_TEXT = {
34
+ LOCKED: 'Too many incorrect codes. Verification is locked for 15 minutes - wait before entering another code.',
35
+ RATE_LIMITED: 'Too many attempts. Please wait a moment and try again.',
36
+ SESSION_REJECTED: 'We could not verify the code for this session. Please try again.',
37
+ VERIFY_UNAVAILABLE: 'We could not verify the code right now. Please try again.',
38
+ VERIFY_FAILED: 'We could not verify that code. Please try again.',
39
+ };
40
+ /**
41
+ * MfaTotpService ErrorCode "locked": the bare code, or `code = locked` inside a deployed
42
+ * ToString envelope.
43
+ *
44
+ * CAUGHT BEFORE SHIPPING (self-test, this file): an earlier draft wrote this with  word
45
+ * boundaries around `code` and `locked`. The literal that landed on disk was not the two
46
+ * characters backslash+b - it was a raw ASCII backspace byte (0x08), invisible in every editor
47
+ * and terminal that rendered this file, INCLUDING Read/cat/grep in this session. A regex with a
48
+ * literal backspace control character where a word-boundary assertion was intended matches
49
+ * nothing real, so `IDP_LOCKED.test(...)` was silently false on every fixture - caught only
50
+ * because the RED-first test stayed red after the earlier "fix" and a hex dump of the compiled
51
+ * line showed 08 where 5c62 (backslash, b) should have been. Word boundaries are not
52
+ * load-bearing for the shipped control cases (a bare code=locked substring is specific enough
53
+ * here - the IdP field name is literally code), so this drops them rather than re-risk the same
54
+ * invisible-byte trap.
55
+ */
56
+ const IDP_LOCKED = /^locked$|code\s*=\s*locked/i;
57
+ /**
58
+ * MfaTotpService ErrorCode "server_error" (PAY-1589: secret cannot be decrypted) or "service_unavailable" (330830): the replay store could not be read or written, so
59
+ * the verify FAILED CLOSED. It is a fault on our side, not a wrong code. Matched on the CODE only, never on
60
+ * the prose - "unavailable" appears in unrelated messages - in the same two forms as IDP_LOCKED.
61
+ */
62
+ const IDP_UNAVAILABLE = /^(?:service_unavailable|server_error)$|code\s*=\s*(?:service_unavailable|server_error)\b/i;
63
+ const WRONG_CODE = /\b(invalid|incorrect|wrong|expired)\b[^.]{0,40}\b(code|otp|totp|token)\b|\binvalid_code\b/i;
64
+ /**
65
+ * Every string the IDP might have put anywhere in the error envelope, unwrapped from
66
+ * `{ message = X }` ToString form. Classification input only.
67
+ */
68
+ function upstreamErrorTexts(json) {
69
+ const out = [];
70
+ const add = (v) => {
71
+ if (typeof v !== 'string' || !v)
72
+ return;
73
+ out.push(v);
74
+ const m = v.match(/^\{\s*\w+\s*=\s*(.*?)\s*\}$/s);
75
+ if (m)
76
+ out.push(m[1]);
77
+ };
78
+ const j = json;
79
+ if (!j || typeof j !== 'object')
80
+ return out;
81
+ add(j.message);
82
+ add(j.code);
83
+ add(j.error);
84
+ if (j.error && typeof j.error === 'object') {
85
+ add(j.error.message);
86
+ add(j.error.code);
87
+ const d = j.error.details;
88
+ if (d && typeof d === 'object') {
89
+ add(d.message);
90
+ add(d.Message);
91
+ add(d.code);
92
+ add(d.errorCode);
93
+ }
94
+ }
95
+ return out;
96
+ }
97
+ function classifyTwoFactorVerifyFailure(status, json) {
98
+ // PAY-902 (BAPert R484): the IdP's lockout CODE, not its prose. Door A is post-authentication, so stating
99
+ // the lockout discloses nothing; telling a locked-out user "try again" sends them to spend attempts.
100
+ //
101
+ // PAY-1574: THIS MUST COME BEFORE THE 429 TEST. PayEz-Core PR 366 answers a TOTP lockout as HTTP 429 carrying
102
+ // code "locked". With `status === 429` first, that status returned RATE_LIMITED and the lock code the IdP had
103
+ // just started sending was discarded unread - the tester saw a plain rate limit. A 429 with NO lock code is
104
+ // still RATE_LIMITED, below.
105
+ if (upstreamErrorTexts(json).some((t) => IDP_LOCKED.test(t)))
106
+ return 'LOCKED';
107
+ if (status === 429)
108
+ return 'RATE_LIMITED';
109
+ // PAY-1574: the verify failed closed on OUR side (replay store down). Not a wrong code, whatever the status.
110
+ if (upstreamErrorTexts(json).some((t) => IDP_UNAVAILABLE.test(t)))
111
+ return 'VERIFY_UNAVAILABLE';
112
+ if (upstreamErrorTexts(json).some((t) => WRONG_CODE.test(t)))
113
+ return 'INVALID_CODE';
114
+ if (status === 401 || status === 403)
115
+ return 'SESSION_REJECTED';
116
+ if (status >= 500 || status === 0)
117
+ return 'VERIFY_UNAVAILABLE';
118
+ return 'VERIFY_FAILED';
119
+ }
120
+ function twoFactorVerifyFailure(result, method) {
121
+ const code = classifyTwoFactorVerifyFailure(result.status, result.json);
122
+ const error = code === 'INVALID_CODE' ? INVALID_CODE_TEXT[method] : FIXED_TEXT[code];
123
+ return server_1.NextResponse.json({ success: false, error, code, meta: { attemptedRefresh: result.attemptedRefresh } }, { status: result.status });
124
+ }
125
+ const SEND_LABEL = { sms: 'text message', email: 'email' };
126
+ function classifyTwoFactorSendFailure(status) {
127
+ if (status === 429)
128
+ return 'RATE_LIMITED';
129
+ if (status === 401 || status === 403)
130
+ return 'SESSION_REJECTED';
131
+ if (status >= 500 || status === 0)
132
+ return 'SEND_UNAVAILABLE';
133
+ return 'SEND_FAILED';
134
+ }
135
+ function twoFactorSendCodeFailure(result, method) {
136
+ const code = classifyTwoFactorSendFailure(result.status);
137
+ const label = SEND_LABEL[method];
138
+ const error = code === 'RATE_LIMITED' ? FIXED_TEXT.RATE_LIMITED
139
+ : code === 'SESSION_REJECTED' ? `We could not send a ${label} code for this session. Please try again.`
140
+ : code === 'SEND_UNAVAILABLE' ? `We could not send a ${label} code right now. Please try again.`
141
+ : `We could not send a ${label} code. Please try again.`;
142
+ return server_1.NextResponse.json({ success: false, error, code, meta: { attemptedRefresh: result.attemptedRefresh } }, { status: result.status });
143
+ }
@@ -28,7 +28,8 @@ export interface AuthContext {
28
28
  }
29
29
  export interface SessionPointer {
30
30
  exists: boolean;
31
- sessionToken?: string;
31
+ /** PAY-1481: tokenLogId of the session token (presence + log correlation), never the token itself. */
32
+ sessionTokenId?: string | null;
32
33
  expired?: boolean;
33
34
  hasRefreshToken?: boolean;
34
35
  roles: string[];
@@ -105,6 +105,34 @@ function redirectTo(request, location, clearCookies = false) {
105
105
  }
106
106
  return response;
107
107
  }
108
+ /**
109
+ * Terminal response for a FAILED token refresh.
110
+ *
111
+ * For `/api/` requests a redirect is the wrong terminator: fetch() follows the 307,
112
+ * receives 200 + an HTML login page, and the caller parses garbage instead of
113
+ * branching on an error. Those callers get a 401 JSON body they can branch on;
114
+ * document requests keep the existing redirect exactly as before.
115
+ *
116
+ * Cookie-clearing is preserved on BOTH arms. That is load-bearing, not incidental:
117
+ * on a permanent failure (401/403) stale cookies must be cleared or the next request
118
+ * refreshes again and loops (see the CRITICAL note in handleRefresh). A JSON arm that
119
+ * skipped clearing would convert a redirect loop into a silent refresh loop.
120
+ *
121
+ * Scoped deliberately to handleRefresh's failure exits. It is NOT folded into
122
+ * redirectTo(), and NOT hoisted above the refresh decision: an `/api/` hatch placed
123
+ * higher would intercept 'refresh_needed' and disable middleware token refresh across
124
+ * the whole API surface (card 340024 C1).
125
+ */
126
+ function refreshFailureResponse(request, location, errorCode, clearCookies = false) {
127
+ if (request.nextUrl.pathname.startsWith('/api/')) {
128
+ const response = server_1.NextResponse.json({ error: errorCode }, { status: 401 });
129
+ if (clearCookies) {
130
+ clearSessionCookies(response, request);
131
+ }
132
+ return response;
133
+ }
134
+ return redirectTo(request, location, clearCookies);
135
+ }
108
136
  // =============================================================================
109
137
  // AUTH DECISION ENGINE
110
138
  // =============================================================================
@@ -311,9 +339,16 @@ function createMvpMiddleware(options = {}) {
311
339
  // The warning is deliberately KEPT: it is the only operational signal that
312
340
  // stale sessions are occurring, and it costs nothing to log before
313
341
  // delegating. Do not re-introduce a `return` in this block.
314
- if (!isPublic && sessionStatus.exists === false && sessionPointer.sessionToken) {
342
+ if (!isPublic && sessionStatus.exists === false && sessionPointer.sessionTokenId) {
343
+ // PAY-1505: the KEY is `sessionTokenId`, not `sessionToken`. The value has
344
+ // always been a tokenLogId fingerprint (viability.ts builds it as
345
+ // `tokenLogId(sessionToken)`), but labelling it `sessionToken` told any
346
+ // operator reading this log that the line carried the token itself. The
347
+ // credential-in-logs source guard reads the key for exactly that reason:
348
+ // a field named `sessionToken` must hold a safe value, and proving that
349
+ // across an API boundary is not possible from the name alone.
315
350
  log.warn('Stale session detected (delegating to decision engine)', {
316
- sessionToken: sessionPointer.sessionToken?.substring(0, 8) + '...',
351
+ sessionTokenId: sessionPointer.sessionTokenId,
317
352
  pathname
318
353
  });
319
354
  }
@@ -348,7 +383,7 @@ async function checkViability(request, endpoint, log) {
348
383
  };
349
384
  const sessionPointer = {
350
385
  exists: false,
351
- sessionToken: undefined,
386
+ sessionTokenId: undefined,
352
387
  expired: false,
353
388
  hasRefreshToken: false,
354
389
  roles: [],
@@ -369,11 +404,11 @@ async function checkViability(request, endpoint, log) {
369
404
  if (response.ok) {
370
405
  const data = await response.json();
371
406
  // Support both response formats (viability and refresh-viability)
372
- sessionStatus.exists = data.authenticated ?? (data.sessionToken && data.reason !== 'session_not_found');
407
+ sessionStatus.exists = data.authenticated ?? (!!data.sessionTokenId && data.reason !== 'session_not_found');
373
408
  sessionStatus.requires2FA = data.requires2FA || false;
374
409
  sessionStatus.twoFactorComplete = data.twoFactorComplete || false;
375
410
  sessionPointer.exists = sessionStatus.exists ?? false;
376
- sessionPointer.sessionToken = data.sessionToken;
411
+ sessionPointer.sessionTokenId = data.sessionTokenId ?? null;
377
412
  sessionPointer.expired = data.accessTokenExpired || false;
378
413
  sessionPointer.hasRefreshToken = data.hasRefreshToken ?? data.canRefresh ?? false;
379
414
  sessionPointer.roles = data.roles || [];
@@ -457,7 +492,7 @@ async function handleRefresh(request, safeCallback, opts) {
457
492
  // Check if circuit breaker allows refresh attempt
458
493
  if (!cb.canAttemptRefresh()) {
459
494
  log.warn('Circuit breaker preventing refresh attempt');
460
- return redirectTo(request, `${LOGIN_PAGE}?callbackUrl=${encodeURIComponent(safeCallback)}&error=ServiceUnavailable`, true);
495
+ return refreshFailureResponse(request, `${LOGIN_PAGE}?callbackUrl=${encodeURIComponent(safeCallback)}&error=ServiceUnavailable`, 'ServiceUnavailable', true);
461
496
  }
462
497
  try {
463
498
  const baseUrl = (0, internal_api_url_1.getInternalApiUrl)(request);
@@ -494,7 +529,7 @@ async function handleRefresh(request, safeCallback, opts) {
494
529
  clearingCookies: isPermanentFailure
495
530
  });
496
531
  onRefreshFailure?.(response.status, false);
497
- return redirectTo(request, `${LOGIN_PAGE}?callbackUrl=${encodeURIComponent(safeCallback)}&error=${errorParam}`, isPermanentFailure);
532
+ return refreshFailureResponse(request, `${LOGIN_PAGE}?callbackUrl=${encodeURIComponent(safeCallback)}&error=${errorParam}`, errorParam, isPermanentFailure);
498
533
  }
499
534
  catch (error) {
500
535
  const isNetwork = cb.isNetworkError(error);
@@ -508,6 +543,6 @@ async function handleRefresh(request, safeCallback, opts) {
508
543
  }
509
544
  onRefreshFailure?.(0, isNetwork);
510
545
  // Network error - don't clear cookies, might be transient
511
- return redirectTo(request, `${LOGIN_PAGE}?callbackUrl=${encodeURIComponent(safeCallback)}&error=RefreshError`, false);
546
+ return refreshFailureResponse(request, `${LOGIN_PAGE}?callbackUrl=${encodeURIComponent(safeCallback)}&error=RefreshError`, 'RefreshError', false);
512
547
  }
513
548
  }
@@ -11,6 +11,7 @@ const SelectMethodStep_1 = require("../../components/recovery/SelectMethodStep")
11
11
  const VerifyCodeStep_1 = require("../../components/recovery/VerifyCodeStep");
12
12
  const SetPasswordStep_1 = require("../../components/recovery/SetPasswordStep");
13
13
  const CompleteStep_1 = require("../../components/recovery/CompleteStep");
14
+ const RequestedStep_1 = require("../../components/recovery/RequestedStep");
14
15
  const useTheme_1 = require("../../theme/useTheme");
15
16
  function RecoveryContent() {
16
17
  const router = (0, navigation_1.useRouter)();
@@ -24,13 +25,18 @@ function RecoveryContent() {
24
25
  const [verificationCode, setVerificationCode] = (0, react_1.useState)('');
25
26
  const [passwordResetToken, setPasswordResetToken] = (0, react_1.useState)(null);
26
27
  const [error, setError] = (0, react_1.useState)(null);
28
+ // PAY-1591: true when the reset failed because the recovery itself is dead; the error box then offers a way back.
29
+ const [recoveryExpired, setRecoveryExpired] = (0, react_1.useState)(false);
27
30
  const [loading, setLoading] = (0, react_1.useState)(false);
31
+ // PAY-1588: true only after the IdP accepted the new password. The 'complete' step is ALSO reached from initiate()
32
+ // when no recovery session comes back, and that path must not tell the user a password was set.
33
+ const [resetSucceeded, setResetSucceeded] = (0, react_1.useState)(false);
28
34
  const handleInitiateRecovery = async () => {
29
35
  setLoading(true);
30
36
  setError(null);
31
37
  try {
32
38
  const response = await api_1.accountApi.initiateRecovery(email);
33
- if (response.success && response.data.recovery_session_token) {
39
+ if (response.success && response.data?.recovery_session_token) {
34
40
  setRecoverySession({
35
41
  recoveryToken: response.data.recovery_session_token,
36
42
  email: email,
@@ -43,7 +49,8 @@ function RecoveryContent() {
43
49
  setCurrentStep('select-method');
44
50
  }
45
51
  else {
46
- setCurrentStep('complete');
52
+ // No recovery session (unknown / locked / throttled): the ONE neutral step, never CompleteStep (rigpert 60711).
53
+ setCurrentStep('requested');
47
54
  }
48
55
  }
49
56
  catch (err) {
@@ -105,9 +112,11 @@ function RecoveryContent() {
105
112
  return;
106
113
  setLoading(true);
107
114
  setError(null);
115
+ setRecoveryExpired(false);
108
116
  try {
109
117
  const response = await api_1.accountApi.resetPasswordWithToken(recoverySession.email, passwordResetToken.token, password, confirmPassword);
110
118
  if (response.success) {
119
+ setResetSucceeded(true);
111
120
  setCurrentStep('complete');
112
121
  }
113
122
  else {
@@ -116,6 +125,7 @@ function RecoveryContent() {
116
125
  }
117
126
  catch (err) {
118
127
  setError(err instanceof Error ? err.message : 'Failed to reset password.');
128
+ setRecoveryExpired(!!err && err.code === 'RECOVERY_EXPIRED');
119
129
  }
120
130
  finally {
121
131
  setLoading(false);
@@ -129,9 +139,9 @@ function RecoveryContent() {
129
139
  else if (currentStep === 'set-password')
130
140
  setCurrentStep('verify-code');
131
141
  };
132
- return ((0, jsx_runtime_1.jsx)("div", { className: "w-full flex items-center justify-center p-6", style: { background: 'var(--bg-default)' }, children: (0, jsx_runtime_1.jsxs)("div", { className: "max-w-md w-full", children: [error && ((0, jsx_runtime_1.jsx)("div", { className: "mb-4 bg-red-50 border border-red-200 rounded-2xl p-3", children: (0, jsx_runtime_1.jsx)("p", { className: "text-red-700 text-sm", children: error }) })), loading && ((0, jsx_runtime_1.jsx)("div", { className: "mb-4 rounded-2xl p-3", style: { background: 'var(--bg-card)', borderColor: 'var(--border-default)' }, children: (0, jsx_runtime_1.jsx)("p", { className: "text-sm", style: { color: 'var(--text-secondary)' }, children: "Processing..." }) })), currentStep === 'initiate' && ((0, jsx_runtime_1.jsx)(InitiateRecoveryStep_1.InitiateRecoveryStep, { email: email, setEmail: setEmail, onSubmit: handleInitiateRecovery, loading: loading })), currentStep === 'select-method' && recoverySession && ((0, jsx_runtime_1.jsx)(SelectMethodStep_1.SelectMethodStep, { session: recoverySession, onSelectMethod: handleSendCode, loading: loading })), currentStep === 'verify-code' && ((0, jsx_runtime_1.jsx)(VerifyCodeStep_1.VerifyCodeStep, { code: verificationCode, setCode: setVerificationCode, onSubmit: handleVerifyCode, onResend: () => selectedMethod && handleSendCode(selectedMethod), loading: loading, maskedDestination: selectedMethod === 'sms'
142
+ return ((0, jsx_runtime_1.jsx)("div", { className: "w-full flex items-center justify-center p-6", style: { background: 'var(--bg-default)' }, children: (0, jsx_runtime_1.jsxs)("div", { className: "max-w-md w-full", children: [error && ((0, jsx_runtime_1.jsxs)("div", { className: "mb-4 bg-red-50 border border-red-200 rounded-2xl p-3", children: [(0, jsx_runtime_1.jsx)("p", { className: "text-red-700 text-sm", children: error }), recoveryExpired && ((0, jsx_runtime_1.jsx)("a", { href: "/account-auth/recovery", className: "text-sm font-medium underline", children: "Start password recovery again" }))] })), loading && ((0, jsx_runtime_1.jsx)("div", { className: "mb-4 rounded-2xl p-3", style: { background: 'var(--bg-card)', borderColor: 'var(--border-default)' }, children: (0, jsx_runtime_1.jsx)("p", { className: "text-sm", style: { color: 'var(--text-secondary)' }, children: "Processing..." }) })), currentStep === 'initiate' && ((0, jsx_runtime_1.jsx)(InitiateRecoveryStep_1.InitiateRecoveryStep, { email: email, setEmail: setEmail, onSubmit: handleInitiateRecovery, loading: loading })), currentStep === 'select-method' && recoverySession && ((0, jsx_runtime_1.jsx)(SelectMethodStep_1.SelectMethodStep, { session: recoverySession, onSelectMethod: handleSendCode, loading: loading })), currentStep === 'verify-code' && ((0, jsx_runtime_1.jsx)(VerifyCodeStep_1.VerifyCodeStep, { code: verificationCode, setCode: setVerificationCode, onSubmit: handleVerifyCode, onResend: () => selectedMethod && handleSendCode(selectedMethod), loading: loading, maskedDestination: selectedMethod === 'sms'
133
143
  ? recoverySession?.maskedPhone
134
- : recoverySession?.maskedEmail })), currentStep === 'set-password' && ((0, jsx_runtime_1.jsx)(SetPasswordStep_1.SetPasswordStep, { onSubmit: handleResetPassword, loading: loading })), currentStep === 'complete' && ((0, jsx_runtime_1.jsx)(CompleteStep_1.CompleteStep, { onGoToLogin: () => router.push('/account-auth/login') })), currentStep !== 'complete' && currentStep !== 'initiate' && !loading && ((0, jsx_runtime_1.jsx)("div", { className: "mt-6 text-center", children: (0, jsx_runtime_1.jsx)("button", { type: "button", onClick: handleGoBack, className: "text-sm hover:underline font-medium", style: { color: 'var(--text-primary)' }, children: "\u2190 Go Back" }) }))] }) }));
144
+ : recoverySession?.maskedEmail })), currentStep === 'set-password' && ((0, jsx_runtime_1.jsx)(SetPasswordStep_1.SetPasswordStep, { onSubmit: handleResetPassword, loading: loading })), currentStep === 'requested' && (0, jsx_runtime_1.jsx)(RequestedStep_1.RequestedStep, { onGoToLogin: () => router.push('/account-auth/login') }), currentStep === 'complete' && ((0, jsx_runtime_1.jsx)(CompleteStep_1.CompleteStep, { onGoToLogin: () => router.push(resetSucceeded ? '/account-auth/login?notice=password-set' : '/account-auth/login') })), currentStep !== 'complete' && currentStep !== 'requested' && currentStep !== 'initiate' && !loading && ((0, jsx_runtime_1.jsx)("div", { className: "mt-6 text-center", children: (0, jsx_runtime_1.jsx)("button", { type: "button", onClick: handleGoBack, className: "text-sm hover:underline font-medium", style: { color: 'var(--text-primary)' }, children: "\u2190 Go Back" }) }))] }) }));
135
145
  }
136
146
  function LoadingFallback() {
137
147
  const colors = (0, useTheme_1.useColors)();